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

Compare commits

...

527 Commits

Author SHA1 Message Date
Thomas Harte
b3012bd89e Update version number. 2024-06-02 22:29:25 -04:00
Thomas Harte
f521c12d85 Merge pull request #1385 from TomHarte/VicBorders
CRT: accept data during retrace.
2024-06-02 22:18:25 -04:00
Thomas Harte
58f04848a9 Fix: allow allocations on invisible areas. 2024-06-02 21:59:27 -04:00
Thomas Harte
6488f46850 Normalise variable names. 2024-06-02 21:58:49 -04:00
Thomas Harte
5f4b798cff Clarify scope of this_state. 2024-06-01 20:05:51 -04:00
Thomas Harte
ffa8f1db04 Merge pull request #1384 from TomHarte/QtArchimedes
Add Archimedes Qt UI options.
2024-05-31 21:25:49 -04:00
Thomas Harte
cf2711f6dd Add Archimedes Qt UI options. 2024-05-31 21:17:56 -04:00
Thomas Harte
25eded1895 Merge pull request #1382 from TomHarte/QtRepair
Qt: Correct Electron target name.
2024-05-29 22:24:52 -04:00
Thomas Harte
a8ac8f4a23 Correct Electron target name. 2024-05-29 22:24:15 -04:00
Thomas Harte
c67a53e95b Merge pull request #1381 from TomHarte/MemoryOrder
Avoid `std::memory_order::memory_order_X` in favour of `std::memory_order_X`.
2024-05-29 22:01:20 -04:00
Thomas Harte
edf4ba2533 Merge pull request #1380 from TomHarte/NoPairs
Eliminate various unnecessary uses of `std::make_pair`.
2024-05-29 21:59:59 -04:00
Thomas Harte
71d337c10e Avoid std::memory_order::. 2024-05-29 21:51:07 -04:00
Thomas Harte
eb9e5fb727 Eliminate various unnecessary uses of std::make_pair. 2024-05-29 21:46:33 -04:00
Thomas Harte
f133000656 Update version number. 2024-05-27 15:05:47 -04:00
Thomas Harte
71361638bb Merge pull request #1379 from TomHarte/UnambiguousRejection
Don't move cursor near disk to folder if not about to load something.
2024-05-27 15:05:04 -04:00
Thomas Harte
edc7fe9c72 Simplify actions. 2024-05-27 14:54:09 -04:00
Thomas Harte
3110041a06 Don't seem to move near disk folder if not loading. 2024-05-27 14:45:37 -04:00
Thomas Harte
ff78e4172d Merge pull request #1378 from TomHarte/FastArchimedesDisks
Support FDC overclocking as 'fast loading'.
2024-05-26 22:19:40 -04:00
Thomas Harte
a1c23be73d Remove development dead ends. 2024-05-26 22:03:48 -04:00
Thomas Harte
eb2b1cb093 Support FDC overclocking as 'fast loading'. 2024-05-26 22:02:40 -04:00
Thomas Harte
f2245b8066 Merge pull request #1377 from TomHarte/TidySWIs
Clean up, slightly.
2024-05-26 14:43:40 -04:00
Thomas Harte
793b6d1deb Clean up, slightly. 2024-05-26 14:42:54 -04:00
Thomas Harte
537b91fa3f Merge pull request #1374 from TomHarte/SWIAnalyser
Add Archimedes autostart behaviour.
2024-05-24 16:32:45 -04:00
Thomas Harte
b7777c9ca3 Avoid linkage warning. 2024-05-24 15:39:38 -04:00
Thomas Harte
5235262855 Add default value. 2024-05-24 15:29:11 -04:00
Thomas Harte
7b90c36463 Shorten clicks, ensure no lost actions. 2024-05-24 15:23:45 -04:00
Thomas Harte
6407ab0673 Clean up, improve application-menu response. 2024-05-24 14:32:55 -04:00
Thomas Harte
78ec9e5a60 Limit character range. 2024-05-23 22:31:43 -04:00
Thomas Harte
778ac6e6d1 Complete autoload loop. 2024-05-23 22:16:40 -04:00
Thomas Harte
5280f5aba2 Attempt to spot screen takeovers. 2024-05-23 22:03:40 -04:00
Thomas Harte
67add0da93 Use both sources. 2024-05-21 22:23:53 -04:00
Thomas Harte
a32da9a6e1 Restore preference against !Boot. 2024-05-21 22:12:55 -04:00
Thomas Harte
b6b70bb7ff Add title fallbacks, ensure 'read' costs even 0.0-weight options. 2024-05-21 22:06:09 -04:00
Thomas Harte
6d769c9e89 Use string similarity as a program differentiator. 2024-05-21 21:49:30 -04:00
Thomas Harte
0c683c2c81 Merge branch 'master' into SWIAnalyser 2024-05-21 07:38:23 -04:00
Thomas Harte
8e51bd7578 Merge branch 'master' of github.com:TomHarte/CLK 2024-05-21 07:38:10 -04:00
Thomas Harte
6d6dfa4f44 Introduce Archimedes analyser tests. 2024-05-20 22:48:20 -04:00
Thomas Harte
7d044ad0ab Merge pull request #1376 from TomHarte/Remove-macos-11
Eliminate macos11 runner.
2024-05-20 16:29:02 -04:00
Thomas Harte
826f4c1d48 Eliminate macos-11.
As per GitHub's announcement that "The macOS 11 runner image will be removed by 6/28/24"
2024-05-20 14:21:42 -04:00
Thomas Harte
3be5d60b1e Eliminate comparison costs. 2024-05-18 22:16:58 -04:00
Thomas Harte
26375dc023 Introduce named constants. 2024-05-16 22:19:26 -04:00
Thomas Harte
8d0d7abd5a Keep track of state separately from scanning. 2024-05-16 22:18:51 -04:00
Thomas Harte
ef03ddf2ae Extend to launching the target program. 2024-05-14 22:23:35 -04:00
Thomas Harte
1d8b33d7ae Attempt to improve trajectory. 2024-05-13 22:26:36 -04:00
Thomas Harte
308b3ca448 Gamely attempt to pick an Arc program to start. 2024-05-13 22:25:02 -04:00
Thomas Harte
ca67afea4c Correct some unique-ptr oddities. 2024-05-13 21:46:03 -04:00
Thomas Harte
0b11fc259b Add Archimedes-specific target class. 2024-05-13 21:42:38 -04:00
Thomas Harte
18ffb9294f Add full cursor automation. 2024-05-12 22:16:29 -04:00
Thomas Harte
c82517c9fd Add mouse position getter. 2024-05-11 13:07:12 -04:00
Thomas Harte
6d42c9aaf9 Start making leeway on interesting SWIs. 2024-05-06 22:40:00 -04:00
Thomas Harte
02ee3a7804 Turf out old debugging cruft. 2024-05-06 20:36:00 -04:00
Thomas Harte
bdf1dff976 Update version number. 2024-05-04 21:16:43 -04:00
Thomas Harte
e6724a701a Merge pull request #1373 from TomHarte/DiskReady
Further adjust RDY.
2024-04-30 22:50:17 -04:00
Thomas Harte
d90eedfc8c Adjust bit polarity, meaning. 2024-04-30 22:49:26 -04:00
Thomas Harte
63009d00b4 Merge pull request #1372 from TomHarte/DiskReady
Add Archimedes disk drive RDY signal.
2024-04-30 22:38:20 -04:00
Thomas Harte
6a2261d217 Merge branch 'master' into DiskReady 2024-04-30 22:38:07 -04:00
Thomas Harte
c3ad2154b5 Merge pull request #1371 from TomHarte/Interlacing
Support Archimedes interlaced video.
2024-04-30 22:37:48 -04:00
Thomas Harte
3d61861737 Ensure switch is complete. 2024-04-30 22:26:19 -04:00
Thomas Harte
7545786436 Ensure extra line types are used. 2024-04-30 22:22:18 -04:00
Thomas Harte
a997b6c677 Wire drives for IBM-style RDY. 2024-04-30 22:18:17 -04:00
Thomas Harte
72d4f638aa Merge branch 'master' into Interlacing 2024-04-30 22:08:57 -04:00
Thomas Harte
b15ff6d442 Support interlaced video timing. 2024-04-30 22:06:08 -04:00
Thomas Harte
cb70967971 Merge pull request #1370 from TomHarte/Zarch
Add automatic runtime frame-rate limiter.
2024-04-30 22:02:06 -04:00
Thomas Harte
42aea2663c Add automatic runtime frame-rate limiter. 2024-04-30 21:38:37 -04:00
Thomas Harte
a882faa7f6 Merge pull request #1369 from TomHarte/UnmaskedSubAddresses
Mildly reduce ARM/Archimedes hot-path costs.
2024-04-29 22:58:22 -04:00
Thomas Harte
5da01e4fd8 Add potential short-circuit. 2024-04-29 22:45:30 -04:00
Thomas Harte
71c5a1d419 Avoid repeated trans comparison. 2024-04-29 22:35:01 -04:00
Thomas Harte
03c3da7338 Bifurcate Zone enum, possibly to help compiler. 2024-04-29 22:34:46 -04:00
Thomas Harte
47b276ca0b Merge pull request #1368 from TomHarte/MinorTweaks
Fix trans for instruction fetches.
2024-04-29 22:29:25 -04:00
Thomas Harte
c7747ec5a0 Remove a conditional from the hot path. 2024-04-29 22:16:06 -04:00
Thomas Harte
5a84e98256 Fix trans for instruction fetches. 2024-04-29 21:54:59 -04:00
Thomas Harte
b66d69b60c Merge pull request #1367 from TomHarte/FloppyHeft
Slightly reduce processing heft of floppy accesses.
2024-04-28 22:49:31 -04:00
Thomas Harte
dfaea5a922 Reduce floppy access cost. 2024-04-28 22:40:54 -04:00
Thomas Harte
f4da417c3a Merge pull request #1366 from TomHarte/VIDCDelays
Add various VIDC output latencies.
2024-04-28 22:39:29 -04:00
Thomas Harte
fa7fff86eb Eject invalid specialisation. 2024-04-28 22:20:26 -04:00
Thomas Harte
d480f9eae2 Reinstate all missing video modes. 2024-04-28 21:49:04 -04:00
Thomas Harte
4f1aef90b8 Relocate pixel collection and cursor shifting. 2024-04-26 21:29:30 -04:00
Thomas Harte
24f4538eb7 Do faulty restoration of 4bpp mode. 2024-04-25 22:50:24 -04:00
Thomas Harte
38d096cad6 Begin new state machine, losing all non-cursor pixels. 2024-04-25 22:01:38 -04:00
Thomas Harte
b82af9c471 Fix vertical timing; don't miss border colour changes. 2024-04-24 20:47:11 -04:00
Thomas Harte
0bff2089c4 Merge pull request #1364 from TomHarte/SomeWarnings
Resolve various warnings.
2024-04-23 19:40:43 -07:00
Thomas Harte
36d9c40d7b Yuckily avoid warning. 2024-04-23 22:23:56 -04:00
Thomas Harte
becb6ce2e0 Fix two more not-really-an-issue warnings. 2024-04-23 22:20:13 -04:00
Thomas Harte
56b65780d2 Avoid loading nonsense value upon data abort. 2024-04-22 22:09:57 -04:00
Thomas Harte
265d151879 Fix data aborts. 2024-04-22 22:08:09 -04:00
Thomas Harte
c485097eed Fix bool combination. 2024-04-22 22:06:09 -04:00
Thomas Harte
f86e9fe086 Eliminate impossible conditional. 2024-04-22 21:58:49 -04:00
Thomas Harte
c91ce4cfea Ensure all routes return. 2024-04-22 21:57:20 -04:00
Thomas Harte
8e64a854fc Ensure all routes return; mildly decrease conditionals. 2024-04-22 21:56:53 -04:00
Thomas Harte
7c9383cd6b Update version number. 2024-04-20 14:45:21 -04:00
Thomas Harte
82d03e3980 Merge pull request #1362 from TomHarte/ThreeStepPipeline
Introduce some degree of an ARM pipeline.
2024-04-19 19:41:46 -07:00
Thomas Harte
0775e3ad58 This is an 8-bit value. 2024-04-19 22:35:43 -04:00
Thomas Harte
ea3eef3817 Put interrupts into pipeline, without delay. 2024-04-19 22:21:23 -04:00
Thomas Harte
83eac172c9 Revoke in-pipeline interrupts.
I'm unclear on what timing should apply here really.
2024-04-19 21:46:09 -04:00
Thomas Harte
5b13d3e893 Attempt the prefetch portion of a pipeline. 2024-04-19 21:30:15 -04:00
Thomas Harte
807835b9fe Merge pull request #1361 from TomHarte/Pipeline
Provide hooks for implementing pipeline prefetch.
2024-04-19 06:58:45 -07:00
Thomas Harte
4bf02122ee Fix disassembler. 2024-04-18 23:17:44 -04:00
Thomas Harte
e6c4454059 Provide a means for SWI interception. 2024-04-18 22:13:58 -04:00
Thomas Harte
d464ce831a Add did_set_pc. 2024-04-18 19:30:07 -04:00
Thomas Harte
018f0e097f Merge pull request #1358 from TomHarte/ADFS-D
Ensure ADFS-D discs are recognised.
2024-04-17 23:06:49 -04:00
Thomas Harte
2acb853021 Merge pull request #1357 from TomHarte/EasyWins
Improve MEMC speed.
2024-04-17 22:47:04 -04:00
Thomas Harte
acd477df39 Ensure ADFS-D discs are recognised. 2024-04-17 22:44:55 -04:00
Thomas Harte
da520de9ef Further appease GCC. 2024-04-17 22:38:32 -04:00
Thomas Harte
e680a973b0 Appease GCC with a 'default'. 2024-04-17 22:17:24 -04:00
Thomas Harte
07984a2f8b Resolve various warnings. 2024-04-17 22:15:05 -04:00
Thomas Harte
16532136e9 Merge branch 'master' into EasyWins 2024-04-17 21:40:01 -04:00
Thomas Harte
30c2c65b77 Eliminate hot-path switch. 2024-04-17 21:36:39 -04:00
Thomas Harte
b63178132d Move trans tests inside switch. 2024-04-17 21:29:42 -04:00
Thomas Harte
6d66c90aad Merge pull request #1356 from TomHarte/ArchimedesGUI
Add macOS route to starting empty Archimedes.
2024-04-17 21:26:03 -04:00
Thomas Harte
c807c75412 Revert version change. 2024-04-17 21:25:12 -04:00
Thomas Harte
f6feaddfe6 Add macOS route to starting empty Archimedes. 2024-04-17 20:44:45 -04:00
Thomas Harte
87d1a476a4 Merge pull request #1355 from TomHarte/Archimedes
Add an inaccurate, basic Archimedes.
2024-04-16 22:45:42 -04:00
Thomas Harte
f7337f2400 Adopt 50%:50% tables throughout. 2024-04-16 22:45:01 -04:00
Thomas Harte
fac94a5d36 Reduce MIPS. Until other performance issues can be resolved. 2024-04-16 22:32:00 -04:00
Thomas Harte
140228a936 Mildly reduce heft of scale read. 2024-04-16 22:31:40 -04:00
Thomas Harte
06fd91f002 Fix period, table lookup. 2024-04-16 22:12:10 -04:00
Thomas Harte
c3d4d0ee38 Introduce panning, threading. 2024-04-16 21:56:34 -04:00
Thomas Harte
30cca54e6c Diagnostically try for a square wave. 2024-04-13 22:10:34 -04:00
Thomas Harte
6ac6e48b95 Attempt audio output. 2024-04-13 21:54:50 -04:00
Thomas Harte
779794632e Generate volume ramp. 2024-04-13 20:23:47 -04:00
Thomas Harte
88bb16f261 Install proper filter frequency. 2024-04-13 15:34:39 -04:00
Thomas Harte
c134c7bdc2 Fix: signal is 'flyback', not sync. 2024-04-10 21:53:38 -04:00
Thomas Harte
6c6cda3db5 Use clocking hints. 2024-04-09 22:22:03 -04:00
Thomas Harte
a29f246536 Move to more natural position of ownership. 2024-04-09 22:10:07 -04:00
Thomas Harte
d9d675a74f Fix scan status scale. 2024-04-09 21:56:42 -04:00
Thomas Harte
d62ea95889 Make some intimation towards audio. 2024-04-09 21:53:40 -04:00
Thomas Harte
e2e951ad0b Fix layout. 2024-04-09 21:49:35 -04:00
Thomas Harte
a5a653d684 Factor vsync state into IO reads. 2024-04-09 21:49:00 -04:00
Thomas Harte
6123350895 Improve state guesswork. 2024-04-09 21:24:08 -04:00
Thomas Harte
ec73c00c3b Silence the routine stuff of interrupt masks. 2024-04-09 20:57:57 -04:00
Thomas Harte
dd24f5f4f3 Don't latch video addresses until almost the last minute. 2024-04-09 20:56:10 -04:00
Thomas Harte
82a2e802ea Life's too short; just do it in HTML. 2024-04-08 23:11:22 -04:00
Thomas Harte
3b75eeb70a Try two divs. 2024-04-08 22:46:45 -04:00
Thomas Harte
ee4575b70f Attempt a div. 2024-04-08 22:45:41 -04:00
Thomas Harte
46a4ec1cb1 Reshuffle images. 2024-04-08 22:42:49 -04:00
Thomas Harte
8ab77a3260 Attempt to even out columns. 2024-04-08 22:39:14 -04:00
Thomas Harte
b875e349c1 Mention the Archimedes. 2024-04-08 22:34:08 -04:00
Thomas Harte
169298af42 Plumb through disk insertion.
Surprisingly: some things now load.
2024-04-08 21:15:40 -04:00
Thomas Harte
5e502df48b Forward motor and drive selection. 2024-04-07 22:29:00 -04:00
Thomas Harte
4f58664f97 Catch interrupt enables. 2024-04-07 22:08:12 -04:00
Thomas Harte
ffd298218c Tie off initial values; fix FIQ usage. 2024-04-07 21:58:16 -04:00
Thomas Harte
d2b077c573 Start wiring in a floppy controller. 2024-04-07 21:22:35 -04:00
Thomas Harte
547dc29a60 Remove done TODOs. 2024-04-07 15:53:42 -04:00
Thomas Harte
69aeca5c0e Aggregate mouse deltas where possible. 2024-04-06 21:24:21 -04:00
Thomas Harte
ed7cd4b277 Fix 8bpp output, all-modes cursor. 2024-04-06 20:58:44 -04:00
Thomas Harte
7bf831e1a6 Add missing 'override'. 2024-04-06 13:51:33 -04:00
Thomas Harte
0092cb8c36 Route enough to be able to mess around. 2024-04-06 13:44:05 -04:00
Thomas Harte
543b1c644a Wire mouse events to the relevant class. 2024-04-06 13:32:59 -04:00
Thomas Harte
cfaea7a90c Add cursor within 4bpp pixel area. 2024-04-05 22:43:10 -04:00
Thomas Harte
b821645644 Capture cursor palette, switch horizontal field. 2024-04-05 22:01:01 -04:00
Thomas Harte
2865190499 Resolve video addressing issues. 2024-04-05 21:56:31 -04:00
Thomas Harte
3f40e409c5 Reduce debugging heft. 2024-04-04 22:16:11 -04:00
Thomas Harte
002e235d90 Force RGB mode. 2024-04-04 22:02:47 -04:00
Thomas Harte
7d8a364658 Reimplement LDM and STM. 2024-04-04 21:59:18 -04:00
Thomas Harte
41c471ca52 Add a force-user-aware accessor. 2024-04-04 20:17:44 -04:00
Thomas Harte
dd127f64fe Simplify range. 2024-04-03 07:23:14 -04:00
Thomas Harte
b19dcfd6dc Take another run at shifts. 2024-04-02 21:57:46 -04:00
Thomas Harte
55369464ad Add a by-eye crop. A better answer will come. 2024-04-01 22:10:05 -04:00
Thomas Harte
609c117267 Switch to English RISC OS. 2024-04-01 21:44:42 -04:00
Thomas Harte
3b62a2fe7a Restrict video buffer to first 512kb. 2024-04-01 21:39:10 -04:00
Thomas Harte
7c9715f00c Change mind about carry behaviour. 2024-04-01 21:38:44 -04:00
Thomas Harte
7de92a9457 Slightly clean up shift code. 2024-04-01 21:24:49 -04:00
Thomas Harte
0866caf934 Flaws remain, but acknowledge that pixel rate is double. 2024-04-01 10:48:20 -04:00
Thomas Harte
914b88d115 Fix non-debug build. 2024-03-31 19:17:55 -04:00
Thomas Harte
cc122a7a68 Add an SWI count, to aid in logging. 2024-03-31 18:18:26 -04:00
Thomas Harte
31979649c6 As it continues to swell, factor out the junk. 2024-03-31 18:15:48 -04:00
Thomas Harte
335d13d06d Mildly improve logging, define a few more ROMs. 2024-03-30 21:49:21 -04:00
Thomas Harte
ec785f3a8a Add URL as comment. 2024-03-30 20:54:17 -04:00
Thomas Harte
1f83a5425e Complete list of all currently-failing SWIs.
... a lot of which are probably failing correctly, i.e. they're appropriately signalling.
2024-03-30 20:48:47 -04:00
Thomas Harte
4882d6d0f2 Start adding SWI detail. 2024-03-30 15:16:48 -04:00
Thomas Harte
722743659b Add missing space. 2024-03-29 21:52:57 -04:00
Thomas Harte
6e64a79b52 Log failed SWIs. 2024-03-29 21:31:33 -04:00
Thomas Harte
8a6bf84cff Keyboard: log more, ignore unrecognised commands. 2024-03-29 20:54:07 -04:00
Thomas Harte
a0fdd8f4eb Resolve magic constant. 2024-03-28 22:15:27 -04:00
Thomas Harte
bda1783624 Make new guess at non-byte IOC reads. 2024-03-28 22:10:49 -04:00
Thomas Harte
2a14557478 Be more disciplined about errant accesses. 2024-03-28 21:31:07 -04:00
Thomas Harte
0ddbc67b1f Switch to default CMOS RAM obtained from RISC OS itself. 2024-03-28 21:23:49 -04:00
Thomas Harte
ffb5149890 Reinstate real CMOS RAM results. 2024-03-28 14:27:07 -04:00
Thomas Harte
bb339d619f Eliminate trace test; I don't think I'm going to work it through. 2024-03-28 14:23:00 -04:00
Thomas Harte
2ed11877e8 Determine a couple of further exclusions. 2024-03-28 14:11:41 -04:00
Thomas Harte
ea6b83815b Add a further category of exclusions. 2024-03-28 14:01:37 -04:00
Thomas Harte
740b0e35d5 Completely bypass ignored tests. 2024-03-28 11:28:37 -04:00
Thomas Harte
2e7c1acb88 Add note on confusion. 2024-03-28 10:34:46 -04:00
Thomas Harte
4fcb85d132 Cleave off most remaining reasons for failure. 2024-03-28 10:32:27 -04:00
Thomas Harte
f175dcea58 Hack in some more potential debugging help. 2024-03-27 22:37:37 -04:00
Thomas Harte
c04c708a9d Trade some depth for breadth. 2024-03-27 22:37:10 -04:00
Thomas Harte
f4cf1e5313 Attempt to cleave by broad reason. 2024-03-27 22:36:37 -04:00
Thomas Harte
0e17f382a1 Capture further detail. 2024-03-27 22:36:03 -04:00
Thomas Harte
f38bca37a2 Take another run at MEMC.
I hadn't spotted that it is valid to map different logical pages to the same physical page with different protection levels.
2024-03-27 10:44:40 -04:00
Thomas Harte
166793ebe6 Reduce I2C chatter. 2024-03-26 21:54:42 -04:00
Thomas Harte
8b04d0e3ef Enhance and better-document I2C states. 2024-03-26 21:52:29 -04:00
Thomas Harte
a3931674dc Seemingly navigate I2C correctly. 2024-03-26 21:33:46 -04:00
Thomas Harte
bd4ef5ec57 Switch to acknowledgement-after. 2024-03-26 14:06:11 -04:00
Thomas Harte
3ba12630ab Quieten. 2024-03-26 12:27:37 -04:00
Thomas Harte
342d90c929 Advance CMOS/I2C to a seemingly-valid read. 2024-03-26 12:24:24 -04:00
Thomas Harte
9078fc994b Try to formalise I2C events. 2024-03-25 22:10:52 -04:00
Thomas Harte
f46af4b702 OS 3.11 seems to be able to get into BASIC. 2024-03-25 22:10:35 -04:00
Thomas Harte
b112987556 Do well enough at other colour depths. 2024-03-25 22:09:55 -04:00
Thomas Harte
fc880ac130 Double down on trans mode. 2024-03-25 21:32:56 -04:00
Thomas Harte
a2d95cb982 Shuffle notes. 2024-03-25 21:31:59 -04:00
Thomas Harte
d2776071e4 Speed up debug mode. 2024-03-25 21:31:33 -04:00
Thomas Harte
72a645ec1e Fix trans; take further crack at MEMC permissions. 2024-03-25 15:50:59 -04:00
Thomas Harte
1154ffd072 Add a 'drive in use' indicator LED. 2024-03-25 15:03:54 -04:00
Thomas Harte
8ba9708942 Hopefully resolve the mystery of the latch writes. 2024-03-25 14:54:30 -04:00
Thomas Harte
521fca6089 Expose full bus to IOC dependents; add notes. 2024-03-25 11:07:44 -04:00
Thomas Harte
ae684edbe1 Formally decode bank/offset/type. 2024-03-25 10:16:36 -04:00
Thomas Harte
fa0a9aa611 Eliminate 'has_moved_rom_'. 2024-03-24 22:36:11 -04:00
Thomas Harte
5da9e0486a Simplify control flow. 2024-03-24 22:30:26 -04:00
Thomas Harte
6980fd760c Add further heavily-manual debugging aids. 2024-03-24 22:18:30 -04:00
Thomas Harte
3549488b7a Add round-trip test for status flags. 2024-03-24 22:18:16 -04:00
Thomas Harte
c1602cc8fe The keyboard and interrupts are currently trusted. 2024-03-23 21:49:52 -04:00
Thomas Harte
189dd176de Reguess state machine, fixing startup display. 2024-03-23 21:38:35 -04:00
Thomas Harte
3cf262d1f7 Improve terminology, add more documentation. 2024-03-23 21:12:01 -04:00
Thomas Harte
ccfc389274 Quieten where now confident. 2024-03-23 21:03:06 -04:00
Thomas Harte
0e07f802ac Use BACK state; accept other ACKs at any time. 2024-03-23 21:02:35 -04:00
Thomas Harte
55f92e2411 Adjust data abort address. 2024-03-23 20:31:47 -04:00
Thomas Harte
c720f3910a Avoid implicit sign cast. 2024-03-23 20:13:25 -04:00
Thomas Harte
4215edd11b Reduce noise. 2024-03-23 20:12:56 -04:00
Thomas Harte
09a61cf1a7 Don't expect an ACK after identifying. 2024-03-23 20:12:38 -04:00
Thomas Harte
5967ad0865 Sketch out whole protocol, albeit faulty. 2024-03-23 17:08:03 -04:00
Thomas Harte
eb34c38332 Add very faulty key input. 2024-03-23 15:58:48 -04:00
Thomas Harte
5ccb18225a Provide key states to the keyboard. 2024-03-23 15:43:04 -04:00
Thomas Harte
58bbce1a15 Avoid display errors upon back-pressure. 2024-03-22 22:01:12 -04:00
Thomas Harte
9ea3e547ee Fix IRQ/FIQ return addresses. 2024-03-22 21:42:34 -04:00
Thomas Harte
fb5fdc9f10 Actually apply video divider. 2024-03-22 10:24:24 -04:00
Thomas Harte
de7b7818f4 Add 4bpp output. 2024-03-22 10:18:25 -04:00
Thomas Harte
c4e6b18294 Manage pixel buffers. 2024-03-22 10:10:13 -04:00
Thomas Harte
ae6cf69449 Move responsibility for clock division; reinstate vsync interrupt. 2024-03-22 10:01:34 -04:00
Thomas Harte
4a2dcff028 Endeavour to map colours properly. 2024-03-21 21:53:50 -04:00
Thomas Harte
aa6acec8fa Don't hoard cycles per line value. 2024-03-21 21:47:27 -04:00
Thomas Harte
4ac4da908c Reduce TODOs, do _something_ with border colour. 2024-03-21 21:40:11 -04:00
Thomas Harte
66e62857c4 Give ostensibly clean timing to the CRT. 2024-03-21 21:29:53 -04:00
Thomas Harte
bbc0d8b050 Count time in phase correctly. 2024-03-21 21:15:25 -04:00
Thomas Harte
0f8bc416d1 Make first, faulty step into displaying a field. 2024-03-21 21:10:55 -04:00
Thomas Harte
2ec235170e Finish the thought on magic constants. 2024-03-21 20:45:17 -04:00
Thomas Harte
2de1a2dd0d Install and properly clock a CRT. 2024-03-21 20:41:24 -04:00
Thomas Harte
1f49c3b113 Give sound and video somewhere to read from. 2024-03-21 20:22:20 -04:00
Thomas Harte
5c645fb3c2 Switch to a fixed output clock; retain addresses. 2024-03-21 11:51:29 -04:00
Thomas Harte
40b5227f0b Deliver all addresses to the video outputter. 2024-03-21 11:24:47 -04:00
Thomas Harte
847dba8f07 Divide input pixel rate. 2024-03-21 11:03:28 -04:00
Thomas Harte
417c6c4629 Announce changes. 2024-03-21 10:51:52 -04:00
Thomas Harte
2d6a4d490e Add dummy retrace interrupt. 2024-03-21 10:02:56 -04:00
Thomas Harte
a6ec870872 Capture more audio detail. 2024-03-21 09:47:53 -04:00
Thomas Harte
389541be6d Pipe further sound parameters; obey divider. 2024-03-20 14:43:47 -04:00
Thomas Harte
208f3e24de Audio ticks are now included. 2024-03-20 14:30:21 -04:00
Thomas Harte
f7e36a1e03 Merge branch 'Archimedes' of github.com:TomHarte/CLK into Archimedes 2024-03-20 14:27:32 -04:00
Thomas Harte
1341816791 Break apart, switching to delegates for interrupts. 2024-03-20 14:26:56 -04:00
Thomas Harte
b986add74a Break apart, switching to delegates for interrupts. 2024-03-20 14:25:20 -04:00
Thomas Harte
08673ff021 Switch to macro blocks of execution; flail around audio. 2024-03-20 11:42:37 -04:00
Thomas Harte
3a2d9c6082 Give user access to ROM; clean up a touch. 2024-03-19 20:26:17 -04:00
Thomas Harte
43a3959b8f Don't data abort on missing low ROM. 2024-03-19 15:06:01 -04:00
Thomas Harte
85a738acff Get rigorous on exception addresses. 2024-03-19 15:03:31 -04:00
Thomas Harte
17dbdce230 Eliminate SDL/scons targets for which brew is broken. 2024-03-19 14:27:46 -04:00
Thomas Harte
9d084782ae Document. 2024-03-19 12:22:19 -04:00
Thomas Harte
106937b679 Run into the shifts wall with LDR/STR. 2024-03-19 12:19:49 -04:00
Thomas Harte
623eda7162 Output branches and nops correctly. 2024-03-19 11:42:41 -04:00
Thomas Harte
2ad6bb099b Begin foray into disassembly. 2024-03-19 11:34:10 -04:00
Thomas Harte
9d858bc61b IRQ and FIQ should also store PC+4. 2024-03-18 14:08:08 -04:00
Thomas Harte
612c9ce49a Transfer logging responsibility. 2024-03-18 11:09:29 -04:00
Thomas Harte
64e025484a Adjust means of waiting out address. 2024-03-17 22:14:07 -04:00
Thomas Harte
7b1f800387 Extend I2C state machine. 2024-03-17 21:55:19 -04:00
Thomas Harte
2712d50e05 Attempt some inspection. 2024-03-16 22:02:16 -04:00
Thomas Harte
47e9279bd4 Add a target for I2C activity. 2024-03-16 15:00:23 -04:00
Thomas Harte
635efd0212 Clear keyboard interrupts. 2024-03-15 23:19:26 -04:00
Thomas Harte
1c1d2891c7 Adjust IRQ/FIQ return addresses. 2024-03-15 21:59:38 -04:00
Thomas Harte
1979d2e5ba Don't set interrupt flags before capture. 2024-03-15 21:34:39 -04:00
Thomas Harte
c25d0e8843 Correctly capture mode upon exception. 2024-03-15 18:39:56 -04:00
Thomas Harte
3a899ea4be Add test coverage for STM descending, proving nothing. 2024-03-15 14:55:17 -04:00
Thomas Harte
9d08282e28 Add enough of a keyboard to respond to reset. 2024-03-15 10:57:18 -04:00
Thomas Harte
18154278d1 Add minor note on where next. 2024-03-14 21:54:20 -04:00
Thomas Harte
9063852857 Undo spurious text change. 2024-03-14 21:16:38 -04:00
Thomas Harte
bc27e3998d Fix downward block data transfers. 2024-03-14 21:09:51 -04:00
Thomas Harte
19fa0b8945 Shush logging, momentarily. 2024-03-14 10:53:38 -04:00
Thomas Harte
4987bdfec9 Throw less. 2024-03-14 10:43:51 -04:00
Thomas Harte
0e4615564d Make bit masks easily testable; expand logging. 2024-03-13 14:31:26 -04:00
Thomas Harte
7aeea535a1 Reduce branchiness. 2024-03-13 11:02:52 -04:00
Thomas Harte
6b18d775ab Eliminate unused variables. 2024-03-12 21:53:26 -04:00
Thomas Harte
2ed031e440 Prepare for additional devices. 2024-03-12 21:23:22 -04:00
Thomas Harte
5d6bb11eb7 Add return. 2024-03-12 11:37:15 -04:00
Thomas Harte
c6b91559e1 Attempt to wire up timer interrupts. 2024-03-12 11:34:31 -04:00
Thomas Harte
6efc41ded7 Come to conclusion on R15; fix link values. 2024-03-12 10:42:09 -04:00
Thomas Harte
e9c5582fe1 Add note on ambiguity to be resolved. 2024-03-12 10:04:02 -04:00
Thomas Harte
8b3c0abe93 Take another swing at R15 as a destination. 2024-03-12 09:13:05 -04:00
Thomas Harte
a5ebac1b29 Add RISC OS 3.11 to catalogue, while bug hunting. 2024-03-11 22:19:14 -04:00
Thomas Harte
1ccfae885c Remove extra slashes. 2024-03-11 15:06:17 -04:00
Thomas Harte
971bfb2ecb Unify subtractions. 2024-03-11 14:52:48 -04:00
Thomas Harte
e7457461ba Reduce magic constants. 2024-03-11 14:49:03 -04:00
Thomas Harte
e8c1e8fd3f Fix RSB carry; unify set_pc. 2024-03-11 14:48:43 -04:00
Thomas Harte
ca779bc841 Expand test set. 2024-03-11 14:48:18 -04:00
Thomas Harte
a28c97c0de Simplify privilege test. 2024-03-11 12:14:00 -04:00
Thomas Harte
db49146efe Figure out what's going on with TEQ. 2024-03-11 09:51:09 -04:00
Thomas Harte
830d70d3aa Trust tests on immediate-opcode ROR 0; limit shift by register. 2024-03-10 23:38:31 -04:00
Thomas Harte
336292bc49 Further correct R15 as a destination. 2024-03-10 22:56:02 -04:00
Thomas Harte
bd62228cc6 The test set doesn't seem to do word rotation. 2024-03-10 22:40:37 -04:00
Thomas Harte
ccdd340c9a Reads also may or may not be aligned. *sigh* 2024-03-10 22:34:56 -04:00
Thomas Harte
0b42f5fb30 Make further test-set allowances. 2024-03-10 22:29:40 -04:00
Thomas Harte
e9e1db7a05 Change LDR writeback to destination. 2024-03-10 22:29:19 -04:00
Thomas Harte
21278d028c Correct unaligned accesses. 2024-03-10 21:56:19 -04:00
Thomas Harte
fbc273f114 Add invented model for tests. 2024-03-10 21:45:56 -04:00
Thomas Harte
06a5df029d Summarise failures. 2024-03-10 16:56:39 -04:00
Thomas Harte
e17700b495 Permit digression for 03110002, temporarily. 2024-03-10 14:47:02 -04:00
Thomas Harte
655b1e516c Test PSR and PC. 2024-03-10 14:14:18 -04:00
Thomas Harte
4e7a63f792 Do a de minimis checking of memory accesses. 2024-03-09 15:18:35 -05:00
Thomas Harte
a2896b9bd0 Test register values. 2024-03-09 15:11:12 -05:00
Thomas Harte
a4cf86268e Provide full access to stored registers. 2024-03-09 15:11:04 -05:00
Thomas Harte
d059e7c5d8 Disallow copying. 2024-03-09 15:10:55 -05:00
Thomas Harte
d6f882a8bb Integrate PC and PSR, guarantee invisible register values. 2024-03-09 14:59:44 -05:00
Thomas Harte
08f50f3eff Box in flags. 2024-03-08 23:01:29 -05:00
Thomas Harte
47f7340dfc Start hacking in some ARM tests. 2024-03-08 22:54:42 -05:00
Thomas Harte
fdef8901ab Double down on uint32_t. 2024-03-08 14:13:34 -05:00
Thomas Harte
ca1c3dc005 Add extra comments.
To persuade myself in the future.
2024-03-08 11:36:17 -05:00
Thomas Harte
9406a97141 Add some register switch tests. 2024-03-08 11:34:10 -05:00
Thomas Harte
a46ec4cffb Up clock rate to 24Mhz. 2024-03-07 22:16:58 -05:00
Thomas Harte
9bb5dc3c2b Fix inclusive range. 2024-03-07 19:40:34 -05:00
Thomas Harte
f6ea442606 Include various debugging detritus. 2024-03-07 14:28:39 -05:00
Thomas Harte
fa8fcd2218 Take another swing at popcount. 2024-03-07 14:28:31 -05:00
Thomas Harte
2a36d0fcbc Adjust user-mode test. 2024-03-07 14:00:38 -05:00
Thomas Harte
0e92885ed5 Fix ad hoc popcount; ARM does carry 'backwards'. 2024-03-07 13:27:41 -05:00
Thomas Harte
f5225b69e5 Add note to self. 2024-03-07 11:48:44 -05:00
Thomas Harte
15ee84b2eb Fix MUL ambiguity. 2024-03-07 11:45:39 -05:00
Thomas Harte
d380cecdb7 Add timers that count. 2024-03-07 11:39:26 -05:00
Thomas Harte
ae3cd924e8 Add a 2Mhz tick for timers. 2024-03-07 11:12:40 -05:00
Thomas Harte
a0f0f73bde Fix MOV as unconditional branch. 2024-03-07 10:31:26 -05:00
Thomas Harte
7cdceb7b4f Add a specific shout-out on prefetch abort, for debugging. 2024-03-07 10:23:46 -05:00
Thomas Harte
38b5624639 Add a little more VIDC detail. 2024-03-07 10:05:22 -05:00
Thomas Harte
3405b3b287 Add power-on bit, moving problems forward. 2024-03-06 22:14:56 -05:00
Thomas Harte
173fc9329a Add a little protection logic. 2024-03-06 22:00:34 -05:00
Thomas Harte
691a42d81e Attempt some logical mapping. 2024-03-06 21:51:19 -05:00
Thomas Harte
4059905f85 Slightly reorder messaging. 2024-03-06 16:45:17 -05:00
Thomas Harte
bbb520fd12 Transcribe some notes. 2024-03-06 15:31:07 -05:00
Thomas Harte
108a056f1c Execution now runs into a prefetch abort loop. 2024-03-06 15:05:24 -05:00
Thomas Harte
ed92e98ca2 Start looking at address translation. 2024-03-06 14:56:06 -05:00
Thomas Harte
0d666f9935 Get a bit more rigorous about reporting. 2024-03-06 09:54:39 -05:00
Thomas Harte
fe467be124 Further stick to existing type. 2024-03-05 10:56:09 -05:00
Thomas Harte
ba5f142515 Take further stab at TEQ PC, etc. 2024-03-05 10:55:44 -05:00
Thomas Harte
ed586e80bc Don't write to the PC with logical operations. 2024-03-05 09:32:35 -05:00
Thomas Harte
871c5467d7 Avoid sign change. 2024-03-05 09:31:42 -05:00
Thomas Harte
387791635e Start to establish a memory map. 2024-03-04 21:43:06 -05:00
Thomas Harte
b7a1363add Add an incorrect execution loop. 2024-03-04 21:09:24 -05:00
Thomas Harte
341b705bef Remove pointless check. 2024-03-04 14:11:44 -05:00
Thomas Harte
0b65aa39cd Add explicit assignment operator. 2024-03-04 14:09:53 -05:00
Thomas Harte
1b7c3644f4 Eliinate meaningless 'const'. 2024-03-04 14:09:27 -05:00
Thomas Harte
0cdca12e06 Resolve type mismatches. 2024-03-04 13:53:46 -05:00
Thomas Harte
61d4c69e45 Fix template parameter reference. 2024-03-04 13:25:40 -05:00
Thomas Harte
79865e295b Avoid ambiguous template parameter; use standard type. 2024-03-04 12:20:40 -05:00
Thomas Harte
1f43047de8 Loop the ARM executor into the build. 2024-03-04 12:08:46 -05:00
Thomas Harte
6f0ad0ab71 Add an empty Archimedes shell. 2024-03-04 12:06:43 -05:00
Thomas Harte
447734b1e9 Merge pull request #1354 from TomHarte/Acorn
Reorganise 'Electron' under 'Acorn'.
2024-03-04 11:55:56 -05:00
Thomas Harte
3e80651a0e Collect 'Electron' under 'Acorn'. 2024-03-04 11:31:25 -05:00
Thomas Harte
692a9da2e4 Merge pull request #1353 from TomHarte/ArchmidesAnalysis
Add a through path for Archimedes disk images.
2024-03-04 10:57:19 -05:00
Thomas Harte
e27312c980 Add to machine lists. 2024-03-04 10:19:06 -05:00
Thomas Harte
eae92a0cdb Add a through path for Archimedes disk images. 2024-03-04 10:13:57 -05:00
Thomas Harte
95cc34ba23 Merge pull request #1352 from TomHarte/ByeByeActive
Obscure storage for active registers.
2024-03-03 22:09:34 -05:00
Thomas Harte
7532b461cd Merge pull request #1351 from TomHarte/PositiveExpression
Express offset test as positive logic.
2024-03-03 22:03:37 -05:00
Thomas Harte
230e9c6327 Obscure active. 2024-03-03 21:43:30 -05:00
Thomas Harte
11c4d2f09e Add further exposition. 2024-03-03 21:38:27 -05:00
Thomas Harte
f2db1b4aae Merge branch 'TiedDown' into PositiveExpression 2024-03-03 21:31:26 -05:00
Thomas Harte
b42a6e447d Tie down more corners. 2024-03-03 21:29:53 -05:00
Thomas Harte
8a83d71560 Fix condition. 2024-03-03 14:40:05 -05:00
Thomas Harte
9fd7d5c10f Switch test and meaning. 2024-03-03 14:34:21 -05:00
Thomas Harte
7a5ed6c427 Merge pull request #1350 from TomHarte/ArchimedesROM
Add RISC OS catalogue entry; do some basic ARM debugging.
2024-03-03 14:32:25 -05:00
Thomas Harte
4e7963ee81 Clarify PC semantics; remove faulty underscore. 2024-03-03 14:11:02 -05:00
Thomas Harte
945b7e90da Add just enough to persuade self that execution is broadly sane. 2024-03-03 14:03:08 -05:00
Thomas Harte
99f0233b76 Fix immediate offset and data processing operation. 2024-03-02 23:27:37 -05:00
Thomas Harte
62da0dee7f Unify reads. 2024-03-02 23:15:17 -05:00
Thomas Harte
1663d3d9d1 Introduce disaster of an attempted test run. 2024-03-02 22:40:12 -05:00
Thomas Harte
37499d493a Fix model name. 2024-03-02 21:47:09 -05:00
Thomas Harte
c0dd96eb7c Add a catalogue entry for RISC OS. 2024-03-02 21:44:27 -05:00
Thomas Harte
2abae4c8bf Merge pull request #1349 from TomHarte/BarrelShifterTests
Introduce barrel-shifter tests.
2024-03-02 15:24:06 -05:00
Thomas Harte
c865da67e0 Introduce further barrel-shifter tests. 2024-03-02 15:12:03 -05:00
Thomas Harte
e6f77a9b80 Add logical right-shift tests. 2024-03-01 18:06:54 -05:00
Thomas Harte
7b28b3d634 Merge pull request #1343 from TomHarte/ARM2Ops
Attempt an implementation of the ARM2 instruction set.
2024-03-01 15:20:28 -05:00
Thomas Harte
42ba6d1281 Relocate execution code appropriately. 2024-03-01 15:02:47 -05:00
Thomas Harte
85b7afd530 Attempt a complete block data transfer. 2024-03-01 14:48:36 -05:00
Thomas Harte
f2f59a4de5 Attempt to deal with data aborts. 2024-03-01 10:38:08 -05:00
Thomas Harte
5759798ad7 Deal with downward write order. 2024-02-29 14:34:20 -05:00
Thomas Harte
ab1dd7f57e Implement a little of block data transfer. 2024-02-29 11:33:40 -05:00
Thomas Harte
53a2ea3a57 Add address exception. 2024-02-29 10:49:11 -05:00
Thomas Harte
1f1e7236be Add rotation. 2024-02-29 10:47:41 -05:00
Thomas Harte
fd2c5b6679 Make a quick first attempt at memory accesses. 2024-02-29 10:18:09 -05:00
Thomas Harte
0b287c55d5 Edge towards single data transfer. 2024-02-29 10:02:57 -05:00
Thomas Harte
0de8240238 Merge branch 'master' into ARM2Ops 2024-02-28 22:21:31 -05:00
Thomas Harte
1449b2a2a6 Merge pull request #1347 from TomHarte/AppleIIFlashRate
Double Apple II flash rate.
2024-02-28 22:21:05 -05:00
Thomas Harte
0f691766ee Double flash rate. 2024-02-28 22:13:22 -05:00
Thomas Harte
3ce05e9de1 Merge pull request #1346 from TomHarte/AppleIIReset
Propagate reset to the auxiliary switches.
2024-02-28 22:02:08 -05:00
Thomas Harte
98f5d0cdb7 Propagate reset to the auxiliary switches. 2024-02-28 21:36:55 -05:00
Thomas Harte
93b4008f81 Localise flags, detect improper carry write. 2024-02-28 21:28:19 -05:00
Thomas Harte
904462b881 Regularise data transfers. 2024-02-28 21:23:57 -05:00
Thomas Harte
3b320bcdef Update coprocessor interface. 2024-02-28 14:43:31 -05:00
Thomas Harte
3368bdb99f Document exceptions, partly for my future self. 2024-02-28 14:34:31 -05:00
Thomas Harte
4d400c3cb7 Add easy exceptions. 2024-02-28 14:25:12 -05:00
Thomas Harte
474f9da3c2 Add banked registers. 2024-02-28 14:09:05 -05:00
Thomas Harte
c49b26701f Relocate and clarify barrel shifts.
With a view to independent testing.
2024-02-28 13:53:13 -05:00
Thomas Harte
9b42d35d56 Update interface. 2024-02-28 11:42:33 -05:00
Thomas Harte
645152a1fd Implement branch. 2024-02-28 11:33:28 -05:00
Thomas Harte
487ade56ed Add basic multiply. 2024-02-28 11:27:27 -05:00
Thomas Harte
60d1b36e9a Implement registers side. 2024-02-28 10:25:14 -05:00
Thomas Harte
5a48c15e46 Add scheduler side of PC writeback. 2024-02-28 10:15:23 -05:00
Thomas Harte
d6bf1808f9 Take a swing at PC-as-input. 2024-02-28 09:33:05 -05:00
Thomas Harte
b676153d21 State intention to merge status with other registers. 2024-02-27 15:36:34 -05:00
Thomas Harte
a3339cf882 Fix indentation. 2024-02-27 15:30:51 -05:00
Thomas Harte
b4e0b46bac Add notes on R15. 2024-02-27 10:04:30 -05:00
Thomas Harte
09c1b2d7db Add missing shifts. 2024-02-27 09:55:24 -05:00
Thomas Harte
4255283e33 Deal with conditionality up front. 2024-02-26 21:36:23 -05:00
Thomas Harte
16e827bb2c Add basic arithmetics. 2024-02-26 21:27:58 -05:00
Thomas Harte
def69ce6d5 Add notes on R15. 2024-02-26 15:12:39 -05:00
Thomas Harte
054a799699 Fill in the easy 50% of operations. 2024-02-26 15:10:00 -05:00
Thomas Harte
580f402bb6 Muddle further towards data processing. 2024-02-26 14:50:45 -05:00
Thomas Harte
030dda34f0 Start poking at implementation. 2024-02-26 14:30:26 -05:00
Thomas Harte
cd21b39f44 Merge pull request #1342 from TomHarte/ARM2Status
Add some degree of ARM 2 status flags.
2024-02-26 10:48:24 -05:00
Thomas Harte
481b6d0e69 Sketch out some status flags. 2024-02-25 22:01:51 -05:00
Thomas Harte
a88d41bf00 List the flags. 2024-02-25 15:21:54 -05:00
Thomas Harte
0ee3b628e8 Merge pull request #1341 from TomHarte/AYEnvelopePeriod
Correct envelope period for internal double-resolution.
2024-02-24 15:38:17 -05:00
Thomas Harte
45628ba9df Merge pull request #1337 from TomHarte/ArchimedesADFs
Add some support for Archimedes ADF files.
2024-02-24 15:32:08 -05:00
Thomas Harte
c843c395ea Correct envelope period for internal double-resolution. 2024-02-24 15:16:33 -05:00
Thomas Harte
9bdaf31d04 Add missing #include. 2024-02-24 15:09:40 -05:00
Thomas Harte
4ac2baeb9d Merge pull request #1340 from ryandesign/patch-1
Mention Macintosh 128K and 512K in README.
2024-02-24 15:08:16 -05:00
Thomas Harte
c56e82207a Extend .ADF support as far as my knowledge currently goes. 2024-02-24 15:07:45 -05:00
Thomas Harte
82abebec6e Add missing #include. 2024-02-23 16:13:26 -05:00
Ryan Carsten Schmidt
60286c3a15 Mention Macintosh 128K and 512K in README. 2024-02-23 06:32:15 -06:00
Thomas Harte
8460fe2118 Flounder around file contents. 2024-02-22 22:19:19 -05:00
Thomas Harte
4b5456c9ba Add Hugo/Nick checks. 2024-02-22 22:19:10 -05:00
Thomas Harte
ddf136556d Add an Archimedes enum, start looking at analysis. 2024-02-22 13:51:44 -05:00
Thomas Harte
4c45f4468e Merge pull request #1335 from TomHarte/ARMDecoding
Add ARM2 operation mapper.
2024-02-22 11:44:03 -05:00
Thomas Harte
73d2acca12 Moderately improve comments. 2024-02-22 11:20:22 -05:00
Thomas Harte
56a5df3783 Do the least possible manual test. 2024-02-22 10:48:19 -05:00
Thomas Harte
d205e538e1 Accept the C++ I'm in; clarify and simplify interface. 2024-02-22 10:16:54 -05:00
Thomas Harte
f9cbec668b Add empty shell for tests. 2024-02-21 15:43:24 -05:00
Thomas Harte
6577f68efc Complete instruction set; consolidate mapper. 2024-02-21 15:32:27 -05:00
Thomas Harte
e986ae2878 Add coprocessor data operations and register transfers. 2024-02-21 15:25:57 -05:00
Thomas Harte
b2696450d5 Bring forwards single data transfers. 2024-02-21 14:51:51 -05:00
Thomas Harte
2bbaf73aa2 Delete was is now duplicated. 2024-02-21 14:18:41 -05:00
Thomas Harte
0fe2c1406b Start mutating towards a form that owns the switch. 2024-02-21 14:17:01 -05:00
Thomas Harte
954d920b9e Extend what's held in the operation enum. 2024-02-20 14:14:18 -05:00
Thomas Harte
57b45076c5 Start dealing with per-instruction fields. 2024-02-17 22:13:51 -05:00
Thomas Harte
d639dc8bcb Hit up some more = default opportunities. 2024-02-17 15:42:31 -05:00
Thomas Harte
9a74ab6a8e Switch to actual mnenomics, temporarily(?) shrink table. 2024-02-17 15:41:57 -05:00
Thomas Harte
4c53414cc3 Merge branch 'master' into ARMDecoding 2024-02-17 08:14:18 -05:00
Thomas Harte
c36288dd6b Merge pull request #1334 from TomHarte/EqualsDefault
Switch to `= default`.
2024-02-17 08:13:53 -05:00
Thomas Harte
bc5727af14 Switch to = default. 2024-02-16 21:50:15 -05:00
Thomas Harte
bd0a15c054 Start working on ARM2 decoding. 2024-02-16 21:36:07 -05:00
Thomas Harte
a758112084 Merge pull request #1333 from TomHarte/DeferredSwitches
Apple II: Apply deferred video actions before getting vapour value.
2024-02-16 10:08:23 -05:00
Thomas Harte
3981f3a874 Merge branch 'master' into DeferredSwitches 2024-02-16 09:19:54 -05:00
Thomas Harte
e8036127fe Add some commentary. 2024-02-16 09:19:22 -05:00
Thomas Harte
17abd87791 Remove further !!s. 2024-02-16 08:57:43 -05:00
Thomas Harte
35545451fe Apply applicable deferred actions before lookahead. 2024-02-16 08:56:01 -05:00
Thomas Harte
64bec0cc3d Merge pull request #1332 from TomHarte/NoPrintf
Trim some printfs.
2024-02-15 21:18:43 -05:00
Thomas Harte
fadd3bc6fc Eliminate 'unused' error. 2024-02-15 13:57:11 -05:00
Thomas Harte
d9ec11c62e Use logger instead of printf. 2024-02-15 13:55:46 -05:00
Thomas Harte
093a029b8c Further reduce printf footprint. 2024-02-15 13:41:35 -05:00
Thomas Harte
b4a3b23571 Eliminate use of printf. 2024-02-15 13:32:49 -05:00
Thomas Harte
be99183f1d Remove outdated TODO. 2024-02-15 13:26:03 -05:00
Thomas Harte
dda5f41487 Merge pull request #1331 from TomHarte/IODeviceSel 2024-02-15 11:16:15 -05:00
Thomas Harte
a09457dab5 Fix IOSEL and DEVSEL assignments. 2024-02-15 10:29:30 -05:00
Thomas Harte
ac171d166e Merge pull request #1321 from TomHarte/Mockingboard
Add Mockingboard support to the Apple II.
2024-02-15 10:24:39 -05:00
Thomas Harte
51de1892c0 With minor infrastructure fixes, switch Mockingboard to stereo. 2024-02-15 09:42:33 -05:00
Thomas Harte
e1fdda928a Add Mockingboard to Qt UI. 2024-02-15 09:13:17 -05:00
Thomas Harte
1c8261dc09 Add Mockingboard to macOS UI. 2024-02-15 09:10:19 -05:00
Thomas Harte
cb22278c7f Switch meaning of bit 2. 2024-02-15 08:54:52 -05:00
Thomas Harte
809bc9d6a8 Add TODO. 2024-02-14 22:46:57 -05:00
Thomas Harte
be11f31d5d Support reset. 2024-02-14 22:22:42 -05:00
Thomas Harte
0103761b7b Corrects AY audio tone. 2024-02-14 22:16:37 -05:00
Thomas Harte
3ac5fdafab Enables AY audio, albeit underclocked. 2024-02-14 22:15:21 -05:00
Thomas Harte
1e877c7563 Add a clock to the 6522s, enable interrupts. 2024-02-14 22:01:03 -05:00
Thomas Harte
07c11e8268 Begin 6522 wiring. 2024-02-14 15:18:19 -05:00
Thomas Harte
0dcceff410 There's actually two AYs. 2024-02-14 14:31:38 -05:00
Thomas Harte
2a684ab302 Include a single AY in the mix if appropriate. 2024-02-14 10:55:53 -05:00
Thomas Harte
27059233b3 Use sample source to simplify stretching AY. 2024-02-13 22:38:18 -05:00
Thomas Harte
7f84d5ac6f Merge branch 'master' into Mockingboard 2024-02-13 14:46:59 -05:00
Thomas Harte
d43f050922 Merge pull request #1330 from TomHarte/SampleProducingAY
Convert AY to a SampleSource.
2024-02-13 14:45:31 -05:00
Thomas Harte
3ba2618547 Fix formatting, add comment. 2024-02-13 13:48:31 -05:00
Thomas Harte
a3e104f8e2 Clean up commentary. 2024-02-13 13:46:27 -05:00
Thomas Harte
1bb82189e9 Add better exposition. 2024-02-13 10:57:22 -05:00
Thomas Harte
e06a66644c Eliminate a macro. 2024-02-13 10:54:53 -05:00
Thomas Harte
6dcc13921f Make first sweep at converting AY to a SampleSource. 2024-02-13 10:51:33 -05:00
Thomas Harte
fd45745600 Merge pull request #1328 from TomHarte/PerSampleAudio
Improve SampleSource infrastructure.
2024-02-12 16:38:01 -05:00
Thomas Harte
507c3da927 Sometimes avoid unnecessary zero-fills. 2024-02-12 14:33:55 -05:00
Thomas Harte
f14e45f93e Remove various instances of ';;'. 2024-02-12 14:23:54 -05:00
Thomas Harte
3d2d9ac45e Remove default set_sample_volume_range implementation too. 2024-02-12 14:00:08 -05:00
Thomas Harte
1895b4ee5d Remove empty implementation, the better for debugging. 2024-02-12 13:59:03 -05:00
Thomas Harte
d49c07687c Unify [get_/skip_]samples, adding a third option for in-place mixing. 2024-02-12 10:55:52 -05:00
Thomas Harte
3a208460e2 Reintroduce mono to stereo conversion. 2024-02-10 21:53:12 -05:00
Thomas Harte
472297e411 Merge pull request #1324 from ryandesign/cmake
Add CMake build system
2024-02-09 14:36:00 -05:00
Thomas Harte
25085cb5af Require good ordering. 2024-02-09 14:34:59 -05:00
Thomas Harte
9909146c59 Fix typo. 2024-02-09 14:26:02 -05:00
Thomas Harte
609d81d75d Distinguish sources of samples and of whole buffers. 2024-02-09 14:25:40 -05:00
Thomas Harte
c105acf1c7 Adopt a full type for stereo samples, gaining + and +=. 2024-02-09 10:48:42 -05:00
Thomas Harte
f3d0827d14 Introduce [Mono/Stereo]Sample types. 2024-02-09 09:15:48 -05:00
Thomas Harte
a4a983eb81 Promote stereo status to template parameter. 2024-02-08 15:21:47 -05:00
Thomas Harte
48be7c677e Avoid inheritance. 2024-02-08 12:07:12 -05:00
Thomas Harte
147d817977 Use fold expression for final-answer is_stereo. 2024-02-08 11:10:08 -05:00
Thomas Harte
d481f335b8 Switch to another std::fill. 2024-02-08 10:47:05 -05:00
Thomas Harte
228012cd0c Make a further deployment of std::fill. 2024-02-01 22:03:13 -05:00
Thomas Harte
f4d8c04f3c Without yet much exposition, draft sample-by-sample interface. 2024-02-01 21:56:33 -05:00
Thomas Harte
c6c9be0b08 Adopt CRTP for SampleSource. 2024-02-01 21:47:44 -05:00
Thomas Harte
3827929a15 Merge branch 'master' into PerSampleAudio 2024-02-01 21:33:25 -05:00
Thomas Harte
ca7e4b3a0e Merge pull request #1327 from ryandesign/macos-ci
Update CI to build on macOS 11, 12, 13, and 14.
2024-02-01 21:33:11 -05:00
Thomas Harte
fd73c24fc3 Use std::fill; update volume with slider. 2024-02-01 21:32:16 -05:00
Thomas Harte
ce0d53b277 Clean up SampleSource's getters. 2024-02-01 21:29:00 -05:00
Ryan Carsten Schmidt
7dc3b5ba06 Update CI to build on macOS 11, 12, 13, and 14. 2024-01-31 06:27:11 -06:00
Thomas Harte
17cad73177 Attempt an implementation of StretchedAudioSource. 2024-01-29 16:45:20 -05:00
Thomas Harte
b28e3eb419 Merge pull request #1326 from ryandesign/MacDown
Compatibility fixes in Markdown files.
2024-01-28 17:35:41 -05:00
Ryan Carsten Schmidt
d811501421 Compatibility fixes in Markdown files.
Improve compatibility with some Markdown readers like MacDown by adding
blank lines before lists. Blank lines around headers were added for
consistency. One header level was fixed. One code block was fixed.
2024-01-27 13:24:35 -06:00
Thomas Harte
c68dd50fde Merge pull request #1323 from ryandesign/BUILD.md
Rename BUILD.txt to BUILD.md and rewrite
2024-01-25 20:38:12 -05:00
Ryan Carsten Schmidt
ad8abf2e05 Add CMake SDL builds to CI workflow. 2024-01-25 10:15:11 -06:00
Ryan Carsten Schmidt
01d9455897 Exclude *AllRAM*.cpp from CMake program sources.
These files serve as documentation and are used in tests.
2024-01-25 10:15:11 -06:00
Ryan Carsten Schmidt
cbf8849004 Add CMake build system, initially for SDL version.
See #1275
2024-01-25 10:15:05 -06:00
Ryan Carsten Schmidt
f3a7d82dc1 Update build instructions with more specifics.
Mention macOS version requirement; see #1179.
2024-01-25 09:10:36 -06:00
Ryan Carsten Schmidt
017674de35 Rename BUILD.txt to BUILD.md. 2024-01-25 08:52:06 -06:00
Thomas Harte
4f211334b5 Merge pull request #1322 from ryandesign/cstring
Add missing include of cstring for memcpy.
2024-01-24 20:53:57 -05:00
Ryan Carsten Schmidt
31e261f7e5 Add missing include of cstring for memcpy. 2024-01-24 09:00:10 -06:00
Thomas Harte
15b5a62e01 Mockingboard: start sketching out intermediate clocking. 2024-01-23 22:05:30 -05:00
Thomas Harte
f5800aa004 Merge pull request #1320 from TomHarte/MoreTemplateElimination
Apply more macro elimination.
2024-01-22 21:46:19 -05:00
Thomas Harte
584aa78695 Avoid macro. 2024-01-22 21:37:04 -05:00
Thomas Harte
030f49db83 Eliminate macro. 2024-01-22 21:33:39 -05:00
Thomas Harte
cc165b65be Switch to lambda. 2024-01-22 21:22:56 -05:00
Thomas Harte
cb125e6336 Use constexpr functions rather than macros. 2024-01-22 21:17:00 -05:00
Thomas Harte
a3337ea90f Remove macro. 2024-01-22 21:15:44 -05:00
Thomas Harte
ae31f85f0c Eliminate macro. 2024-01-22 21:11:15 -05:00
Thomas Harte
5e9f484662 Avoid macro. 2024-01-22 21:09:13 -05:00
Thomas Harte
f2e29357bf Merge pull request #1318 from TomHarte/MorePragmas
Switch trailing files to #pragma once.
2024-01-22 14:00:51 -05:00
Thomas Harte
8a1a14ba4c Switch trailing files to #pragma once. 2024-01-21 21:49:59 -05:00
Thomas Harte
31cbcb206f Commit new version number. 2024-01-21 21:25:27 -05:00
Thomas Harte
bf9e913a7b Merge pull request #1317 from TomHarte/IIgsIncludes
Add missing include of cstdint.
2024-01-21 21:24:43 -05:00
Thomas Harte
d3cea4a10f Note that arguments may be unused. 2024-01-21 21:19:51 -05:00
Thomas Harte
a6df20ff84 Eliminate printf. 2024-01-21 21:19:38 -05:00
Thomas Harte
7122a9ee16 Add missing include of cstdint. 2024-01-21 21:08:04 -05:00
235 changed files with 9155 additions and 1219 deletions

View File

@@ -1,11 +1,11 @@
name: Build
on: [pull_request]
jobs:
build-mac:
name: Mac UI on ${{ matrix.os }}
build-mac-xcodebuild:
name: Mac UI / xcodebuild / ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
os: [macos-12, macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -13,13 +13,49 @@ jobs:
- name: Make
working-directory: OSBindings/Mac
run: xcodebuild CODE_SIGN_IDENTITY=-
build-sdl:
name: SDL UI on ${{ matrix.os }}
build-sdl-cmake:
name: SDL UI / cmake / ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
shell: bash
run: |
case $RUNNER_OS in
Linux)
sudo apt-get --allow-releaseinfo-change update
sudo apt-get --fix-missing install cmake gcc-10 libsdl2-dev
;;
macOS)
brew install cmake sdl2
;;
esac
- name: Make
shell: bash
run: |
case $RUNNER_OS in
Linux)
jobs=$(nproc --all)
;;
macOS)
jobs=$(sysctl -n hw.activecpu)
;;
*)
jobs=1
esac
cmake -S. -Bbuild -DCLK_UI=SDL -DCMAKE_BUILD_TYPE=Release
cmake --build build -v -j"$jobs"
build-sdl-scons:
name: SDL UI / scons / ${{ matrix.os }}
strategy:
matrix:
os: [macos-14, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies

View File

@@ -23,7 +23,7 @@ namespace Activity {
*/
class Observer {
public:
virtual ~Observer() {}
virtual ~Observer() = default;
/// Provides hints as to the sort of information presented on an LED.
enum LEDPresentation: uint8_t {

View File

@@ -61,7 +61,7 @@ void MultiSpeaker::set_output_volume(float volume) {
}
void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) {
auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
auto delegate = delegate_.load(std::memory_order_relaxed);
if(!delegate) return;
{
std::lock_guard lock_guard(front_speaker_mutex_);
@@ -71,7 +71,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec
}
void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) {
auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
auto delegate = delegate_.load(std::memory_order_relaxed);
if(!delegate) return;
{
std::lock_guard lock_guard(front_speaker_mutex_);
@@ -85,7 +85,7 @@ void MultiSpeaker::set_new_front_machine(::Machine::DynamicMachine *machine) {
std::lock_guard lock_guard(front_speaker_mutex_);
front_speaker_ = machine->audio_producer()->get_speaker();
}
auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
auto delegate = delegate_.load(std::memory_order_relaxed);
if(delegate) {
delegate->speaker_did_change_input_clock(this);
}

View File

@@ -17,6 +17,7 @@ enum class Machine {
Atari2600,
AtariST,
Amiga,
Archimedes,
ColecoVision,
Electron,
Enterprise,

View File

@@ -13,6 +13,7 @@
#include "../../../Numeric/CRC.hpp"
#include <algorithm>
#include <cstring>
using namespace Analyser::Static::Acorn;
@@ -86,34 +87,56 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
auto catalogue = std::make_unique<Catalogue>();
Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk);
// Grab the second half of the free-space map because it has the boot option in it.
const Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.sector(0, 0, 1);
if(!free_space_map_second_half) return nullptr;
catalogue->has_large_sectors = free_space_map_second_half->samples[0].size() == 1024;
// Possibility: this is a large-sector disk with an old-style free space map. In which
// case the above just read the start of the root directory.
uint8_t first_directory_sector = 2;
if(catalogue->has_large_sectors && !memcmp(&free_space_map_second_half->samples[0][1], "Hugo", 4)) {
free_space_map_second_half = parser.sector(0, 0, 0);
if(!free_space_map_second_half) return nullptr;
first_directory_sector = 1;
}
std::vector<uint8_t> root_directory;
root_directory.reserve(5 * 256);
for(uint8_t c = 2; c < 7; c++) {
root_directory.reserve(catalogue->has_large_sectors ? 2*1024 : 5*256);
for(uint8_t c = first_directory_sector; c < first_directory_sector + (catalogue->has_large_sectors ? 2 : 5); c++) {
const Storage::Encodings::MFM::Sector *const sector = parser.sector(0, 0, c);
if(!sector) return nullptr;
root_directory.insert(root_directory.end(), sector->samples[0].begin(), sector->samples[0].end());
}
// Quick sanity checks.
if(root_directory[0x4cb]) return nullptr;
if(root_directory[1] != 'H' || root_directory[2] != 'u' || root_directory[3] != 'g' || root_directory[4] != 'o') return nullptr;
if(root_directory[0x4FB] != 'H' || root_directory[0x4FC] != 'u' || root_directory[0x4FD] != 'g' || root_directory[0x4FE] != 'o') return nullptr;
// Check for end of directory marker.
if(root_directory[catalogue->has_large_sectors ? 0x7d7 : 0x4cb]) return nullptr;
switch(free_space_map_second_half->samples[0][0xfd]) {
default: catalogue->bootOption = Catalogue::BootOption::None; break;
case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT; break;
case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT; break;
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
// Check for both directory identifiers.
const uint8_t *const start_id = &root_directory[1];
const uint8_t *const end_id = &root_directory[root_directory.size() - 5];
catalogue->is_hugo = !memcmp(start_id, "Hugo", 4) && !memcmp(end_id, "Hugo", 4);
const bool is_nick = !memcmp(start_id, "Nick", 4) && !memcmp(end_id, "Nick", 4);
if(!catalogue->is_hugo && !is_nick) {
return nullptr;
}
if(!catalogue->has_large_sectors) {
// TODO: I don't know where the boot option rests with large sectors.
switch(free_space_map_second_half->samples[0][0xfd]) {
default: catalogue->bootOption = Catalogue::BootOption::None; break;
case 1: catalogue->bootOption = Catalogue::BootOption::LoadBOOT; break;
case 2: catalogue->bootOption = Catalogue::BootOption::RunBOOT; break;
case 3: catalogue->bootOption = Catalogue::BootOption::ExecBOOT; break;
}
}
// Parse the root directory, at least.
for(std::size_t file_offset = 0x005; file_offset < 0x4cb; file_offset += 0x1a) {
for(std::size_t file_offset = 0x005; file_offset < (catalogue->has_large_sectors ? 0x7d7 : 0x4cb); file_offset += 0x1a) {
// Obtain the name, which will be at most ten characters long, and will
// be terminated by either a NULL character or a \r.
char name[11];
char name[11]{};
std::size_t c = 0;
for(; c < 10; c++) {
const char next = root_directory[file_offset + c] & 0x7f;
@@ -122,8 +145,9 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
}
name[c] = '\0';
// Skip if the name is empty.
if(name[0] == '\0') continue;
// An empty name implies the directory has ended; files are always listed in case-insensitive
// sorted order, with that list being terminated by a '\0'.
if(name[0] == '\0') break;
// Populate a file then.
File new_file;
@@ -177,5 +201,20 @@ std::unique_ptr<Catalogue> Analyser::Static::Acorn::GetADFSCatalogue(const std::
catalogue->files.push_back(std::move(new_file));
}
// Include the directory title.
const char *title, *name;
if(catalogue->has_large_sectors) {
title = reinterpret_cast<const char *>(&root_directory[0x7dd]);
name = reinterpret_cast<const char *>(&root_directory[0x7f0]);
} else {
title = reinterpret_cast<const char *>(&root_directory[0x4d9]);
name = reinterpret_cast<const char *>(&root_directory[0x4cc]);
}
catalogue->name = std::string(title, strnlen(title, 19));
if(catalogue->name.empty() || catalogue->name == "$") {
catalogue->name = std::string(name, strnlen(name, 10));
}
return catalogue;
}

View File

@@ -15,6 +15,8 @@ namespace Analyser::Static::Acorn {
/// Describes a DFS- or ADFS-format catalogue(/directory): the list of files available and the catalogue's boot option.
struct Catalogue {
bool is_hugo = false;
bool has_large_sectors = false;
std::string name;
std::vector<File> files;
enum class BootOption {

View File

@@ -12,7 +12,10 @@
#include "Tape.hpp"
#include "Target.hpp"
#include "../../../Numeric/StringSimilarity.hpp"
#include <algorithm>
#include <map>
using namespace Analyser::Static::Acorn;
@@ -59,13 +62,14 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
return acorn_cartridges;
}
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &, TargetPlatform::IntType) {
auto target = std::make_unique<Target>();
Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType) {
auto target8bit = std::make_unique<ElectronTarget>();
auto targetArchimedes = std::make_unique<ArchimedesTarget>();
// strip out inappropriate cartridges
target->media.cartridges = AcornCartridgesFrom(media.cartridges);
// Copy appropriate cartridges to the 8-bit target.
target8bit->media.cartridges = AcornCartridgesFrom(media.cartridges);
// if there are any tapes, attempt to get data from the first
// If there are any tapes, attempt to get data from the first.
if(!media.tapes.empty()) {
std::shared_ptr<Storage::Tape::Tape> tape = media.tapes.front();
std::vector<File> files = GetFiles(tape);
@@ -94,30 +98,34 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
// Inspect first file. If it's protected or doesn't look like BASIC
// then the loading command is *RUN. Otherwise it's CHAIN"".
target->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
target8bit->loading_command = is_basic ? "CHAIN\"\"\n" : "*RUN\n";
target->media.tapes = media.tapes;
target8bit->media.tapes = media.tapes;
}
}
if(!media.disks.empty()) {
std::shared_ptr<Storage::Disk::Disk> disk = media.disks.front();
std::unique_ptr<Catalogue> dfs_catalogue, adfs_catalogue;
// Get any sort of catalogue that can be found.
dfs_catalogue = GetDFSCatalogue(disk);
if(dfs_catalogue == nullptr) adfs_catalogue = GetADFSCatalogue(disk);
if(dfs_catalogue || adfs_catalogue) {
// 8-bit options: DFS and Hugo-style ADFS.
if(dfs_catalogue || (adfs_catalogue && !adfs_catalogue->has_large_sectors && adfs_catalogue->is_hugo)) {
// Accept the disk and determine whether DFS or ADFS ROMs are implied.
// Use the Pres ADFS if using an ADFS, as it leaves Page at &EOO.
target->media.disks = media.disks;
target->has_dfs = bool(dfs_catalogue);
target->has_pres_adfs = bool(adfs_catalogue);
target8bit->media.disks = media.disks;
target8bit->has_dfs = bool(dfs_catalogue);
target8bit->has_pres_adfs = bool(adfs_catalogue);
// Check whether a simple shift+break will do for loading this disk.
Catalogue::BootOption bootOption = (dfs_catalogue ?: adfs_catalogue)->bootOption;
if(bootOption != Catalogue::BootOption::None) {
target->should_shift_restart = true;
target8bit->should_shift_restart = true;
} else {
target->loading_command = "*CAT\n";
target8bit->loading_command = "*CAT\n";
}
// Check whether adding the AP6 ROM is justified.
@@ -133,39 +141,79 @@ Analyser::Static::TargetList Analyser::Static::Acorn::GetTargets(const Media &me
"VERIFY", "ZERO"
}) {
if(std::search(file.data.begin(), file.data.end(), command, command+strlen(command)) != file.data.end()) {
target->has_ap6_rom = true;
target->has_sideways_ram = true;
target8bit->has_ap6_rom = true;
target8bit->has_sideways_ram = true;
}
}
}
} else if(adfs_catalogue) {
// Archimedes options, implicitly: ADFS, non-Hugo.
targetArchimedes->media.disks = media.disks;
// Also look for the best possible startup program name, if it can be discerned.
std::multimap<double, std::string, std::greater<double>> options;
for(const auto &file: adfs_catalogue->files) {
// Skip non-Pling files.
if(file.name[0] != '!') continue;
// Take whatever else comes with a preference for things that don't
// have 'boot' or 'read' in them (the latter of which will tend to be
// read_me or read_this or similar).
constexpr char read[] = "read";
constexpr char boot[] = "boot";
const auto has = [&](const char *begin, const char *end) {
return std::search(
file.name.begin(), file.name.end(),
begin, end - 1, // i.e. don't compare the trailing NULL.
[](char lhs, char rhs) {
return std::tolower(lhs) == rhs;
}
) != file.name.end();
};
const auto has_read = has(std::begin(read), std::end(read));
const auto has_boot = has(std::begin(boot), std::end(boot));
const auto probability =
Numeric::similarity(file.name, adfs_catalogue->name) +
Numeric::similarity(file.name, file_name) -
((has_read || has_boot) ? 0.2 : 0.0);
options.emplace(probability, file.name);
}
if(!options.empty()) {
targetArchimedes->main_program = options.begin()->second;
}
}
}
// Enable the Acorn ADFS if a mass-storage device is attached;
// unlike the Pres ADFS it retains SCSI logic.
if(!media.mass_storage_devices.empty()) {
target->has_pres_adfs = false; // To override a floppy selection, if one was made.
target->has_acorn_adfs = true;
target8bit->has_pres_adfs = false; // To override a floppy selection, if one was made.
target8bit->has_acorn_adfs = true;
// Assume some sort of later-era Acorn work is likely to happen;
// so ensure *TYPE, etc are present.
target->has_ap6_rom = true;
target->has_sideways_ram = true;
target8bit->has_ap6_rom = true;
target8bit->has_sideways_ram = true;
target->media.mass_storage_devices = media.mass_storage_devices;
target8bit->media.mass_storage_devices = media.mass_storage_devices;
// Check for a boot option.
const auto sector = target->media.mass_storage_devices.front()->get_block(1);
const auto sector = target8bit->media.mass_storage_devices.front()->get_block(1);
if(sector[0xfd]) {
target->should_shift_restart = true;
target8bit->should_shift_restart = true;
} else {
target->loading_command = "*CAT\n";
target8bit->loading_command = "*CAT\n";
}
}
TargetList targets;
if(!target->media.empty()) {
targets.push_back(std::move(target));
if(!target8bit->media.empty()) {
targets.push_back(std::move(target8bit));
}
if(!targetArchimedes->media.empty()) {
targets.push_back(std::move(targetArchimedes));
}
return targets;
}

View File

@@ -19,8 +19,10 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::
auto new_chunk = std::make_unique<File::Chunk>();
int shift_register = 0;
// TODO: move this into the parser
#define shift() shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9)
// TODO: move this into the parser
const auto shift = [&] {
shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9);
};
// find next area of high tone
while(!tape->is_at_end() && (shift_register != 0x3ff)) {
@@ -32,8 +34,6 @@ static std::unique_ptr<File::Chunk> GetNextChunk(const std::shared_ptr<Storage::
shift();
}
#undef shift
parser.reset_crc();
parser.reset_error_flag();

View File

@@ -14,7 +14,7 @@
namespace Analyser::Static::Acorn {
struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<Target> {
struct ElectronTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<ElectronTarget> {
bool has_acorn_adfs = false;
bool has_pres_adfs = false;
bool has_dfs = false;
@@ -23,7 +23,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<
bool should_shift_restart = false;
std::string loading_command;
Target() : Analyser::Static::Target(Machine::Electron) {
ElectronTarget() : Analyser::Static::Target(Machine::Electron) {
if(needs_declare()) {
DeclareField(has_pres_adfs);
DeclareField(has_acorn_adfs);
@@ -34,4 +34,10 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<
}
};
struct ArchimedesTarget: public ::Analyser::Static::Target, public Reflection::StructImpl<ArchimedesTarget> {
std::string main_program;
ArchimedesTarget() : Analyser::Static::Target(Machine::Archimedes) {}
};
}

View File

@@ -34,12 +34,14 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
Model model = Model::IIe;
DiskController disk_controller = DiskController::None;
SCSIController scsi_controller = SCSIController::None;
bool has_mockingboard = true;
Target() : Analyser::Static::Target(Machine::AppleII) {
if(needs_declare()) {
DeclareField(model);
DeclareField(disk_controller);
DeclareField(scsi_controller);
DeclareField(has_mockingboard);
AnnounceEnum(Model);
AnnounceEnum(DiskController);
@@ -48,4 +50,8 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl<Ta
}
};
constexpr bool is_iie(Target::Model model) {
return model == Target::Model::IIe || model == Target::Model::EnhancedIIe;
}
}

View File

@@ -38,12 +38,15 @@ struct Media {
}
Media &operator +=(const Media &rhs) {
#define append(name) name.insert(name.end(), rhs.name.begin(), rhs.name.end());
append(disks);
append(tapes);
append(cartridges);
append(mass_storage_devices);
#undef append
const auto append = [&](auto &destination, auto &source) {
destination.insert(destination.end(), source.begin(), source.end());
};
append(disks, rhs.disks);
append(tapes, rhs.tapes);
append(cartridges, rhs.cartridges);
append(mass_storage_devices, rhs.mass_storage_devices);
return *this;
}
};
@@ -54,7 +57,7 @@ struct Media {
*/
struct Target {
Target(Machine machine) : machine(machine) {}
virtual ~Target() {}
virtual ~Target() = default;
// This field is entirely optional.
std::unique_ptr<Reflection::Struct> state;

99
BUILD.md Normal file
View File

@@ -0,0 +1,99 @@
![Clock Signal Application Icon](READMEImages/Icon.png)
# Building Clock Signal
Clock Signal is available as [a macOS native application using
Metal](#macos-app) or as [a cross-platform command-line-driven SDL executable
using OpenGL](#sdl-app).
## macOS app
The macOS native application requires a Metal-capable Mac running macOS 10.13 or
later and has no prerequisites beyond the normal system libraries. It can be
built [using Xcode](#building-the-macos-app-using-xcode) or on the command line
[using `xcodebuild`](#building-the-macos-app-using-xcodebuild).
Machine ROMs are intended to be built into the application bundle; populate the
dummy folders below ROMImages before building.
The Xcode project is configured to sign the application using the developer's
certificate, but if you are not the developer then you will get a "No signing
certificate" error. To avoid this, you'll specify that you want to sign the
application to run locally.
### Building the macOS app using Xcode
Open the Clock Signal Xcode project in OSBindings/Mac.
To avoid signing errors, edit the project, select the Signing & Capabilities
tab, and change the Signing Certificate drop-down menu from "Development" to
"Sign to Run Locally".
To avoid crashes when running Clock Signal via Xcode on older Macs due to
"unrecognized selector sent to instance" errors, edit the scheme, and in the Run
section, scroll down to the Metal heading and uncheck the "API Validation"
checkbox.
To build, choose "Build" from Xcode's Product menu or press
<kbd>Command</kbd> + <kbd>B</kbd>.
To build and run, choose "Run" from the Product menu or press
<kbd>Command</kbd> + <kbd>R</kbd>.
To see the folder where the Clock Signal application was built, choose "Show
Build Folder in Finder" from the Product menu. Look in the "Products" folder for
a folder named after the configuration (e.g. "Debug" or "Release").
### Building the macOS app using `xcodebuild`
To build, change to the OSBindings/Mac directory in the Terminal, then run
`xcodebuild`, specifying `-` as the code sign identity to sign the application
to run locally to avoid signing errors:
cd OSBindings/Mac
xcodebuild CODE_SIGN_IDENTITY=-
`xcodebuild` will create a "build" folder in this directory which is where you
can find the Clock Signal application after it's compiled, in a directory named
after the configuration (e.g. "Debug" or "Release").
## SDL app
The SDL app can be built on Linux, BSD, macOS, and other Unix-like operating
systems. Prerequisites are SDL 2, ZLib and OpenGL (or Mesa). OpenGL 3.2 or
better is required at runtime. It can be built [using
SCons](#building-the-sdl-app-using-scons).
### Building the SDL app using SCons
To build, change to the OSBindings/SDL directory and run `scons`. You can add a
`-j` flag to build in parallel. For example, if you have 8 processor cores:
cd OSBindings/SDL
scons -j8
The `clksignal` executable will be created in this directory. You can run it
from here or install it by copying it where you want it, for example:
cp clksignal /usr/local/bin
To start an emulator with a particular disk image `file`, if you've installed
`clksignal` to a directory in your `PATH`, run:
clksignal file
Or if you're running it from the current directory:
./clksignal file
Other options are availble. Run `clksignal` or `./clksignal` with no arguments
to learn more.
Setting up `clksignal` as the associated program for supported file types in
your favoured filesystem browser is recommended; it has no file navigation
abilities of its own.
Some emulated systems require the provision of original machine ROMs. These are
not included and may be located in either /usr/local/share/CLK/ or
/usr/share/CLK/. You will be prompted for them if they are found to be missing.
The structure should mirror that under OSBindings in the source archive; see the
readme.txt in each folder to determine the proper files and names ahead of time.

View File

@@ -1,30 +0,0 @@
Linux, BSD
==========
Prerequisites are SDL 2, ZLib and OpenGL (or Mesa), and SCons for the provided build script. OpenGL 3.2 or better is required at runtime.
Build:
cd OSBindings/SDL
scons
Optionally:
cp clksignal /usr/bin
To launch:
clksignal file
Setting up clksignal as the associated program for supported file types in your favoured filesystem browser is recommended; it has no file navigation abilities of its own.
Some emulated systems require the provision of original machine ROMs. These are not included and may be located in either /usr/local/share/CLK/ or /usr/share/CLK/. You will be prompted for them if they are found to be missing. The structure should mirror that under OSBindings in the source archive; see the readme.txt in each folder to determine the proper files and names ahead of time.
macOS
=====
There are no prerequisites beyond the normal system libraries; the macOS build is a native Cocoa application.
Build: open the Xcode project in OSBindings/Mac and press command+b.
Machine ROMs are intended to be built into the application bundle; populate the dummy folders below ROMImages before building.

70
CMakeLists.txt Normal file
View File

@@ -0,0 +1,70 @@
cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
project(CLK
LANGUAGES CXX
VERSION 24.01.22
)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CLK_UIS "SDL")
#list(PREPEND CLK_UIS "QT")
#if(APPLE)
# list(PREPEND CLK_UIS "MAC")
# set(CLK_DEFAULT_UI "MAC")
#else()
set(CLK_DEFAULT_UI "SDL")
#endif()
set(CLK_UI ${CLK_DEFAULT_UI} CACHE STRING "User interface")
set_property(CACHE CLK_UI PROPERTY STRINGS ${CLK_UIS})
if(NOT CLK_UI IN_LIST CLK_UIS)
list(JOIN CLK_UIS ", " CLK_UIS_PRETTY)
message(FATAL_ERROR "Invalid value for 'CLK_UI'; must be one of ${CLK_UIS_PRETTY}")
endif()
message(STATUS "Configuring for ${CLK_UI} UI")
list(PREPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include("CLK_SOURCES")
add_executable(clksignal ${CLK_SOURCES})
if(MSVC)
target_compile_options(clksignal PRIVATE /W4)
else()
# TODO: Add -Wpedandic.
target_compile_options(clksignal PRIVATE -Wall -Wextra)
endif()
find_package(ZLIB REQUIRED)
target_link_libraries(clksignal PRIVATE ZLIB::ZLIB)
if(CLK_UI STREQUAL "MAC")
enable_language(OBJC OBJCXX SWIFT)
# TODO: Build the Mac version.
else()
find_package(OpenGL REQUIRED)
target_link_libraries(clksignal PRIVATE OpenGL::GL)
if(APPLE)
target_compile_definitions(clksignal PRIVATE "GL_SILENCE_DEPRECATION" "IGNORE_APPLE")
endif()
endif()
if(CLK_UI STREQUAL "QT")
# TODO: Build the Qt version.
elseif(APPLE)
set(BLA_VENDOR Apple)
find_package(BLAS REQUIRED)
target_link_libraries(clksignal PRIVATE BLAS::BLAS)
endif()
if(CLK_UI STREQUAL "SDL")
find_package(SDL2 REQUIRED CONFIG REQUIRED COMPONENTS SDL2)
target_link_libraries(clksignal PRIVATE SDL2::SDL2)
endif()
# TODO: Investigate building on Windows.

View File

@@ -73,6 +73,11 @@ template <typename TimeUnit> class DeferredQueue {
}
}
/// @returns @c true if no actions are enqueued; @c false otherwise.
bool empty() const {
return pending_actions_.empty();
}
private:
// The list of deferred actions.
struct DeferredAction {

View File

@@ -30,7 +30,7 @@ class WD1770: public Storage::Disk::MFMController {
@param p The type of controller to emulate.
*/
WD1770(Personality p);
virtual ~WD1770() {}
virtual ~WD1770() = default;
/// Sets the value of the double-density input; when @c is_double_density is @c true, reads and writes double-density format data.
using Storage::Disk::MFMController::set_is_double_density;

View File

@@ -68,7 +68,7 @@ class IRQDelegatePortHandler: public PortHandler {
/// Sets the delegate that will receive notification of changes in the interrupt line.
void set_interrupt_delegate(Delegate *delegate);
/// Overrides PortHandler::set_interrupt_status, notifying the delegate if one is set.
/// Overrides @c PortHandler::set_interrupt_status, notifying the delegate if one is set.
void set_interrupt_status(bool new_status);
private:

View File

@@ -82,6 +82,8 @@ template <typename PortHandlerT, Personality personality> class MOS6526:
void advance_counters(int);
bool serial_line_did_produce_bit(Serial::Line<true> *line, int bit) final;
Log::Logger<Log::Source::MOS6526> log;
};
}

View File

@@ -8,9 +8,6 @@
#pragma once
#include <cassert>
#include <cstdio>
namespace MOS::MOS6526 {
enum Interrupts: uint8_t {
@@ -132,13 +129,11 @@ void MOS6526<BusHandlerT, personality>::write(int address, uint8_t value) {
// Shift control.
case 12:
printf("TODO: write to shift register\n");
log.error().append("TODO: write to shift register");
break;
default:
printf("Unhandled 6526 write: %02x to %d\n", value, address);
assert(false);
break;
// Logically unreachable.
default: break;
}
}
@@ -179,10 +174,8 @@ uint8_t MOS6526<BusHandlerT, personality>::read(int address) {
// Shift register.
case 12: return shift_data_;
default:
printf("Unhandled 6526 read from %d\n", address);
assert(false);
break;
// Logically unreachable.
default: break;
}
return 0xff;
}

View File

@@ -11,6 +11,7 @@
#include <array>
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Outputs/Log.hpp"
namespace MOS::MOS6526 {
@@ -220,7 +221,8 @@ struct MOS6526Storage {
control = v;
if(v&2) {
printf("UNIMPLEMENTED: PB strobe\n");
Log::Logger<Log::Source::MOS6526> log;
log.error().append("UNIMPLEMENTED: PB strobe");
}
}

View File

@@ -19,6 +19,7 @@ AudioGenerator::AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue)
void AudioGenerator::set_volume(uint8_t volume) {
audio_queue_.enqueue([this, volume]() {
volume_ = int16_t(volume) * range_multiplier_;
dc_offset_ = volume_ >> 4;
});
}
@@ -105,7 +106,8 @@ static uint8_t noise_pattern[] = {
// testing against 0x80. The effect should be the same: loading with 0x7f means an output update every cycle, loading with 0x7e
// means every second cycle, etc.
void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target) {
template <Outputs::Speaker::Action action>
void AudioGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
for(unsigned int c = 0; c < number_of_samples; ++c) {
update(0, 2, shift);
update(1, 1, shift);
@@ -114,23 +116,22 @@ void AudioGenerator::get_samples(std::size_t number_of_samples, int16_t *target)
// this sums the output of all three sounds channels plus a DC offset for volume;
// TODO: what's the real ratio of this stuff?
target[c] = int16_t(
const int16_t sample =
(shift_registers_[0]&1) +
(shift_registers_[1]&1) +
(shift_registers_[2]&1) +
((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1)
) * volume_ + (volume_ >> 4);
}
}
((noise_pattern[shift_registers_[3] >> 3] >> (shift_registers_[3]&7))&(control_registers_[3] >> 7)&1);
void AudioGenerator::skip_samples(std::size_t number_of_samples) {
for(unsigned int c = 0; c < number_of_samples; ++c) {
update(0, 2, shift);
update(1, 1, shift);
update(2, 0, shift);
update(3, 1, increment);
Outputs::Speaker::apply<action>(
target[c],
Outputs::Speaker::MonoSample(
sample * volume_ + dc_offset_
));
}
}
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void AudioGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void AudioGenerator::set_sample_volume_range(std::int16_t range) {
range_multiplier_ = int16_t(range / 64);

View File

@@ -12,12 +12,12 @@
#include "../../Concurrency/AsyncTaskQueue.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
namespace MOS::MOS6560 {
// audio state
class AudioGenerator: public ::Outputs::Speaker::SampleSource {
class AudioGenerator: public Outputs::Speaker::BufferSource<AudioGenerator, false> {
public:
AudioGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
@@ -25,10 +25,9 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
void set_control(int channel, uint8_t value);
// For ::SampleSource.
void get_samples(std::size_t number_of_samples, int16_t *target);
void skip_samples(std::size_t number_of_samples);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;
@@ -37,6 +36,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
unsigned int shift_registers_[4] = {0, 0, 0, 0};
uint8_t control_registers_[4] = {0, 0, 0, 0};
int16_t volume_ = 0;
int16_t dc_offset_ = 0;
int16_t range_multiplier_ = 1;
};
@@ -177,16 +177,16 @@ template <class BusHandler> class MOS6560 {
int previous_vertical_counter = vertical_counter_;
// keep track of internal time relative to this scanline
horizontal_counter_++;
++horizontal_counter_;
if(horizontal_counter_ == timing_.cycles_per_line) {
if(horizontal_drawing_latch_) {
current_character_row_++;
++current_character_row_;
if(
(current_character_row_ == 16) ||
(current_character_row_ == 8 && !registers_.tall_characters)
) {
current_character_row_ = 0;
current_row_++;
++current_row_;
}
pixel_line_cycle_ = -1;
@@ -198,7 +198,7 @@ template <class BusHandler> class MOS6560 {
if(output_mode_ == OutputMode::PAL) is_odd_line_ ^= true;
horizontal_drawing_latch_ = false;
vertical_counter_ ++;
++vertical_counter_;
if(vertical_counter_ == lines_this_field()) {
vertical_counter_ = 0;
@@ -215,7 +215,7 @@ template <class BusHandler> class MOS6560 {
vertical_drawing_latch_ |= registers_.first_row_location == (previous_vertical_counter >> 1);
horizontal_drawing_latch_ |= vertical_drawing_latch_ && (horizontal_counter_ == registers_.first_column_location);
if(pixel_line_cycle_ >= 0) pixel_line_cycle_++;
if(pixel_line_cycle_ >= 0) ++pixel_line_cycle_;
switch(pixel_line_cycle_) {
case -1:
if(horizontal_drawing_latch_) {
@@ -234,7 +234,7 @@ template <class BusHandler> class MOS6560 {
fetch_address = registers_.character_cell_start_address + (character_code_*(registers_.tall_characters ? 16 : 8)) + current_character_row_;
} else {
fetch_address = uint16_t(registers_.video_matrix_start_address + video_matrix_address_counter_);
video_matrix_address_counter_++;
++video_matrix_address_counter_;
if(
(current_character_row_ == 15) ||
(current_character_row_ == 7 && !registers_.tall_characters)
@@ -254,10 +254,11 @@ template <class BusHandler> class MOS6560 {
// divide the byte it is set for 3:1 and then continue as usual.
// determine output state; colour burst and sync timing are currently a guess
if(horizontal_counter_ > timing_.cycles_per_line-4) this_state_ = State::ColourBurst;
else if(horizontal_counter_ > timing_.cycles_per_line-7) this_state_ = State::Sync;
State this_state;
if(horizontal_counter_ > timing_.cycles_per_line-4) this_state = State::ColourBurst;
else if(horizontal_counter_ > timing_.cycles_per_line-7) this_state = State::Sync;
else {
this_state_ = (column_counter_ >= 0 && column_counter_ < columns_this_line_*2) ? State::Pixels : State::Border;
this_state = (column_counter_ >= 0 && column_counter_ < columns_this_line_*2) ? State::Pixels : State::Border;
}
// apply vertical sync
@@ -270,17 +271,17 @@ template <class BusHandler> class MOS6560 {
(vertical_counter_ == 3 && horizontal_counter_ <= 32)
)
))
this_state_ = State::Sync;
this_state = State::Sync;
// update the CRT
if(this_state_ != output_state_) {
if(this_state != output_state_) {
switch(output_state_) {
case State::Sync: crt_.output_sync(cycles_in_state_ * 4); break;
case State::ColourBurst: crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
case State::Border: crt_.output_level<uint16_t>(cycles_in_state_ * 4, registers_.borderColour); break;
case State::Border: crt_.output_level<uint16_t>(cycles_in_state_ * 4, registers_.border_colour); break;
case State::Pixels: crt_.output_data(cycles_in_state_ * 4); break;
}
output_state_ = this_state_;
output_state_ = this_state;
cycles_in_state_ = 0;
pixel_pointer = nullptr;
@@ -288,9 +289,9 @@ template <class BusHandler> class MOS6560 {
pixel_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(260));
}
}
cycles_in_state_++;
++cycles_in_state_;
if(this_state_ == State::Pixels) {
if(output_state_ == State::Pixels) {
// TODO: palette changes can happen within half-characters; the below needs to be divided.
// Also: a perfect opportunity to rearrange this inner loop for no longer needing to be
// two parts with a cooperative owner?
@@ -303,9 +304,9 @@ template <class BusHandler> class MOS6560 {
uint16_t colours[2];
if(registers_.invertedCells) {
colours[0] = cell_colour;
colours[1] = registers_.backgroundColour;
colours[1] = registers_.background_colour;
} else {
colours[0] = registers_.backgroundColour;
colours[0] = registers_.background_colour;
colours[1] = cell_colour;
}
pixel_pointer[0] = colours[(character_value_ >> 7)&1];
@@ -317,7 +318,7 @@ template <class BusHandler> class MOS6560 {
pixel_pointer[6] = colours[(character_value_ >> 1)&1];
pixel_pointer[7] = colours[(character_value_ >> 0)&1];
} else {
uint16_t colours[4] = {registers_.backgroundColour, registers_.borderColour, cell_colour, registers_.auxiliary_colour};
uint16_t colours[4] = {registers_.background_colour, registers_.border_colour, cell_colour, registers_.auxiliary_colour};
pixel_pointer[0] =
pixel_pointer[1] = colours[(character_value_ >> 6)&3];
pixel_pointer[2] =
@@ -338,7 +339,7 @@ template <class BusHandler> class MOS6560 {
// Keep counting columns even if sync or the colour burst have interceded.
if(column_counter_ >= 0 && column_counter_ < columns_this_line_*2) {
column_counter_++;
++column_counter_;
}
}
}
@@ -397,14 +398,16 @@ template <class BusHandler> class MOS6560 {
break;
case 0xf: {
uint16_t new_border_colour = colours_[value & 0x07];
if(this_state_ == State::Border && new_border_colour != registers_.borderColour) {
crt_.output_level<uint16_t>(cycles_in_state_ * 4, registers_.borderColour);
cycles_in_state_ = 0;
const uint16_t new_border_colour = colours_[value & 0x07];
if(new_border_colour != registers_.border_colour) {
if(output_state_ == State::Border) {
crt_.output_level<uint16_t>(cycles_in_state_ * 4, registers_.border_colour);
cycles_in_state_ = 0;
}
registers_.border_colour = new_border_colour;
}
registers_.invertedCells = !((value >> 3)&1);
registers_.borderColour = new_border_colour;
registers_.backgroundColour = colours_[value >> 4];
registers_.background_colour = colours_[value >> 4];
}
break;
@@ -446,16 +449,18 @@ template <class BusHandler> class MOS6560 {
uint8_t first_column_location = 0, first_row_location = 0;
uint8_t number_of_columns = 0, number_of_rows = 0;
uint16_t character_cell_start_address = 0, video_matrix_start_address = 0;
uint16_t backgroundColour = 0, borderColour = 0, auxiliary_colour = 0;
uint16_t border_colour = 0;
uint16_t background_colour = 0;
uint16_t auxiliary_colour = 0;
bool invertedCells = false;
uint8_t direct_values[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
uint8_t direct_values[16]{};
} registers_;
// output state
enum State {
Sync, ColourBurst, Border, Pixels
} this_state_ = State::Sync, output_state_ = State::Sync;
} output_state_ = State::Sync;
int cycles_in_state_ = 0;
// counters that cover an entire field

View File

@@ -21,7 +21,7 @@ namespace Intel::i8272 {
class BusHandler {
public:
virtual ~BusHandler() {}
virtual ~BusHandler() = default;
virtual void set_dma_data_request([[maybe_unused]] bool drq) {}
virtual void set_interrupt([[maybe_unused]] bool irq) {}
};

View File

@@ -163,7 +163,7 @@ void Base<personality>::posit_sprite(int sprite_number, int sprite_position, uin
}
const auto sprite_row = uint8_t(screen_row - sprite_position);
if(sprite_row < 0 || sprite_row >= sprite_height_) return;
if(sprite_row >= sprite_height_) return; // The less-than-zero case is dealt with by the cast to unsigned.
if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) {
status_ |= StatusSpriteOverflow;

View File

@@ -38,6 +38,7 @@ constexpr size_t memory_size(Personality p) {
case TI::TMS::V9938: return 128 * 1024;
case TI::TMS::V9958: return 192 * 1024;
}
return 0;
}
constexpr size_t memory_mask(Personality p) {

View File

@@ -59,7 +59,7 @@ struct YamahaFetcher {
type(type),
id(id) {}
constexpr Event() noexcept {}
constexpr Event() noexcept = default;
};
// State that tracks fetching position within a line.

View File

@@ -132,7 +132,7 @@ struct Command {
CommandContext &context;
ModeDescription &mode_description;
Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {}
virtual ~Command() {}
virtual ~Command() = default;
/// @returns @c true if all output from this command is done; @c false otherwise.
virtual bool done() = 0;

View File

@@ -10,17 +10,24 @@
#include "AY38910.hpp"
//namespace GI {
//namespace AY38910 {
using namespace GI::AY38910;
// Note on dividers: the real AY has a built-in divider of 8
// prior to applying its tone and noise dividers. But the YM fills the
// same total periods for noise and tone with double-precision envelopes.
// Therefore this class implements a divider of 4 and doubles the tone
// and noise periods. The envelope ticks along at the divide-by-four rate,
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
// matching the YM datasheet's depiction of envelope level 31 as equal to
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
template <bool is_stereo>
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
AY38910SampleSource<is_stereo>::AY38910SampleSource(Personality personality, Concurrency::AsyncTaskQueue<false> &task_queue) : task_queue_(task_queue) {
// Don't use the low bit of the envelope position if this is an AY.
envelope_position_mask_ |= personality == Personality::AY38910;
// Set up envelope lookup tables.
// Set up envelope lookup tables; these are based on 32 volume levels as used by the YM2149F.
// The AY38910 will just use only even table entries, and therefore only even volumes.
for(int c = 0; c < 16; c++) {
for(int p = 0; p < 64; p++) {
switch(c) {
@@ -74,7 +81,8 @@ AY38910<is_stereo>::AY38910(Personality personality, Concurrency::AsyncTaskQueue
set_sample_volume_range(0);
}
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_sample_volume_range(std::int16_t range) {
// Set up volume lookup table; the function below is based on a combination of the graph
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
// values reported elsewhere.
@@ -92,7 +100,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::
evaluate_output_volume();
}
template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
a_left_ = uint8_t(a_left * 255.0f);
b_left_ = uint8_t(b_left * 255.0f);
c_left_ = uint8_t(c_left * 255.0f);
@@ -101,78 +110,50 @@ template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_lef
c_right_ = uint8_t(c_right * 255.0f);
}
template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
// Note on structure below: the real AY has a built-in divider of 8
// prior to applying its tone and noise dividers. But the YM fills the
// same total periods for noise and tone with double-precision envelopes.
// Therefore this class implements a divider of 4 and doubles the tone
// and noise periods. The envelope ticks along at the divide-by-four rate,
// but if this is an AY rather than a YM then its lowest bit is forced to 1,
// matching the YM datasheet's depiction of envelope level 31 as equal to
// programmatic volume 15, envelope level 29 as equal to programmatic 14, etc.
std::size_t c = 0;
while((master_divider_&3) && c < number_of_samples) {
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
master_divider_++;
c++;
}
while(c < number_of_samples) {
#define step_channel(c) \
if(tone_counters_[c]) tone_counters_[c]--;\
else {\
tone_outputs_[c] ^= 1;\
tone_counters_[c] = tone_periods_[c] << 1;\
}
// Update the tone channels.
step_channel(0);
step_channel(1);
step_channel(2);
#undef step_channel
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
// it into the official 17 upon divider underflow.
if(noise_counter_) noise_counter_--;
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::advance() {
const auto step_channel = [&](int c) {
if(tone_counters_[c]) --tone_counters_[c];
else {
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
noise_output_ ^= noise_shift_register_&1;
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
noise_shift_register_ >>= 1;
tone_outputs_[c] ^= 1;
tone_counters_[c] = tone_periods_[c] << 1;
}
};
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
// implementing non-repeating patterns by locking them to the final table position.
if(envelope_divider_) envelope_divider_--;
else {
envelope_divider_ = envelope_period_;
envelope_position_ ++;
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
}
// Update the tone channels.
step_channel(0);
step_channel(1);
step_channel(2);
evaluate_output_volume();
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
c++;
master_divider_++;
}
// Update the noise generator. This recomputes the new bit repeatedly but harmlessly, only shifting
// it into the official 17 upon divider underflow.
if(noise_counter_) --noise_counter_;
else {
noise_counter_ = noise_period_ << 1; // To cover the double resolution of envelopes.
noise_output_ ^= noise_shift_register_&1;
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
noise_shift_register_ >>= 1;
}
master_divider_ &= 3;
// Update the envelope generator. Table based for pattern lookup, with a 'refill' step: a way of
// implementing non-repeating patterns by locking them to the final table position.
if(envelope_divider_) --envelope_divider_;
else {
envelope_divider_ = envelope_period_ << 1;
++envelope_position_;
if(envelope_position_ == 64) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
}
evaluate_output_volume();
}
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
template <bool is_stereo>
typename Outputs::Speaker::SampleT<is_stereo>::type AY38910SampleSource<is_stereo>::level() const {
return output_volume_;
}
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::evaluate_output_volume() {
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
// The output level for a channel is:
@@ -214,19 +195,18 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
// Mix additively, weighting if in stereo.
if constexpr (is_stereo) {
int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_);
output_volumes[0] = int16_t((
output_volume_.left = int16_t((
volumes_[volumes[0]] * channel_levels[0] * a_left_ +
volumes_[volumes[1]] * channel_levels[1] * b_left_ +
volumes_[volumes[2]] * channel_levels[2] * c_left_
) >> 8);
output_volumes[1] = int16_t((
output_volume_.right = int16_t((
volumes_[volumes[0]] * channel_levels[0] * a_right_ +
volumes_[volumes[1]] * channel_levels[1] * b_right_ +
volumes_[volumes[2]] * channel_levels[2] * c_right_
) >> 8);
} else {
output_volume_ = uint32_t(
output_volume_ = int16_t(
volumes_[volumes[0]] * channel_levels[0] +
volumes_[volumes[1]] * channel_levels[1] +
volumes_[volumes[2]] * channel_levels[2]
@@ -234,18 +214,21 @@ template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
}
}
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() const {
template <bool is_stereo>
bool AY38910SampleSource<is_stereo>::is_zero_level() const {
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
}
// MARK: - Register manipulation
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::select_register(uint8_t r) {
selected_register_ = r;
}
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_register_value(uint8_t value) {
// There are only 16 registers.
if(selected_register_ > 15) return;
@@ -314,7 +297,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t va
if(update_port_a) set_port_output(false);
}
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_register_value() {
// This table ensures that bits that aren't defined within the AY are returned as 0s
// when read, conforming to CPC-sourced unit tests.
const uint8_t register_masks[16] = {
@@ -328,24 +312,28 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
// MARK: - Port querying
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_port_output(bool port_b) {
return registers_[port_b ? 15 : 14];
}
// MARK: - Bus handling
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_port_handler(PortHandler *handler) {
port_handler_ = handler;
set_port_output(true);
set_port_output(false);
}
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_data_input(uint8_t r) {
data_input_ = r;
update_bus();
}
template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_port_output(bool port_b) {
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
// so that when in the "input" mode, all pins will read normally high". Therefore,
// report programmer selection of input mode as creating an output of 0xff.
@@ -355,7 +343,8 @@ template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b)
}
}
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
template <bool is_stereo>
uint8_t AY38910SampleSource<is_stereo>::get_data_output() {
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
// value returned to the CPU when reading it is the and of the output value and any input.
@@ -371,7 +360,8 @@ template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
return data_output_;
}
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_control_lines(ControlLines control_lines) {
switch(int(control_lines)) {
default: control_state_ = Inactive; break;
@@ -386,7 +376,32 @@ template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLine
update_bus();
}
template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::set_reset(bool active) {
if(active == reset_) return;
reset_ = active;
// Reset upon the leading edge; TODO: is this right?
if(reset_) {
reset();
}
}
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::reset() {
// TODO: the below is a guess. Look up real answers.
selected_register_ = 0;
std::fill(registers_, registers_ + 16, 0);
task_queue_.enqueue([&] {
std::fill(output_registers_, output_registers_ + 16, 0);
evaluate_output_volume();
});
}
template <bool is_stereo>
void AY38910SampleSource<is_stereo>::update_bus() {
// Assume no output, unless this turns out to be a read.
data_output_ = 0xff;
switch(control_state_) {
@@ -398,5 +413,10 @@ template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
}
// Ensure both mono and stereo versions of the AY are built.
template class GI::AY38910::AY38910<true>;
template class GI::AY38910::AY38910<false>;
template class GI::AY38910::AY38910SampleSource<true>;
template class GI::AY38910::AY38910SampleSource<false>;
// Perform an explicit instantiation of the BufferSource to hope for
// appropriate inlining of advance() and level().
template struct GI::AY38910::AY38910<true>;
template struct GI::AY38910::AY38910<false>;

View File

@@ -8,7 +8,7 @@
#pragma once
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
#include "../../Reflection/Struct.hpp"
@@ -66,10 +66,11 @@ enum class Personality {
This AY has an attached mono or stereo mixer.
*/
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
template <bool stereo> class AY38910SampleSource {
public:
/// Creates a new AY38910.
AY38910(Personality, Concurrency::AsyncTaskQueue<false> &);
AY38910SampleSource(Personality, Concurrency::AsyncTaskQueue<false> &);
AY38910SampleSource(const AY38910SampleSource &) = delete;
/// Sets the value the AY would read from its data lines if it were not outputting.
void set_data_input(uint8_t r);
@@ -80,6 +81,12 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
/// Sets the current control line state, as a bit field.
void set_control_lines(ControlLines control_lines);
/// Strobes the reset line.
void reset();
/// Sets the current value of the reset line.
void set_reset(bool reset);
/*!
Gets the value that would appear on the requested interface port if it were in output mode.
@parameter port_b @c true to get the value for Port B, @c false to get the value for Port A.
@@ -105,24 +112,24 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
*/
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target);
// Sample generation.
typename Outputs::Speaker::SampleT<stereo>::type level() const;
void advance();
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return is_stereo; }
private:
Concurrency::AsyncTaskQueue<false> &task_queue_;
bool reset_ = false;
int selected_register_ = 0;
uint8_t registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint8_t output_registers_[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
uint8_t registers_[16]{};
uint8_t output_registers_[16]{};
int master_divider_ = 0;
int tone_periods_[3] = {0, 0, 0};
int tone_counters_[3] = {0, 0, 0};
int tone_outputs_[3] = {0, 0, 0};
int tone_periods_[3]{};
int tone_counters_[3]{};
int tone_outputs_[3]{};
int noise_period_ = 0;
int noise_counter_ = 0;
@@ -150,7 +157,7 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
uint8_t data_input_, data_output_;
uint32_t output_volume_;
typename Outputs::Speaker::SampleT<stereo>::type output_volume_;
void update_bus();
PortHandler *port_handler_ = nullptr;
@@ -166,6 +173,20 @@ template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource
friend struct State;
};
/// Defines a default AY to be the sample source with a master divider of 4;
/// real AYs have a divide-by-8 step built in but YMs apply only a divide by 4.
///
/// The implementation of AY38910SampleSource combines those two worlds
/// by always applying a divide by four and scaling other things as appropriate.
template <bool stereo> struct AY38910:
public AY38910SampleSource<stereo>,
public Outputs::Speaker::SampleSource<AY38910<stereo>, stereo, 4> {
// Use the same constructor as `AY38910SampleSource` (along with inheriting
// the rest of its interface).
using AY38910SampleSource<stereo>::AY38910SampleSource;
};
/*!
Provides helper code, to provide something closer to the interface exposed by many
AY-deploying machines of the era.

View File

@@ -21,8 +21,6 @@ namespace Apple::Clock {
*/
class ClockStorage {
public:
ClockStorage() {}
/*!
Advances the clock by 1 second.

View File

@@ -8,27 +8,23 @@
#include "AudioToggle.hpp"
#include <algorithm>
using namespace Audio;
Audio::Toggle::Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue) :
audio_queue_(audio_queue) {}
void Toggle::get_samples(std::size_t number_of_samples, std::int16_t *target) {
for(std::size_t sample = 0; sample < number_of_samples; ++sample) {
target[sample] = level_;
}
}
void Toggle::set_sample_volume_range(std::int16_t range) {
volume_ = range;
level_ = level_active_ ? volume_ : 0;
}
void Toggle::skip_samples(std::size_t) {}
void Toggle::set_output(bool enabled) {
if(is_enabled_ == enabled) return;
is_enabled_ = enabled;
audio_queue_.enqueue([this, enabled] {
level_active_ = enabled;
level_ = enabled ? volume_ : 0;
});
}

View File

@@ -8,7 +8,7 @@
#pragma once
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
namespace Audio {
@@ -16,13 +16,18 @@ namespace Audio {
/*!
Provides a sample source that can programmatically be set to one of two values.
*/
class Toggle: public Outputs::Speaker::SampleSource {
class Toggle: public Outputs::Speaker::BufferSource<Toggle, false> {
public:
Toggle(Concurrency::AsyncTaskQueue<false> &audio_queue);
void get_samples(std::size_t number_of_samples, std::int16_t *target);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
Outputs::Speaker::fill<action>(target, target + number_of_samples, level_);
}
void set_sample_volume_range(std::int16_t range);
void skip_samples(const std::size_t number_of_samples);
bool is_zero_level() const {
return !level_;
}
void set_output(bool enabled);
bool get_output() const;
@@ -34,6 +39,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
// Accessed on the audio thread.
int16_t level_ = 0, volume_ = 0;
bool level_active_ = false;
};
}

View File

@@ -55,10 +55,6 @@ uint8_t IWM::read(int address) {
logger.info().append("Invalid read\n");
return 0xff;
// "Read all 1s".
// printf("Reading all 1s\n");
// return 0xff;
case 0:
case ENABLE: { /* Read data register. Zeroing afterwards is a guess. */
const auto result = data_register_;

View File

@@ -30,7 +30,6 @@ DoubleDensityDrive::DoubleDensityDrive(int input_clock_rate, bool is_800k) :
// MARK: - Speed Selection
void DoubleDensityDrive::did_step(Storage::Disk::HeadPosition to_position) {
// printf("At track %d\n", to_position.as_int());
// The 800kb drive automatically selects rotation speed as a function of
// head position; the 400kb drive doesn't do so.
if(is_800k_) {

226
Components/I2C/I2C.cpp Normal file
View File

@@ -0,0 +1,226 @@
//
// I2C.cpp
// Clock Signal
//
// Created by Thomas Harte on 16/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#include "I2C.hpp"
#include "../../Outputs/Log.hpp"
using namespace I2C;
namespace {
Log::Logger<Log::Source::I2C> logger;
}
void Bus::set_data(bool pulled) {
set_clock_data(clock_, pulled);
}
bool Bus::data() {
bool result = data_;
if(peripheral_bits_) {
result |= !(peripheral_response_ & 0x80);
}
return result;
}
void Bus::set_clock(bool pulled) {
set_clock_data(pulled, data_);
}
bool Bus::clock() {
return clock_;
}
void Bus::set_clock_data(bool clock_pulled, bool data_pulled) {
// Proceed only if changes are evidenced.
if(clock_pulled == clock_ && data_pulled == data_) {
return;
}
const bool prior_clock = clock_;
const bool prior_data = data_;
clock_ = clock_pulled;
data_ = data_pulled;
// If currently serialising from a peripheral then shift onwards on
// every clock trailing edge.
if(peripheral_bits_) {
// Trailing edge of clock => bit has been consumed.
if(!prior_clock && clock_) {
logger.info().append("<< %d", (peripheral_response_ >> 7) & 1);
--peripheral_bits_;
peripheral_response_ <<= 1;
if(!peripheral_bits_) {
signal(Event::FinishedOutput);
}
}
return;
}
// Not currently serialising implies listening.
if(!clock_ && prior_data != data_) {
// A data transition outside of a clock cycle implies a start or stop.
in_bit_ = false;
if(data_) {
logger.info().append("S");
signal(Event::Start);
} else {
logger.info().append("W");
signal(Event::Stop);
}
} else if(clock_ != prior_clock) {
// Bits: wait until the falling edge of the cycle.
if(!clock_) {
// Rising edge: clock period begins.
in_bit_ = true;
} else if(in_bit_) {
// Falling edge: clock period ends (assuming it began; otherwise this is a preparatory
// clock transition only, immediately after a start bit).
in_bit_ = false;
if(data_) {
logger.info().append("0");
signal(Event::Zero);
} else {
logger.info().append("1");
signal(Event::One);
}
}
}
}
void Bus::signal(Event event) {
const auto capture_bit = [&]() {
input_ = uint16_t((input_ << 1) | (event == Event::Zero ? 0 : 1));
++input_count_;
};
const auto acknowledge = [&]() {
// Post an acknowledgement bit.
peripheral_response_ = 0;
peripheral_bits_ = 1;
};
const auto set_state = [&](State state) {
state_ = state;
input_count_ = 0;
input_ = 0;
};
const auto enqueue = [&](std::optional<uint8_t> next) {
if(next) {
peripheral_response_ = static_cast<uint16_t>(*next);
peripheral_bits_ = 8;
set_state(State::AwaitingByteAcknowledge);
} else {
set_state(State::AwaitingAddress);
}
};
const auto stop = [&]() {
set_state(State::AwaitingAddress);
active_peripheral_ = nullptr;
};
// Allow start and stop conditions at any time.
if(event == Event::Start) {
set_state(State::CollectingAddress);
active_peripheral_ = nullptr;
return;
}
if(event == Event::Stop) {
if(active_peripheral_) {
active_peripheral_->stop();
}
stop();
return;
}
switch(state_) {
// While waiting for an address, don't respond to anything other than a
// start bit, which is actually dealt with above.
case State::AwaitingAddress: break;
// To collect an address: shift in eight bits, and if there's a device
// at that address then acknowledge the address and segue into a read
// or write loop.
case State::CollectingAddress:
capture_bit();
if(input_count_ == 8) {
auto pair = peripherals_.find(uint8_t(input_) & 0xfe);
if(pair != peripherals_.end()) {
active_peripheral_ = pair->second;
active_peripheral_->start(input_ & 1);
if(input_&1) {
acknowledge();
set_state(State::CompletingReadAcknowledge);
} else {
acknowledge();
set_state(State::ReceivingByte);
}
} else {
state_ = State::AwaitingAddress;
}
}
break;
// Receiving byte: wait until a scheduled acknowledgment has
// happened, then collect eight bits, then see whether the
// active peripheral will accept them. If so, acknowledge and repeat.
// Otherwise fall silent.
case State::ReceivingByte:
if(event == Event::FinishedOutput) {
return;
}
capture_bit();
if(input_count_ == 8) {
if(active_peripheral_->write(static_cast<uint8_t>(input_))) {
acknowledge();
set_state(State::ReceivingByte);
} else {
stop();
}
}
break;
// The initial state immediately after a peripheral has been started
// in read mode and the address-select acknowledgement is still
// being serialised.
//
// Once that is completed, enqueues the first byte from the peripheral.
case State::CompletingReadAcknowledge:
if(event != Event::FinishedOutput) {
break;
}
enqueue(active_peripheral_->read());
break;
// Repeating state during reading; waits until the previous byte has
// been fully serialised, and if the host acknowledged it then posts
// the next. If the host didn't acknowledge, stops the connection.
case State::AwaitingByteAcknowledge:
if(event == Event::FinishedOutput) {
break;
}
if(event != Event::Zero) {
stop();
break;
}
// Add a new byte if there is one.
enqueue(active_peripheral_->read());
break;
}
}
void Bus::add_peripheral(Peripheral *peripheral, int address) {
peripherals_[address] = peripheral;
}

82
Components/I2C/I2C.hpp Normal file
View File

@@ -0,0 +1,82 @@
//
// I2C.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include <cstdint>
#include <optional>
#include <unordered_map>
namespace I2C {
/// Provides the virtual interface for an I2C peripheral; attaching this to a bus
/// provides automatic protocol handling.
struct Peripheral {
/// Indicates that the host signalled the start condition and addressed this
/// peripheral, along with whether it indicated a read or write.
virtual void start([[maybe_unused]] bool is_read) {}
/// Indicates that the host signalled a stop.
virtual void stop() {}
/// Requests the next byte to serialise onto the I2C bus after this peripheral has
/// been started in read mode.
///
/// @returns A byte to serialise or std::nullopt if the peripheral declines to
/// continue to communicate.
virtual std::optional<uint8_t> read() { return std::nullopt; }
/// Provides a byte received from the bus after this peripheral has been started
/// in write mode.
///
/// @returns @c true if the write should be acknowledged; @c false otherwise.
virtual bool write(uint8_t) { return false; }
};
class Bus {
public:
void set_data(bool pulled);
bool data();
void set_clock(bool pulled);
bool clock();
void set_clock_data(bool clock_pulled, bool data_pulled);
void add_peripheral(Peripheral *, int address);
private:
bool data_ = false;
bool clock_ = false;
bool in_bit_ = false;
std::unordered_map<int, Peripheral *> peripherals_;
uint16_t input_ = 0xffff;
int input_count_ = -1;
Peripheral *active_peripheral_ = nullptr;
uint16_t peripheral_response_ = 0xffff;
int peripheral_bits_ = 0;
enum class Event {
Zero, One, Start, Stop, FinishedOutput,
};
void signal(Event);
enum class State {
AwaitingAddress,
CollectingAddress,
CompletingReadAcknowledge,
AwaitingByteAcknowledge,
ReceivingByte,
} state_ = State::AwaitingAddress;
};
}

View File

@@ -19,15 +19,16 @@ bool SCC::is_zero_level() const {
return !(channel_enable_ & 0x1f);
}
void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) {
template <Outputs::Speaker::Action action>
void SCC::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
if(is_zero_level()) {
std::memset(target, 0, sizeof(std::int16_t) * number_of_samples);
Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample());
return;
}
std::size_t c = 0;
while((master_divider_&7) && c < number_of_samples) {
target[c] = transient_output_level_;
Outputs::Speaker::apply<action>(target[c], transient_output_level_);
master_divider_++;
c++;
}
@@ -44,12 +45,15 @@ void SCC::get_samples(std::size_t number_of_samples, std::int16_t *target) {
evaluate_output_volume();
for(int ic = 0; ic < 8 && c < number_of_samples; ++ic) {
target[c] = transient_output_level_;
Outputs::Speaker::apply<action>(target[c], transient_output_level_);
c++;
master_divider_++;
}
}
}
template void SCC::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SCC::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SCC::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void SCC::write(uint16_t address, uint8_t value) {
address &= 0xff;
@@ -111,5 +115,3 @@ uint8_t SCC::read(uint16_t address) {
}
return 0xff;
}

View File

@@ -8,7 +8,7 @@
#pragma once
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
namespace Konami {
@@ -20,7 +20,7 @@ namespace Konami {
and five channels of output. The original SCC uses the same wave for channels
four and five, the SCC+ supports different waves for the two channels.
*/
class SCC: public ::Outputs::Speaker::SampleSource {
class SCC: public ::Outputs::Speaker::BufferSource<SCC, false> {
public:
/// Creates a new SCC.
SCC(Concurrency::AsyncTaskQueue<false> &task_queue);
@@ -29,9 +29,9 @@ class SCC: public ::Outputs::Speaker::SampleSource {
bool is_zero_level() const;
/// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
/// Writes to the SCC.
void write(uint16_t address, uint8_t value);
@@ -45,7 +45,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
// State from here on down is accessed ony from the audio thread.
int master_divider_ = 0;
std::int16_t master_volume_ = 0;
int16_t transient_output_level_ = 0;
Outputs::Speaker::MonoSample transient_output_level_ = 0;
struct Channel {
int period = 0;

View File

@@ -8,12 +8,12 @@
#pragma once
#include "../../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../../Concurrency/AsyncTaskQueue.hpp"
namespace Yamaha::OPL {
template <typename Child> class OPLBase: public ::Outputs::Speaker::SampleSource {
template <typename Child, bool stereo> class OPLBase: public ::Outputs::Speaker::BufferSource<Child, stereo> {
public:
void write(uint16_t address, uint8_t value) {
if(address & 1) {

View File

@@ -278,7 +278,8 @@ void OPLL::set_sample_volume_range(std::int16_t range) {
total_volume_ = range;
}
void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) {
template <Outputs::Speaker::Action action>
void OPLL::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
// Both the OPLL and the OPL2 divide the input clock by 72 to get the base tick frequency;
// unlike the OPL2 the OPLL time-divides the output for 'mixing'.
@@ -289,12 +290,16 @@ void OPLL::get_samples(std::size_t number_of_samples, std::int16_t *target) {
while(number_of_samples--) {
if(!audio_offset_) update_all_channels();
*target = output_levels_[audio_offset_ / channel_output_period];
Outputs::Speaker::apply<action>(*target, output_levels_[audio_offset_ / channel_output_period]);
++target;
audio_offset_ = (audio_offset_ + 1) % update_period;
}
}
template void OPLL::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void OPLL::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void OPLL::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void OPLL::update_all_channels() {
oscillator_.update();

View File

@@ -19,24 +19,26 @@
namespace Yamaha::OPL {
class OPLL: public OPLBase<OPLL> {
class OPLL: public OPLBase<OPLL, false> {
public:
/// Creates a new OPLL or VRC7.
OPLL(Concurrency::AsyncTaskQueue<false> &task_queue, int audio_divider = 1, bool is_vrc7 = false);
/// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range);
bool is_zero_level() const { return false; } // TODO.
// The OPLL is generally 'half' as loud as it's told to be. This won't strictly be true in
// rhythm mode, but it's correct for melodic output.
double get_average_output_peak() const { return 0.5; }
double average_output_peak() const { return 0.5; }
/// Reads from the OPL.
uint8_t read(uint16_t address);
private:
friend OPLBase<OPLL>;
friend OPLBase<OPLL, false>;
void write_register(uint8_t address, uint8_t value);
int audio_divider_ = 0;

View File

@@ -99,10 +99,11 @@ void SN76489::evaluate_output_volume() {
);
}
void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
template <Outputs::Speaker::Action action>
void SN76489::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
std::size_t c = 0;
while((master_divider_& (master_divider_period_ - 1)) && c < number_of_samples) {
target[c] = output_volume_;
Outputs::Speaker::apply<action>(target[c], output_volume_);
master_divider_++;
c++;
}
@@ -151,7 +152,7 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
evaluate_output_volume();
for(int ic = 0; ic < master_divider_period_ && c < number_of_samples; ++ic) {
target[c] = output_volume_;
Outputs::Speaker::apply<action>(target[c], output_volume_);
c++;
master_divider_++;
}
@@ -159,3 +160,6 @@ void SN76489::get_samples(std::size_t number_of_samples, std::int16_t *target) {
master_divider_ &= (master_divider_period_ - 1);
}
template void SN76489::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SN76489::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SN76489::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);

View File

@@ -8,12 +8,12 @@
#pragma once
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
namespace TI {
class SN76489: public Outputs::Speaker::SampleSource {
class SN76489: public Outputs::Speaker::BufferSource<SN76489, false> {
public:
enum class Personality {
SN76489,
@@ -28,10 +28,10 @@ class SN76489: public Outputs::Speaker::SampleSource {
void write(uint8_t value);
// As per SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
bool is_zero_level() const;
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
int master_divider_ = 0;

View File

@@ -161,9 +161,7 @@ void Line<include_clock>::update_delegate(bool level) {
// Forward as many bits as occur.
Storage::Time time_left(cycles_to_forward, int(clock_rate_.as_integral()));
const int bit = level ? 1 : 0;
int bits = 0;
while(time_left >= time_left_in_bit_) {
++bits;
if(!read_delegate_->serial_line_did_produce_bit(this, bit)) {
read_delegate_phase_ = ReadDelegatePhase::WaitingForZero;
if(bit) return;

View File

@@ -19,7 +19,7 @@ namespace Inputs {
*/
class Joystick {
public:
virtual ~Joystick() {}
virtual ~Joystick() = default;
/*!
Defines a single input, any individually-measured thing — a fire button or

View File

@@ -45,7 +45,7 @@ class Keyboard {
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
virtual ~Keyboard() {}
virtual ~Keyboard() = default;
// Host interface.

View File

@@ -0,0 +1,133 @@
//
// BarrelShifter.hpp
// Clock Signal
//
// Created by Thomas Harte on 28/02/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
namespace InstructionSet::ARM {
enum class ShiftType {
LogicalLeft = 0b00,
LogicalRight = 0b01,
ArithmeticRight = 0b10,
RotateRight = 0b11,
};
template <bool writeable> struct Carry;
template <> struct Carry<true> {
using type = uint32_t &;
};
template <> struct Carry<false> {
using type = const uint32_t;
};
/// Apply a rotation of @c type to @c source of @c amount; @c carry should be either @c 1 or @c 0
/// at call to represent the current value of the carry flag. If @c set_carry is @c true then @c carry will
/// receive the new value of the carry flag following the rotation — @c 0 for no carry, @c non-0 for carry.
///
/// Shift amounts of 0 are given the meaning attributed to them for immediate shift counts.
template <ShiftType type, bool set_carry, bool is_immediate_shift>
void shift(uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
switch(type) {
case ShiftType::LogicalLeft:
if(amount > 32) {
if constexpr (set_carry) carry = 0;
source = 0;
} else if(amount == 32) {
if constexpr (set_carry) carry = source & 1;
source = 0;
} else if(amount > 0) {
if constexpr (set_carry) carry = source & (0x8000'0000 >> (amount - 1));
source <<= amount;
}
break;
case ShiftType::LogicalRight:
if(!amount && is_immediate_shift) {
// An immediate logical shift right by '0' is treated as a shift by 32;
// assemblers are supposed to map LSR #0 to LSL #0.
amount = 32;
}
if(amount > 32) {
if constexpr (set_carry) carry = 0;
source = 0;
} else if(amount == 32) {
if constexpr (set_carry) carry = source & 0x8000'0000;
source = 0;
} else if(amount > 0) {
if constexpr (set_carry) carry = source & (1 << (amount - 1));
source >>= amount;
}
break;
case ShiftType::ArithmeticRight: {
if(!amount && is_immediate_shift) {
// An immediate arithmetic shift of '0' is treated as a shift by 32.
amount = 32;
}
const uint32_t sign = (source & 0x8000'0000) ? 0xffff'ffff : 0x0000'0000;
if(amount >= 32) {
if constexpr (set_carry) carry = source & 0x8000'0000;
source = sign;
} else if(amount > 0) {
if constexpr (set_carry) carry = source & (1 << (amount - 1));
source = (source >> amount) | (sign << (32 - amount));
}
} break;
case ShiftType::RotateRight: {
if(!amount) {
if(is_immediate_shift) {
// Immediate rotate right by 0 is treated as a rotate right by 1 through carry.
const uint32_t high = carry << 31;
if constexpr (set_carry) carry = source & 1;
source = (source >> 1) | high;
}
break;
}
// "ROR by 32 has result equal to Rm, carry out equal to bit 31 ...
// [for] ROR by n where n is greater than 32 ... repeatedly subtract 32 from n
// until the amount is in the range 1 to 32"
amount &= 31;
if(amount) {
if constexpr (set_carry) carry = source & (1 << (amount - 1));
source = (source >> amount) | (source << (32 - amount));
} else {
if constexpr (set_carry) carry = source & 0x8000'0000;
}
} break;
// TODO: upon adoption of C++20, use std::rotr.
default: break;
}
}
/// Acts as per @c shift above, but applies runtime shift-type selection.
template <bool set_carry, bool is_immediate_shift>
void shift(ShiftType type, uint32_t &source, uint32_t amount, typename Carry<set_carry>::type carry) {
switch(type) {
case ShiftType::LogicalLeft:
shift<ShiftType::LogicalLeft, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::LogicalRight:
shift<ShiftType::LogicalRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::ArithmeticRight:
shift<ShiftType::ArithmeticRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
case ShiftType::RotateRight:
shift<ShiftType::RotateRight, set_carry, is_immediate_shift>(source, amount, carry);
break;
}
}
}

View File

@@ -0,0 +1,270 @@
//
// Disassembler.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "OperationMapper.hpp"
#include <string>
#include <sstream>
namespace InstructionSet::ARM {
/// Holds a single ARM operand, whether a source/destination or immediate value, potentially including a shift.
struct Operand {
enum class Type {
Immediate, Register, RegisterList, None
} type = Type::None;
uint32_t value = 0;
// TODO: encode shifting
operator std::string() const {
switch(type) {
default: return "";
case Type::Register: return std::string("r") + std::to_string(value);
case Type::RegisterList: {
std::stringstream stream;
stream << '[';
bool first = true;
for(int c = 0; c < 16; c++) {
if(value & (1 << c)) {
if(!first) stream << ", ";
first = false;
stream << 'r' << c;
}
}
stream << ']';
return stream.str();
}
}
}
};
/// Describes a single ARM instruction, suboptimally but such that all relevant detail has been extracted
/// by the OperationMapper and is now easy to inspect or to turn into a string.
struct Instruction {
Condition condition = Condition::AL;
enum class Operation {
AND, EOR, SUB, RSB,
ADD, ADC, SBC, RSC,
TST, TEQ, CMP, CMN,
ORR, MOV, BIC, MVN,
LDR, STR,
LDM, STM,
B, BL,
SWI,
MRC, MCR,
Undefined,
} operation = Operation::Undefined;
Operand destination, operand1, operand2;
bool sets_flags = false;
bool is_byte = false;
std::string to_string(uint32_t address) const {
std::ostringstream result;
// Treat all nevers as nops.
if(condition == Condition::NV) {
return "nop";
}
// Print operation.
switch(operation) {
case Operation::Undefined: return "undefined";
case Operation::SWI: return "swi";
case Operation::B: result << "b"; break;
case Operation::BL: result << "bl"; break;
case Operation::AND: result << "and"; break;
case Operation::EOR: result << "eor"; break;
case Operation::SUB: result << "sub"; break;
case Operation::RSB: result << "rsb"; break;
case Operation::ADD: result << "add"; break;
case Operation::ADC: result << "adc"; break;
case Operation::SBC: result << "sbc"; break;
case Operation::RSC: result << "rsc"; break;
case Operation::TST: result << "tst"; break;
case Operation::TEQ: result << "teq"; break;
case Operation::CMP: result << "cmp"; break;
case Operation::CMN: result << "cmn"; break;
case Operation::ORR: result << "orr"; break;
case Operation::MOV: result << "mov"; break;
case Operation::BIC: result << "bic"; break;
case Operation::MVN: result << "mvn"; break;
case Operation::LDR: result << "ldr"; break;
case Operation::STR: result << "str"; break;
case Operation::LDM: result << "ldm"; break;
case Operation::STM: result << "stm"; break;
case Operation::MRC: result << "mrc"; break;
case Operation::MCR: result << "mcr"; break;
}
// Append the sets-flags modifier if applicable.
if(sets_flags) result << 's';
// Possibly a condition code.
switch(condition) {
case Condition::EQ: result << "eq"; break;
case Condition::NE: result << "ne"; break;
case Condition::CS: result << "cs"; break;
case Condition::CC: result << "cc"; break;
case Condition::MI: result << "mi"; break;
case Condition::PL: result << "pl"; break;
case Condition::VS: result << "vs"; break;
case Condition::VC: result << "vc"; break;
case Condition::HI: result << "hi"; break;
case Condition::LS: result << "ls"; break;
case Condition::GE: result << "ge"; break;
case Condition::LT: result << "lt"; break;
case Condition::GT: result << "gt"; break;
case Condition::LE: result << "le"; break;
default: break;
}
// If this is a branch, append the target.
if(operation == Operation::B || operation == Operation::BL) {
result << " 0x" << std::hex << ((address + 8 + operand1.value) & 0x3fffffc);
}
if(
operation == Operation::LDR || operation == Operation::STR ||
operation == Operation::LDM || operation == Operation::STM
) {
if(is_byte) result << 'b';
result << ' ' << static_cast<std::string>(destination);
result << ", [" << static_cast<std::string>(operand1) << "]";
// TODO: learn how ARM shifts/etc are normally presented.
}
return result.str();
}
};
/// A target for @c dispatch that merely captures a description of the decoded instruction, being
/// able to vend it later via @c last().
template <Model model>
struct Disassembler {
Instruction last() {
return instruction_;
}
bool should_schedule(Condition condition) {
instruction_ = Instruction();
instruction_.condition = condition;
return true;
}
template <Flags f> void perform(DataProcessing fields) {
constexpr DataProcessingFlags flags(f);
instruction_.operand1.type = Operand::Type::Register;
instruction_.operand1.value = fields.operand1();
instruction_.destination.type = Operand::Type::Register;
instruction_.destination.value = fields.destination();
if(flags.operand2_is_immediate()) {
instruction_.operand2.type = Operand::Type::Immediate;
// instruction_.operand2.value = fields.immediate(), fields.rotate();
// TODO: decode immediate.
} else {
instruction_.operand2.type = Operand::Type::Register;
instruction_.operand2.value = fields.operand2();
// TODO: capture shift_type(), etc.
}
instruction_.sets_flags = flags.set_condition_codes();
switch(flags.operation()) {
case DataProcessingOperation::AND: instruction_.operation = Instruction::Operation::AND; break;
case DataProcessingOperation::EOR: instruction_.operation = Instruction::Operation::EOR; break;
case DataProcessingOperation::ORR: instruction_.operation = Instruction::Operation::ORR; break;
case DataProcessingOperation::BIC: instruction_.operation = Instruction::Operation::BIC; break;
case DataProcessingOperation::MOV: instruction_.operation = Instruction::Operation::MOV; break;
case DataProcessingOperation::MVN: instruction_.operation = Instruction::Operation::MVN; break;
case DataProcessingOperation::TST: instruction_.operation = Instruction::Operation::TST; break;
case DataProcessingOperation::TEQ: instruction_.operation = Instruction::Operation::TEQ; break;
case DataProcessingOperation::ADD: instruction_.operation = Instruction::Operation::ADD; break;
case DataProcessingOperation::ADC: instruction_.operation = Instruction::Operation::ADC; break;
case DataProcessingOperation::CMN: instruction_.operation = Instruction::Operation::CMN; break;
case DataProcessingOperation::SUB: instruction_.operation = Instruction::Operation::SUB; break;
case DataProcessingOperation::SBC: instruction_.operation = Instruction::Operation::SBC; break;
case DataProcessingOperation::CMP: instruction_.operation = Instruction::Operation::CMP; break;
case DataProcessingOperation::RSB: instruction_.operation = Instruction::Operation::RSB; break;
case DataProcessingOperation::RSC: instruction_.operation = Instruction::Operation::RSC; break;
}
}
template <Flags> void perform(Multiply) {}
template <Flags f> void perform(SingleDataTransfer fields) {
constexpr SingleDataTransferFlags flags(f);
instruction_.operation =
(flags.operation() == SingleDataTransferFlags::Operation::STR) ?
Instruction::Operation::STR : Instruction::Operation::LDR;
instruction_.destination.type = Operand::Type::Register;
instruction_.destination.value = fields.destination();
instruction_.operand1.type = Operand::Type::Register;
instruction_.operand1.value = fields.base();
}
template <Flags f> void perform(BlockDataTransfer fields) {
constexpr BlockDataTransferFlags flags(f);
instruction_.operation =
(flags.operation() == BlockDataTransferFlags::Operation::STM) ?
Instruction::Operation::STM : Instruction::Operation::LDM;
instruction_.destination.type = Operand::Type::Register;
instruction_.destination.value = fields.base();
instruction_.operand1.type = Operand::Type::RegisterList;
instruction_.operand1.value = fields.register_list();
}
template <Flags f> void perform(Branch fields) {
constexpr BranchFlags flags(f);
instruction_.operation =
(flags.operation() == BranchFlags::Operation::BL) ?
Instruction::Operation::BL : Instruction::Operation::B;
instruction_.operand1.type = Operand::Type::Immediate;
instruction_.operand1.value = fields.offset();
}
template <Flags f> void perform(CoprocessorRegisterTransfer) {
constexpr CoprocessorRegisterTransferFlags flags(f);
instruction_.operation =
(flags.operation() == CoprocessorRegisterTransferFlags::Operation::MRC) ?
Instruction::Operation::MRC : Instruction::Operation::MCR;
}
template <Flags> void perform(CoprocessorDataOperation) {}
template <Flags> void perform(CoprocessorDataTransfer) {}
void software_interrupt(SoftwareInterrupt) {
instruction_.operation = Instruction::Operation::SWI;
}
void unknown() {
instruction_.operation = Instruction::Operation::Undefined;
}
private:
Instruction instruction_;
};
}

View File

@@ -0,0 +1,669 @@
//
// Executor.hpp
// Clock Signal
//
// Created by Thomas Harte on 01/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "BarrelShifter.hpp"
#include "OperationMapper.hpp"
#include "Registers.hpp"
#include "../../Numeric/Carry.hpp"
namespace InstructionSet::ARM {
/// Maps from a semantic ARM read of type @c SourceT to either the 8- or 32-bit value observed
/// by watching the low 8 bits or all 32 bits of the data bus.
template <typename DestinationT, typename SourceT>
DestinationT read_bus(SourceT value) {
if constexpr (std::is_same_v<DestinationT, SourceT>) {
return value;
}
if constexpr (std::is_same_v<DestinationT, uint8_t>) {
return uint8_t(value);
} else {
return value | (value << 8) | (value << 16) | (value << 24);
}
}
struct NullControlFlowHandler {
/// Indicates that a potential pipeline-affecting status flag change occurred,
/// i.e. a change to processor mode or interrupt flags.
void did_set_status() {}
/// Indicates that the PC was altered by the instruction.
void did_set_pc() {}
/// Provides notification that an SWI is about to happen along with the option of skipping it; this gives handlers the
/// chance to substitute a high-level reimplementation of the service call.
bool should_swi([[maybe_unused]] uint32_t comment) { return true; }
};
/// A class compatible with the @c OperationMapper definition of a scheduler which applies all actions
/// immediately, updating either a set of @c Registers or using the templated @c MemoryT to access
/// memory. No hooks are currently provided for applying realistic timing.
///
/// If a ControlFlowHandlerT is specified, it'll receive calls as defined in the NullControlFlowHandler above.
template <Model model, typename MemoryT, typename ControlFlowHandlerT = NullControlFlowHandler>
struct Executor {
template <typename... Args>
Executor(ControlFlowHandlerT &handler, Args &&...args) : bus(std::forward<Args>(args)...), control_flow_handler_(handler) {}
template <typename... Args>
Executor(Args &&...args) : bus(std::forward<Args>(args)...) {}
/// @returns @c true if @c condition implies an appropriate perform call should be made for this instruction,
/// @c false otherwise.
bool should_schedule(Condition condition) {
// This short-circuit of registers_.test provides the necessary compiler clue that
// Condition::AL is not only [[likely]] but [[exceedingly likely]].
return condition == Condition::AL ? true : registers_.test(condition);
}
template <bool allow_register, bool set_carry, typename FieldsT>
uint32_t decode_shift(FieldsT fields, uint32_t &rotate_carry, uint32_t pc_offset) {
// "When R15 appears in the Rm position it will give the value of the PC together
// with the PSR flags to the barrel shifter. ...
//
// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead.
// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead
// when used as Rn or Rm."
uint32_t operand2;
if(fields.operand2() == 15) {
operand2 = registers_.pc_status(pc_offset);
} else {
operand2 = registers_[fields.operand2()];
}
// TODO: in C++20, a quick `if constexpr (requires` can eliminate the `allow_register` parameter.
if constexpr (allow_register) {
if(fields.shift_count_is_register()) {
uint32_t shift_amount;
// "When R15 appears in either of the Rn or Rs positions it will give the value
// of the PC alone, with the PSR bits replaced by zeroes. ...
//
// If a register is used to specify the shift amount, the
// PC will be 8 bytes ahead when used as Rs."
shift_amount =
fields.shift_register() == 15 ?
registers_.pc(4) :
registers_[fields.shift_register()];
// "The amount by which the register should be shifted may be contained in
// ... **the bottom byte** of another register".
shift_amount &= 0xff;
shift<set_carry, false>(fields.shift_type(), operand2, shift_amount, rotate_carry);
return operand2;
}
}
shift<set_carry, true>(fields.shift_type(), operand2, fields.shift_amount(), rotate_carry);
return operand2;
}
template <Flags f> void perform(DataProcessing fields) {
constexpr DataProcessingFlags flags(f);
const bool shift_by_register = !flags.operand2_is_immediate() && fields.shift_count_is_register();
// Write a raw result into the PC proxy if the target is R15; it'll be stored properly later.
uint32_t pc_proxy = 0;
auto &destination = fields.destination() == 15 ? pc_proxy : registers_[fields.destination()];
// "When R15 appears in either of the Rn or Rs positions it will give the value
// of the PC alone, with the PSR bits replaced by zeroes. ...
//
// If the shift amount is specified in the instruction, the PC will be 8 bytes ahead.
// If a register is used to specify the shift amount, the PC will be ... 12 bytes ahead
// when used as Rn or Rm."
const uint32_t operand1 =
(fields.operand1() == 15) ?
registers_.pc(shift_by_register ? 8 : 4) :
registers_[fields.operand1()];
uint32_t operand2;
uint32_t rotate_carry = registers_.c();
// Populate carry from the shift only if it'll be used.
constexpr bool shift_sets_carry = is_logical(flags.operation()) && flags.set_condition_codes();
// Get operand 2.
if constexpr (flags.operand2_is_immediate()) {
operand2 = fields.immediate();
shift<ShiftType::RotateRight, shift_sets_carry, false>(operand2, fields.rotate(), rotate_carry);
} else {
operand2 = decode_shift<true, shift_sets_carry>(fields, rotate_carry, shift_by_register ? 8 : 4);
}
uint32_t conditions = 0;
const auto sub = [&](uint32_t lhs, uint32_t rhs) {
conditions = lhs - rhs;
if constexpr (flags.operation() == DataProcessingOperation::SBC || flags.operation() == DataProcessingOperation::RSC) {
conditions += registers_.c() - 1;
}
if constexpr (flags.set_condition_codes()) {
// "For a subtraction, including the comparison instruction CMP, C is set to 0 if
// the subtraction produced a borrow (that is, an unsigned underflow), and to 1 otherwise."
registers_.set_c(!Numeric::carried_out<false, 31>(lhs, rhs, conditions));
registers_.set_v(Numeric::overflow<false>(lhs, rhs, conditions));
}
if constexpr (!is_comparison(flags.operation())) {
destination = conditions;
}
};
// Perform the data processing operation.
switch(flags.operation()) {
// Logical operations.
case DataProcessingOperation::AND: conditions = destination = operand1 & operand2; break;
case DataProcessingOperation::EOR: conditions = destination = operand1 ^ operand2; break;
case DataProcessingOperation::ORR: conditions = destination = operand1 | operand2; break;
case DataProcessingOperation::BIC: conditions = destination = operand1 & ~operand2; break;
case DataProcessingOperation::MOV: conditions = destination = operand2; break;
case DataProcessingOperation::MVN: conditions = destination = ~operand2; break;
case DataProcessingOperation::TST: conditions = operand1 & operand2; break;
case DataProcessingOperation::TEQ: conditions = operand1 ^ operand2; break;
case DataProcessingOperation::ADD:
case DataProcessingOperation::ADC:
case DataProcessingOperation::CMN:
conditions = operand1 + operand2;
if constexpr (flags.operation() == DataProcessingOperation::ADC) {
conditions += registers_.c();
}
if constexpr (flags.set_condition_codes()) {
registers_.set_c(Numeric::carried_out<true, 31>(operand1, operand2, conditions));
registers_.set_v(Numeric::overflow<true>(operand1, operand2, conditions));
}
if constexpr (!is_comparison(flags.operation())) {
destination = conditions;
}
break;
case DataProcessingOperation::SUB:
case DataProcessingOperation::SBC:
case DataProcessingOperation::CMP:
sub(operand1, operand2);
break;
case DataProcessingOperation::RSB:
case DataProcessingOperation::RSC:
sub(operand2, operand1);
break;
}
if(!is_comparison(flags.operation()) && fields.destination() == 15) {
set_pc<true>(pc_proxy);
}
if constexpr (flags.set_condition_codes()) {
// "When Rd is R15 and the S flag in the instruction is set, the PSR is overwritten by the
// corresponding bits in the ALU result... [even] if the instruction is of a type that does not
// normally produce a result (CMP, CMN, TST, TEQ) ... the result will be used to update those
// PSR flags which are not protected by virtue of the processor mode"
if(fields.destination() == 15) {
set_status(conditions);
} else {
// Set N and Z in a unified way.
registers_.set_nz(conditions);
// Set C from the barrel shifter if applicable.
if constexpr (shift_sets_carry) {
registers_.set_c(rotate_carry);
}
}
}
}
template <Flags f> void perform(Multiply fields) {
constexpr MultiplyFlags flags(f);
// R15 rules:
//
// * Rs: no PSR, 8 bytes ahead;
// * Rn: with PSR, 8 bytes ahead;
// * Rm: with PSR, 12 bytes ahead.
const uint32_t multiplicand = fields.multiplicand() == 15 ? registers_.pc(4) : registers_[fields.multiplicand()];
const uint32_t multiplier = fields.multiplier() == 15 ? registers_.pc_status(4) : registers_[fields.multiplier()];
const uint32_t accumulator =
flags.operation() == MultiplyFlags::Operation::MUL ? 0 :
(fields.multiplicand() == 15 ? registers_.pc_status(8) : registers_[fields.accumulator()]);
const uint32_t result = multiplicand * multiplier + accumulator;
if constexpr (flags.set_condition_codes()) {
registers_.set_nz(result);
// V is unaffected; C is undefined.
}
if(fields.destination() != 15) {
registers_[fields.destination()] = result;
}
}
template <Flags f> void perform(Branch branch) {
constexpr BranchFlags flags(f);
if constexpr (flags.operation() == BranchFlags::Operation::BL) {
registers_[14] = registers_.pc_status(0);
}
set_pc<true>(registers_.pc(4) + branch.offset());
}
template <Flags f> void perform(SingleDataTransfer transfer) {
constexpr SingleDataTransferFlags flags(f);
// Calculate offset.
uint32_t offset;
if constexpr (flags.offset_is_register()) {
// The 8 shift control bits are described in 6.2.3, but
// the register specified shift amounts are not available
// in this instruction class.
uint32_t carry = registers_.c();
offset = decode_shift<false, false>(transfer, carry, 4);
} else {
offset = transfer.immediate();
}
// Obtain base address.
uint32_t address =
transfer.base() == 15 ?
registers_.pc(4) :
registers_[transfer.base()];
// Determine what the address will be after offsetting.
uint32_t offsetted_address = address;
if constexpr (flags.add_offset()) {
offsetted_address += offset;
} else {
offsetted_address -= offset;
}
// If preindexing, apply now.
if constexpr (flags.pre_index()) {
address = offsetted_address;
}
// Check for an address exception.
if(is_invalid_address(address)) {
exception<Registers::Exception::Address>();
return;
}
// Decide whether to write back — when either postindexing or else write back is requested.
//
// Note to future self on write-back:
//
// It's currently unclear what to do in the case of e.g. `str r13, [r13, #0x10]!`. Is the value
// written r13 as modified or the original r13? If it's as modified, does that imply that
// write back has occurred regardless of a data abort?
//
// TODO: resolve uncertainty.
constexpr bool should_write_back = !flags.pre_index() || flags.write_back_address();
// "... post-indexed data transfers always write back the modified base. The only use of the [write-back address]
// bit in a post-indexed data transfer is in non-user mode code, where setting the W bit forces the /TRANS pin
// to go LOW for the transfer"
const bool trans = (registers_.mode() == Mode::User) || (!flags.pre_index() && flags.write_back_address());
if constexpr (flags.operation() == SingleDataTransferFlags::Operation::STR) {
const uint32_t source =
transfer.source() == 15 ?
registers_.pc_status(8) :
registers_[transfer.source()];
bool did_write;
if constexpr (flags.transfer_byte()) {
did_write = bus.template write<uint8_t>(address, uint8_t(source), registers_.mode(), trans);
} else {
// "The data presented to the data bus are not affected if the address is not word aligned".
did_write = bus.template write<uint32_t>(address, source, registers_.mode(), trans);
}
if(!did_write) {
exception<Registers::Exception::DataAbort>();
return;
}
} else {
bool did_read;
uint32_t value = 0;
if constexpr (flags.transfer_byte()) {
uint8_t target = 0; // Value should never be used; this avoids a spurious GCC warning.
did_read = bus.template read<uint8_t>(address, target, registers_.mode(), trans);
if(did_read) {
value = target;
}
} else {
did_read = bus.template read<uint32_t>(address, value, registers_.mode(), trans);
if constexpr (model != Model::ARMv2with32bitAddressing) {
// "An address offset from a word boundary will cause the data to be rotated into the
// register so that the addressed byte occuplies bits 0 to 7."
//
// (though the test set that inspired 'ARMv2with32bitAddressing' appears not to honour this;
// test below assumes it went away by the version of ARM that set supports)
switch(address & 3) {
case 0: break;
case 1: value = (value >> 8) | (value << 24); break;
case 2: value = (value >> 16) | (value << 16); break;
case 3: value = (value >> 24) | (value << 8); break;
}
}
}
if(!did_read) {
exception<Registers::Exception::DataAbort>();
return;
}
if(transfer.destination() == 15) {
set_pc<true>(value);
} else {
registers_[transfer.destination()] = value;
}
}
if constexpr (should_write_back) {
// Empirically: I think order of operations for a load is: (i) write back; (ii) store value from bus.
// So if this is a load, don't allow write back to overwrite what was loaded.
if(flags.operation() == SingleDataTransferFlags::Operation::STR || transfer.base() != transfer.destination()) {
if(transfer.base() == 15) {
set_pc<true>(offsetted_address);
} else {
registers_[transfer.base()] = offsetted_address;
}
}
}
}
template <Flags f> void perform(BlockDataTransfer transfer) {
constexpr BlockDataTransferFlags flags(f);
constexpr bool is_ldm = flags.operation() == BlockDataTransferFlags::Operation::LDM;
// Ensure that *base points to the base register if it can be written back;
// also set address to the base.
uint32_t *base = nullptr;
uint32_t address;
if(transfer.base() == 15) {
address = registers_.pc_status(4);
} else {
base = &registers_[transfer.base()];
address = *base;
}
// For an LDM pc_proxy will receive any read R15 value;
// for an STM it'll hold the value to be written.
uint32_t pc_proxy = 0;
// Read the base address and take a copy in case a data abort means that
// it has to be restored later.
uint32_t initial_address = address;
// Grab the register list and decide whether user registers are being used.
const uint16_t list = transfer.register_list();
const bool adopt_user_mode = flags.load_psr() && (!is_ldm || !(list & (1 << 15)));
// Write back will prima facie occur if:
// (i) the instruction asks for it; and
// (ii) the write-back register isn't R15.
bool write_back = base && flags.write_back_address();
// Collate a transfer list; this is a very long-winded way of implementing STM
// and LDM but right now the objective is merely correctness.
//
// If this is LDM and it turns out that base is also in the transfer list,
// disable write back.
uint32_t *transfer_sources[16];
uint32_t total = 0;
for(uint32_t c = 0; c < 15; c++) {
if(list & (1 << c)) {
uint32_t *const next = &registers_.reg(adopt_user_mode, c);
if(is_ldm && next == base) write_back = false;
transfer_sources[total++] = next;
}
}
// If the last thing in the list is R15, redirect it to the PC proxy,
// possibly populating with a meaningful value.
if(list & (1 << 15)) {
if(!is_ldm) {
pc_proxy = registers_.pc_status(8);
}
transfer_sources[total++] = &pc_proxy;
}
// If this is STM and the first thing in the list is the same as base,
// point it at initial_address instead.
if(!is_ldm && total && transfer_sources[0] == base) {
transfer_sources[0] = &initial_address;
}
// Calculate final_address, which is what will be written back if required;
// update address to point to the low end of the transfer block.
//
// Writes are always ordered from lowest address to highest; adjust the
// start address if this write is supposed to fill memory downward from
// the base.
uint32_t final_address;
if constexpr (!flags.add_offset()) {
// Decrementing mode; final_address is the value the base register should
// have after this operation if writeback is enabled, so it's below
// the original address. But also writes always occur from lowest address
// to highest, so push the current address to the bottom.
final_address = address - total * 4;
address = final_address;
} else {
final_address = address + total * 4;
}
// Write back if enabled.
if(write_back) {
*base = final_address;
}
// Update address in advance for:
// * pre-indexed upward stores; and
// * post-indxed downward stores.
if constexpr (flags.pre_index() == flags.add_offset()) {
address += 4;
}
// Perform all memory accesses, tracking whether either kind of abort will be
// required.
const bool trans = registers_.mode() == Mode::User;
const bool address_error = is_invalid_address(address);
bool accesses_succeeded = true;
if constexpr (is_ldm) {
// Keep a record of the value replaced by the last load and
// where it came from. A data abort cancels both the current load and
// the one before it, so this might be used by this implementation to
// undo the previous load.
struct {
uint32_t *target = nullptr;
uint32_t value = 0;
} last_replacement;
for(uint32_t c = 0; c < total; c++) {
uint32_t &value = *transfer_sources[c];
// When ARM detects a data abort during a load multiple instruction, it modifies the operation of
// the instruction to ensure that recovery is possible.
//
// * Overwriting of registers stops when the abort happens. The aborting load will not
// take place, nor will the preceding one ...
// * The base register is restored, to its modified value if write-back was requested.
if(accesses_succeeded) {
const uint32_t replaced = value;
accesses_succeeded &= bus.template read<uint32_t>(address, value, registers_.mode(), trans);
// Update the last-modified slot if the access succeeded; otherwise
// undo the last modification if there was one, and undo the base
// address change.
if(accesses_succeeded) {
last_replacement.value = replaced;
last_replacement.target = transfer_sources[c];
} else {
if(last_replacement.target) {
*last_replacement.target = last_replacement.value;
}
// Also restore the base register, including to its original value
// if write back was disabled.
if(base) {
if(write_back) {
*base = final_address;
} else {
*base = initial_address;
}
}
}
} else {
// Implicitly: do the access anyway, but don't store the value. I think.
uint32_t throwaway;
bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans);
}
// Advance.
address += 4;
}
} else {
for(uint32_t c = 0; c < total; c++) {
uint32_t &value = *transfer_sources[c];
if(!address_error) {
// "If the abort occurs during a store multiple instruction, ARM takes little action until
// the instruction completes, whereupon it enters the data abort trap. The memory manager is
// responsible for preventing erroneous writes to the memory."
accesses_succeeded &= bus.template write<uint32_t>(address, value, registers_.mode(), trans);
} else {
// Do a throwaway read.
uint32_t throwaway;
bus.template read<uint32_t>(address, throwaway, registers_.mode(), trans);
}
// Advance.
address += 4;
}
}
// Finally throw an exception if necessary.
if(address_error) {
exception<Registers::Exception::Address>();
} else if(!accesses_succeeded) {
exception<Registers::Exception::DataAbort>();
} else {
// If this was an LDM to R15 then apply it appropriately.
if(is_ldm && list & (1 << 15)) {
set_pc<true>(pc_proxy);
if constexpr (flags.load_psr()) {
set_status(pc_proxy);
}
}
}
}
void software_interrupt(SoftwareInterrupt swi) {
if(control_flow_handler_.should_swi(swi.comment())) {
exception<Registers::Exception::SoftwareInterrupt>();
}
}
void unknown() {
exception<Registers::Exception::UndefinedInstruction>();
}
// Act as if no coprocessors present.
template <Flags> void perform(CoprocessorRegisterTransfer) {
exception<Registers::Exception::UndefinedInstruction>();
}
template <Flags> void perform(CoprocessorDataOperation) {
exception<Registers::Exception::UndefinedInstruction>();
}
template <Flags> void perform(CoprocessorDataTransfer) {
exception<Registers::Exception::UndefinedInstruction>();
}
/// @returns The current registers state.
const Registers &registers() const {
return registers_;
}
// Included primarily for testing; my full opinion on this is still
// incompletely-formed.
Registers &registers() {
return registers_;
}
/// Indicates a prefetch abort exception.
void prefetch_abort() {
exception<Registers::Exception::PrefetchAbort>();
}
/// Sets the expected address of the instruction after whichever is about to be executed.
/// So it's PC+4 compared to most other systems.
///
/// By default this is not forwarded to the control-flow handler.
template <bool notify = false>
void set_pc(uint32_t pc) {
registers_.set_pc(pc);
if constexpr (notify) {
control_flow_handler_.did_set_pc();
}
}
/// @returns The address of the instruction that should be fetched next. So as execution of each instruction
/// begins, this will be +4 from the instruction being executed; at the end of the instruction it'll either still be +4
/// or else be some other address if a branch or exception has occurred.
uint32_t pc() const {
return registers_.pc(0);
}
MemoryT bus;
private:
template <Registers::Exception type>
void exception() {
registers_.exception<type>();
control_flow_handler_.did_set_pc();
}
void set_status(uint32_t status) {
registers_.set_status(status);
control_flow_handler_.did_set_status();
}
using ControlFlowHandlerTStorage =
typename std::conditional<
std::is_same_v<ControlFlowHandlerT, NullControlFlowHandler>,
ControlFlowHandlerT,
ControlFlowHandlerT &>::type;
ControlFlowHandlerTStorage control_flow_handler_;
Registers registers_;
static bool is_invalid_address(uint32_t address) {
if constexpr (model == Model::ARMv2with32bitAddressing) {
return false;
}
return address >= 1 << 26;
}
};
/// Executes the instruction @c instruction which should have been fetched from @c executor.pc(),
/// modifying @c executor.
template <Model model, typename MemoryT, typename StatusObserverT>
void execute(uint32_t instruction, Executor<model, MemoryT, StatusObserverT> &executor) {
executor.set_pc(executor.pc() + 4);
dispatch<model>(instruction, executor);
}
}

View File

@@ -0,0 +1,553 @@
//
// OperationMapper.hpp
// Clock Signal
//
// Created by Thomas Harte on 16/02/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../Reflection/Dispatcher.hpp"
#include "BarrelShifter.hpp"
namespace InstructionSet::ARM {
enum class Model {
ARMv2,
/// Like an ARMv2 but all non-PC addressing is 64-bit. Primarily useful for a particular set of test
/// cases that I want to apply retroactively; not a real iteration.
ARMv2with32bitAddressing,
};
enum class Condition {
EQ, NE, CS, CC,
MI, PL, VS, VC,
HI, LS, GE, LT,
GT, LE, AL, NV,
};
//
// Implementation details.
//
static constexpr int FlagsStartBit = 20;
using Flags = uint8_t;
template <int position>
constexpr bool flag_bit(uint8_t flags) {
static_assert(position >= 20 && position < 28);
return flags & (1 << (position - FlagsStartBit));
}
//
// Methods common to data processing and data transfer.
//
struct WithShiftControlBits {
constexpr WithShiftControlBits(uint32_t opcode) noexcept : opcode_(opcode) {}
/// The operand 2 register index if @c operand2_is_immediate() is @c false; meaningless otherwise.
uint32_t operand2() const { return opcode_ & 0xf; }
/// The type of shift to apply to operand 2 if @c operand2_is_immediate() is @c false; meaningless otherwise.
ShiftType shift_type() const { return ShiftType((opcode_ >> 5) & 3); }
/// @returns @c true if the amount to shift by should be taken from a register; @c false if it is an immediate value.
bool shift_count_is_register() const { return opcode_ & (1 << 4); }
/// The shift amount register index if @c shift_count_is_register() is @c true; meaningless otherwise.
uint32_t shift_register() const { return (opcode_ >> 8) & 0xf; }
/// The amount to shift by if @c shift_count_is_register() is @c false; meaningless otherwise.
uint32_t shift_amount() const { return (opcode_ >> 7) & 0x1f; }
protected:
uint32_t opcode_;
};
//
// Branch (i.e. B and BL).
//
struct BranchFlags {
constexpr BranchFlags(uint8_t flags) noexcept : flags_(flags) {}
enum class Operation {
B, /// Add offset to PC; programmer allows for PC being two words ahead.
BL, /// Copy PC and PSR to R14, then branch. Copied PC points to next instruction.
};
/// @returns The operation to apply.
constexpr Operation operation() const {
return flag_bit<24>(flags_) ? Operation::BL : Operation::B;
}
private:
uint8_t flags_;
};
struct Branch {
constexpr Branch(uint32_t opcode) noexcept : opcode_(opcode) {}
/// The 26-bit offset to add to the PC.
uint32_t offset() const { return (opcode_ & 0xff'ffff) << 2; }
private:
uint32_t opcode_;
};
//
// Data processing (i.e. AND to MVN).
//
enum class DataProcessingOperation {
AND, /// Rd = Op1 AND Op2.
EOR, /// Rd = Op1 EOR Op2.
SUB, /// Rd = Op1 - Op2.
RSB, /// Rd = Op2 - Op1.
ADD, /// Rd = Op1 + Op2.
ADC, /// Rd = Op1 + Ord2 + C.
SBC, /// Rd = Op1 - Op2 + C.
RSC, /// Rd = Op2 - Op1 + C.
TST, /// Set condition codes on Op1 AND Op2.
TEQ, /// Set condition codes on Op1 EOR Op2.
CMP, /// Set condition codes on Op1 - Op2.
CMN, /// Set condition codes on Op1 + Op2.
ORR, /// Rd = Op1 OR Op2.
MOV, /// Rd = Op2
BIC, /// Rd = Op1 AND NOT Op2.
MVN, /// Rd = NOT Op2.
};
constexpr bool is_logical(DataProcessingOperation operation) {
switch(operation) {
case DataProcessingOperation::AND:
case DataProcessingOperation::EOR:
case DataProcessingOperation::TST:
case DataProcessingOperation::TEQ:
case DataProcessingOperation::ORR:
case DataProcessingOperation::MOV:
case DataProcessingOperation::BIC:
case DataProcessingOperation::MVN:
return true;
default: return false;
}
}
constexpr bool is_comparison(DataProcessingOperation operation) {
switch(operation) {
case DataProcessingOperation::TST:
case DataProcessingOperation::TEQ:
case DataProcessingOperation::CMP:
case DataProcessingOperation::CMN:
return true;
default: return false;
}
}
struct DataProcessingFlags {
constexpr DataProcessingFlags(uint8_t flags) noexcept : flags_(flags) {}
/// @returns The operation to apply.
constexpr DataProcessingOperation operation() const {
return DataProcessingOperation((flags_ >> (21 - FlagsStartBit)) & 0xf);
}
/// @returns @c true if operand 2 is defined by the @c rotate() and @c immediate() fields;
/// @c false if it is defined by the @c shift_*() and @c operand2() fields.
constexpr bool operand2_is_immediate() const { return flag_bit<25>(flags_); }
/// @c true if the status register should be updated; @c false otherwise.
constexpr bool set_condition_codes() const { return flag_bit<20>(flags_); }
private:
uint8_t flags_;
};
struct DataProcessing: public WithShiftControlBits {
using WithShiftControlBits::WithShiftControlBits;
/// The destination register index. i.e. Rd.
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
/// The operand 1 register index. i.e. Rn.
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
//
// Immediate values for operand 2.
//
/// An 8-bit value to rotate right @c rotate() places if @c operand2_is_immediate() is @c true; meaningless otherwise.
uint32_t immediate() const { return opcode_ & 0xff; }
/// The number of bits to rotate @c immediate() by to the right if @c operand2_is_immediate() is @c true; meaningless otherwise.
uint32_t rotate() const { return (opcode_ >> 7) & 0x1e; }
};
//
// MUL and MLA.
//
struct MultiplyFlags {
constexpr MultiplyFlags(uint8_t flags) noexcept : flags_(flags) {}
/// @c true if the status register should be updated; @c false otherwise.
constexpr bool set_condition_codes() const { return flag_bit<20>(flags_); }
enum class Operation {
MUL, /// Rd = Rm * Rs
MLA, /// Rd = Rm * Rs + Rn
};
/// @returns The operation to apply.
constexpr Operation operation() const {
return flag_bit<21>(flags_) ? Operation::MLA : Operation::MUL;
}
private:
uint8_t flags_;
};
struct Multiply {
constexpr Multiply(uint32_t opcode) noexcept : opcode_(opcode) {}
/// The destination register index. i.e. 'Rd'.
uint32_t destination() const { return (opcode_ >> 16) & 0xf; }
/// The accumulator register index for multiply-add. i.e. 'Rn'.
uint32_t accumulator() const { return (opcode_ >> 12) & 0xf; }
/// The multiplicand register index. i.e. 'Rs'.
uint32_t multiplicand() const { return (opcode_ >> 8) & 0xf; }
/// The multiplier register index. i.e. 'Rm'.
uint32_t multiplier() const { return opcode_ & 0xf; }
private:
uint32_t opcode_;
};
//
// Single data transfer (LDR, STR).
//
struct SingleDataTransferFlags {
constexpr SingleDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
enum class Operation {
LDR, /// Read single byte or word from [base + offset], possibly mutating the base.
STR, /// Write a single byte or word to [base + offset], possibly mutating the base.
};
constexpr Operation operation() const {
return flag_bit<20>(flags_) ? Operation::LDR : Operation::STR;
}
constexpr bool offset_is_register() const { return flag_bit<25>(flags_); }
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
constexpr bool transfer_byte() const { return flag_bit<22>(flags_); }
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
private:
uint8_t flags_;
};
struct SingleDataTransfer: public WithShiftControlBits {
using WithShiftControlBits::WithShiftControlBits;
/// The destination register index. i.e. 'Rd' for LDR.
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
/// The destination register index. i.e. 'Rd' for STR.
uint32_t source() const { return (opcode_ >> 12) & 0xf; }
/// The base register index. i.e. 'Rn'.
uint32_t base() const { return (opcode_ >> 16) & 0xf; }
/// The immediate offset, if @c offset_is_register() was @c false; meaningless otherwise.
uint32_t immediate() const { return opcode_ & 0xfff; }
};
//
// Block data transfer (LDR, STR).
//
struct BlockDataTransferFlags {
constexpr BlockDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
enum class Operation {
LDM, /// Read 116 words from [base], possibly mutating it.
STM, /// Write 1-16 words to [base], possibly mutating it.
};
constexpr Operation operation() const {
return flag_bit<20>(flags_) ? Operation::LDM : Operation::STM;
}
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
constexpr bool load_psr() const { return flag_bit<22>(flags_); }
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
private:
uint8_t flags_;
};
struct BlockDataTransfer: public WithShiftControlBits {
using WithShiftControlBits::WithShiftControlBits;
/// The base register index. i.e. 'Rn'.
uint32_t base() const { return (opcode_ >> 16) & 0xf; }
/// A bitfield indicating which registers to load or store.
uint16_t register_list() const { return static_cast<uint16_t>(opcode_); }
uint32_t popcount() const {
const uint16_t list = register_list();
// TODO: just use std::popcount when adopting C++20.
uint32_t total = ((list & 0xaaaa) >> 1) + (list & 0x5555);
total = ((total & 0xcccc) >> 2) + (total & 0x3333);
total = ((total & 0xf0f0) >> 4) + (total & 0x0f0f);
total = ((total & 0xff00) >> 8) + (total & 0x00ff);
return total;
}
};
//
// Coprocessor data operation.
//
struct CoprocessorDataOperationFlags {
constexpr CoprocessorDataOperationFlags(uint8_t flags) noexcept : flags_(flags) {}
constexpr int coprocessor_operation() const { return (flags_ >> (FlagsStartBit - 20)) & 0xf; }
private:
uint8_t flags_;
};
struct CoprocessorDataOperation {
constexpr CoprocessorDataOperation(uint32_t opcode) noexcept : opcode_(opcode) {}
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
uint32_t operand2() const { return opcode_ & 0xf; }
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; }
uint32_t information() const { return (opcode_ >> 5) & 0x7; }
private:
uint32_t opcode_;
};
//
// Coprocessor register transfer.
//
struct CoprocessorRegisterTransferFlags {
constexpr CoprocessorRegisterTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
enum class Operation {
MRC, /// Move from coprocessor register to ARM register.
MCR, /// Move from ARM register to coprocessor register.
};
constexpr Operation operation() const {
return flag_bit<20>(flags_) ? Operation::MRC : Operation::MCR;
}
constexpr int coprocessor_operation() const { return (flags_ >> (FlagsStartBit - 20)) & 0x7; }
private:
uint8_t flags_;
};
struct CoprocessorRegisterTransfer {
constexpr CoprocessorRegisterTransfer(uint32_t opcode) noexcept : opcode_(opcode) {}
uint32_t operand1() const { return (opcode_ >> 16) & 0xf; }
uint32_t operand2() const { return opcode_ & 0xf; }
uint32_t destination() const { return (opcode_ >> 12) & 0xf; }
uint32_t coprocessor() const { return (opcode_ >> 8) & 0xf; }
uint32_t information() const { return (opcode_ >> 5) & 0x7; }
private:
uint32_t opcode_;
};
//
// Coprocessor data transfer.
//
struct CoprocessorDataTransferFlags {
constexpr CoprocessorDataTransferFlags(uint8_t flags) noexcept : flags_(flags) {}
enum class Operation {
LDC, /// Coprocessor data transfer load.
STC, /// Coprocessor data transfer store.
};
constexpr Operation operation() const {
return flag_bit<20>(flags_) ? Operation::LDC : Operation::STC;
}
constexpr bool pre_index() const { return flag_bit<24>(flags_); }
constexpr bool add_offset() const { return flag_bit<23>(flags_); }
constexpr bool transfer_length() const { return flag_bit<22>(flags_); }
constexpr bool write_back_address() const { return flag_bit<21>(flags_); }
private:
uint8_t flags_;
};
//
// Software interrupt.
//
struct SoftwareInterrupt {
constexpr SoftwareInterrupt(uint32_t opcode) noexcept : opcode_(opcode) {}
/// The 24-bit comment field, often decoded by the receiver of an SWI.
uint32_t comment() const { return opcode_ & 0xff'ffff; }
private:
uint32_t opcode_;
};
struct CoprocessorDataTransfer {
constexpr CoprocessorDataTransfer(uint32_t opcode) noexcept : opcode_(opcode) {}
int base() const { return (opcode_ >> 16) & 0xf; }
int source() const { return (opcode_ >> 12) & 0xf; }
int destination() const { return (opcode_ >> 12) & 0xf; }
int coprocessor() const { return (opcode_ >> 8) & 0xf; }
int offset() const { return opcode_ & 0xff; }
private:
uint32_t opcode_;
};
/// Operation mapper; use the free function @c dispatch as defined below.
template <Model>
struct OperationMapper {
static Condition condition(uint32_t instruction) {
return Condition(instruction >> 28);
}
template <int i, typename SchedulerT>
static void dispatch(uint32_t instruction, SchedulerT &scheduler) {
// Put the 8-bit segment of instruction back into its proper place;
// this allows all the tests below to be written so as to coordinate
// properly with the data sheet, and since it's all compile-time work
// it doesn't cost anything.
constexpr auto partial = uint32_t(i << 20);
// Cf. the ARM2 datasheet, p.45. Tests below match its ordering
// other than that 'undefined' is the fallthrough case. More specific
// page references are provided were more detailed versions of the
// decoding are depicted.
// Multiply and multiply-accumulate (MUL, MLA); cf. p.23.
//
// This usurps a potential data processing decoding, so needs priority.
if constexpr (((partial >> 22) & 0b111'111) == 0b000'000) {
// This implementation provides only eight bits baked into the template parameters so
// an additional dynamic test is required to check whether this is really, really MUL or MLA.
if((instruction & 0b1111'0000) == 0b1001'0000) {
scheduler.template perform<i>(Multiply(instruction));
return;
}
}
// Data processing; cf. p.17.
if constexpr (((partial >> 26) & 0b11) == 0b00) {
scheduler.template perform<i>(DataProcessing(instruction));
return;
}
// Single data transfer (LDR, STR); cf. p.25.
if constexpr (((partial >> 26) & 0b11) == 0b01) {
scheduler.template perform<i>(SingleDataTransfer(instruction));
return;
}
// Block data transfer (LDM, STM); cf. p.29.
if constexpr (((partial >> 25) & 0b111) == 0b100) {
scheduler.template perform<i>(BlockDataTransfer(instruction));
return;
}
// Branch and branch with link (B, BL); cf. p.15.
if constexpr (((partial >> 25) & 0b111) == 0b101) {
scheduler.template perform<i>(Branch(instruction));
return;
}
// Software interreupt; cf. p.35.
if constexpr (((partial >> 24) & 0b1111) == 0b1111) {
scheduler.software_interrupt(SoftwareInterrupt(instruction));
return;
}
// Both:
// Coprocessor data operation; cf. p. 37; and
// Coprocessor register transfers; cf. p. 42.
if constexpr (((partial >> 24) & 0b1111) == 0b1110) {
if(instruction & (1 << 4)) {
// Register transfer.
scheduler.template perform<i>(CoprocessorRegisterTransfer(instruction));
} else {
// Data operation.
scheduler.template perform<i>(CoprocessorDataOperation(instruction));
}
return;
}
// Coprocessor data transfers; cf. p.39.
if constexpr (((partial >> 25) & 0b111) == 0b110) {
scheduler.template perform<i>(CoprocessorDataTransfer(instruction));
return;
}
// Fallback position.
scheduler.unknown();
}
};
/// A brief documentation of the interface expected by @c dispatch below; will be a concept if/when this project adopts C++20.
struct SampleScheduler {
/// @returns @c true if the rest of the instruction should be decoded and supplied
/// to the scheduler as defined below; @c false otherwise.
bool should_schedule(Condition condition);
// Template argument:
//
// Flags, an opaque type which can be converted into a DataProcessingFlags, MultiplyFlags, etc,
// by simple construction, to provide all flags that can be baked into the template parameters.
//
// Function argument:
//
// An operation-specific encapsulation of the operation code for decoding of fields that didn't
// fit into the template parameters.
//
// Either or both may be omitted if unnecessary.
template <Flags> void perform(DataProcessing);
template <Flags> void perform(Multiply);
template <Flags> void perform(SingleDataTransfer);
template <Flags> void perform(BlockDataTransfer);
template <Flags> void perform(Branch);
template <Flags> void perform(CoprocessorRegisterTransfer);
template <Flags> void perform(CoprocessorDataOperation);
template <Flags> void perform(CoprocessorDataTransfer);
// Irregular operations.
void software_interrupt(SoftwareInterrupt);
void unknown();
};
/// Decodes @c instruction, making an appropriate call into @c scheduler.
///
/// In lieu of C++20, see the sample definition of SampleScheduler above for the expected interface.
template <Model model, typename SchedulerT> void dispatch(uint32_t instruction, SchedulerT &scheduler) {
OperationMapper<model> mapper;
// Test condition.
const auto condition = mapper.condition(instruction);
if(!scheduler.should_schedule(condition)) {
return;
}
// Dispatch body.
Reflection::dispatch(mapper, (instruction >> FlagsStartBit) & 0xff, instruction, scheduler);
}
}

View File

@@ -0,0 +1,388 @@
//
// Status.hpp
// Clock Signal
//
// Created by Thomas Harte on 25/02/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "OperationMapper.hpp"
#include <array>
#include <cstdint>
namespace InstructionSet::ARM {
namespace ConditionCode {
static constexpr uint32_t Negative = 1u << 31;
static constexpr uint32_t Zero = 1u << 30;
static constexpr uint32_t Carry = 1u << 29;
static constexpr uint32_t Overflow = 1u << 28;
static constexpr uint32_t IRQDisable = 1u << 27;
static constexpr uint32_t FIQDisable = 1u << 26;
static constexpr uint32_t Mode = (1u << 1) | (1u << 0);
static constexpr uint32_t Address = FIQDisable - Mode - 1;
}
enum class Mode {
User = 0b00,
FIQ = 0b01,
IRQ = 0b10,
Supervisor = 0b11,
};
/// Combines the ARM registers and status flags into a single whole, given that the architecture
/// doesn't have the same degree of separation as others.
///
/// The PC contained here is always taken to be **the address of the current instruction + 4**,
/// i.e. whatever should be executed next, disregarding pipeline differences.
///
/// Appropriate prefetch offsets are left to other code to handle.
/// This is to try to keep this structure independent of a specific ARM implementation.
struct Registers {
public:
// Don't allow copying.
Registers(const Registers &) = delete;
Registers &operator =(const Registers &) = delete;
Registers() = default;
/// Sets the N and Z flags according to the value of @c result.
void set_nz(uint32_t value) {
zero_result_ = negative_flag_ = value;
}
/// Sets C if @c value is non-zero; resets it otherwise.
void set_c(uint32_t value) {
carry_flag_ = value;
}
/// @returns @c 1 if carry is set; @c 0 otherwise.
uint32_t c() const {
return carry_flag_ ? 1 : 0;
}
/// Sets V if the highest bit of @c value is set; resets it otherwise.
void set_v(uint32_t value) {
overflow_flag_ = value;
}
/// @returns The current status bits, separate from the PC — mode, NVCZ and the two interrupt flags.
uint32_t status() const {
return
uint32_t(mode_) |
(negative_flag_ & ConditionCode::Negative) |
(zero_result_ ? 0 : ConditionCode::Zero) |
(carry_flag_ ? ConditionCode::Carry : 0) |
((overflow_flag_ >> 3) & ConditionCode::Overflow) |
interrupt_flags_;
}
/// @returns The full PC + status bits.
uint32_t pc_status(uint32_t offset) const {
return
((active_[15] + offset) & ConditionCode::Address) |
status();
}
/// Sets status bits only, subject to mode.
void set_status(uint32_t status) {
// ... in user mode the other flags (I, F, M1, M0) are protected from direct change
// but in non-user modes these will also be affected, accepting copies of bits 27, 26,
// 1 and 0 of the result respectively.
negative_flag_ = status;
overflow_flag_ = status << 3;
carry_flag_ = status & ConditionCode::Carry;
zero_result_ = ~status & ConditionCode::Zero;
if(mode_ != Mode::User) {
set_mode(Mode(status & 3));
interrupt_flags_ = status & (ConditionCode::IRQDisable | ConditionCode::FIQDisable);
}
}
/// @returns The current mode.
Mode mode() const {
return mode_;
}
/// Sets a new PC.
void set_pc(uint32_t value) {
active_[15] = value & ConditionCode::Address;
}
/// @returns The stored PC plus @c offset limited to 26 bits.
uint32_t pc(uint32_t offset) const {
return (active_[15] + offset) & ConditionCode::Address;
}
// MARK: - Exceptions.
enum class Exception {
/// Reset line went from high to low.
Reset = 0x00,
/// Either an undefined instruction or a coprocessor instruction for which no coprocessor was found.
UndefinedInstruction = 0x04,
/// Code executed a software interrupt.
SoftwareInterrupt = 0x08,
/// The memory subsystem indicated an abort during prefetch and that instruction has now come to the head of the queue.
PrefetchAbort = 0x0c,
/// The memory subsystem indicated an abort during an instruction; if it is an LDR or STR then this should be signalled
/// before any instruction execution. If it was an LDM then loading stops upon a data abort but both an LDM and STM
/// otherwise complete, including pointer writeback.
DataAbort = 0x10,
/// The first data transfer attempted within an instruction was above address 0x3ff'ffff.
Address = 0x14,
/// The IRQ line was low at the end of an instruction and ConditionCode::IRQDisable was not set.
IRQ = 0x18,
/// The FIQ went low at least one cycle ago and ConditionCode::FIQDisable was not set.
FIQ = 0x1c,
};
static constexpr uint32_t pc_offset_during(Exception exception) {
// The below is somewhat convoluted by the assumed execution model:
//
// * exceptions occuring during execution of an instruction are taken
// to occur after R15 has already been incremented by 4; but
// * exceptions occurring instead of execution of an instruction are
// taken to occur with R15 pointing to an instruction that hasn't begun.
//
// i.e. in net R15 always refers to the next instruction
// that has not yet started.
switch(exception) {
// "To return normally from FIQ use SUBS PC, R14_fiq, #4".
case Exception::FIQ: return 4;
// "To return normally from IRQ use SUBS PC, R14_irq, #4".
case Exception::IRQ: return 4;
// "If a return is required from [address exception traps], use
// SUBS PC, R14_svc, #4. This will return to the instruction after
// the one causing the trap".
case Exception::Address: return 4;
// "A Data Abort requires [work before a return], the return being
// done by SUBS PC, R14_svc, #8" (and returning to the instruction
// that aborted).
case Exception::DataAbort: return 4;
// "To continue after a Prefetch Abort use SUBS PC, R14_svc #4.
// This will attempt to re-execute the aborting instruction."
case Exception::PrefetchAbort: return 4;
// "To return from a SWI, use MOVS PC, R14_svc. This returns to the instruction
// following the SWI".
case Exception::SoftwareInterrupt: return 0;
// "To return from [an undefined instruction trap] use MOVS PC, R14_svc.
// This returns to the instruction following the undefined instruction".
case Exception::UndefinedInstruction: return 0;
// Unspecified; a guess.
case Exception::Reset: return 0;
}
return 4;
}
/// Updates the program counter, interupt flags and link register if applicable to begin @c exception.
template <Exception type>
void exception() {
const auto r14 = pc_status(pc_offset_during(type));
switch(type) {
case Exception::IRQ: set_mode(Mode::IRQ); break;
case Exception::FIQ: set_mode(Mode::FIQ); break;
default: set_mode(Mode::Supervisor); break;
}
active_[14] = r14;
interrupt_flags_ |= ConditionCode::IRQDisable;
if constexpr (type == Exception::Reset || type == Exception::FIQ) {
interrupt_flags_ |= ConditionCode::FIQDisable;
}
set_pc(uint32_t(type));
}
/// Returns @c true if: (i) the exception type is IRQ or FIQ; and (ii) the processor is currently accepting such interrupts.
/// Otherwise returns @c false.
template <Exception type>
bool would_interrupt() {
switch(type) {
case Exception::IRQ:
if(interrupt_flags_ & ConditionCode::IRQDisable) {
return false;
}
break;
case Exception::FIQ:
if(interrupt_flags_ & ConditionCode::FIQDisable) {
return false;
}
break;
default: return false;
}
return true;
}
// MARK: - Condition tests.
/// @returns @c true if @c condition tests as true; @c false otherwise.
bool test(Condition condition) const {
const auto ne = [&]() -> bool {
return zero_result_;
};
const auto cs = [&]() -> bool {
return carry_flag_;
};
const auto mi = [&]() -> bool {
return negative_flag_ & ConditionCode::Negative;
};
const auto vs = [&]() -> bool {
return overflow_flag_ & ConditionCode::Negative;
};
const auto hi = [&]() -> bool {
return carry_flag_ && zero_result_;
};
const auto lt = [&]() -> bool {
return (negative_flag_ ^ overflow_flag_) & ConditionCode::Negative;
};
const auto le = [&]() -> bool {
return !zero_result_ || lt();
};
switch(condition) {
case Condition::EQ: return !ne();
case Condition::NE: return ne();
case Condition::CS: return cs();
case Condition::CC: return !cs();
case Condition::MI: return mi();
case Condition::PL: return !mi();
case Condition::VS: return vs();
case Condition::VC: return !vs();
case Condition::HI: return hi();
case Condition::LS: return !hi();
case Condition::GE: return !lt();
case Condition::LT: return lt();
case Condition::GT: return !le();
case Condition::LE: return le();
case Condition::AL: return true;
case Condition::NV: return false;
}
return false;
}
/// Sets current execution mode.
void set_mode(Mode target_mode) {
if(mode_ == target_mode) {
return;
}
// For outgoing modes other than FIQ, only save the final two registers for now;
// if the incoming mode is FIQ then the other five will be saved in the next switch.
// For FIQ, save all seven up front.
switch(mode_) {
// FIQ outgoing: save R8 to R14.
case Mode::FIQ:
std::copy(active_.begin() + 8, active_.begin() + 15, fiq_registers_.begin());
break;
// Non-FIQ outgoing: save R13 and R14. If saving to the user registers,
// use only the final two slots.
case Mode::User:
std::copy(active_.begin() + 13, active_.begin() + 15, user_registers_.begin() + 5);
break;
case Mode::Supervisor:
std::copy(active_.begin() + 13, active_.begin() + 15, supervisor_registers_.begin());
break;
case Mode::IRQ:
std::copy(active_.begin() + 13, active_.begin() + 15, irq_registers_.begin());
break;
}
// For all modes except FIQ: restore the final two registers to their appropriate values.
// For FIQ: save an additional five, then overwrite seven.
switch(target_mode) {
case Mode::FIQ:
// FIQ is incoming, so save registers 8 to 12 to the first five slots of the user registers.
std::copy(active_.begin() + 8, active_.begin() + 13, user_registers_.begin());
// Replace R8 to R14.
std::copy(fiq_registers_.begin(), fiq_registers_.end(), active_.begin() + 8);
break;
case Mode::User:
std::copy(user_registers_.begin() + 5, user_registers_.end(), active_.begin() + 13);
break;
case Mode::Supervisor:
std::copy(supervisor_registers_.begin(), supervisor_registers_.end(), active_.begin() + 13);
break;
case Mode::IRQ:
std::copy(irq_registers_.begin(), irq_registers_.end(), active_.begin() + 13);
break;
}
// If FIQ is outgoing then there's another five registers to restore.
if(mode_ == Mode::FIQ) {
std::copy(user_registers_.begin(), user_registers_.begin() + 5, active_.begin() + 8);
}
mode_ = target_mode;
}
uint32_t &operator[](uint32_t offset) {
return active_[static_cast<size_t>(offset)];
}
uint32_t operator[](uint32_t offset) const {
return active_[static_cast<size_t>(offset)];
}
/// @returns A reference to the register at @c offset. If @c force_user_mode is true,
/// this will the the user-mode register. Otherwise it'll be that for the current mode. These references
/// are guaranteed to remain valid only until the next mode change.
uint32_t &reg(bool force_user_mode, uint32_t offset) {
switch(mode_) {
default:
case Mode::User: return active_[offset];
case Mode::Supervisor:
case Mode::IRQ:
if(force_user_mode && (offset == 13 || offset == 14)) {
return user_registers_[offset - 8];
}
return active_[offset];
case Mode::FIQ:
if(force_user_mode && (offset >= 8 && offset < 15)) {
return user_registers_[offset - 8];
}
return active_[offset];
}
}
private:
Mode mode_ = Mode::Supervisor;
uint32_t zero_result_ = 1;
uint32_t negative_flag_ = 0;
uint32_t interrupt_flags_ = ConditionCode::IRQDisable | ConditionCode::FIQDisable;
uint32_t carry_flag_ = 0;
uint32_t overflow_flag_ = 0;
// Various shadow registers.
std::array<uint32_t, 7> user_registers_{};
std::array<uint32_t, 7> fiq_registers_{};
std::array<uint32_t, 2> irq_registers_{};
std::array<uint32_t, 2> supervisor_registers_{};
// The active register set.
std::array<uint32_t, 16> active_{};
};
}

View File

@@ -222,7 +222,7 @@ struct Instruction {
Instruction(Operation operation, AddressingMode addressing_mode, uint8_t opcode) : operation(operation), addressing_mode(addressing_mode), opcode(opcode) {}
Instruction(uint8_t opcode) : opcode(opcode) {}
Instruction() {}
Instruction() = default;
};
/*!

View File

@@ -478,7 +478,7 @@ class Preinstruction {
static constexpr int SizeShift = 0;
};
Preinstruction() {}
Preinstruction() = default;
/// Produces a string description of this instruction; if @c opcode
/// is supplied then any quick fields in this instruction will be decoded;

View File

@@ -1362,9 +1362,9 @@ struct Instruction {
bool is_supervisor = false;
uint32_t opcode = 0;
Instruction() noexcept {}
Instruction(uint32_t opcode) noexcept : opcode(opcode) {}
Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {}
constexpr Instruction() noexcept = default;
constexpr Instruction(uint32_t opcode) noexcept : opcode(opcode) {}
constexpr Instruction(Operation operation, uint32_t opcode, bool is_supervisor = false) noexcept : operation(operation), is_supervisor(is_supervisor), opcode(opcode) {}
// Instruction fields are decoded below; naming is a compromise between
// Motorola's documentation and IBM's.

View File

@@ -1,8 +1,9 @@
# Instruction Sets
# Instruction Sets
Code in here provides the means to disassemble, and to execute code for certain instruction sets.
It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So:
* it doesn't involve itself in the actual bus signalling of real processors; and
* instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete.
@@ -13,6 +14,7 @@ This part of CLK is intended primarily to provide disassembly services for stati
A decoder extracts fully-decoded instructions from a data stream for its associated architecture.
The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least:
* the operation in use;
* its addressing mode; and
* relevant registers.
@@ -20,6 +22,7 @@ The meaning of 'fully-decoded' is flexible but it means that a caller can easily
It may be assumed that callers will have access to the original data stream for immediate values, if it is sensible to do so.
In deciding what to expose, what to store ahead of time and what to obtain just-in-time a decoder should have an eye on two principal consumers:
1. disassemblers; and
2. instruction executors.
@@ -50,6 +53,7 @@ A sample interface:
std::pair<int, Instruction> decode(word_type *stream, size_t length) { ... }
In this sample the returned pair provides an `int` size that is one of:
* a positive number, indicating a completed decoding that consumed that many `word_type`s; or
* a negative number, indicating the [negatived] minimum number of `word_type`s that the caller should try to get hold of before calling `decode` again.
@@ -58,6 +62,7 @@ A caller is permitted to react in any way it prefers to negative numbers; they'r
## Parsers
A parser sits one level above a decoder; it is handed:
* a start address;
* a closing bound; and
* a target.
@@ -65,6 +70,7 @@ A parser sits one level above a decoder; it is handed:
It is responsible for parsing the instruction stream from the start address up to and not beyond the closing bound, and no further than any unconditional branches.
It should post to the target:
* any instructions fully decoded;
* any conditional branch destinations encountered;
* any immediately-knowable accessed addresses; and
@@ -75,6 +81,7 @@ So a parser has the same two primary potential recipients as a decoder: diassemb
## Executors
An executor is responsible for only one thing:
* mapping from decoded instructions to objects that can perform those instructions.
An executor is assumed to bundle all the things that go into instruction set execution: processor state and memory, alongside a parser.

View File

@@ -564,7 +564,7 @@ constexpr Operation rep_operation(Operation operation, Repetition repetition) {
/// It cannot natively describe a base of ::None.
class ScaleIndexBase {
public:
constexpr ScaleIndexBase() noexcept {}
constexpr ScaleIndexBase() noexcept = default;
constexpr ScaleIndexBase(uint8_t sib) noexcept : sib_(sib) {}
constexpr ScaleIndexBase(int scale, Source index, Source base) noexcept :
sib_(uint8_t(
@@ -703,7 +703,7 @@ template<bool is_32bit> class Instruction {
using ImmediateT = typename std::conditional<is_32bit, uint32_t, uint16_t>::type;
using AddressT = ImmediateT;
constexpr Instruction() noexcept {}
constexpr Instruction() noexcept = default;
constexpr Instruction(Operation operation) noexcept :
Instruction(operation, Source::None, Source::None, ScaleIndexBase(), false, AddressSize::b16, Source::None, DataSize::None, 0, 0) {}
constexpr Instruction(

View File

@@ -0,0 +1,752 @@
//
// Archimedes.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#include "Archimedes.hpp"
#include "HalfDuplexSerial.hpp"
#include "InputOutputController.hpp"
#include "Keyboard.hpp"
#include "KeyboardMapper.hpp"
#include "MemoryController.hpp"
#include "Sound.hpp"
#include "../../../Configurable/Configurable.hpp"
#include "../../AudioProducer.hpp"
#include "../../KeyboardMachine.hpp"
#include "../../MediaTarget.hpp"
#include "../../MouseMachine.hpp"
#include "../../ScanProducer.hpp"
#include "../../TimedMachine.hpp"
#include "../../../Activity/Source.hpp"
#include "../../../InstructionSets/ARM/Disassembler.hpp"
#include "../../../InstructionSets/ARM/Executor.hpp"
#include "../../../Outputs/Log.hpp"
#include "../../../Components/I2C/I2C.hpp"
#include "../../../Analyser/Static/Acorn/Target.hpp"
#include <algorithm>
#include <array>
#include <set>
#include <vector>
namespace Archimedes {
class ConcreteMachine:
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaTarget,
public MachineTypes::MouseMachine,
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
public Activity::Source,
public Configurable::Device
{
private:
Log::Logger<Log::Source::Archimedes> logger;
// This fictitious clock rate just means '24 MIPS, please'; it's divided elsewhere.
static constexpr int ClockRate = 24'000'000;
// Runs for 24 cycles, distributing calls to the various ticking subsystems
// 'correctly' (i.e. correctly for the approximation in use).
//
// The implementation of this is coupled to the ClockRate above, hence its
// appearance here.
template <int video_divider, bool original_speed>
void macro_tick() {
macro_counter_ -= 24;
// This is a 24-cycle window, so at 24Mhz macro_tick() is called at 1Mhz.
// Hence, required ticks are:
//
// * CPU: 24;
// * video: 24 / video_divider;
// * floppy: 8;
// * timers: 2;
// * sound: 1.
tick_cpu_video<0, video_divider, original_speed>(); tick_cpu_video<1, video_divider, original_speed>();
tick_cpu_video<2, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<3, video_divider, original_speed>(); tick_cpu_video<4, video_divider, original_speed>();
tick_cpu_video<5, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<6, video_divider, original_speed>(); tick_cpu_video<7, video_divider, original_speed>();
tick_cpu_video<8, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<9, video_divider, original_speed>(); tick_cpu_video<10, video_divider, original_speed>();
tick_cpu_video<11, video_divider, original_speed>(); tick_floppy();
tick_timers();
tick_cpu_video<12, video_divider, original_speed>(); tick_cpu_video<13, video_divider, original_speed>();
tick_cpu_video<14, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<15, video_divider, original_speed>(); tick_cpu_video<16, video_divider, original_speed>();
tick_cpu_video<17, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<18, video_divider, original_speed>(); tick_cpu_video<19, video_divider, original_speed>();
tick_cpu_video<20, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<21, video_divider, original_speed>(); tick_cpu_video<22, video_divider, original_speed>();
tick_cpu_video<23, video_divider, original_speed>(); tick_floppy();
tick_timers();
tick_sound();
}
int macro_counter_ = 0;
template <int offset, int video_divider, bool original_speed>
void tick_cpu_video() {
if constexpr (!(offset % video_divider)) {
tick_video();
}
// Debug mode: run CPU a lot slower. Actually at close to original advertised MIPS speed.
if constexpr (original_speed && (offset & 7)) return;
if constexpr (offset & 1) return;
tick_cpu();
}
public:
ConcreteMachine(
const Analyser::Static::Acorn::ArchimedesTarget &target,
const ROMMachine::ROMFetcher &rom_fetcher
) : executor_(*this, *this, *this) {
set_clock_rate(ClockRate);
constexpr ROM::Name risc_os = ROM::Name::AcornRISCOS311;
ROM::Request request(risc_os);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
executor_.bus.set_rom(roms.find(risc_os)->second);
insert_media(target.media);
if(!target.media.disks.empty()) {
autoload_phase_ = AutoloadPhase::WaitingForStartup;
target_program_ = target.main_program;
}
fill_pipeline(0);
}
void update_interrupts() {
using Exception = InstructionSet::ARM::Registers::Exception;
const int requests = executor_.bus.interrupt_mask();
if((requests & InterruptRequests::FIQ) && executor_.registers().would_interrupt<Exception::FIQ>()) {
pipeline_.reschedule(Pipeline::SWISubversion::FIQ);
return;
}
if((requests & InterruptRequests::IRQ) && executor_.registers().would_interrupt<Exception::IRQ>()) {
pipeline_.reschedule(Pipeline::SWISubversion::IRQ);
}
}
void did_set_status() {
// This might have been a change of mode, so...
trans_ = executor_.registers().mode() == InstructionSet::ARM::Mode::User;
fill_pipeline(executor_.pc());
update_interrupts();
}
void did_set_pc() {
fill_pipeline(executor_.pc());
}
bool should_swi(uint32_t comment) {
using Exception = InstructionSet::ARM::Registers::Exception;
using SWISubversion = Pipeline::SWISubversion;
switch(pipeline_.swi_subversion()) {
case Pipeline::SWISubversion::None: {
// TODO: 400C1 to intercept create window 400C1 and positioning; then
// plot icon 400e2 to listen for icons in window. That'll give a click area.
// Probably also 400c2 which seems to be used to add icons to the icon bar.
//
// 400D4 for menus?
const auto get_string = [&](uint32_t address, bool indirect) -> std::string {
std::string desc;
if(indirect) {
executor_.bus.read(address, address, false);
}
while(true) {
uint8_t next = 0;
executor_.bus.read(address, next, false);
if(next < 0x20) break;
desc.push_back(static_cast<char>(next) & 0x7f);
++address;
}
return desc;
};
const uint32_t swi_code = comment & static_cast<uint32_t>(~(1 << 17));
switch(swi_code) {
//
// Passive monitoring traps, for automatic loading.
//
case 0x400e3: // Wimp_SetMode
case 0x65: // OS_ScreenMode
case 0x3f: // OS_CheckModeValid
if(autoload_phase_ == AutoloadPhase::OpeningProgram) {
autoload_phase_ = AutoloadPhase::Ended;
}
break;
case 0x400d4: {
if(autoload_phase_ == AutoloadPhase::TestingMenu) {
autoload_phase_ = AutoloadPhase::Ended;
uint32_t address = executor_.registers()[1] + 28;
bool should_left_click = true;
while(true) {
uint32_t icon_flags;
uint32_t item_flags;
executor_.bus.read(address, item_flags, false);
executor_.bus.read(address + 8, icon_flags, false);
auto desc = get_string(address + 12, icon_flags & (1 << 8));
should_left_click &=
(desc == "Info") ||
(desc == "Quit");
address += 24;
if(item_flags & (1 << 7)) break;
}
if(should_left_click) {
// Exit the application menu, then click once further to launch.
CursorActionBuilder(cursor_actions_)
.move_to(IconBarProgramX - 128, IconBarY - 32)
.click(0)
.move_to(IconBarProgramX, IconBarY)
.click(0);
}
}
} break;
// Wimp_OpenWindow.
case 0x400c5: {
const uint32_t address = executor_.registers()[1];
uint32_t x1, y1, x2, y2;
executor_.bus.read(address + 4, x1, false);
executor_.bus.read(address + 8, y1, false);
executor_.bus.read(address + 12, x2, false);
executor_.bus.read(address + 16, y2, false);
switch(autoload_phase_) {
default: break;
case AutoloadPhase::WaitingForDiskContents: {
autoload_phase_ = AutoloadPhase::WaitingForTargetIcon;
// Crib top left of window content.
target_window_[0] = static_cast<int32_t>(x1);
target_window_[1] = static_cast<int32_t>(y2);
} break;
case AutoloadPhase::WaitingForStartup:
if(static_cast<int32_t>(y1) == -268435472) { // VERY TEMPORARY. TODO: find better trigger.
// Creation of any icon is used to spot that RISC OS has started up.
//
// Wait a further second, mouse down to (32, 240), left click.
// That'll trigger disk access. Then move up to the top left,
// in anticipation of the appearance of a window.
auto &builder = CursorActionBuilder(cursor_actions_)
.move_to(IconBarDriveX, IconBarY)
.click(0);
if(target_program_.empty()) {
autoload_phase_ = AutoloadPhase::Ended;
} else {
autoload_phase_ = AutoloadPhase::WaitingForDiskContents;
builder.move_to(IconBarDriveX, 80); // Just a guess of 'close' to where the program to launch
// will probably be, to have the cursor broadly nearby.
}
}
break;
}
} break;
// Wimp_CreateIcon, which also adds to the icon bar.
case 0x400c2:
switch(autoload_phase_) {
case AutoloadPhase::OpeningProgram: {
const uint32_t address = executor_.registers()[1];
uint32_t handle;
executor_.bus.read(address, handle, false);
// Test whether the program has added an icon on the right.
if(static_cast<int32_t>(handle) == -1) {
CursorActionBuilder(cursor_actions_)
.move_to(IconBarProgramX, IconBarY)
.click(1);
autoload_phase_ = AutoloadPhase::TestingMenu;
}
} break;
default: break;
}
break;
// Wimp_PlotIcon.
case 0x400e2: {
if(autoload_phase_ == AutoloadPhase::WaitingForTargetIcon) {
const uint32_t address = executor_.registers()[1];
uint32_t flags;
executor_.bus.read(address + 16, flags, false);
std::string desc;
if(flags & 1) {
desc = get_string(address + 20, flags & (1 << 8));
}
if(desc == target_program_) {
uint32_t x1, y1, x2, y2;
executor_.bus.read(address + 0, x1, false);
executor_.bus.read(address + 4, y1, false);
executor_.bus.read(address + 8, x2, false);
executor_.bus.read(address + 12, y2, false);
autoload_phase_ = AutoloadPhase::OpeningProgram;
// Some default icon sizing assumptions are baked in here.
const auto x_target = target_window_[0] + (static_cast<int32_t>(x1) + static_cast<int32_t>(x2)) / 2;
const auto y_target = target_window_[1] + static_cast<int32_t>(y1) + 24;
CursorActionBuilder(cursor_actions_)
.move_to(x_target >> 1, 256 - (y_target >> 2))
.double_click(0);
}
}
} break;
}
} return true;
case SWISubversion::DataAbort:
// executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::DataAbort>();
break;
// FIQ and IRQ decrement the PC because their apperance in the pipeline causes
// it to look as though they were fetched, but they weren't.
case SWISubversion::FIQ:
executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::FIQ>();
break;
case SWISubversion::IRQ:
executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::IRQ>();
break;
}
did_set_pc();
return false;
}
void update_clock_rates() {
video_divider_ = executor_.bus.video().clock_divider();
}
private:
// MARK: - ScanProducer.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
executor_.bus.video().crt().set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const override {
return executor_.bus.video().crt().get_scaled_scan_status() * video_divider_;
}
// MARK: - TimedMachine.
bool use_original_speed() const {
#ifndef NDEBUG
// Debug mode: always run 'slowly' because that's less of a burden, and
// because it allows me to peer at problems with greater leisure.
return true;
#else
// As a first, blunt implementation: try to model something close
// to original speed if there have been 10 frame rate overages in total.
return executor_.bus.video().frame_rate_overages() > 10;
#endif
}
int video_divider_ = 1;
void run_for(Cycles cycles) override {
const auto run = [&](Cycles cycles) {
if(use_original_speed()) run_for<true>(cycles);
else run_for<false>(cycles);
};
//
// Short-circuit: no cursor actions means **just run**.
//
if(cursor_actions_.empty()) {
run(cycles);
return;
}
//
// Mouse scripting; tick at a minimum of frame length.
//
static constexpr int TickFrequency = 24'000'000 / 50;
cursor_action_subcycle_ += cycles;
auto segments = cursor_action_subcycle_.divide(Cycles(TickFrequency)).as<int>();
while(segments--) {
Cycles next = Cycles(TickFrequency);
if(next > cycles) next = cycles;
cycles -= next;
if(!cursor_actions_.empty()) {
const auto move_to_next = [&]() {
cursor_action_waited_ = 0;
cursor_actions_.erase(cursor_actions_.begin());
};
const auto &action = cursor_actions_.front();
switch(action.type) {
case CursorAction::Type::MoveTo: {
// A measure of where within the tip lies within
// the default RISC OS cursor.
constexpr int ActionPointOffset = 20;
constexpr int MaxStep = 24;
const auto position = executor_.bus.video().cursor_location();
if(!position) break;
const auto [x, y] = *position;
auto x_diff = action.value.move_to.x - (x + ActionPointOffset);
auto y_diff = action.value.move_to.y - y;
if(abs(x_diff) < 2 && abs(y_diff) < 2) {
move_to_next();
break;
}
if(abs(y_diff) > MaxStep || abs(x_diff) > MaxStep) {
if(abs(y_diff) > abs(x_diff)) {
x_diff = (x_diff * MaxStep + (abs(y_diff) >> 1)) / abs(y_diff);
y_diff = std::clamp(y_diff, -MaxStep, MaxStep);
} else {
y_diff = (y_diff * MaxStep + (abs(x_diff) >> 1)) / abs(x_diff);
x_diff = std::clamp(x_diff, -MaxStep, MaxStep);
}
}
get_mouse().move(x_diff, y_diff);
} break;
case CursorAction::Type::Wait:
cursor_action_waited_ += next.as<int>();
if(cursor_action_waited_ >= action.value.wait.duration) {
move_to_next();
}
break;
case CursorAction::Type::Button:
get_mouse().set_button_pressed(action.value.button.button, action.value.button.down);
move_to_next();
break;
case CursorAction::Type::SetPhase:
autoload_phase_ = action.value.set_phase.phase;
move_to_next();
break;
}
}
//
// Execution proper.
//
run(next);
}
}
template <bool original_speed>
void run_for(Cycles cycles) {
macro_counter_ += cycles.as<int>();
while(macro_counter_ > 0) {
switch(video_divider_) {
default: macro_tick<2, original_speed>(); break;
case 3: macro_tick<3, original_speed>(); break;
case 4: macro_tick<4, original_speed>(); break;
case 6: macro_tick<6, original_speed>(); break;
}
}
}
void tick_cpu() {
const uint32_t instruction = advance_pipeline(executor_.pc() + 8);
InstructionSet::ARM::execute(instruction, executor_);
}
void tick_timers() { executor_.bus.tick_timers(); }
void tick_sound() { executor_.bus.sound().tick(); }
void tick_video() { executor_.bus.video().tick(); }
bool accelerate_loading_ = true;
void tick_floppy() {
executor_.bus.tick_floppy(
accelerate_loading_ ? (use_original_speed() ? 12 : 18) : 1
);
}
// MARK: - MediaTarget
bool insert_media(const Analyser::Static::Media &media) override {
size_t c = 0;
for(auto &disk : media.disks) {
executor_.bus.set_disk(disk, c);
c++;
if(c == 4) break;
}
return true;
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->quickload = accelerate_loading_;
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
const auto options = dynamic_cast<Options *>(str.get());
accelerate_loading_ = options->quickload;
}
// MARK: - AudioProducer
Outputs::Speaker::Speaker *get_speaker() override {
return executor_.bus.speaker();
}
// MARK: - Activity::Source.
void set_activity_observer(Activity::Observer *observer) final {
executor_.bus.set_activity_observer(observer);
}
// MARK: - MappedKeyboardMachine.
MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override {
return &keyboard_mapper_;
}
Archimedes::KeyboardMapper keyboard_mapper_;
void set_key_state(uint16_t key, bool is_pressed) override {
executor_.bus.keyboard().set_key_state(key, is_pressed);
}
// MARK: - MouseMachine.
Inputs::Mouse &get_mouse() override {
return executor_.bus.keyboard().mouse();
}
// MARK: - ARM execution.
static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2;
using Executor = InstructionSet::ARM::Executor<arm_model, MemoryController<ConcreteMachine, ConcreteMachine>, ConcreteMachine>;
Executor executor_;
bool trans_ = false;
void fill_pipeline(uint32_t pc) {
if(pipeline_.interrupt_next()) return;
advance_pipeline(pc);
advance_pipeline(pc + 4);
}
uint32_t advance_pipeline(uint32_t pc) {
uint32_t instruction = 0; // Value should never be used; this avoids a spurious GCC warning.
const bool did_read = executor_.bus.read(pc, instruction, trans_);
return pipeline_.exchange(
did_read ? instruction : Pipeline::SWI,
did_read ? Pipeline::SWISubversion::None : Pipeline::SWISubversion::DataAbort);
}
struct Pipeline {
enum SWISubversion: uint8_t {
None,
DataAbort,
IRQ,
FIQ,
};
static constexpr uint32_t SWI = 0xef'000000;
uint32_t exchange(uint32_t next, SWISubversion subversion) {
const uint32_t result = upcoming_[active_].opcode;
latched_subversion_ = upcoming_[active_].subversion;
upcoming_[active_].opcode = next;
upcoming_[active_].subversion = subversion;
active_ ^= 1;
return result;
}
SWISubversion swi_subversion() const {
return latched_subversion_;
}
// TODO: one day, possibly: schedule the subversion one slot further into the future
// (i.e. active_ ^ 1) to allow one further instruction to occur as usual before the
// action paplies. That is, if interrupts take effect one instruction later after a flags
// change, which I don't yet know.
//
// In practice I got into a bit of a race condition between interrupt scheduling and
// flags changes, so have backed off for now.
void reschedule(SWISubversion subversion) {
upcoming_[active_].opcode = SWI;
upcoming_[active_].subversion = subversion;
}
bool interrupt_next() const {
return upcoming_[active_].subversion == SWISubversion::IRQ || upcoming_[active_].subversion == SWISubversion::FIQ;
}
private:
struct Stage {
uint32_t opcode;
SWISubversion subversion = SWISubversion::None;
};
Stage upcoming_[2];
int active_ = 0;
SWISubversion latched_subversion_;
} pipeline_;
// MARK: - Autoload, including cursor scripting.
enum class AutoloadPhase {
WaitingForStartup,
WaitingForDiskContents,
WaitingForTargetIcon,
OpeningProgram,
TestingMenu,
Ended,
};
AutoloadPhase autoload_phase_ = AutoloadPhase::Ended;
std::string target_program_;
struct CursorAction {
enum class Type {
MoveTo,
Button,
Wait,
SetPhase,
} type;
union {
struct {
int x, y;
} move_to;
struct {
int duration;
} wait;
struct {
int button;
bool down;
} button;
struct {
AutoloadPhase phase;
} set_phase;
} value;
static CursorAction move_to(int x, int y) {
CursorAction action;
action.type = Type::MoveTo;
action.value.move_to.x = x;
action.value.move_to.y = y;
return action;
}
static CursorAction wait(int duration) {
CursorAction action;
action.type = Type::Wait;
action.value.wait.duration = duration;
return action;
}
static CursorAction button(int button, bool down) {
CursorAction action;
action.type = Type::Button;
action.value.button.button = button;
action.value.button.down = down;
return action;
}
static CursorAction set_phase(AutoloadPhase phase) {
CursorAction action;
action.type = Type::SetPhase;
action.value.set_phase.phase = phase;
return action;
}
};
std::vector<CursorAction> cursor_actions_;
struct CursorActionBuilder {
CursorActionBuilder(std::vector<CursorAction> &actions) : actions_(actions) {}
CursorActionBuilder &wait(int duration) {
actions_.push_back(CursorAction::wait(duration));
return *this;
}
CursorActionBuilder &move_to(int x, int y) {
// Special case: if this sets a move_to when one is in progress,
// just update the target.
if(!actions_.empty() && actions_.back().type == CursorAction::Type::MoveTo) {
actions_.back().value.move_to.x = x;
actions_.back().value.move_to.y = y;
return *this;
}
actions_.push_back(CursorAction::move_to(x, y));
return *this;
}
CursorActionBuilder &click(int button) {
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
return *this;
}
CursorActionBuilder &double_click(int button) {
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
return *this;
}
CursorActionBuilder &set_phase(AutoloadPhase phase) {
actions_.push_back(CursorAction::set_phase(phase));
return *this;
}
std::vector<CursorAction> &actions_;
};
static constexpr int IconBarY = 240;
static constexpr int IconBarProgramX = 532;
static constexpr int IconBarDriveX = 32;
std::vector<CursorAction> &begin();
Cycles cursor_action_subcycle_;
int cursor_action_waited_;
int32_t target_window_[2];
};
}
using namespace Archimedes;
std::unique_ptr<Machine> Machine::Archimedes(
const Analyser::Static::Target *target,
const ROMMachine::ROMFetcher &rom_fetcher
) {
const auto archimedes_target = dynamic_cast<const Analyser::Static::Acorn::ArchimedesTarget *>(target);
return std::make_unique<ConcreteMachine>(*archimedes_target, rom_fetcher);
}

View File

@@ -0,0 +1,40 @@
//
// Archimedes.hpp
// Clock Signal
//
// Created by Thomas Harte on 04/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../../Configurable/Configurable.hpp"
#include "../../../Configurable/StandardOptions.hpp"
#include "../../../Analyser/Static/StaticAnalyser.hpp"
#include "../../ROMMachine.hpp"
#include <memory>
namespace Archimedes {
class Machine {
public:
virtual ~Machine() = default;
static std::unique_ptr<Machine> Archimedes(
const Analyser::Static::Target *target,
const ROMMachine::ROMFetcher &rom_fetcher
);
class Options: public Reflection::StructImpl<Options>, public Configurable::QuickloadOption<Options> {
friend Configurable::QuickloadOption<Options>;
public:
Options(Configurable::OptionsType type) :
Configurable::QuickloadOption<Options>(type == Configurable::OptionsType::UserFriendly) {
if(needs_declare()) {
declare_quickload_option();
}
}
};
};
}

View File

@@ -0,0 +1,85 @@
//
// CMOSRAM.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../../Components/I2C/I2C.hpp"
#include "../../../Outputs/Log.hpp"
#include <array>
namespace Archimedes {
struct CMOSRAM: public I2C::Peripheral {
CMOSRAM() {
ram_ = default_ram;
}
void start(bool is_read) override {
expecting_address_ = !is_read;
}
// TODO: first 16 addresses are registers, not RAM.
std::optional<uint8_t> read() override {
if(address_ < 16) {
logger.error().append("TODO: read at %d", address_);
}
const uint8_t result = ram_[address_];
++address_;
return result;
}
bool write(uint8_t value) override {
if(expecting_address_) {
address_ = value;
expecting_address_ = false;
return true;
}
if(address_ < 16) {
logger.error().append("TODO: write at %d", address_);
return true;
}
ram_[address_] = value;
++address_;
return true;
}
private:
bool expecting_address_ = false;
uint8_t address_;
std::array<uint8_t, 256> ram_{};
// This is the default contents of RAM as written by RISC OS 3.11.
static constexpr std::array<uint8_t, 256> default_ram = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0x14, 0x00, 0x6f, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6d,
0x00, 0xfe, 0x00, 0xeb, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x10, 0x50, 0x20, 0x08, 0x0a, 0x2c,
0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x5c, 0x13, 0x00, 0x00, 0x04, 0xfd, 0x08, 0x01, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
};
Log::Logger<Log::Source::CMOSRTC> logger;
};
}

View File

@@ -0,0 +1,51 @@
//
// FloppyDisc.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/04/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../../Components/1770/1770.hpp"
namespace Archimedes {
template <typename InterruptObserverT>
class FloppyDisc: public WD::WD1770, public WD::WD1770::Delegate {
public:
FloppyDisc(InterruptObserverT &observer) : WD::WD1770(P1772), observer_(observer) {
emplace_drives(1, 8000000, 300, 2, Storage::Disk::Drive::ReadyType::ShugartModifiedRDY); // A guess at RDY type.
set_delegate(this);
}
void wd1770_did_change_output(WD::WD1770 *) override {
observer_.update_interrupts();
}
void set_control(uint8_t value) {
// b0, b1, b2, b3 = drive selects;
// b4 = side select;
// b5 = motor on/off
// b6 = floppy in use (i.e. LED?);
// b7 = disc eject/change reset.
set_drive((value & 0x1) ^ 0x1);
get_drive().set_head(1 ^ ((value >> 4) & 1));
get_drive().set_motor_on(!(value & 0x20));
}
void reset() {}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
get_drive(drive).set_disk(disk);
}
bool ready() const {
return get_drive().get_is_ready();
}
private:
InterruptObserverT &observer_;
};
}

View File

@@ -0,0 +1,88 @@
//
// HalfDuplexSerial.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
namespace Archimedes {
/// Models a half-duplex serial link between two parties, framing bytes with one start bit and two stop bits.
struct HalfDuplexSerial {
static constexpr uint16_t ShiftMask = 0b1111'1110'0000'0000;
/// Enqueues @c value for output.
void output(int party, uint8_t value) {
parties_[party].output_count = 11;
parties_[party].input = 0x7ff;
parties_[party].output = uint16_t((value << 1) | ShiftMask);
}
/// @returns The last observed input.
uint8_t input(int party) const {
return uint8_t(parties_[party].input >> 1);
}
static constexpr uint8_t Receive = 1 << 0;
static constexpr uint8_t Transmit = 1 << 1;
/// @returns A bitmask of events that occurred during the last shift.
uint8_t events(int party) {
const auto result = parties_[party].events;
parties_[party].events = 0;
return result;
}
bool is_outputting(int party) const {
return parties_[party].output_count != 11;
}
/// Updates the shifters on both sides of the serial link.
void shift() {
const uint16_t next = parties_[0].output & parties_[1].output & 1;
for(int c = 0; c < 2; c++) {
if(parties_[c].output_count) {
--parties_[c].output_count;
if(!parties_[c].output_count) {
parties_[c].events |= Transmit;
parties_[c].input_count = -1;
}
parties_[c].output = (parties_[c].output >> 1) | ShiftMask;
} else {
// Check for a start bit.
if(parties_[c].input_count == -1 && !next) {
parties_[c].input_count = 0;
}
// Shift in if currently observing.
if(parties_[c].input_count >= 0 && parties_[c].input_count < 11) {
parties_[c].input = uint16_t((parties_[c].input >> 1) | (next << 10));
++parties_[c].input_count;
if(parties_[c].input_count == 11) {
parties_[c].events |= Receive;
parties_[c].input_count = -1;
}
}
}
}
}
private:
struct Party {
int output_count = 0;
int input_count = -1;
uint16_t output = 0xffff;
uint16_t input = 0;
uint8_t events = 0;
} parties_[2];
};
static constexpr int IOCParty = 0;
static constexpr int KeyboardParty = 1;
}

View File

@@ -0,0 +1,565 @@
//
// InputOutputController.h
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "CMOSRAM.hpp"
#include "FloppyDisc.hpp"
#include "Keyboard.hpp"
#include "Sound.hpp"
#include "Video.hpp"
#include "../../../Outputs/Log.hpp"
#include "../../../Activity/Observer.hpp"
#include "../../../ClockReceiver/ClockingHintSource.hpp"
namespace Archimedes {
// IRQ A flags
namespace IRQA {
static constexpr uint8_t PrinterBusy = 0x01;
static constexpr uint8_t SerialRinging = 0x02;
static constexpr uint8_t PrinterAcknowledge = 0x04;
static constexpr uint8_t VerticalFlyback = 0x08;
static constexpr uint8_t PowerOnReset = 0x10;
static constexpr uint8_t Timer0 = 0x20;
static constexpr uint8_t Timer1 = 0x40;
static constexpr uint8_t Force = 0x80;
}
// IRQ B flags
namespace IRQB {
static constexpr uint8_t PoduleFIQRequest = 0x01;
static constexpr uint8_t SoundBufferPointerUsed = 0x02;
static constexpr uint8_t SerialLine = 0x04;
static constexpr uint8_t IDE = 0x08;
static constexpr uint8_t FloppyDiscChanged = 0x10;
static constexpr uint8_t PoduleIRQRequest = 0x20;
static constexpr uint8_t KeyboardTransmitEmpty = 0x40;
static constexpr uint8_t KeyboardReceiveFull = 0x80;
}
// FIQ flags
namespace FIQ {
static constexpr uint8_t FloppyDiscData = 0x01;
static constexpr uint8_t FloppyDiscInterrupt = 0x02;
static constexpr uint8_t Econet = 0x04;
static constexpr uint8_t PoduleFIQRequest = 0x40;
static constexpr uint8_t Force = 0x80;
}
namespace InterruptRequests {
static constexpr int IRQ = 0x01;
static constexpr int FIQ = 0x02;
};
template <typename InterruptObserverT, typename ClockRateObserverT>
struct InputOutputController: public ClockingHint::Observer {
InputOutputController(InterruptObserverT &observer, ClockRateObserverT &clock_observer, const uint8_t *ram) :
observer_(observer),
keyboard_(serial_),
floppy_(*this),
sound_(*this, ram),
video_(*this, clock_observer, sound_, ram)
{
irq_a_.status = IRQA::Force | IRQA::PowerOnReset;
irq_b_.status = 0x00;
fiq_.status = FIQ::Force;
floppy_.set_clocking_hint_observer(this);
i2c_.add_peripheral(&cmos_, 0xa0);
update_interrupts();
}
int interrupt_mask() const {
return
((irq_a_.request() | irq_b_.request()) ? InterruptRequests::IRQ : 0) |
(fiq_.request() ? InterruptRequests::FIQ : 0);
}
template <int c>
bool tick_timer() {
if(!counters_[c].value && !counters_[c].reload) {
return false;
}
--counters_[c].value;
if(!counters_[c].value) {
counters_[c].value = counters_[c].reload;
switch(c) {
case 0: return irq_a_.set(IRQA::Timer0);
case 1: return irq_a_.set(IRQA::Timer1);
case 3: {
serial_.shift();
keyboard_.update();
const uint8_t events = serial_.events(IOCParty);
bool did_interrupt = false;
if(events & HalfDuplexSerial::Receive) {
did_interrupt |= irq_b_.set(IRQB::KeyboardReceiveFull);
}
if(events & HalfDuplexSerial::Transmit) {
did_interrupt |= irq_b_.set(IRQB::KeyboardTransmitEmpty);
}
return did_interrupt;
}
default: break;
}
// TODO: events for timers 2 (baud).
}
return false;
}
void tick_timers() {
bool did_change_interrupts = false;
did_change_interrupts |= tick_timer<0>();
did_change_interrupts |= tick_timer<1>();
did_change_interrupts |= tick_timer<2>();
did_change_interrupts |= tick_timer<3>();
if(did_change_interrupts) {
observer_.update_interrupts();
}
}
void tick_floppy(int clock_multiplier) {
if(floppy_clocking_ != ClockingHint::Preference::None) {
floppy_.run_for(Cycles(clock_multiplier));
}
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
floppy_.set_disk(disk, drive);
}
/// Decomposes an Archimedes bus address into bank, offset and type.
struct Address {
constexpr Address(uint32_t bus_address) noexcept {
bank = (bus_address >> 16) & 0b111;
type = Type((bus_address >> 19) & 0b11);
offset = bus_address & 0b1111100;
}
/// A value from 0 to 7 indicating the device being addressed.
uint32_t bank;
/// A seven-bit value which is a multiple of 4, indicating the address within the bank.
uint32_t offset;
/// Access type.
enum class Type {
Sync = 0b00,
Fast = 0b10,
Medium = 0b01,
Slow = 0b11
} type;
};
// Peripheral addresses on the A500:
//
// fast/1 = FDC
// sync/2 = econet
// sync/3 = serial line
//
// bank 4 = podules
//
// fast/5
template <typename IntT>
bool read(uint32_t address, IntT &destination) {
const Address target(address);
const auto set_byte = [&](uint8_t value) {
if constexpr (std::is_same_v<IntT, uint32_t>) {
destination = static_cast<uint32_t>(value << 16) | 0xff'00'ff'ff;
} else {
destination = value;
}
};
// TODO: flatten the switch below, and the equivalent in `write`.
switch(target.bank) {
default:
logger.error().append("Unrecognised IOC read from %08x i.e. bank %d / type %d", address, target.bank, target.type);
destination = IntT(~0);
break;
// Bank 0: internal registers.
case 0:
switch(target.offset) {
default:
logger.error().append("Unrecognised IOC bank 0 read; offset %02x", target.offset);
break;
case 0x00: {
uint8_t value = control_ | 0xc0;
value &= ~(i2c_.data() ? 0x01 : 0x00);
value &= ~(i2c_.clock() ? 0x02 : 0x00);
value &= ~(floppy_.ready() ? 0x00 : 0x04);
value &= ~(video_.flyback_active() ? 0x00 : 0x80); // i.e. high during flyback.
set_byte(value);
// logger.error().append("IOC control read: C:%d D:%d", !(value & 2), !(value & 1));
} break;
case 0x04:
set_byte(serial_.input(IOCParty));
irq_b_.clear(IRQB::KeyboardReceiveFull);
observer_.update_interrupts();
// logger.error().append("IOC keyboard receive: %02x", value);
break;
// IRQ A.
case 0x10:
set_byte(irq_a_.status);
// logger.error().append("IRQ A status is %02x", value);
break;
case 0x14:
set_byte(irq_a_.request());
// logger.error().append("IRQ A request is %02x", value);
break;
case 0x18:
set_byte(irq_a_.mask);
// logger.error().append("IRQ A mask is %02x", value);
break;
// IRQ B.
case 0x20:
set_byte(irq_b_.status);
// logger.error().append("IRQ B status is %02x", value);
break;
case 0x24:
set_byte(irq_b_.request());
// logger.error().append("IRQ B request is %02x", value);
break;
case 0x28:
set_byte(irq_b_.mask);
// logger.error().append("IRQ B mask is %02x", value);
break;
// FIQ.
case 0x30:
set_byte(fiq_.status);
// logger.error().append("FIQ status is %02x", fiq_.status);
break;
case 0x34:
set_byte(fiq_.request());
// logger.error().append("FIQ request is %02x", fiq_.request());
break;
case 0x38:
set_byte(fiq_.mask);
// logger.error().append("FIQ mask is %02x", fiq_.mask);
break;
// Counters.
case 0x40: case 0x50: case 0x60: case 0x70:
set_byte(counters_[(target.offset >> 4) - 0x4].output & 0xff);
// logger.error().append("%02x: Counter %d low is %02x", target, (target >> 4) - 0x4, value);
break;
case 0x44: case 0x54: case 0x64: case 0x74:
set_byte(counters_[(target.offset >> 4) - 0x4].output >> 8);
// logger.error().append("%02x: Counter %d high is %02x", target, (target >> 4) - 0x4, value);
break;
}
break;
// Bank 1: the floppy disc controller.
case 1:
set_byte(floppy_.read(target.offset >> 2));
// logger.error().append("Floppy read; offset %02x", target.offset);
break;
}
return true;
}
template <typename IntT>
bool write(uint32_t address, IntT bus_value) {
const Address target(address);
// Empirically, RISC OS 3.19:
// * at 03801e88 and 03801e8c loads R8 and R9 with 0xbe0000 and 0xff0000 respectively; and
// * subsequently uses 32-bit strs (e.g. at 03801eac) to write those values to latch A.
//
// Given that 8-bit ARM writes duplicate the 8-bit value four times across the data bus,
// my conclusion is that the IOC is probably connected to data lines 1523.
//
// Hence: use @c byte to get a current 8-bit value.
const auto byte = [](IntT original) -> uint8_t {
if constexpr (std::is_same_v<IntT, uint32_t>) {
return static_cast<uint8_t>(original >> 16);
} else {
return original;
}
};
switch(target.bank) {
default:
logger.error().append("Unrecognised IOC write of %02x to %08x i.e. bank %d / type %d", bus_value, address, target.bank, target.type);
break;
// Bank 0: internal registers.
case 0:
switch(target.offset) {
default:
logger.error().append("Unrecognised IOC bank 0 write; %02x to offset %02x", bus_value, target.offset);
break;
case 0x00:
control_ = byte(bus_value);
i2c_.set_clock_data(!(bus_value & 2), !(bus_value & 1));
// Per the A500 documentation:
// b7: vertical sync/test input bit, so should be programmed high;
// b6: input for printer acknowledgement, so should be programmed high;
// b5: speaker mute; 1 = muted;
// b4: "Available on the auxiliary I/O connector"
// b3: "Programmed HIGH, unless Reset Mask is required."
// b2: Used as the floppy disk (READY) input and must be programmed high;
// b1 and b0: I2C connections as above.
break;
case 0x04:
serial_.output(IOCParty, byte(bus_value));
irq_b_.clear(IRQB::KeyboardTransmitEmpty);
observer_.update_interrupts();
break;
case 0x14:
// b2: clear IF.
// b3: clear IR.
// b4: clear POR.
// b5: clear TM[0].
// b6: clear TM[1].
irq_a_.clear(byte(bus_value) & 0x7c);
observer_.update_interrupts();
break;
// Interrupts.
case 0x18:
irq_a_.mask = byte(bus_value);
// logger.error().append("IRQ A mask set to %02x", byte(bus_value));
break;
case 0x28:
irq_b_.mask = byte(bus_value);
// logger.error().append("IRQ B mask set to %02x", byte(bus_value));
break;
case 0x38:
fiq_.mask = byte(bus_value);
// logger.error().append("FIQ mask set to %02x", byte(bus_value));
break;
// Counters.
case 0x40: case 0x50: case 0x60: case 0x70:
counters_[(target.offset >> 4) - 0x4].reload = uint16_t(
(counters_[(target.offset >> 4) - 0x4].reload & 0xff00) | byte(bus_value)
);
break;
case 0x44: case 0x54: case 0x64: case 0x74:
counters_[(target.offset >> 4) - 0x4].reload = uint16_t(
(counters_[(target.offset >> 4) - 0x4].reload & 0x00ff) | (byte(bus_value) << 8)
);
break;
case 0x48: case 0x58: case 0x68: case 0x78:
counters_[(target.offset >> 4) - 0x4].value = counters_[(target.offset >> 4) - 0x4].reload;
break;
case 0x4c: case 0x5c: case 0x6c: case 0x7c:
counters_[(target.offset >> 4) - 0x4].output = counters_[(target.offset >> 4) - 0x4].value;
break;
}
break;
// Bank 1: the floppy disc controller.
case 1:
// logger.error().append("Floppy write; %02x to offset %02x", bus_value, target.offset);
floppy_.write(target.offset >> 2, byte(bus_value));
// set_byte(floppy_.read(target.offset >> 2));
break;
// Bank 5: both the hard disk and the latches, depending on type.
case 5:
switch(target.type) {
default:
logger.error().append("Unrecognised IOC bank 5 type %d write; %02x to offset %02x", target.type, bus_value, target.offset);
break;
case Address::Type::Fast:
switch(target.offset) {
default:
logger.error().append("Unrecognised IOC fast bank 5 write; %02x to offset %02x", bus_value, target.offset);
break;
case 0x00:
logger.error().append("TODO: printer data write; %02x", byte(bus_value));
break;
case 0x18: {
// TODO, per the A500 documentation:
//
// Latch B:
// b0: ?
// b1: double/single density; 0 = double.
// b2: ?
// b3: floppy drive reset; 0 = reset.
// b4: printer strobe
// b5: ?
// b6: ?
// b7: Head select 3?
const uint8_t value = byte(bus_value);
floppy_.set_is_double_density(!(value & 0x2));
if(value & 0x08) floppy_.reset();
// logger.error().append("TODO: latch B write; %02x", byte(bus_value));
} break;
case 0x40: {
const uint8_t value = byte(bus_value);
floppy_.set_control(value);
// Set the floppy indicator on if any drive is selected,
// because this emulator is compressing them all into a
// single LED, and the machine has indicated 'in use'.
if(activity_observer_) {
activity_observer_->set_led_status(FloppyActivityLED,
!(value & 0x40) && ((value & 0xf) != 0xf)
);
}
} break;
case 0x48:
// TODO, per the A500 documentation:
//
// Latch C:
// (probably not present on earlier machines?)
// b2/b3: sync polarity [b3 = V polarity, b2 = H?]
// b0/b1: VIDC master clock; 00 = 24Mhz, 01 = 25.175Mhz; 10 = 36Mhz; 11 = reserved.
logger.error().append("TODO: latch C write; %02x", byte(bus_value));
break;
}
break;
}
break;
}
// case 0x327'0000 & AddressMask: // Bank 7
// logger.error().append("TODO: exteded external podule space");
// return true;
//
// case 0x336'0000 & AddressMask:
// logger.error().append("TODO: podule interrupt request");
// return true;
//
// case 0x336'0004 & AddressMask:
// logger.error().append("TODO: podule interrupt mask");
// return true;
//
// case 0x33a'0000 & AddressMask:
// logger.error().append("TODO: 6854 / econet write");
// return true;
//
// case 0x33b'0000 & AddressMask:
// logger.error().append("TODO: 6551 / serial line write");
// return true;
return true;
}
auto &sound() { return sound_; }
const auto &sound() const { return sound_; }
auto &video() { return video_; }
const auto &video() const { return video_; }
auto &keyboard() { return keyboard_; }
const auto &keyboard() const { return keyboard_; }
void update_interrupts() {
const auto set = [&](Interrupt &target, uint8_t flag, bool set) {
if(set) {
target.set(flag);
} else {
target.clear(flag);
}
};
set(irq_b_, IRQB::SoundBufferPointerUsed, sound_.interrupt());
set(fiq_, FIQ::FloppyDiscInterrupt, floppy_.get_interrupt_request_line());
set(fiq_, FIQ::FloppyDiscData, floppy_.get_data_request_line());
if(video_.interrupt()) {
irq_a_.set(IRQA::VerticalFlyback);
}
observer_.update_interrupts();
}
void set_activity_observer(Activity::Observer *observer) {
activity_observer_ = observer;
if(activity_observer_) {
activity_observer_->register_led(FloppyActivityLED);
}
}
private:
Log::Logger<Log::Source::ARMIOC> logger;
InterruptObserverT &observer_;
Activity::Observer *activity_observer_ = nullptr;
static inline const std::string FloppyActivityLED = "Drive";
// IRQA, IRQB and FIQ states.
struct Interrupt {
uint8_t status = 0x00, mask = 0x00;
uint8_t request() const {
return status & mask;
}
bool set(uint8_t value) {
status |= value;
return status & mask;
}
void clear(uint8_t bits) {
status &= ~bits;
}
};
Interrupt irq_a_, irq_b_, fiq_;
// The IOCs four counters.
struct Counter {
uint16_t value = 0;
uint16_t reload = 0;
uint16_t output = 0;
};
Counter counters_[4];
// The KART and keyboard beyond it.
HalfDuplexSerial serial_;
Keyboard keyboard_;
// The control register.
uint8_t control_ = 0xff;
// The floppy disc interface.
FloppyDisc<InputOutputController> floppy_;
ClockingHint::Preference floppy_clocking_ = ClockingHint::Preference::None;
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) override {
floppy_clocking_ = clocking;
}
// The I2C bus.
I2C::Bus i2c_;
CMOSRAM cmos_;
// Audio and video.
Sound<InputOutputController> sound_;
Video<InputOutputController, ClockRateObserverT, Sound<InputOutputController>> video_;
};
}

View File

@@ -0,0 +1,334 @@
//
// Keyboard.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "HalfDuplexSerial.hpp"
#include "../../../Outputs/Log.hpp"
#include "../../../Inputs/Mouse.hpp"
#include <bitset>
namespace Archimedes {
namespace {
constexpr uint16_t map(int row, int column) {
return static_cast<uint16_t>((row << 4) | column);
}
constexpr uint8_t row(uint16_t key) {
return static_cast<uint8_t>(key >> 4);
}
constexpr uint8_t column(uint16_t key) {
return static_cast<uint8_t>(key & 0xf);
}
}
struct Key {
/// Named key codes that the machine wlll accept directly.
enum Value: uint16_t {
Escape = map(0, 0), F1 = map(0, 1), F2 = map(0, 2), F3 = map(0, 3),
F4 = map(0, 4), F5 = map(0, 5), F6 = map(0, 6), F7 = map(0, 7),
F8 = map(0, 8), F9 = map(0, 9), F10 = map(0, 10), F11 = map(0, 11),
F12 = map(0, 12), Print = map(0, 13), Scroll = map(0, 14), Break = map(0, 15),
Tilde = map(1, 0), k1 = map(1, 1), k2 = map(1, 2), k3 = map(1, 3),
k4 = map(1, 4), k5 = map(1, 5), k6 = map(1, 6), k7 = map(1, 7),
k8 = map(1, 8), k9 = map(1, 9), k0 = map(1, 10), Hyphen = map(1, 11),
Equals = map(1, 12), GBPound = map(1, 13), Backspace = map(1, 14), Insert = map(1, 15),
Home = map(2, 0), PageUp = map(2, 1), NumLock = map(2, 2), KeypadSlash = map(2, 3),
KeypadAsterisk = map(2, 4), KeypadHash = map(2, 5), Tab = map(2, 6), Q = map(2, 7),
W = map(2, 8), E = map(2, 9), R = map(2, 10), T = map(2, 11),
Y = map(2, 12), U = map(2, 13), I = map(2, 14), O = map(2, 15),
P = map(3, 0), OpenSquareBracket = map(3, 1), CloseSquareBracket = map(3, 2), Backslash = map(3, 3),
Delete = map(3, 4), Copy = map(3, 5), PageDown = map(3, 6), Keypad7 = map(3, 7),
Keypad8 = map(3, 8), Keypad9 = map(3, 9), KeypadMinus = map(3, 10), LeftControl = map(3, 11),
A = map(3, 12), S = map(3, 13), D = map(3, 14), F = map(3, 15),
G = map(4, 0), H = map(4, 1), J = map(4, 2), K = map(4, 3),
L = map(4, 4), Semicolon = map(4, 5), Quote = map(4, 6), Return = map(4, 7),
Keypad4 = map(4, 8), Keypad5 = map(4, 9), Keypad6 = map(4, 10), KeypadPlus = map(4, 11),
LeftShift = map(4, 12), /* unused */ Z = map(4, 14), X = map(4, 15),
C = map(5, 0), V = map(5, 1), B = map(5, 2), N = map(5, 3),
M = map(5, 4), Comma = map(5, 5), FullStop = map(5, 6), ForwardSlash = map(5, 7),
RightShift = map(5, 8), Up = map(5, 9), Keypad1 = map(5, 10), Keypad2 = map(5, 11),
Keypad3 = map(5, 12), CapsLock = map(5, 13), LeftAlt = map(5, 14), Space = map(5, 15),
RightAlt = map(6, 0), RightControl = map(6, 1), Left = map(6, 2), Down = map(6, 3),
Right = map(6, 4), Keypad0 = map(6, 5), KeypadDecimalPoint = map(6, 6), KeypadEnter = map(6, 7),
Max = KeypadEnter,
};
};
// Resource for the keyboard protocol: https://github.com/tmk/tmk_keyboard/wiki/ACORN-ARCHIMEDES-Keyboard
struct Keyboard {
Keyboard(HalfDuplexSerial &serial) : serial_(serial), mouse_(*this) {}
void set_key_state(uint16_t key, bool is_pressed) {
states_[key] = is_pressed;
if(!scan_keyboard_) {
logger_.info().append("Ignored key event as key scanning disabled");
return;
}
// Don't waste bandwidth on repeating facts.
if(posted_states_[key] == is_pressed) return;
// Post new key event.
enqueue_key_event(key, is_pressed);
consider_dequeue();
}
void set_mouse_button(uint8_t button, bool is_pressed) {
if(!scan_mouse_) {
return;
}
// Post new key event.
enqueue_key_event(7, button, is_pressed);
consider_dequeue();
}
void update() {
if(serial_.events(KeyboardParty) & HalfDuplexSerial::Receive) {
const auto reset = [&]() {
serial_.output(KeyboardParty, HRST);
state_ = State::Idle;
};
const uint8_t input = serial_.input(KeyboardParty);
// A reset command is always accepted, usurping any other state.
if(input == HRST) {
logger_.info().append("HRST; resetting");
state_ = State::ExpectingRAK1;
event_queue_.clear();
serial_.output(KeyboardParty, HRST);
return;
}
switch(state_) {
case State::ExpectingACK:
if(input != NACK && input != SMAK && input != MACK && input != SACK) {
logger_.error().append("No ack; requesting reset");
reset();
break;
}
state_ = State::Idle;
[[fallthrough]];
case State::Idle:
switch(input) {
case RQID: // Post keyboard ID.
serial_.output(KeyboardParty, 0x81); // Declare this to be a UK keyboard.
logger_.info().append("RQID; responded with 0x81");
break;
case PRST: // "1-byte command, does nothing."
logger_.info().append("PRST; ignored");
break;
case RQMP:
logger_.error().append("RQMP; TODO: respond something other than 0, 0");
enqueue(0, 0);
break;
case NACK: case SMAK: case MACK: case SACK: {
const bool was_scanning_keyboard = input & 1;
scan_keyboard_ = input & 1;
if(!scan_keyboard_) {
posted_states_.reset();
} else if(!was_scanning_keyboard) {
needs_state_check_ = true;
}
scan_mouse_ = input & 2;
logger_.info().append("ACK; keyboard:%d mouse:%d", scan_keyboard_, scan_mouse_);
} break;
default:
if((input & 0b1111'0000) == 0b0100'0000) {
// RQPD; request to echo the low nibble.
serial_.output(KeyboardParty, 0b1110'0000 | (input & 0b1111));
logger_.info().append("RQPD; echoing %x", input & 0b1111);
} else if(!(input & 0b1111'1000)) {
// LEDS: should set LED outputs.
logger_.error().append("TODO: set LEDs %d%d%d", static_cast<bool>(input&4), static_cast<bool>(input&2), static_cast<bool>(input&1));
} else {
logger_.info().append("Ignoring unrecognised command %02x received in idle state", input);
}
break;
}
break;
case State::ExpectingRAK1:
if(input != RAK1) {
logger_.info().append("Didn't get RAK1; resetting");
reset();
break;
}
logger_.info().append("Got RAK1; echoing");
serial_.output(KeyboardParty, input);
state_ = State::ExpectingRAK2;
break;
case State::ExpectingRAK2:
if(input != RAK2) {
logger_.info().append("Didn't get RAK2; resetting");
reset();
break;
}
logger_.info().append("Got RAK2; echoing");
serial_.output(KeyboardParty, input);
state_ = State::ExpectingACK;
break;
case State::ExpectingBACK:
if(input != BACK) {
logger_.info().append("Didn't get BACK; resetting");
reset();
break;
}
logger_.info().append("Got BACK; posting next byte");
dequeue_next();
state_ = State::ExpectingACK;
break;
}
}
consider_dequeue();
}
void consider_dequeue() {
if(state_ == State::Idle) {
// If the key event queue is empty but keyboard scanning is enabled, check for
// any disparity between posted keys states and actuals.
if(needs_state_check_) {
needs_state_check_ = false;
if(states_ != posted_states_) {
for(size_t key = 0; key < Key::Max; key++) {
if(states_[key] != posted_states_[key]) {
enqueue_key_event(static_cast<uint16_t>(key), states_[key]);
}
}
}
}
// If the key event queue is _still_ empty, grab as much mouse motion
// as available.
if(event_queue_.empty()) {
const int x = std::clamp(mouse_x_, -0x3f, 0x3f);
const int y = std::clamp(mouse_y_, -0x3f, 0x3f);
mouse_x_ -= x;
mouse_y_ -= y;
if(x || y) {
enqueue(static_cast<uint8_t>(x) & 0x7f, static_cast<uint8_t>(-y) & 0x7f);
}
}
if(dequeue_next()) {
state_ = State::ExpectingBACK;
}
}
}
Inputs::Mouse &mouse() {
return mouse_;
}
private:
HalfDuplexSerial &serial_;
Log::Logger<Log::Source::Keyboard> logger_;
std::bitset<Key::Max> states_;
std::bitset<Key::Max> posted_states_;
bool needs_state_check_ = false;
bool scan_keyboard_ = false;
bool scan_mouse_ = false;
enum class State {
ExpectingRAK1, // Post a RAK1 and proceed to ExpectingRAK2 if RAK1 is received; otherwise request a reset.
ExpectingRAK2, // Post a RAK2 and proceed to ExpectingACK if RAK2 is received; otherwise request a reset.
ExpectingACK, // Process NACK, SACK, MACK or SMAK if received; otherwise request a reset.
Idle, // Process any of: NACK, SACK, MACK, SMAK, RQID, RQMP, RQPD or LEDS if received; also
// unilaterally begin post a byte pair enqueued but not yet sent if any are waiting.
ExpectingBACK, // Dequeue and post one further byte if BACK is received; otherwise request a reset.
} state_ = State::Idle;
std::vector<uint8_t> event_queue_;
void enqueue(uint8_t first, uint8_t second) {
event_queue_.push_back(first);
event_queue_.push_back(second);
}
bool dequeue_next() {
// To consider: a cheaper approach to the queue than this; in practice events
// are 'rare' so it's not high priority.
if(event_queue_.empty()) return false;
serial_.output(KeyboardParty, event_queue_[0]);
event_queue_.erase(event_queue_.begin());
return true;
}
void enqueue_key_event(uint16_t key, bool is_pressed) {
posted_states_[key] = is_pressed;
enqueue_key_event(row(key), column(key), is_pressed);
}
void enqueue_key_event(uint8_t row, uint8_t column, bool is_pressed) {
logger_.info().append("Posting row %d, column %d is now %s", row, column, is_pressed ? "pressed" : "released");
const uint8_t prefix = is_pressed ? 0b1100'0000 : 0b1101'0000;
enqueue(static_cast<uint8_t>(prefix | row), static_cast<uint8_t>(prefix | column));
}
static constexpr uint8_t HRST = 0b1111'1111; // Keyboard reset.
static constexpr uint8_t RAK1 = 0b1111'1110; // Reset response #1.
static constexpr uint8_t RAK2 = 0b1111'1101; // Reset response #2.
static constexpr uint8_t RQID = 0b0010'0000; // Request for keyboard ID.
static constexpr uint8_t RQMP = 0b0010'0010; // Request for mouse data.
static constexpr uint8_t BACK = 0b0011'1111; // Acknowledge for first keyboard data byte pair.
static constexpr uint8_t NACK = 0b0011'0000; // Acknowledge for last keyboard data byte pair, disables both scanning and mouse.
static constexpr uint8_t SACK = 0b0011'0001; // Last data byte acknowledge, enabling scanning but disabling mouse.
static constexpr uint8_t MACK = 0b0011'0010; // Last data byte acknowledge, disabling scanning but enabling mouse.
static constexpr uint8_t SMAK = 0b0011'0011; // Last data byte acknowledge, enabling scanning and mouse.
static constexpr uint8_t PRST = 0b0010'0001; // Does nothing.
struct Mouse: public Inputs::Mouse {
Mouse(Keyboard &keyboard): keyboard_(keyboard) {}
void move(int x, int y) override {
keyboard_.mouse_x_ += x;
keyboard_.mouse_y_ += y;
}
int get_number_of_buttons() override {
return 3;
}
virtual void set_button_pressed(int index, bool is_pressed) override {
keyboard_.set_mouse_button(static_cast<uint8_t>(index), is_pressed);
}
private:
Keyboard &keyboard_;
};
Mouse mouse_;
int mouse_x_ = 0;
int mouse_y_ = 0;
};
}

View File

@@ -0,0 +1,138 @@
//
// KeyboardMapper.hpp
// Clock Signal
//
// Created by Thomas Harte on 23/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../KeyboardMachine.hpp"
#include "Keyboard.hpp"
namespace Archimedes {
/// Converter from this emulator's custom definition of a generic keyboard to the machine-specific key set defined above.
class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper {
public:
// Adapted from the A500 Series Technical Reference Manual.
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) const override {
using k = Inputs::Keyboard::Key;
switch(key) {
case k::Escape: return Key::Escape;
case k::F1: return Key::F1;
case k::F2: return Key::F2;
case k::F3: return Key::F3;
case k::F4: return Key::F4;
case k::F5: return Key::F5;
case k::F6: return Key::F6;
case k::F7: return Key::F7;
case k::F8: return Key::F8;
case k::F9: return Key::F9;
case k::F10: return Key::F10;
case k::F11: return Key::F11;
case k::F12: return Key::F12;
case k::PrintScreen: return Key::Print;
case k::ScrollLock: return Key::Scroll;
case k::Pause: return Key::Break;
case k::BackTick: return Key::Tilde;
case k::k1: return Key::k1;
case k::k2: return Key::k2;
case k::k3: return Key::k3;
case k::k4: return Key::k4;
case k::k5: return Key::k5;
case k::k6: return Key::k6;
case k::k7: return Key::k7;
case k::k8: return Key::k8;
case k::k9: return Key::k9;
case k::k0: return Key::k0;
case k::Hyphen: return Key::Hyphen;
case k::Equals: return Key::Equals;
// TODO: pound key.
case k::Backspace: return Key::Backspace;
case k::Insert: return Key::Insert;
case k::Home: return Key::Home;
case k::PageUp: return Key::PageUp;
case k::NumLock: return Key::NumLock;
case k::KeypadSlash: return Key::KeypadSlash;
case k::KeypadAsterisk: return Key::KeypadAsterisk;
// TODO: keypad hash key
case k::Tab: return Key::Tab;
case k::Q: return Key::Q;
case k::W: return Key::W;
case k::E: return Key::E;
case k::R: return Key::R;
case k::T: return Key::T;
case k::Y: return Key::Y;
case k::U: return Key::U;
case k::I: return Key::I;
case k::O: return Key::O;
case k::P: return Key::P;
case k::OpenSquareBracket: return Key::OpenSquareBracket;
case k::CloseSquareBracket: return Key::CloseSquareBracket;
case k::Backslash: return Key::Backslash;
case k::Delete: return Key::Delete;
case k::End: return Key::Copy;
case k::PageDown: return Key::PageDown;
case k::Keypad7: return Key::Keypad7;
case k::Keypad8: return Key::Keypad8;
case k::Keypad9: return Key::Keypad9;
case k::KeypadMinus: return Key::KeypadMinus;
case k::LeftControl: return Key::LeftControl;
case k::A: return Key::A;
case k::S: return Key::S;
case k::D: return Key::D;
case k::F: return Key::F;
case k::G: return Key::G;
case k::H: return Key::H;
case k::J: return Key::J;
case k::K: return Key::K;
case k::L: return Key::L;
case k::Semicolon: return Key::Semicolon;
case k::Quote: return Key::Quote;
case k::Enter: return Key::Return;
case k::Keypad4: return Key::Keypad4;
case k::Keypad5: return Key::Keypad5;
case k::Keypad6: return Key::Keypad6;
case k::KeypadPlus: return Key::KeypadPlus;
case k::LeftShift: return Key::LeftShift;
case k::Z: return Key::Z;
case k::X: return Key::X;
case k::C: return Key::C;
case k::V: return Key::V;
case k::B: return Key::B;
case k::N: return Key::N;
case k::M: return Key::M;
case k::Comma: return Key::Comma;
case k::FullStop: return Key::FullStop;
case k::ForwardSlash: return Key::ForwardSlash;
case k::RightShift: return Key::RightShift;
case k::Up: return Key::Up;
case k::Keypad1: return Key::Keypad1;
case k::Keypad2: return Key::Keypad2;
case k::Keypad3: return Key::Keypad3;
case k::CapsLock: return Key::CapsLock;
case k::LeftOption: return Key::LeftAlt;
case k::Space: return Key::Space;
case k::RightOption: return Key::RightAlt;
case k::RightControl: return Key::RightControl;
case k::Left: return Key::Left;
case k::Down: return Key::Down;
case k::Right: return Key::Right;
case k::Keypad0: return Key::Keypad0;
case k::KeypadDecimalPoint: return Key::KeypadDecimalPoint;
case k::KeypadEnter: return Key::KeypadEnter;
default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped;
}
}
};
}

View File

@@ -0,0 +1,510 @@
//
// MemoryController.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "InputOutputController.hpp"
#include "Video.hpp"
#include "Sound.hpp"
#include "../../../InstructionSets/ARM/Registers.hpp"
#include "../../../Outputs/Log.hpp"
#include "../../../Activity/Observer.hpp"
namespace Archimedes {
/// Provides the mask with all bits set in the range [start, end], where start must be >= end.
template <int start, int end> struct BitMask {
static_assert(start >= end);
static constexpr uint32_t value = ((1 << (start + 1)) - 1) - ((1 << end) - 1);
};
static_assert(BitMask<0, 0>::value == 1);
static_assert(BitMask<1, 1>::value == 2);
static_assert(BitMask<15, 15>::value == 32768);
static_assert(BitMask<15, 0>::value == 0xffff);
static_assert(BitMask<15, 14>::value == 49152);
/// Models the MEMC, making this the Archimedes bus. Owns various other chips on the bus as a result.
template <typename InterruptObserverT, typename ClockRateObserverT>
struct MemoryController {
MemoryController(InterruptObserverT &observer, ClockRateObserverT &clock_rate_observer) :
ioc_(observer, clock_rate_observer, ram_.data()) {
read_zones_[0] = ReadZone::HighROM; // Temporarily put high ROM at address 0.
// TODO: could I just copy it in? Or, at least,
// could I detect at ROM loading time whether I can?
}
int interrupt_mask() const {
return ioc_.interrupt_mask();
}
void set_rom(const std::vector<uint8_t> &rom) {
if(rom_.size() % rom.size() || rom.size() > rom_.size()) {
// TODO: throw.
return;
}
// Copy in as many times as it'll fit.
std::size_t base = 0;
while(base < rom_.size()) {
std::copy(
rom.begin(),
rom.end(),
rom_.begin() + base);
base += rom.size();
}
}
template <typename IntT>
uint32_t aligned(uint32_t address) {
if constexpr (std::is_same_v<IntT, uint32_t>) {
return address & static_cast<uint32_t>(~3);
}
return address;
}
template <typename IntT>
bool write(uint32_t address, IntT source, InstructionSet::ARM::Mode, bool trans) {
switch(write_zones_[(address >> 21) & 31]) {
case WriteZone::LogicallyMappedRAM: {
const auto item = logical_ram<IntT, false>(address, trans);
if(item < reinterpret_cast<IntT *>(ram_.data())) {
return false;
}
*item = source;
} break;
case WriteZone::PhysicallyMappedRAM:
if(trans) return false;
physical_ram<IntT>(address) = source;
break;
case WriteZone::DMAAndMEMC: {
if(trans) return false;
const auto buffer_address = [](uint32_t source) -> uint32_t {
return (source & 0x1'fffc) << 2;
};
// The MEMC itself isn't on the data bus; all values below should be taken from `address`.
switch((address >> 17) & 0b111) {
case 0b000: ioc_.video().set_frame_start(buffer_address(address)); break;
case 0b001: ioc_.video().set_buffer_start(buffer_address(address)); break;
case 0b010: ioc_.video().set_buffer_end(buffer_address(address)); break;
case 0b011: ioc_.video().set_cursor_start(buffer_address(address)); break;
case 0b100: ioc_.sound().set_next_start(buffer_address(address)); break;
case 0b101: ioc_.sound().set_next_end(buffer_address(address)); break;
case 0b110: ioc_.sound().swap(); break;
case 0b111:
os_mode_ = address & (1 << 12);
sound_dma_enable_ = address & (1 << 11);
video_dma_enable_ = address & (1 << 10);
ioc_.sound().set_dma_enabled(sound_dma_enable_);
ioc_.video().set_dma_enabled(video_dma_enable_);
switch((address >> 8) & 3) {
default:
dynamic_ram_refresh_ = DynamicRAMRefresh::None;
break;
case 0b01:
case 0b11:
dynamic_ram_refresh_ = DynamicRAMRefresh((address >> 8) & 3);
break;
}
high_rom_access_time_ = ROMAccessTime((address >> 6) & 3);
low_rom_access_time_ = ROMAccessTime((address >> 4) & 3);
page_size_ = PageSize((address >> 2) & 3);
switch(page_size_) {
default:
case PageSize::kb4:
page_address_shift_ = 12;
page_adddress_mask_ = 0x0fff;
break;
case PageSize::kb8:
page_address_shift_ = 13;
page_adddress_mask_ = 0x1fff;
break;
case PageSize::kb16:
page_address_shift_ = 14;
page_adddress_mask_ = 0x3fff;
break;
case PageSize::kb32:
page_address_shift_ = 15;
page_adddress_mask_ = 0x7fff;
break;
}
logger.info().append("MEMC Control: %08x -> OS:%d sound:%d video:%d refresh:%d high:%d low:%d size:%d", address, os_mode_, sound_dma_enable_, video_dma_enable_, dynamic_ram_refresh_, high_rom_access_time_, low_rom_access_time_, page_size_);
map_dirty_ = true;
break;
}
} break;
case WriteZone::IOControllers:
if(trans) return false;
ioc_.template write<IntT>(address, source);
break;
case WriteZone::VideoController:
if(trans) return false;
// TODO: handle byte writes correctly.
ioc_.video().write(source);
break;
case WriteZone::AddressTranslator:
if(trans) return false;
// printf("Translator write at %08x; replaces %08x\n", address, pages_[address & 0x7f]);
pages_[address & 0x7f] = address;
map_dirty_ = true;
break;
}
return true;
}
template <typename IntT>
bool read(uint32_t address, IntT &source, bool trans) {
switch(read_zones_[(address >> 21) & 31]) {
case ReadZone::LogicallyMappedRAM: {
const auto item = logical_ram<IntT, true>(address, trans);
if(item < reinterpret_cast<IntT *>(ram_.data())) {
return false;
}
source = *item;
} break;
case ReadZone::HighROM:
// Real test is: require A24=A25=0, then A25=1.
read_zones_[0] = ReadZone::LogicallyMappedRAM;
source = high_rom<IntT>(address);
break;
case ReadZone::PhysicallyMappedRAM:
if(trans) return false;
source = physical_ram<IntT>(address);
break;
case ReadZone::LowROM:
// logger.error().append("TODO: Low ROM read from %08x", address);
source = IntT(~0);
break;
case ReadZone::IOControllers:
if(trans) return false;
ioc_.template read<IntT>(address, source);
break;
}
return true;
}
template <typename IntT>
bool read(uint32_t address, IntT &source, InstructionSet::ARM::Mode, bool trans) {
return read(address, source, trans);
}
//
// Expose various IOC-owned things.
//
void tick_timers() { ioc_.tick_timers(); }
void tick_floppy(int clock_multiplier) {
ioc_.tick_floppy(clock_multiplier);
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
ioc_.set_disk(disk, drive);
}
Outputs::Speaker::Speaker *speaker() {
return ioc_.sound().speaker();
}
auto &sound() { return ioc_.sound(); }
const auto &sound() const { return ioc_.sound(); }
auto &video() { return ioc_.video(); }
const auto &video() const { return ioc_.video(); }
auto &keyboard() { return ioc_.keyboard(); }
const auto &keyboard() const { return ioc_.keyboard(); }
void set_activity_observer(Activity::Observer *observer) {
ioc_.set_activity_observer(observer);
}
private:
Log::Logger<Log::Source::ARMIOC> logger;
enum class ReadZone {
LogicallyMappedRAM,
PhysicallyMappedRAM,
IOControllers,
LowROM,
HighROM,
};
enum class WriteZone {
LogicallyMappedRAM,
PhysicallyMappedRAM,
IOControllers,
VideoController,
DMAAndMEMC,
AddressTranslator,
};
template <bool is_read>
using Zone = std::conditional_t<is_read, ReadZone, WriteZone>;
template <bool is_read>
static std::array<Zone<is_read>, 0x20> zones() {
std::array<Zone<is_read>, 0x20> zones{};
for(size_t c = 0; c < zones.size(); c++) {
const auto address = c << 21;
if(address < 0x200'0000) {
zones[c] = Zone<is_read>::LogicallyMappedRAM;
} else if(address < 0x300'0000) {
zones[c] = Zone<is_read>::PhysicallyMappedRAM;
} else if(address < 0x340'0000) {
zones[c] = Zone<is_read>::IOControllers;
} else if(address < 0x360'0000) {
if constexpr (is_read) {
zones[c] = Zone<is_read>::LowROM;
} else {
zones[c] = Zone<is_read>::VideoController;
}
} else if(address < 0x380'0000) {
if constexpr (is_read) {
zones[c] = Zone<is_read>::LowROM;
} else {
zones[c] = Zone<is_read>::DMAAndMEMC;
}
} else {
if constexpr (is_read) {
zones[c] = Zone<is_read>::HighROM;
} else {
zones[c] = Zone<is_read>::AddressTranslator;
}
}
}
return zones;
}
bool has_moved_rom_ = false;
std::array<uint8_t, 2*1024*1024> rom_;
std::array<uint8_t, 4*1024*1024> ram_{};
InputOutputController<InterruptObserverT, ClockRateObserverT> ioc_;
template <typename IntT>
IntT &physical_ram(uint32_t address) {
address = aligned<IntT>(address);
address &= (ram_.size() - 1);
return *reinterpret_cast<IntT *>(&ram_[address]);
}
template <typename IntT>
IntT &high_rom(uint32_t address) {
address = aligned<IntT>(address);
return *reinterpret_cast<IntT *>(&rom_[address & (rom_.size() - 1)]);
}
std::array<ReadZone, 0x20> read_zones_ = zones<true>();
const std::array<WriteZone, 0x20> write_zones_ = zones<false>();
// Control register values.
bool os_mode_ = false;
bool sound_dma_enable_ = false;
bool video_dma_enable_ = false; // "Unaffected" by reset, so here picked arbitrarily.
enum class DynamicRAMRefresh {
None = 0b00,
DuringFlyback = 0b01,
Continuous = 0b11,
} dynamic_ram_refresh_ = DynamicRAMRefresh::None; // State at reset is undefined; constrain to a valid enum value.
enum class ROMAccessTime {
ns450 = 0b00,
ns325 = 0b01,
ns200 = 0b10,
ns200with60nsNibble = 0b11,
} high_rom_access_time_ = ROMAccessTime::ns450, low_rom_access_time_ = ROMAccessTime::ns450;
enum class PageSize {
kb4 = 0b00,
kb8 = 0b01,
kb16 = 0b10,
kb32 = 0b11,
} page_size_ = PageSize::kb4;
int page_address_shift_ = 12;
uint32_t page_adddress_mask_ = 0xffff;
// Address translator.
//
// MEMC contains one entry per a physical page number, indicating where it goes logically.
// Any logical access is tested against all 128 mappings. So that's backwards compared to
// the ideal for an emulator, which would map from logical to physical, even if a lot more
// compact — there are always 128 physical pages; there are up to 8192 logical pages.
//
// So captured here are both the physical -> logical map as representative of the real
// hardware, and the reverse logical -> physical map, which is built (and rebuilt, and rebuilt)
// from the other.
// Physical to logical mapping.
std::array<uint32_t, 128> pages_{};
// Logical to physical mapping; this is divided by 'access mode'
// (i.e. the combination of read/write, trans and OS mode flags,
// as multipliexed by the @c mapping() function) because mapping
// varies by mode — not just in terms of restricting access, but
// actually presenting different memory.
using MapTarget = std::array<uint8_t *, 8192>;
std::array<MapTarget, 6> mapping_;
template <bool is_read>
MapTarget &mapping(bool trans, bool os_mode) {
const size_t index = (is_read ? 1 : 0) | (os_mode ? 2 : 0) | ((trans && !os_mode) ? 4 : 0);
return mapping_[index];
}
bool map_dirty_ = true;
/// @returns A pointer to somewhere in @c ram_ if RAM is mapped to this area, or a pointer to somewhere lower than @c ram_.data() otherwise.
template <typename IntT, bool is_read>
IntT *logical_ram(uint32_t address, bool trans) {
// Possibly TODO: this recompute-if-dirty flag is supposed to ameliorate for an expensive
// mapping process. It can be eliminated when the process is improved.
if(map_dirty_) {
update_mapping();
map_dirty_ = false;
}
address = aligned<IntT>(address);
address &= 0x1ff'ffff;
const size_t page = address >> page_address_shift_;
const auto &map = mapping<is_read>(trans, os_mode_);
address &= page_adddress_mask_;
return reinterpret_cast<IntT *>(&map[page][address]);
}
void update_mapping() {
// For each physical page, project it into logical space.
switch(page_size_) {
default:
case PageSize::kb4: update_mapping<PageSize::kb4>(); break;
case PageSize::kb8: update_mapping<PageSize::kb8>(); break;
case PageSize::kb16: update_mapping<PageSize::kb16>(); break;
case PageSize::kb32: update_mapping<PageSize::kb32>(); break;
}
}
template <PageSize size>
void update_mapping() {
// Clear all logical mappings.
for(auto &map: mapping_) {
// Seed all pointers to an address sufficiently far lower than the beginning of RAM as to mark
// the entire page as unmapped no matter what offset is added.
std::fill(map.begin(), map.end(), ram_.data() - 32768);
}
// For each physical page, project it into logical space
// and store it.
for(const auto page: pages_) {
uint32_t physical, logical;
switch(size) {
case PageSize::kb4:
// 4kb:
// A[6:0] -> PPN[6:0]
// A[11:10] -> LPN[12:11]; A[22:12] -> LPN[10:0] i.e. 8192 logical pages
physical = page & BitMask<6, 0>::value;
physical <<= 12;
logical = (page & BitMask<11, 10>::value) << 1;
logical |= (page & BitMask<22, 12>::value) >> 12;
break;
case PageSize::kb8:
// 8kb:
// A[0] -> PPN[6]; A[6:1] -> PPN[5:0]
// A[11:10] -> LPN[11:10]; A[22:13] -> LPN[9:0] i.e. 4096 logical pages
physical = (page & BitMask<0, 0>::value) << 6;
physical |= (page & BitMask<6, 1>::value) >> 1;
physical <<= 13;
logical = page & BitMask<11, 10>::value;
logical |= (page & BitMask<22, 13>::value) >> 13;
break;
case PageSize::kb16:
// 16kb:
// A[1:0] -> PPN[6:5]; A[6:2] -> PPN[4:0]
// A[11:10] -> LPN[10:9]; A[22:14] -> LPN[8:0] i.e. 2048 logical pages
physical = (page & BitMask<1, 0>::value) << 5;
physical |= (page & BitMask<6, 2>::value) >> 2;
physical <<= 14;
logical = (page & BitMask<11, 10>::value) >> 1;
logical |= (page & BitMask<22, 14>::value) >> 14;
break;
case PageSize::kb32:
// 32kb:
// A[1] -> PPN[6]; A[2] -> PPN[5]; A[0] -> PPN[4]; A[6:3] -> PPN[3:0]
// A[11:10] -> LPN[9:8]; A[22:15] -> LPN[7:0] i.e. 1024 logical pages
physical = (page & BitMask<1, 1>::value) << 5;
physical |= (page & BitMask<2, 2>::value) << 3;
physical |= (page & BitMask<0, 0>::value) << 4;
physical |= (page & BitMask<6, 3>::value) >> 3;
physical <<= 15;
logical = (page & BitMask<11, 10>::value) >> 2;
logical |= (page & BitMask<22, 15>::value) >> 15;
break;
}
// printf("%08x => physical %d -> logical %d\n", page, (physical >> 15), logical);
// TODO: consider clashes.
// TODO: what if there's less than 4mb present?
const auto target = &ram_[physical];
const auto set_supervisor = [&](bool read, bool write) {
if(read) mapping<true>(false, false)[logical] = target;
if(write) mapping<false>(false, false)[logical] = target;
};
const auto set_os = [&](bool read, bool write) {
if(read) mapping<true>(true, true)[logical] = target;
if(write) mapping<false>(true, true)[logical] = target;
};
const auto set_user = [&](bool read, bool write) {
if(read) mapping<true>(true, false)[logical] = target;
if(write) mapping<false>(true, false)[logical] = target;
};
set_supervisor(true, true);
switch((page >> 8) & 3) {
case 0b00:
set_os(true, true);
set_user(true, true);
break;
case 0b01:
set_os(true, true);
set_user(true, false);
break;
default:
set_os(true, false);
set_user(false, false);
break;
}
}
}
};
}

View File

@@ -0,0 +1,211 @@
//
// Audio.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../../Concurrency/AsyncTaskQueue.hpp"
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include <array>
#include <cstdint>
namespace Archimedes {
// Generate lookup table for sound output levels, and hold it only once regardless
// of how many template instantiations there are of @c Sound.
static constexpr std::array<int16_t, 256> generate_levels() {
std::array<int16_t, 256> result{};
// There are 8 segments of 16 steps; each segment is a linear
// interpolation from its start level to its end level and
// each level is double the previous.
//
// Bit 7 provides a sign.
for(size_t c = 0; c < 256; c++) {
// This is the VIDC1 rule.
// const bool is_negative = c & 128;
// const auto point = static_cast<int>(c & 0xf);
// const auto chord = static_cast<int>((c >> 4) & 7);
// VIDC2 rule, which seems to be effective. I've yet to spot the rule by which
// VIDC1/2 is detected.
const bool is_negative = c & 1;
const auto point = static_cast<int>((c >> 1) & 0xf);
const auto chord = static_cast<int>((c >> 5) & 7);
const int start = (1 << chord) - 1;
const int end = (chord == 7) ? 247 : ((start << 1) + 1);
const int level = start * (16 - point) + end * point;
result[c] = static_cast<int16_t>((level * 32767) / 3832);
if(is_negative) result[c] = -result[c];
}
return result;
}
struct SoundLevels {
static constexpr auto levels = generate_levels();
};
/// Models the Archimedes sound output; in a real machine this is a joint efort between the VIDC and the MEMC.
template <typename InterruptObserverT>
struct Sound: private SoundLevels {
Sound(InterruptObserverT &observer, const uint8_t *ram) : ram_(ram), observer_(observer) {
speaker_.set_input_rate(1'000'000);
speaker_.set_high_frequency_cutoff(2'200.0f);
}
void set_next_end(uint32_t value) {
next_.end = value;
}
void set_next_start(uint32_t value) {
next_.start = value;
set_buffer_valid(true); // My guess: this is triggered on next buffer start write.
// Definitely wrong; testing.
// set_halted(false);
}
bool interrupt() const {
return !next_buffer_valid_;
}
void swap() {
current_.start = next_.start;
std::swap(current_.end, next_.end);
set_buffer_valid(false);
set_halted(false);
}
void set_frequency(uint8_t frequency) {
divider_ = reload_ = frequency;
}
void set_stereo_image(uint8_t channel, uint8_t value) {
if(!value) {
positions_[channel].left =
positions_[channel].right = 0;
return;
}
positions_[channel].right = value - 1;
positions_[channel].left = 6 - positions_[channel].right;
}
void set_dma_enabled(bool enabled) {
dma_enabled_ = enabled;
}
void tick() {
// Write silence if not currently outputting.
if(halted_ || !dma_enabled_) {
post_sample(Outputs::Speaker::StereoSample());
return;
}
// Apply user-programmed clock divider.
--divider_;
if(!divider_) {
divider_ = reload_ + 2;
// Grab a single byte from the FIFO.
const uint8_t raw = ram_[static_cast<std::size_t>(current_.start) + static_cast<std::size_t>(byte_)];
sample_ = Outputs::Speaker::StereoSample( // TODO: pan, volume.
static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].left) / 6),
static_cast<int16_t>((levels[raw] * positions_[byte_ & 7].right) / 6)
);
++byte_;
// If the FIFO is exhausted, consider triggering a DMA request.
if(byte_ == 16) {
byte_ = 0;
current_.start += 16;
if(current_.start == current_.end) {
if(next_buffer_valid_) {
swap();
} else {
set_halted(true);
}
}
}
}
post_sample(sample_);
}
Outputs::Speaker::Speaker *speaker() {
return &speaker_;
}
~Sound() {
while(is_posting_.test_and_set());
}
private:
const uint8_t *ram_ = nullptr;
uint8_t divider_ = 0, reload_ = 0;
int byte_ = 0;
void set_buffer_valid(bool valid) {
next_buffer_valid_ = valid;
observer_.update_interrupts();
}
void set_halted(bool halted) {
if(halted_ != halted && !halted) {
byte_ = 0;
divider_ = reload_;
}
halted_ = halted;
}
bool next_buffer_valid_ = false;
bool halted_ = true; // This is a bit of a guess.
bool dma_enabled_ = false;
struct Buffer {
uint32_t start = 0, end = 0;
};
Buffer current_, next_;
struct StereoPosition {
// These are maintained as sixths, i.e. a value of 6 means 100%.
int left, right;
} positions_[8];
InterruptObserverT &observer_;
Outputs::Speaker::PushLowpass<true> speaker_;
Concurrency::AsyncTaskQueue<true> queue_;
void post_sample(Outputs::Speaker::StereoSample sample) {
samples_[sample_target_][sample_pointer_++] = sample;
if(sample_pointer_ == samples_[0].size()) {
while(is_posting_.test_and_set());
const auto post_source = sample_target_;
sample_target_ ^= 1;
sample_pointer_ = 0;
queue_.enqueue([this, post_source]() {
speaker_.push(reinterpret_cast<int16_t *>(samples_[post_source].data()), samples_[post_source].size());
is_posting_.clear();
});
}
}
std::size_t sample_pointer_ = 0;
std::size_t sample_target_ = 0;
Outputs::Speaker::StereoSample sample_;
using SampleBuffer = std::array<Outputs::Speaker::StereoSample, 4096>;
std::array<SampleBuffer, 2> samples_;
std::atomic_flag is_posting_ = ATOMIC_FLAG_INIT;
};
}

View File

@@ -0,0 +1,655 @@
//
// Video.hpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "../../../Outputs/Log.hpp"
#include "../../../Outputs/CRT/CRT.hpp"
#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <optional>
namespace Archimedes {
template <typename InterruptObserverT, typename ClockRateObserverT, typename SoundT>
struct Video {
Video(InterruptObserverT &interrupt_observer, ClockRateObserverT &clock_rate_observer, SoundT &sound, const uint8_t *ram) :
interrupt_observer_(interrupt_observer),
clock_rate_observer_(clock_rate_observer),
sound_(sound),
ram_(ram),
crt_(Outputs::Display::InputDataType::Red4Green4Blue4) {
set_clock_divider(3);
crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f));
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
}
static constexpr uint16_t colour(uint32_t value) {
uint8_t packed[2]{};
packed[0] = value & 0xf;
packed[1] = (value & 0xf0) | ((value & 0xf00) >> 8);
#if TARGET_RT_BIG_ENDIAN
return static_cast<uint16_t>(packed[1] | (packed[0] << 8));
#else
return static_cast<uint16_t>(packed[0] | (packed[1] << 8));
#endif
};
static constexpr uint16_t high_spread[] = {
colour(0b0000'0000'0000), colour(0b0000'0000'1000), colour(0b0000'0100'0000), colour(0b0000'0100'1000),
colour(0b0000'1000'0000), colour(0b0000'1000'1000), colour(0b0000'1100'0000), colour(0b0000'1100'1000),
colour(0b1000'0000'0000), colour(0b1000'0000'1000), colour(0b1000'0100'0000), colour(0b1000'0100'1000),
colour(0b1000'1000'0000), colour(0b1000'1000'1000), colour(0b1000'1100'0000), colour(0b1000'1100'1000),
};
void write(uint32_t value) {
const auto target = (value >> 24) & 0xfc;
const auto timing_value = [](uint32_t value) -> uint32_t {
return (value >> 14) & 0x3ff;
};
switch(target) {
case 0x00: case 0x04: case 0x08: case 0x0c:
case 0x10: case 0x14: case 0x18: case 0x1c:
case 0x20: case 0x24: case 0x28: case 0x2c:
case 0x30: case 0x34: case 0x38: case 0x3c:
colours_[target >> 2] = colour(value);
break;
case 0x40: border_colour_ = colour(value); break;
case 0x44: case 0x48: case 0x4c:
cursor_colours_[(target - 0x40) >> 2] = colour(value);
break;
case 0x80: horizontal_timing_.period = timing_value(value); break;
case 0x84: horizontal_timing_.sync_width = timing_value(value); break;
case 0x88: horizontal_timing_.border_start = timing_value(value); break;
case 0x8c: horizontal_timing_.display_start = timing_value(value); break;
case 0x90: horizontal_timing_.display_end = timing_value(value); break;
case 0x94: horizontal_timing_.border_end = timing_value(value); break;
case 0x98:
horizontal_timing_.cursor_start = (value >> 13) & 0x7ff;
cursor_shift_ = (value >> 11) & 3;
break;
case 0x9c: horizontal_timing_.interlace_sync_position = timing_value(value); break;
case 0xa0: vertical_timing_.period = timing_value(value); break;
case 0xa4: vertical_timing_.sync_width = timing_value(value); break;
case 0xa8: vertical_timing_.border_start = timing_value(value); break;
case 0xac: vertical_timing_.display_start = timing_value(value); break;
case 0xb0: vertical_timing_.display_end = timing_value(value); break;
case 0xb4: vertical_timing_.border_end = timing_value(value); break;
case 0xb8: vertical_timing_.cursor_start = timing_value(value); break;
case 0xbc: vertical_timing_.cursor_end = timing_value(value); break;
case 0xe0:
// Set pixel rate. This is the value that a 24Mhz clock should be divided
// by to get half the pixel rate.
switch(value & 0b11) {
case 0b00: set_clock_divider(6); break; // i.e. pixel clock = 8Mhz.
case 0b01: set_clock_divider(4); break; // 12Mhz.
case 0b10: set_clock_divider(3); break; // 16Mhz.
case 0b11: set_clock_divider(2); break; // 24Mhz.
}
// Set colour depth.
colour_depth_ = Depth((value >> 2) & 0b11);
// Crib interlace-enable.
vertical_timing_.is_interlaced = value & (1 << 6);
break;
//
// Sound parameters.
//
case 0x60: case 0x64: case 0x68: case 0x6c:
case 0x70: case 0x74: case 0x78: case 0x7c: {
const uint8_t channel = ((value >> 26) + 7) & 7;
sound_.set_stereo_image(channel, value & 7);
} break;
case 0xc0:
sound_.set_frequency(value & 0xff);
break;
default:
logger.error().append("TODO: unrecognised VIDC write of %08x", value);
break;
}
}
void tick() {
// Pick new horizontal state, possibly rolling over into the vertical.
horizontal_state_.increment_position(horizontal_timing_);
if(horizontal_state_.did_restart()) {
end_horizontal();
const auto old_phase = vertical_state_.phase();
vertical_state_.increment_position(vertical_timing_);
const auto phase = vertical_state_.phase();
if(phase != old_phase) {
// Copy frame and cursor start addresses into counters at the
// start of the first vertical display line.
if(phase == Phase::Display) {
address_ = frame_start_;
cursor_address_ = cursor_start_;
// Accumulate a count of how many times the processor has tried
// to update the visible buffer more than once in a frame; this
// will usually indicate that the software being run isn't properly
// synchronised to actual machine speed.
++frame_starts_;
if(frame_start_sets_ > 10) {
overages_ += frame_start_sets_ > frame_starts_;
frame_start_sets_ = frame_starts_ = 0;
}
}
if(old_phase == Phase::Display) {
entered_flyback_ = true;
interrupt_observer_.update_interrupts();
}
}
// Determine which next 8 bytes will be the cursor image for this line.
// Pragmatically, updating cursor_address_ once per line avoids probable
// errors in getting it to appear appropriately over both pixels and border.
if(vertical_state_.cursor_active) {
uint8_t *cursor_pixel = cursor_image_.data();
for(int byte = 0; byte < 8; byte ++) {
cursor_pixel[0] = (ram_[cursor_address_] >> 0) & 3;
cursor_pixel[1] = (ram_[cursor_address_] >> 2) & 3;
cursor_pixel[2] = (ram_[cursor_address_] >> 4) & 3;
cursor_pixel[3] = (ram_[cursor_address_] >> 6) & 3;
cursor_pixel += 4;
++cursor_address_;
}
}
cursor_pixel_ = 32;
}
// Fetch if relevant display signals are active.
if(vertical_state_.display_active() && horizontal_state_.display_active()) {
const auto next_byte = [&]() {
const auto next = ram_[address_];
++address_;
// `buffer_end_` is the final address that a 16-byte block will be fetched from;
// the +16 here papers over the fact that I'm not accurately implementing DMA.
if(address_ == buffer_end_ + 16) {
address_ = buffer_start_;
}
bitmap_queue_[bitmap_queue_pointer_ & 7] = next;
++bitmap_queue_pointer_;
};
switch(colour_depth_) {
case Depth::EightBPP: next_byte(); next_byte(); break;
case Depth::FourBPP: next_byte(); break;
case Depth::TwoBPP: if(!(pixel_count_&3)) next_byte(); break;
case Depth::OneBPP: if(!(pixel_count_&7)) next_byte(); break;
}
}
// Move along line.
switch(vertical_state_.phase()) {
case Phase::Sync: tick_horizontal<Phase::Sync>(); break;
case Phase::Blank: tick_horizontal<Phase::Blank>(); break;
case Phase::Border: tick_horizontal<Phase::Border>(); break;
case Phase::Display: tick_horizontal<Phase::Display>(); break;
case Phase::StartInterlacedSync: tick_horizontal<Phase::StartInterlacedSync>(); break;
case Phase::EndInterlacedSync: tick_horizontal<Phase::EndInterlacedSync>(); break;
}
++time_in_phase_;
}
/// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise.
bool interrupt() {
// Guess: edge triggered?
const bool interrupt = entered_flyback_;
entered_flyback_ = false;
return interrupt;
}
bool flyback_active() const {
return vertical_state_.phase() != Phase::Display;
}
void set_frame_start(uint32_t address) {
frame_start_ = address;
++frame_start_sets_;
}
void set_buffer_start(uint32_t address) { buffer_start_ = address; }
void set_buffer_end(uint32_t address) { buffer_end_ = address; }
void set_cursor_start(uint32_t address) { cursor_start_ = address; }
Outputs::CRT::CRT &crt() { return crt_; }
const Outputs::CRT::CRT &crt() const { return crt_; }
int clock_divider() const {
return static_cast<int>(clock_divider_);
}
int frame_rate_overages() const {
return overages_;
}
void set_dma_enabled(bool dma_enabled) {
dma_enabled_ = dma_enabled;
}
//
// The following is provided for input automation;
// it does not correlate with real hardware functionality.
//
std::optional<std::pair<int, int>> cursor_location() {
if(
!dma_enabled_ ||
vertical_timing_.cursor_end <= vertical_timing_.cursor_start ||
horizontal_timing_.cursor_start >= (horizontal_timing_.period * 2)
) {
return std::nullopt;
}
const auto horizontal_start = horizontal_timing_.display_start + horizontal_state_.output_latency(colour_depth_);
return std::make_pair(
int(horizontal_timing_.cursor_start) + 6 - int(horizontal_start * 2),
int(vertical_timing_.cursor_start) - int(vertical_timing_.display_start));
}
private:
Log::Logger<Log::Source::ARMIOC> logger;
InterruptObserverT &interrupt_observer_;
ClockRateObserverT &clock_rate_observer_;
SoundT &sound_;
// In the current version of this code, video DMA occurrs costlessly,
// being deferred to the component itself.
const uint8_t *ram_ = nullptr;
Outputs::CRT::CRT crt_;
bool dma_enabled_ = false;
// Horizontal and vertical timing.
struct Timing {
uint32_t period = 0;
uint32_t sync_width = 0;
uint32_t border_start = 0;
uint32_t border_end = 0;
uint32_t display_start = 0;
uint32_t display_end = 0;
uint32_t cursor_start = 0;
uint32_t cursor_end = 0;
uint32_t interlace_sync_position = 0;
bool is_interlaced = false;
};
uint32_t cursor_shift_ = 0;
Timing horizontal_timing_, vertical_timing_;
enum class Depth {
OneBPP = 0b00,
TwoBPP = 0b01,
FourBPP = 0b10,
EightBPP = 0b11,
};
// Current video state.
enum class Phase {
Sync, Blank, Border, Display, StartInterlacedSync, EndInterlacedSync,
};
template <bool is_vertical>
struct State {
uint32_t position = 0;
uint32_t display_start = 0;
uint32_t display_end = 0;
bool is_odd_iteration_ = false;
void increment_position(const Timing &timing) {
const auto previous_override = interlace_override_;
if constexpr (is_vertical) {
interlace_override_ = Phase::Sync; // i.e. no override.
}
if(position == timing.sync_width) {
state |= SyncEnded;
if(is_vertical && timing.is_interlaced && is_odd_iteration_ && previous_override == Phase::Sync) {
--position;
interlace_override_ = Phase::EndInterlacedSync;
}
}
if(position == timing.display_start) { state |= DisplayStarted; display_start = position; }
if(position == timing.display_end) { state |= DisplayEnded; display_end = position; }
if(position == timing.border_start) state |= BorderStarted;
if(position == timing.border_end) state |= BorderEnded;
cursor_active |= position == timing.cursor_start;
cursor_active &= position != timing.cursor_end;
if(position == timing.period) {
state = DidRestart;
position = 0;
is_odd_iteration_ ^= true;
// Both display start and end need to be seeded as bigger than can be reached,
// while having some overhead for addition.
display_end = display_start = std::numeric_limits<uint32_t>::max() >> 1;
// Possibly label the next as a start-of-interlaced-sync.
if(is_vertical && timing.is_interlaced && is_odd_iteration_) {
interlace_override_ = Phase::StartInterlacedSync;
}
} else {
++position;
if(position == 1024) position = 0;
}
}
bool is_outputting(Depth depth) const {
const auto latency = output_latency(depth);
return position >= display_start + latency && position < display_end + latency;
}
uint32_t output_cycle(Depth depth) const {
return position - display_start - output_latency(depth);
}
static constexpr uint32_t output_latencies[] = {
19 >> 1, // 1 bpp.
11 >> 1, // 2 bpp.
7 >> 1, // 4 bpp.
5 >> 1 // 8 bpp.
};
uint32_t output_latency(Depth depth) const {
return output_latencies[static_cast<uint32_t>(depth)];
}
static constexpr uint8_t SyncEnded = 0x1;
static constexpr uint8_t BorderStarted = 0x2;
static constexpr uint8_t BorderEnded = 0x4;
static constexpr uint8_t DisplayStarted = 0x8;
static constexpr uint8_t DisplayEnded = 0x10;
static constexpr uint8_t DidRestart = 0x20;
uint8_t state = 0;
Phase interlace_override_ = Phase::Sync;
bool cursor_active = false;
bool did_restart() {
const bool result = state & DidRestart;
state &= ~DidRestart;
return result;
}
bool display_active() const {
return (state & DisplayStarted) && !(state & DisplayEnded);
}
Phase phase(Phase horizontal_fallback = Phase::Border) const {
if(is_vertical && interlace_override_ != Phase::Sync) {
return interlace_override_;
}
// TODO: turn the following logic into a lookup table.
if(!(state & SyncEnded)) {
return Phase::Sync;
}
if(!(state & BorderStarted) || (state & BorderEnded)) {
return Phase::Blank;
}
if constexpr (!is_vertical) {
return horizontal_fallback;
}
if(!(state & DisplayStarted) || (state & DisplayEnded)) {
return Phase::Border;
}
return Phase::Display;
}
};
State<false> horizontal_state_;
State<true> vertical_state_;
int time_in_phase_ = 0;
Phase phase_;
uint16_t phased_border_colour_;
int pixel_count_ = 0;
int display_area_start_ = 0;
uint16_t *pixels_ = nullptr;
// It is elsewhere assumed that this size is a multiple of 8.
static constexpr size_t PixelBufferSize = 256;
// Programmer-set addresses.
uint32_t buffer_start_ = 0;
uint32_t buffer_end_ = 0;
uint32_t frame_start_ = 0;
uint32_t cursor_start_ = 0;
int frame_start_sets_ = 0;
int frame_starts_ = 0;
int overages_ = 0;
// Ephemeral address state.
uint32_t address_ = 0;
// Horizontal cursor output state.
uint32_t cursor_address_ = 0;
int cursor_pixel_ = 0;
std::array<uint8_t, 32> cursor_image_;
// Colour palette, converted to internal format.
uint16_t border_colour_;
std::array<uint16_t, 16> colours_{};
std::array<uint16_t, 4> cursor_colours_{};
// An interrupt flag; more closely related to the interface by which
// my implementation of the IOC picks up an interrupt request than
// to hardware.
bool entered_flyback_ = false;
// The divider that would need to be applied to a 24Mhz clock to
// get half the current pixel clock; counting is in units of half
// the pixel clock because that's the fidelity at which the programmer
// places horizontal events — display start, end, sync period, etc.
uint32_t clock_divider_ = 0;
Depth colour_depth_;
// A temporary buffer that holds video contents during the latency
// period between their generation and their output.
uint8_t bitmap_queue_[8];
int bitmap_queue_pointer_ = 0;
void set_clock_divider(uint32_t divider) {
if(divider == clock_divider_) {
return;
}
clock_divider_ = divider;
const auto cycles_per_line = static_cast<int>(24'000'000 / (divider * 312 * 50));
crt_.set_new_timing(
cycles_per_line,
312, /* Height of display. */
Outputs::CRT::PAL::ColourSpace,
Outputs::CRT::PAL::ColourCycleNumerator,
Outputs::CRT::PAL::ColourCycleDenominator,
Outputs::CRT::PAL::VerticalSyncLength,
Outputs::CRT::PAL::AlternatesPhase);
clock_rate_observer_.update_clock_rates();
}
void flush_pixels() {
crt_.output_data(time_in_phase_, static_cast<size_t>(pixel_count_));
time_in_phase_ = 0;
pixel_count_ = 0;
pixels_ = nullptr;
}
void set_phase(Phase phase) {
if(time_in_phase_) {
switch(phase_) {
default: crt_.output_sync(time_in_phase_); break;
case Phase::Blank: crt_.output_blank(time_in_phase_); break;
case Phase::Border: crt_.output_level<uint16_t>(time_in_phase_, phased_border_colour_); break;
case Phase::Display: flush_pixels(); break;
}
}
phase_ = phase;
time_in_phase_ = 0;
phased_border_colour_ = border_colour_;
pixel_count_ = 0;
}
void end_horizontal() {
set_phase(Phase::Sync);
display_area_start_ = -1;
bitmap_queue_pointer_ = 0;
}
template <Phase vertical_phase> void tick_horizontal() {
// Sync lines: obey nothing. All sync, all the time.
if constexpr (vertical_phase == Phase::Sync) {
return;
}
// Start interlaced sync lines: do blank from horizontal sync up to the programmed
// cutoff, then do sync.
if constexpr (vertical_phase == Phase::StartInterlacedSync) {
if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) {
set_phase(Phase::Blank);
}
if(phase_ == Phase::Blank && horizontal_state_.position == horizontal_timing_.interlace_sync_position) {
set_phase(Phase::Sync);
}
return;
}
// End interlaced sync lines: do sync up to the programmed cutoff, then do blank.
if constexpr (vertical_phase == Phase::EndInterlacedSync) {
if(phase_ == Phase::Sync && horizontal_state_.position == horizontal_timing_.interlace_sync_position) {
set_phase(Phase::Blank);
}
return;
}
// Blank lines: obey only the transition from sync to non-sync.
if constexpr (vertical_phase == Phase::Blank) {
if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) {
set_phase(Phase::Blank);
}
return;
}
// Border lines: ignore display phases; also reset the border phase if the colour changes.
if constexpr (vertical_phase == Phase::Border) {
const auto phase = horizontal_state_.phase(Phase::Border);
if(phase != phase_ || (phase_ == Phase::Border && border_colour_ != phased_border_colour_)) {
set_phase(phase);
}
return;
}
if constexpr (vertical_phase != Phase::Display) {
// Should be impossible.
assert(false);
}
// Some timing facts, to explain what would otherwise be magic constants.
static constexpr int CursorDelay = 5; // The cursor will appear six pixels after its programmed trigger point.
// ... BUT! Border and display are currently a pixel early. So move the
// cursor for alignment.
// Deal with sync and blank via set_phase(); collapse display and border into Phase::Display.
const auto phase = horizontal_state_.phase(Phase::Display);
if(phase != phase_) set_phase(phase);
// Update cursor pixel counter if applicable; this might mean triggering it
// and it might just mean advancing it if it has already been triggered.
cursor_pixel_ += 2;
if(vertical_state_.cursor_active) {
const auto pixel_position = horizontal_state_.position << 1;
if(pixel_position <= horizontal_timing_.cursor_start && (pixel_position + 2) > horizontal_timing_.cursor_start) {
cursor_pixel_ = int(horizontal_timing_.cursor_start) - int(pixel_position) - CursorDelay;
}
}
// If this is not [collapsed] Phase::Display, just stop here.
if(phase_ != Phase::Display) return;
// Display phase: maintain an output buffer (if available).
if(pixel_count_ == PixelBufferSize) flush_pixels();
if(!pixel_count_) pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(PixelBufferSize));
// Output.
if(pixels_) {
// Paint the border colour for potential painting over.
if(horizontal_state_.is_outputting(colour_depth_)) {
const auto source = horizontal_state_.output_cycle(colour_depth_);
// TODO: all below should be delayed an extra pixel. As should the border, actually. Fix up externally?
switch(colour_depth_) {
case Depth::EightBPP: {
const uint8_t *bitmap = &bitmap_queue_[(source << 1) & 7];
pixels_[0] = (colours_[bitmap[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[0] >> 4];
pixels_[1] = (colours_[bitmap[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[1] >> 4];
} break;
case Depth::FourBPP:
pixels_[0] = colours_[bitmap_queue_[source & 7] & 0xf];
pixels_[1] = colours_[bitmap_queue_[source & 7] >> 4];
break;
case Depth::TwoBPP: {
uint8_t &bitmap = bitmap_queue_[(source >> 1) & 7];
pixels_[0] = colours_[bitmap & 3];
pixels_[1] = colours_[(bitmap >> 2) & 3];
bitmap >>= 4;
} break;
case Depth::OneBPP: {
uint8_t &bitmap = bitmap_queue_[(source >> 2) & 7];
pixels_[0] = colours_[bitmap & 1];
pixels_[1] = colours_[(bitmap >> 1) & 1];
bitmap >>= 2;
} break;
}
} else {
pixels_[0] = pixels_[1] = border_colour_;
}
// Overlay cursor if applicable.
if(cursor_pixel_ < 32) {
if(cursor_pixel_ >= 0) {
const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_)];
if(pixel) {
pixels_[0] = cursor_colours_[pixel];
}
}
if(cursor_pixel_ >= -1 && cursor_pixel_ < 31) {
const auto pixel = cursor_image_[static_cast<size_t>(cursor_pixel_ + 1)];
if(pixel) {
pixels_[1] = cursor_colours_[pixel];
}
}
}
pixels_ += 2;
}
pixel_count_ += 2;
}
};
}

View File

@@ -8,24 +8,24 @@
#include "Electron.hpp"
#include "../../Activity/Source.hpp"
#include "../MachineTypes.hpp"
#include "../../Configurable/Configurable.hpp"
#include "../../../Activity/Source.hpp"
#include "../../MachineTypes.hpp"
#include "../../../Configurable/Configurable.hpp"
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "../../ClockReceiver/ForceInline.hpp"
#include "../../Configurable/StandardOptions.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Processors/6502/6502.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../ClockReceiver/ForceInline.hpp"
#include "../../../Configurable/StandardOptions.hpp"
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../../Processors/6502/6502.hpp"
#include "../../Storage/MassStorage/SCSI/SCSI.hpp"
#include "../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
#include "../../Storage/Tape/Tape.hpp"
#include "../../../Storage/MassStorage/SCSI/SCSI.hpp"
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
#include "../../../Storage/Tape/Tape.hpp"
#include "../Utility/Typer.hpp"
#include "../../Analyser/Static/Acorn/Target.hpp"
#include "../../Utility/Typer.hpp"
#include "../../../Analyser/Static/Acorn/Target.hpp"
#include "../../ClockReceiver/JustInTime.hpp"
#include "../../../ClockReceiver/JustInTime.hpp"
#include "Interrupts.hpp"
#include "Keyboard.hpp"
@@ -51,7 +51,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
public SCSI::Bus::Observer,
public ClockingHint::Observer {
public:
ConcreteMachine(const Analyser::Static::Acorn::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
ConcreteMachine(const Analyser::Static::Acorn::ElectronTarget &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6502_(*this),
scsi_bus_(4'000'000),
hard_drive_(scsi_bus_, 0),
@@ -787,7 +787,7 @@ template <bool has_scsi_bus> class ConcreteMachine:
using namespace Electron;
std::unique_ptr<Machine> Machine::Electron(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
using Target = Analyser::Static::Acorn::Target;
using Target = Analyser::Static::Acorn::ElectronTarget;
const Target *const acorn_target = dynamic_cast<const Target *>(target);
if(acorn_target->media.mass_storage_devices.empty()) {
@@ -796,5 +796,3 @@ std::unique_ptr<Machine> Machine::Electron(const Analyser::Static::Target *targe
return std::make_unique<Electron::ConcreteMachine<true>>(*acorn_target, rom_fetcher);
}
}
Machine::~Machine() {}

View File

@@ -8,10 +8,10 @@
#pragma once
#include "../../Configurable/Configurable.hpp"
#include "../../Configurable/StandardOptions.hpp"
#include "../../Analyser/Static/StaticAnalyser.hpp"
#include "../ROMMachine.hpp"
#include "../../../Configurable/Configurable.hpp"
#include "../../../Configurable/StandardOptions.hpp"
#include "../../../Analyser/Static/StaticAnalyser.hpp"
#include "../../ROMMachine.hpp"
#include <memory>
@@ -25,7 +25,7 @@ namespace Electron {
*/
class Machine {
public:
virtual ~Machine();
virtual ~Machine() = default;
/// Creates and returns an Electron.
static std::unique_ptr<Machine> Electron(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);

View File

@@ -8,8 +8,8 @@
#pragma once
#include "../KeyboardMachine.hpp"
#include "../Utility/Typer.hpp"
#include "../../KeyboardMachine.hpp"
#include "../../Utility/Typer.hpp"
namespace Electron {

View File

@@ -8,8 +8,8 @@
#pragma once
#include "../../Components/1770/1770.hpp"
#include "../../Activity/Observer.hpp"
#include "../../../Components/1770/1770.hpp"
#include "../../../Activity/Observer.hpp"
namespace Electron {

View File

@@ -0,0 +1,54 @@
//
// Speaker.cpp
// Clock Signal
//
// Created by Thomas Harte on 03/12/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "SoundGenerator.hpp"
#include <cstring>
using namespace Electron;
SoundGenerator::SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue) :
audio_queue_(audio_queue) {}
void SoundGenerator::set_sample_volume_range(std::int16_t range) {
volume_ = unsigned(range / 2);
}
template <Outputs::Speaker::Action action>
void SoundGenerator::apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target) {
if constexpr (action == Outputs::Speaker::Action::Ignore) {
counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2);
return;
}
if(is_enabled_) {
while(number_of_samples--) {
Outputs::Speaker::apply<action>(*target, Outputs::Speaker::MonoSample((counter_ / (divider_+1)) * volume_));
target++;
counter_ = (counter_ + 1) % ((divider_+1) * 2);
}
} else {
Outputs::Speaker::fill<action>(target, target + number_of_samples, Outputs::Speaker::MonoSample(0));
}
}
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Mix>(std::size_t, Outputs::Speaker::MonoSample *);
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Store>(std::size_t, Outputs::Speaker::MonoSample *);
template void SoundGenerator::apply_samples<Outputs::Speaker::Action::Ignore>(std::size_t, Outputs::Speaker::MonoSample *);
void SoundGenerator::set_divider(uint8_t divider) {
audio_queue_.enqueue([this, divider]() {
divider_ = divider * 32 / clock_rate_divider;
});
}
void SoundGenerator::set_is_enabled(bool is_enabled) {
audio_queue_.enqueue([this, is_enabled]() {
is_enabled_ = is_enabled;
counter_ = 0;
});
}

View File

@@ -8,12 +8,12 @@
#pragma once
#include "../../Outputs/Speaker/Implementation/SampleSource.hpp"
#include "../../Concurrency/AsyncTaskQueue.hpp"
#include "../../../Outputs/Speaker/Implementation/BufferSource.hpp"
#include "../../../Concurrency/AsyncTaskQueue.hpp"
namespace Electron {
class SoundGenerator: public ::Outputs::Speaker::SampleSource {
class SoundGenerator: public ::Outputs::Speaker::BufferSource<SoundGenerator, false> {
public:
SoundGenerator(Concurrency::AsyncTaskQueue<false> &audio_queue);
@@ -23,11 +23,10 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
static constexpr unsigned int clock_rate_divider = 8;
// To satisfy ::SampleSource.
void get_samples(std::size_t number_of_samples, int16_t *target);
void skip_samples(std::size_t number_of_samples);
// For BufferSource.
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::MonoSample *target);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
Concurrency::AsyncTaskQueue<false> &audio_queue_;

View File

@@ -10,9 +10,9 @@
#include <cstdint>
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "../../Storage/Tape/Tape.hpp"
#include "../../Storage/Tape/Parsers/Acorn.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Storage/Tape/Tape.hpp"
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
#include "Interrupts.hpp"
namespace Electron {

View File

@@ -8,8 +8,8 @@
#pragma once
#include "../../Outputs/CRT/CRT.hpp"
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Outputs/CRT/CRT.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "Interrupts.hpp"
#include <vector>

View File

@@ -221,7 +221,7 @@ class ConcreteMachine:
// MARK: - MachineTypes::MouseMachine.
Inputs::Mouse &get_mouse() final {
return chipset_.get_mouse();;
return chipset_.get_mouse();
}
// MARK: - MachineTypes::JoystickMachine.
@@ -256,5 +256,3 @@ std::unique_ptr<Machine> Machine::Amiga(const Analyser::Static::Target *target,
const Target *const amiga_target = dynamic_cast<const Target *>(target);
return std::make_unique<Amiga::ConcreteMachine>(*amiga_target, rom_fetcher);
}
Machine::~Machine() {}

View File

@@ -17,7 +17,7 @@ namespace Amiga {
class Machine {
public:
virtual ~Machine();
virtual ~Machine() = default;
/// Creates and returns an Amiga.
static std::unique_ptr<Machine> Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);

View File

@@ -20,7 +20,7 @@ Audio::Audio(Chipset &chipset, uint16_t *ram, size_t word_size, float output_rat
// Mark all buffers as available.
for(auto &flag: buffer_available_) {
flag.store(true, std::memory_order::memory_order_relaxed);
flag.store(true, std::memory_order_relaxed);
}
speaker_.set_input_rate(output_rate);
@@ -130,7 +130,7 @@ void Audio::output() {
// Spin until the next buffer is available if just entering it for the first time.
// Contention here should be essentially non-existent.
if(!sample_pointer_) {
while(!buffer_available_[buffer_pointer_].load(std::memory_order::memory_order_relaxed));
while(!buffer_available_[buffer_pointer_].load(std::memory_order_relaxed));
}
// Left.
@@ -155,10 +155,10 @@ void Audio::output() {
const auto &buffer = buffer_[buffer_pointer_];
auto &flag = buffer_available_[buffer_pointer_];
flag.store(false, std::memory_order::memory_order_release);
flag.store(false, std::memory_order_release);
queue_.enqueue([this, &buffer, &flag] {
speaker_.push(buffer.data(), buffer.size() >> 1);
flag.store(true, std::memory_order::memory_order_relaxed);
flag.store(true, std::memory_order_relaxed);
});
buffer_pointer_ = (buffer_pointer_ + 1) % BufferCount;

View File

@@ -28,9 +28,6 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> {
using DMADevice::DMADevice;
template <int id, int shift> void set_pointer(uint16_t value) {
if(get_status() & 0x4000) {
printf(">>>");
}
DMADevice<4, 4>::set_pointer<id, shift>(value);
}
@@ -65,7 +62,7 @@ template <bool record_bus = false> class Blitter: public DMADevice<4, 4> {
uint32_t address = 0;
uint16_t value = 0;
Transaction() {}
Transaction() = default;
Transaction(Type type) : type(type) {}
Transaction(Type type, uint32_t address, uint16_t value) : type(type), address(address), value(value) {}

View File

@@ -1293,5 +1293,3 @@ std::unique_ptr<Machine> Machine::AmstradCPC(const Analyser::Static::Target *tar
case Target::Model::CPC464: return std::make_unique<AmstradCPC::ConcreteMachine<false>>(*cpc_target, rom_fetcher);
}
}
Machine::~Machine() {}

View File

@@ -22,7 +22,7 @@ namespace AmstradCPC {
*/
class Machine {
public:
virtual ~Machine();
virtual ~Machine() = default;
/// Creates and returns an Amstrad CPC.
static std::unique_ptr<Machine> AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);

View File

@@ -34,7 +34,7 @@ struct Command {
uint8_t device = AllDevices;
uint8_t reg = NoRegister;
Command() {}
Command() = default;
Command(Type type) : type(type) {}
Command(Type type, uint8_t device) : type(type), device(device) {}
Command(Type type, uint8_t device, uint8_t reg) : type(type), device(device), reg(reg) {}

View File

@@ -25,15 +25,15 @@ void Mouse::perform_command(const Command &command) {
// There's some small chance of creating negative feedback here — taking too much off delta_x_ or delta_y_
// due to a change in the underlying value between the load and the arithmetic. But if that occurs it means
// the user moved the mouse again in the interim, so it'll just play out as very slight latency.
auto delta_x = delta_x_.load(std::memory_order::memory_order_relaxed);
auto delta_y = delta_y_.load(std::memory_order::memory_order_relaxed);
auto delta_x = delta_x_.load(std::memory_order_relaxed);
auto delta_y = delta_y_.load(std::memory_order_relaxed);
if(abs(delta_x) > max_delta || abs(delta_y) > max_delta) {
int max = std::max(abs(delta_x), abs(delta_y));
delta_x = delta_x * max_delta / max;
delta_y = delta_y * max_delta / max;
}
const int buttons = button_flags_.load(std::memory_order::memory_order_relaxed);
const int buttons = button_flags_.load(std::memory_order_relaxed);
delta_x_ -= delta_x;
delta_y_ -= delta_y;

View File

@@ -13,7 +13,7 @@
using namespace Apple::ADB;
namespace {
Log::Logger<Log::Source::ADBDevice> logger;
[[maybe_unused]] Log::Logger<Log::Source::ADBDevice> logger;
}
ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) :

View File

@@ -15,7 +15,9 @@
#include "../../../Processors/6502/6502.hpp"
#include "../../../Components/AudioToggle/AudioToggle.hpp"
#include "../../../Components/AY38910/AY38910.hpp"
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../../Outputs/Log.hpp"
@@ -24,6 +26,7 @@
#include "DiskIICard.hpp"
#include "Joystick.hpp"
#include "LanguageCardSwitches.hpp"
#include "Mockingboard.hpp"
#include "SCSICard.hpp"
#include "Video.hpp"
@@ -41,17 +44,71 @@
namespace {
constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II.
constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II.
constexpr int DiskIISlot = 6; // Apple recommended slot 6 for the (first) Disk II.
constexpr int SCSISlot = 7; // Install the SCSI card in slot 7, to one-up any connected Disk II.
constexpr int MockingboardSlot = 4; // Conventional Mockingboard slot.
// The system's master clock rate.
//
// Quick note on this:
//
// * 64 out of 65 CPU cycles last for 14 cycles of the master clock;
// * every 65th cycle lasts for 16 cycles of the master clock;
// * that keeps CPU cycles in-phase with the colour subcarrier: each line of output is 64*14 + 16 = 912 master cycles long;
// * ... and since hsync is placed to make each line 228 colour clocks long that means 4 master clocks per colour clock;
// * ... which is why all Apple II video modes are expressible as four binary outputs per colour clock;
// * ... and hence seven pixels per memory access window clock in high-res mode, 14 in double high-res, etc.
constexpr float master_clock = 14318180.0;
/// Provides an AY that runs at the CPU rate divided by 4 given an input of the master clock divided by 2,
/// allowing for stretched CPU clock cycles.
struct StretchedAYPair:
Apple::II::AYPair,
public Outputs::Speaker::BufferSource<StretchedAYPair, true> {
using AYPair::AYPair;
template <Outputs::Speaker::Action action>
void apply_samples(std::size_t number_of_samples, Outputs::Speaker::StereoSample *target) {
// (1) take 64 windows of 7 input cycles followed by one window of 8 input cycles;
// (2) after each four windows, advance the underlying AY.
//
// i.e. advance after:
//
// * 28 cycles, {16 times, then 15 times, then 15 times, then 15 times};
// * 29 cycles, once.
//
// so:
// 16, 1; 15, 1; 15, 1; 15, 1
//
// i.e. add an extra one on the 17th, 33rd, 49th and 65th ticks in a 65-tick loop.
for(std::size_t c = 0; c < number_of_samples; c++) {
++subdivider_;
if(subdivider_ == 28) {
++phase_;
subdivider_ = (phase_ & 15) ? 0 : -1;
if(phase_ == 65) phase_ = 0;
advance();
}
target[c] = level();
}
}
private:
int phase_ = 0;
int subdivider_ = 0;
};
}
namespace Apple {
namespace II {
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
template <Analyser::Static::AppleII::Target::Model model, bool has_mockingboard> class ConcreteMachine:
public Apple::II::Machine,
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
@@ -83,14 +140,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
false>;
Processor m6502_;
VideoBusHandler video_bus_handler_;
Apple::II::Video::Video<VideoBusHandler, is_iie()> video_;
Apple::II::Video::Video<VideoBusHandler, is_iie(model)> video_;
int cycles_into_current_line_ = 0;
Cycles cycles_since_video_update_;
void update_video() {
video_.run_for(cycles_since_video_update_.flush<Cycles>());
}
static constexpr int audio_divider = 8;
static constexpr int audio_divider = has_mockingboard ? 1 : 8;
void update_audio() {
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
}
@@ -109,9 +166,23 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
Concurrency::AsyncTaskQueue<false> audio_queue_;
Audio::Toggle audio_toggle_;
Outputs::Speaker::PullLowpass<Audio::Toggle> speaker_;
StretchedAYPair ays_;
using SourceT =
std::conditional_t<has_mockingboard, Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle>, Audio::Toggle>;
using LowpassT = Outputs::Speaker::PullLowpass<SourceT>;
Outputs::Speaker::CompoundSource<StretchedAYPair, Audio::Toggle> mixer_;
Outputs::Speaker::PullLowpass<SourceT> speaker_;
Cycles cycles_since_audio_update_;
constexpr SourceT &lowpass_source() {
if constexpr (has_mockingboard) {
return mixer_;
} else {
return audio_toggle_;
}
}
// MARK: - Cards
static constexpr size_t NoActiveCard = 7; // There is no 'card 0' in internal numbering.
size_t active_card_ = NoActiveCard;
@@ -155,6 +226,24 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
pick_card_messaging_group(card);
}
void card_did_change_interrupt_flags(Apple::II::Card *) final {
bool nmi = false;
bool irq = false;
for(const auto &card: cards_) {
if(card) {
nmi |= card->nmi();
irq |= card->irq();
}
}
m6502_.set_nmi_line(nmi);
m6502_.set_irq_line(irq);
}
Apple::II::Mockingboard *mockingboard() {
return dynamic_cast<Apple::II::Mockingboard *>(cards_[MockingboardSlot - 1].get());
}
Apple::II::DiskIICard *diskii_card() {
return dynamic_cast<Apple::II::DiskIICard *>(cards_[DiskIISlot - 1].get());
}
@@ -225,7 +314,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
const auto zero_state = auxiliary_switches_.zero_state();
uint8_t *const ram = zero_state ? aux_ram_ : ram_;
uint8_t *const rom = is_iie() ? &rom_[3840] : rom_.data();
uint8_t *const rom = is_iie(model) ? &rom_[3840] : rom_.data();
// Which way the region here is mapped to be banks 1 and 2 is
// arbitrary.
@@ -273,7 +362,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// MARK: - Keyboard and typing.
struct Keyboard: public Inputs::Keyboard {
Keyboard(Processor *m6502) : m6502_(m6502) {}
Keyboard(Processor &m6502, AuxiliaryMemorySwitches<ConcreteMachine> &switches) : m6502_(m6502), auxiliary_switches_(switches) {}
void reset_all_keys() final {
open_apple_is_pressed =
@@ -286,7 +375,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final {
if constexpr (!is_iie()) {
if constexpr (!is_iie(model)) {
if(is_repeat && !repeat_is_pressed_) return true;
}
@@ -297,7 +386,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case Key::Down: value = 0x0a; break;
case Key::Up: value = 0x0b; break;
case Key::Backspace:
if(is_iie()) {
if(is_iie(model)) {
value = 0x7f;
break;
} else {
@@ -305,7 +394,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
case Key::Enter: value = 0x0d; break;
case Key::Tab:
if (is_iie()) {
if (is_iie(model)) {
value = '\t';
break;
} else {
@@ -316,7 +405,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case Key::LeftOption:
case Key::RightMeta:
if (is_iie()) {
if (is_iie(model)) {
open_apple_is_pressed = is_pressed;
return true;
} else {
@@ -325,7 +414,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case Key::RightOption:
case Key::LeftMeta:
if (is_iie()) {
if (is_iie(model)) {
closed_apple_is_pressed = is_pressed;
return true;
} else {
@@ -346,7 +435,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case Key::F9: case Key::F10: case Key::F11:
repeat_is_pressed_ = is_pressed;
if constexpr (!is_iie()) {
if constexpr (!is_iie(model)) {
if(is_pressed && (!is_repeat || character_is_pressed_)) {
keyboard_input_ = uint8_t(last_pressed_character_ | 0x80);
}
@@ -365,7 +454,10 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// 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);
m6502_.set_reset_line(is_pressed);
if(!is_pressed) {
auxiliary_switches_.reset();
}
return true;
default:
@@ -374,12 +466,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
// Prior to the IIe, the keyboard could produce uppercase only.
if(!is_iie()) value = char(toupper(value));
if(!is_iie(model)) value = char(toupper(value));
if(control_is_pressed_ && isalpha(value)) value &= 0xbf;
// TODO: properly map IIe keys
if(!is_iie() && shift_is_pressed_) {
if(!is_iie(model) && shift_is_pressed_) {
switch(value) {
case 0x27: value = 0x22; break; // ' -> "
case 0x2c: value = 0x3c; break; // , -> <
@@ -467,7 +559,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
// 6502 connection, for applying the reset button.
Processor *const m6502_;
Processor &m6502_;
AuxiliaryMemorySwitches<ConcreteMachine> &auxiliary_switches_;
};
Keyboard keyboard_;
@@ -480,12 +573,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
video_bus_handler_(ram_, aux_ram_),
video_(video_bus_handler_),
audio_toggle_(audio_queue_),
speaker_(audio_toggle_),
ays_(audio_queue_),
mixer_(ays_, audio_toggle_),
speaker_(lowpass_source()),
language_card_(*this),
auxiliary_switches_(*this),
keyboard_(&m6502_) {
// The system's master clock rate.
constexpr float master_clock = 14318180.0;
keyboard_(m6502_, auxiliary_switches_) {
// This is where things get slightly convoluted: establish the machine as having a clock rate
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
@@ -559,6 +652,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
install_card(SCSISlot, new Apple::II::SCSICard(roms, int(master_clock / 14.0f)));
}
if(target.has_mockingboard) {
// The Mockingboard has a parasitic relationship with this class due to the way
// that audio outputs are implemented in this emulator.
install_card(MockingboardSlot, new Apple::II::Mockingboard(ays_));
}
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) {
@@ -629,7 +728,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
}
if(is_iie()) {
if(is_iie(model)) {
auxiliary_switches_.access(address, isReadOperation(operation));
}
} else {
@@ -647,6 +746,11 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// actor, but this will actually be the result most of the time so it's not
// too terrible.
if(isReadOperation(operation) && address != 0xc000) {
// Ensure any enqueued video changes are applied before grabbing the
// vapour value.
if(video_.has_deferred_actions()) {
update_video();
}
*value = video_.get_last_read_value(cycles_since_video_update_);
}
@@ -669,7 +773,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
*value &= 0x7f;
if(
joysticks_.button(0) ||
(is_iie() && keyboard_.open_apple_is_pressed)
(is_iie(model) && keyboard_.open_apple_is_pressed)
)
*value |= 0x80;
break;
@@ -677,7 +781,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
*value &= 0x7f;
if(
joysticks_.button(1) ||
(is_iie() && keyboard_.closed_apple_is_pressed)
(is_iie(model) && keyboard_.closed_apple_is_pressed)
)
*value |= 0x80;
break;
@@ -699,7 +803,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
} break;
// The IIe-only state reads follow...
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie()) *value = (*value & 0x7f) | (s ? 0x80 : 0x00);
#define IIeSwitchRead(s) *value = keyboard_.get_keyboard_input(); if(is_iie(model)) *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;
@@ -718,12 +822,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
#undef IIeSwitchRead
case 0xc07f:
if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
if(is_iie(model)) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
break;
}
} else {
// Write-only switches. All IIe as currently implemented.
if(is_iie()) {
if(is_iie(model)) {
auxiliary_switches_.access(address, false);
switch(address) {
default: break;
@@ -731,19 +835,19 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc000:
case 0xc001:
update_video();
video_.set_80_store(!!(address&1));
video_.set_80_store(address&1);
break;
case 0xc00c:
case 0xc00d:
update_video();
video_.set_80_columns(!!(address&1));
video_.set_80_columns(address&1);
break;
case 0xc00e:
case 0xc00f:
update_video();
video_.set_alternative_character_set(!!(address&1));
video_.set_alternative_character_set(address&1);
break;
}
}
@@ -756,7 +860,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc050:
case 0xc051:
update_video();
video_.set_text(!!(address&1));
video_.set_text(address&1);
break;
case 0xc052: update_video(); video_.set_mixed(false); break;
case 0xc053: update_video(); video_.set_mixed(true); break;
@@ -775,7 +879,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc05e:
case 0xc05f:
if(is_iie()) {
if(is_iie(model)) {
update_video();
video_.set_annunciator_3(!(address&1));
}
@@ -785,7 +889,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
keyboard_.clear_keyboard_input();
// On the IIe, reading C010 returns additional key info.
if(is_iie() && isReadOperation(operation)) {
if(is_iie(model) && isReadOperation(operation)) {
*value = (keyboard_.get_key_is_down() ? 0x80 : 0x00) | (keyboard_.get_keyboard_input() & 0x7f);
}
break;
@@ -834,14 +938,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
This also sets the active card for the C8 region.
*/
active_card_ = card_number = (address - 0xc100) >> 8;
select = Apple::II::Card::Device;
select = Apple::II::Card::IO;
} else {
/*
Decode the area conventionally used by cards for registers:
C0n0 to C0nF: card n - 8.
*/
card_number = (address - 0xc090) >> 4;
select = Apple::II::Card::IO;
select = Apple::II::Card::Device;
}
// If the selected card is a just-in-time card, update the just-in-time cards,
@@ -922,7 +1026,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
bool prefers_logical_input() final {
return is_iie();
return is_iie(model);
}
Inputs::Keyboard &get_keyboard() final {
@@ -988,13 +1092,22 @@ using namespace Apple::II;
std::unique_ptr<Machine> Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
using Target = Analyser::Static::AppleII::Target;
const Target *const appleii_target = dynamic_cast<const Target *>(target);
switch(appleii_target->model) {
default: return nullptr;
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II>>(*appleii_target, rom_fetcher);
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus>>(*appleii_target, rom_fetcher);
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe>>(*appleii_target, rom_fetcher);
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe>>(*appleii_target, rom_fetcher);
if(appleii_target->has_mockingboard) {
switch(appleii_target->model) {
default: return nullptr;
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, true>>(*appleii_target, rom_fetcher);
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, true>>(*appleii_target, rom_fetcher);
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, true>>(*appleii_target, rom_fetcher);
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, true>>(*appleii_target, rom_fetcher);
}
} else {
switch(appleii_target->model) {
default: return nullptr;
case Target::Model::II: return std::make_unique<ConcreteMachine<Target::Model::II, false>>(*appleii_target, rom_fetcher);
case Target::Model::IIplus: return std::make_unique<ConcreteMachine<Target::Model::IIplus, false>>(*appleii_target, rom_fetcher);
case Target::Model::IIe: return std::make_unique<ConcreteMachine<Target::Model::IIe, false>>(*appleii_target, rom_fetcher);
case Target::Model::EnhancedIIe: return std::make_unique<ConcreteMachine<Target::Model::EnhancedIIe, false>>(*appleii_target, rom_fetcher);
}
}
}
Machine::~Machine() {}

View File

@@ -19,7 +19,7 @@ namespace Apple::II {
class Machine {
public:
virtual ~Machine();
virtual ~Machine() = default;
/// Creates and returns an AppleII.
static std::unique_ptr<Machine> AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher);

View File

@@ -90,6 +90,10 @@ template <typename Machine> class AuxiliaryMemorySwitches {
bool alternative_zero_page = false;
bool video_page_2 = false;
bool high_resolution = false;
void reset() {
*this = SwitchState();
}
};
AuxiliaryMemorySwitches(Machine &machine) : machine_(machine) {}
@@ -208,6 +212,14 @@ template <typename Machine> class AuxiliaryMemorySwitches {
return switches_;
}
void reset() {
switches_.reset();
set_main_paging();
set_zero_page_paging();
set_card_paging();
}
private:
Machine &machine_;
SwitchState switches_;

View File

@@ -41,12 +41,12 @@ class Card {
virtual ~Card() {}
enum Select: int {
None = 0, // No select line is active.
IO = 1 << 0, // IO select is active; i.e. access is in range $C0x0 to $C0xf.
Device = 1 << 1, // Device select is active; i.e. access is in range $Cx00 to $Cxff.
Device = 1 << 0, // Device select ('DEVSEL') is active; i.e. access is in range $C0x0 to $C0xf.
IO = 1 << 1, // IO select ('IOSEL') is active; i.e. access is in range $Cx00 to $Cxff.
C8Region = 1 << 2, // Access is to the region $c800 to $cfff, was preceded by at least
// one Device access to this card, and has not yet been followed up
// by an access to $cfff.
// by an access to $cfff. IOSTRB on original hardware.
};
/*!
@@ -54,7 +54,7 @@ class Card {
This is posted only to cards that announced a select constraint. Cards with
no constraints, that want to be informed of every machine cycle, will receive
a call to perform_bus_operation every cycle and should use that for time keeping.
a call to @c perform_bus_operation every cycle and should use that for time keeping.
*/
virtual void run_for([[maybe_unused]] Cycles cycles, [[maybe_unused]] int stretches) {}
@@ -93,15 +93,22 @@ class Card {
/*! Cards may supply a target for activity observation if desired. */
virtual void set_activity_observer([[maybe_unused]] Activity::Observer *observer) {}
/// @returns The current semantic NMI line output of this card.
virtual bool nmi() { return false; }
/// @returns The current semantic IRQ line output of this card.
virtual bool irq() { return false; }
struct Delegate {
virtual void card_did_change_select_constraints(Card *card) = 0;
virtual void card_did_change_interrupt_flags(Card *card) = 0;
};
void set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
protected:
int select_constraints_ = IO | Device;
int select_constraints_ = Device | IO;
Delegate *delegate_ = nullptr;
void set_select_constraints(int constraints) {
if(constraints == select_constraints_) return;

View File

@@ -46,14 +46,14 @@ void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t add
diskii_.set_data_input(*value);
switch(select) {
default: break;
case IO: {
case Device: {
const int disk_value = diskii_.read_address(address);
if(is_read) {
if(disk_value != diskii_.DidNotLoad)
*value = uint8_t(disk_value);
}
} break;
case Device:
case IO:
if(is_read) *value = boot_[address & 0xff];
break;
}
@@ -74,7 +74,7 @@ void DiskIICard::set_activity_observer(Activity::Observer *observer) {
void DiskIICard::set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference preference) {
diskii_clocking_preference_ = preference;
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (IO | Device) : None);
set_select_constraints((preference != ClockingHint::Preference::RealTime) ? (Device | IO) : None);
}
Storage::Disk::Drive &DiskIICard::get_drive(int drive) {

View File

@@ -0,0 +1,135 @@
//
// Mockingboard.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/02/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "Card.hpp"
#include "../../../Components/6522/6522.hpp"
namespace Apple::II {
class AYPair {
public:
AYPair(Concurrency::AsyncTaskQueue<false> &queue) :
ays_{
{GI::AY38910::Personality::AY38910, queue},
{GI::AY38910::Personality::AY38910, queue},
} {}
void advance() {
ays_[0].advance();
ays_[1].advance();
}
void set_sample_volume_range(std::int16_t range) {
ays_[0].set_sample_volume_range(range);
ays_[1].set_sample_volume_range(range);
}
bool is_zero_level() const {
return ays_[0].is_zero_level() && ays_[1].is_zero_level();
}
Outputs::Speaker::StereoSample level() const {
Outputs::Speaker::StereoSample level;
level.left = ays_[0].level();
level.right = ays_[1].level();
return level;
}
GI::AY38910::AY38910SampleSource<false> &get(int index) {
return ays_[index];
}
private:
GI::AY38910::AY38910SampleSource<false> ays_[2];
};
class Mockingboard: public Card {
public:
Mockingboard(AYPair &ays) :
vias_{ {handlers_[0]}, {handlers_[1]} },
handlers_{ {*this, ays.get(0)}, {*this, ays.get(1)}} {
set_select_constraints(0);
}
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final {
if(!(select & IO)) {
return;
}
int index = (address >> 7) & 1;
if(is_read) {
*value = vias_[index].read(address);
} else {
vias_[index].write(address, *value);
}
}
void run_for(Cycles cycles, int) final {
vias_[0].run_for(cycles);
vias_[1].run_for(cycles);
}
bool nmi() final {
return handlers_[1].interrupt;
}
bool irq() final {
return handlers_[0].interrupt;
}
void did_change_interrupt_flags() {
delegate_->card_did_change_interrupt_flags(this);
}
private:
struct AYVIA: public MOS::MOS6522::PortHandler {
AYVIA(Mockingboard &card, GI::AY38910::AY38910SampleSource<false> &ay) :
card(card), ay(ay) {}
void set_interrupt_status(bool status) {
interrupt = status;
card.did_change_interrupt_flags();
}
void set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t) {
if(port) {
using ControlLines = GI::AY38910::ControlLines;
ay.set_control_lines(
ControlLines(
((value & 1) ? ControlLines::BC1 : 0) |
((value & 2) ? ControlLines::BDIR : 0) |
ControlLines::BC2
)
);
ay.set_reset(!(value & 4));
} else {
ay.set_data_input(value);
}
}
uint8_t get_port_input(MOS::MOS6522::Port port) {
if(!port) {
return ay.get_data_output();
}
return 0xff;
}
bool interrupt;
Mockingboard &card;
GI::AY38910::AY38910SampleSource<false> &ay;
};
MOS::MOS6522::MOS6522<AYVIA> vias_[2];
AYVIA handlers_[2];
};
}

View File

@@ -79,13 +79,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
switch(select) {
default: break;
case Select::Device:
case Select::IO:
if(is_read) {
*value = rom_[address & 255];
}
break;
case Select::IO:
case Select::Device:
address &= 0xf;
switch(address) {
case 0x0: case 0x1: case 0x2: case 0x3:
@@ -125,13 +125,13 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
case 0xb:
if(!is_read) {
printf("TODO: NCR reset\n");
logger_.error().append("TODO: NCR reset");
}
break;
case 0xd:
if(!is_read) {
printf("TODO: Enable PDMA\n");
logger_.error().append("TODO: Enable PDMA");
}
break;
@@ -143,7 +143,7 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre
break;
default:
printf("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value);
logger_.error().append("Unhandled: %04x %c %02x\n", address, is_read ? 'r' : 'w', *value);
break;
}
break;

View File

@@ -17,6 +17,8 @@
#include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp"
#include "../../../Storage/MassStorage/MassStorageDevice.hpp"
#include "../../../Outputs/Log.hpp"
#include <array>
#include <memory>
@@ -47,6 +49,7 @@ class SCSICard: public Card {
SCSI::Bus scsi_bus_;
NCR::NCR5380::NCR5380 ncr5380_;
SCSI::Target::Target<SCSI::DirectAccessDevice> storage_;
Log::Logger<Log::Source::AppleIISCSICard> logger_;
};
}

View File

@@ -10,7 +10,6 @@
#include "../../../Outputs/CRT/CRT.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../ClockReceiver/DeferredQueue.hpp"
#include "VideoSwitches.hpp"
@@ -124,7 +123,8 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
bus_handler_(bus_handler) {}
/*!
Obtains the last value the video read prior to time now+offset.
Obtains the last value the video read prior to time now+offset, according to the *current*
video mode, i.e. not allowing for any changes still enqueued.
*/
uint8_t get_last_read_value(Cycles offset) {
// Rules of generation:

View File

@@ -247,6 +247,10 @@ template <typename TimeUnit> class VideoSwitches {
}
}
bool has_deferred_actions() const {
return !deferrer_.empty();
}
protected:
GraphicsMode graphics_mode(int row) const {
if(
@@ -306,7 +310,14 @@ template <typename TimeUnit> class VideoSwitches {
bool annunciator_3 = false;
} external_, internal_;
int flash_length = 8406;
// 8406 lines covers a complete on-off cycle, i.e. because the Apple II has a line
// rate of around 15734.26 lines/second, flashing has a frequency of roughly
// 15734.26 / 8406, or roughly 1.83Hz.
//
// Internally that's modelled by a counter that goes to **twice** the value of the
// constant specified below. Hence the divide by two.
static constexpr int flash_length = 8406 / 2;
int flash_ = 0;
uint8_t flash_mask() const {
return uint8_t((flash_ / flash_length) * 0xff);

View File

@@ -8,16 +8,18 @@
#include "AppleIIgs.hpp"
#include "../../../Activity/Source.hpp"
#include "../../MachineTypes.hpp"
#include "../../../Analyser/Static/AppleIIgs/Target.hpp"
#include "ADB.hpp"
#include "MemoryMap.hpp"
#include "Video.hpp"
#include "Sound.hpp"
#include "../AppleII/Joystick.hpp"
#include "../../../Activity/Source.hpp"
#include "../../MachineTypes.hpp"
#include "../../../Analyser/Static/AppleIIgs/Target.hpp"
#include "../../../Processors/65816/65816.hpp"
#include "../../../Components/8530/z8530.hpp"
#include "../../../Components/AppleClock/AppleClock.hpp"
@@ -26,8 +28,6 @@
#include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp"
#include "../../../Components/DiskII/DiskIIDrive.hpp"
#include "../AppleII/Joystick.hpp"
#include "../../../Outputs/Speaker/Implementation/CompoundSource.hpp"
#include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
@@ -1013,6 +1013,3 @@ using namespace Apple::IIgs;
std::unique_ptr<Machine> Machine::AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
return std::make_unique<ConcreteMachine>(*dynamic_cast<const Analyser::Static::AppleIIgs::Target *>(target), rom_fetcher);
}
Machine::~Machine() {}

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