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

Compare commits

...

461 Commits

Author SHA1 Message Date
Thomas Harte
15394358df Merge pull request #126 from TomHarte/GCRAnalysis
Corrects infinite loop when performing GCR analysis
2017-05-16 20:54:24 -04:00
Thomas Harte
df4d4467b3 Ensured GCR parser spins the disk. 2017-05-16 20:53:06 -04:00
Thomas Harte
67ec0b9e6c Merge pull request #125 from TomHarte/SampledComposite
Formalises reasoning for the colour phase clamp and offset...
2017-05-16 20:47:20 -04:00
Thomas Harte
2ee8a7056e Corrected TIA no longer to assume phase is an automatic quarter askew. 2017-05-16 20:43:28 -04:00
Thomas Harte
a5075d9eb5 Formalised the reasoning behind the colour phase fix-up and made it an opt-in per-caller value. Only the Oric currently needs to opt in. 2017-05-16 20:31:39 -04:00
Thomas Harte
abeaedf16f Merge pull request #123 from TomHarte/RemovedDeadOption
Formally withdraws the 'load automatically' option for the Vic
2017-05-14 17:03:33 -04:00
Thomas Harte
8e35e913bb Formally withdrew the 'load automatically' option for the Vic, having removed that option elsewhere. 2017-05-14 16:59:24 -04:00
Thomas Harte
81c5f4ab19 Merge pull request #122 from TomHarte/16bit6560
Increases VIC-I intermediate format to 16 bit and corrects output colours
2017-05-13 22:03:08 -04:00
Thomas Harte
e270b726b3 Tweaked blue, increased saturation. 2017-05-13 22:01:02 -04:00
Thomas Harte
c2b5a9bb1f Minor fix: given that phase is now a function of position, stop nudging position. 2017-05-13 21:50:48 -04:00
Thomas Harte
44ce7fa54c Corrected luminances across the board, and PAL colours. 2017-05-13 21:50:09 -04:00
Thomas Harte
b0142cf050 Made an updated stab at NTSC colours. 2017-05-13 14:29:36 -04:00
Thomas Harte
a340331229 Introduced 1-bit of saturation, returning black and white as black and white. 2017-05-11 21:31:58 -04:00
Thomas Harte
b14c892740 Switched to a safer RAII approach to this lock. 2017-05-10 21:29:39 -04:00
Thomas Harte
15d17c12d5 Switched the 6560 to two bytes per pixel, since one isn't sufficient for precision and because mixing up the implementation might help me to figure out what's amiss. 2017-05-09 21:22:01 -04:00
Thomas Harte
99800d9840 Merge pull request #121 from TomHarte/VicColours
Permits ROM-area located PRGs that are not a power-of-two in size
2017-05-08 22:18:47 -04:00
Thomas Harte
5d91a2600d Permitted ROM-style PRGs that are not a power-of-two in size, and added extra safety checks on loading data from a tape. 2017-05-08 22:15:35 -04:00
Thomas Harte
cb66c7e2dc Performed some minor tidying. 2017-05-08 21:05:35 -04:00
Thomas Harte
58488c93be Merge pull request #120 from TomHarte/Vic20Colours
Corrects — and improves — Vic-20 tape loading
2017-05-08 20:59:29 -04:00
Thomas Harte
61f8f2f18c Switched to a more straightforward way of exiting from tape data loading. 2017-05-08 20:58:55 -04:00
Thomas Harte
7b43ae0a92 Implemented a catch for loading the data portion of files. 2017-05-07 22:22:59 -04:00
Thomas Harte
2807e3134f Implemented speedy header finding. So that's half of it. 2017-05-07 20:32:48 -04:00
Thomas Harte
0771363f3b Removed one piece of unnecessary logging. 2017-05-06 22:22:03 -04:00
Thomas Harte
1f56e85f6d Centralised resetting of tape files within the static analyser, having implemented it patchily. 2017-05-06 22:19:08 -04:00
Thomas Harte
2edf73908c Temporarily disabled the existing fast loading implementation in pursuit of another, and started trying to correct the lack of connection between the userport VIA and the tape drive. 2017-05-06 22:00:12 -04:00
Thomas Harte
6a37a02eee Switched to std::ostringstream to avoid the need to make a string length count (and I was one off when loading a disk). 2017-05-06 19:55:42 -04:00
Thomas Harte
5998123868 Added some consts, for a minor safety improvement. 2017-05-06 19:53:24 -04:00
Thomas Harte
26cb903b08 Merge pull request #119 from TomHarte/C11
Ejects the deprecated OSAtomicTestAnd[Re]Set test-and-[re]set in favour of C11's atomic_flag.
2017-04-15 21:45:48 -04:00
Thomas Harte
92a8b68859 Dumped Mach-specific test-and-set in favour of ordinary C11. 2017-04-15 21:41:59 -04:00
Thomas Harte
4127350abe Merge pull request #118 from TomHarte/HighResolutionAtariAudio
Introduces a higher source sampling rate for Atari audio
2017-04-15 21:21:58 -04:00
Thomas Harte
ed6b135015 Made final switch to permit high-sampling rate Atari audio. 2017-04-15 21:18:00 -04:00
Thomas Harte
f95015c7f6 Pulled out the divisor for audio. 2017-04-03 21:16:39 -04:00
Thomas Harte
defec2c9b0 Fixed: operation reads now fulfil the promise of seeding the value to be read with 0xff. 2017-03-26 20:56:27 -04:00
Thomas Harte
04921e64de Merge pull request #117 from TomHarte/AudioRace
Works around what appears to be a bug in Apple's AudioQueue.
2017-03-26 20:29:41 -04:00
Thomas Harte
bdd432fe1d Added an ugly workaround for the empirical sound shutdown issues. 2017-03-26 20:28:04 -04:00
Thomas Harte
6e9ab9f330 Merge pull request #116 from TomHarte/YarsBug
Fixes a bug in which TIA access during the border would flush all motion steps.
2017-03-26 18:38:51 -04:00
Thomas Harte
814c0ada13 Fixed action counts for border motion. 2017-03-26 18:33:05 -04:00
Thomas Harte
dfc468f220 Locked down all initial state. 2017-03-26 15:47:04 -04:00
Thomas Harte
6817b38322 Merge pull request #115 from TomHarte/Style
Settles my C++ curly bracket style indecision
2017-03-26 14:37:23 -04:00
Thomas Harte
e01f3f06c8 Completed curly bracket movement. 2017-03-26 14:34:47 -04:00
Thomas Harte
3229502fa1 Standardised curly bracket placement across the Atari. 2017-03-23 21:59:16 -04:00
Thomas Harte
a4c5eebd1e The latest Atari Age-discovered numbers suggest this starts up in 1024T mode. 2017-03-21 18:22:50 -04:00
Thomas Harte
a3c22d5abb Merge pull request #114 from TomHarte/Pitfall2
Adds support for the Pitfall 2 DPC. And therefore for Pitfall 2.
2017-03-20 20:44:43 -04:00
Thomas Harte
a26b87f348 Fixed: mistake was failure to count ready cycles. 2017-03-20 20:44:03 -04:00
Thomas Harte
4c3cc42c91 This gives a very noisy version of the real audio. 2017-03-20 20:38:29 -04:00
Thomas Harte
f3f4e1a541 Made a first, hacky, attempt at audio. 2017-03-20 19:35:51 -04:00
Thomas Harte
4722f6b5c4 Fixed sprite disappearance: test should be applied predecrement, not post — it relates to the address being used this access, not the next one. 2017-03-19 18:58:35 -04:00
Thomas Harte
7d8d1c7828 Fixed 'random' number generator. 2017-03-19 18:54:35 -04:00
Thomas Harte
4bb70e7d31 Resetting the mask upon low byte write appears to resolve some issues. 2017-03-19 18:49:37 -04:00
Thomas Harte
321030bb44 Added a slightly faulty but seemingly 'close' version of masking. 2017-03-19 18:28:06 -04:00
Thomas Harte
6c161b1150 This gives something that might be the correct background. 2017-03-19 17:49:48 -04:00
Thomas Harte
d5c37c8619 Shushed a little, so as to be able to see a reasonable amount of output during my lifetime. 2017-03-19 17:38:54 -04:00
Thomas Harte
c445eaec3e Switched startup values, following a comment on AtariAge. May or may not be correct, the thread was speculative. 2017-03-19 17:38:26 -04:00
Thomas Harte
7c66c36d3f Attempted at least to manage appropriate data storage. 2017-03-19 17:31:08 -04:00
Thomas Harte
031a68000a Added a class to contain the Pitfall 2 pager and a skeleton of initial work. 2017-03-18 22:08:47 -04:00
Thomas Harte
7d7b665be8 Merge pull request #113 from TomHarte/PagingTemplates
Pulls the Atari 2600 paging schemes out into individual classes
2017-03-18 21:05:01 -04:00
Thomas Harte
c3d82f88a5 Tidied up and commented on the Activision stack implementation. 2017-03-18 21:01:58 -04:00
Thomas Harte
c033bad0b9 Here's MNetwork! 2017-03-18 20:51:49 -04:00
Thomas Harte
c31d85f820 Re-emplaced the MegaBoy. Also cut detritus from the main Atari header. 2017-03-18 19:02:34 -04:00
Thomas Harte
217fbf257e CBS RAM Plus returns. 2017-03-18 18:56:20 -04:00
Thomas Harte
0b611a14b9 Tigervision paging returns. 2017-03-18 18:50:13 -04:00
Thomas Harte
df6861c9dc Parker Bros paging is back. 2017-03-18 18:21:01 -04:00
Thomas Harte
a4cd12394e Reinstated the Activision stack pager. 2017-03-18 18:03:48 -04:00
Thomas Harte
e0bca1e37b Reinstated the 16 and 32 kb Atari pagers, and ensured the 6532 always starts in a valid state. 2017-03-18 17:34:34 -04:00
Thomas Harte
55ce851bb2 Fixed types of the 8k cartridges, ensured the 6502 starts without an IRQ request history. 2017-03-18 17:04:01 -04:00
Thomas Harte
e8d34f2eb4 Having farmed out the bus, the Atari itself no longer is/owns a 6502. 2017-03-18 16:34:41 -04:00
Thomas Harte
bb3daaa99b Sought to reintroduce the Atari 8k paging scheme, at the same time deciding to do away with the copy and paste of holding on to ROM data. 2017-03-18 15:04:01 -04:00
Thomas Harte
36b58d03b7 Formalised read bus value guarantee from the 6502, fixed missing clock signal wiring on the Atari cartridge class, reintroduced CommaVid support. 2017-03-18 14:46:46 -04:00
Thomas Harte
7958459db9 In theory unpaged cartridges should now work. Time to debug. 2017-03-18 14:01:04 -04:00
Thomas Harte
14a76af0d3 Started trying to float out bus control to cartridges. 2017-03-17 20:28:07 -04:00
Thomas Harte
a04a58e01f Merge pull request #112 from TomHarte/LineTiming
Fixes declared Atari line length.
2017-03-14 21:34:19 -04:00
Thomas Harte
afbd9fd41b Fixed declared line length. 2017-03-14 21:33:38 -04:00
Thomas Harte
3d53d4e55e Merge pull request #111 from TomHarte/ActivisionStack
Implements the Activision stack paging scheme.
2017-03-14 20:41:04 -04:00
Thomas Harte
7302703039 Implemented the Activision stack paging scheme. 2017-03-14 20:24:05 -04:00
Thomas Harte
97a8a96593 Rejigged how the memory map is handled and implemented MNetwork support. 2017-03-14 20:07:54 -04:00
Thomas Harte
be2e99077e Merge pull request #110 from TomHarte/MegaBoy
Adds emulation of the MegaBoy paging scheme.
2017-03-14 17:40:41 -04:00
Thomas Harte
3b29276228 Implemented the MegaBoy paging scheme. 2017-03-14 17:40:01 -04:00
Thomas Harte
4a528b9ecb Merge pull request #109 from TomHarte/CBSRamPlus
Adds emulation of the CBS RAM Plus paging scheme.
2017-03-14 17:26:09 -04:00
Thomas Harte
b3632a4e86 Institutes CBS RAM Plus emulation. 2017-03-14 17:25:10 -04:00
Thomas Harte
8a659e3117 Merge pull request #108 from TomHarte/PitfallIIDetection
Adds tested detection of the Pitfall II and MegaBoy paging schemes.
2017-03-13 20:44:28 -04:00
Thomas Harte
a6897ebde0 Added an attempt to distinguish the MegaBoy (now with proper capitalisation) and a test for it. 2017-03-13 20:43:12 -04:00
Thomas Harte
582da14a14 Added an enumerated type and detection of Pitfall 2. 2017-03-13 08:15:36 -04:00
Thomas Harte
b81bf6b547 Merge pull request #107 from TomHarte/PagingTests
Introduces unit tests for the Atari static analyser
2017-03-12 22:17:17 -04:00
Thomas Harte
8e147444d5 Added a readme, as is traditional for folders I'm excluding from Git. 2017-03-12 22:16:12 -04:00
Thomas Harte
a9964ee0c8 Simplified CommaVid test. 2017-03-12 22:14:39 -04:00
Thomas Harte
b671df9906 'Perfected' the Activision stack paging detector. 2017-03-12 22:11:58 -04:00
Thomas Harte
0bcf9c30de Added an attempt, at least, at spotting the Activision titles. Which reduces back to only the one breakage, but this is getting a bit too much like spotting the hard-coded bytes. 2017-03-12 21:14:12 -04:00
Thomas Harte
2c07cce282 Had the wrong paging scheme listed for Robot Tank and Thwocker. Better to get this right before trying to come up with a test for the Activision stack scheme. 2017-03-12 21:03:10 -04:00
Thomas Harte
8c5e39c0c5 Adjusted Super Chip test to look for two portions of 128 bytes. It now spots Dig Dug and doesn't acquire any further false positives amongst the tested set. So failure to spot the Activision stack-based paging mechanism is now the only remaining failure. 2017-03-12 20:39:45 -04:00
Thomas Harte
cae48aaa95 Added an MNetwork test. Reduces failures to two of the set that I have: Dig Dug, which uses a Super Chip but has inconsistent bytes in its image, and Decathlon, which uses the Activision stack paging mechanism for which I don't yet have detection code. 2017-03-12 18:54:49 -04:00
Thomas Harte
37f4f6ba14 Cut down to one disassembly, now I know a bit more about how the non-Atari schemes work. 2017-03-12 17:50:24 -04:00
Thomas Harte
597bd97b01 Corrected two more table errors. 2017-03-12 15:46:25 -04:00
Thomas Harte
38de5300e5 Elevator Action seemingly uses a Super Chip. 2017-03-12 15:43:42 -04:00
Thomas Harte
62b3c9dda8 Corrected 8k Tigervision test, putting detection failures below 10 (i.e. at 9) for the first time. 2017-03-12 15:41:48 -04:00
Thomas Harte
146f3ea0f5 Fixed: Crystal Castles is 16kb. 2017-03-12 15:39:07 -04:00
Thomas Harte
af9b7fbc30 Switched to a voting method for classifying 8kb ROMs. 2017-03-12 15:36:01 -04:00
Thomas Harte
78213f1e95 Fixed a couple more table entries, introduced per-size tests (plus a catch-all), to speed up the development/testing cycle. 2017-03-12 15:35:36 -04:00
Thomas Harte
de347ad7c8 Improved CBS RAM Plus and Super Chip detection exclusion, reducing error count to 15. 2017-03-12 14:03:17 -04:00
Thomas Harte
a4bba8a92e Made a couple of lookup table fixes and corrected RAM region detection windows; failures now down to 19. 2017-03-11 23:18:30 -05:00
Thomas Harte
fcacfc2726 Tidied up spacing, slightly. 2017-03-11 23:01:42 -05:00
Thomas Harte
bab464e765 I'm far from confident, but this should reduce the deviations close to those that result from mistakes by the static analyser, rather than table errors. 2017-03-11 22:58:11 -05:00
Thomas Harte
2879763c34 Reduced to 84 failures through more accurate tabulation. 2017-03-11 21:52:52 -05:00
Thomas Harte
ea2ea30193 Fleshed entire table out with most common values. Exceptions now to fix. 2017-03-11 21:11:25 -05:00
Thomas Harte
608569cc48 Typed out all the 'A's that I am aware of. So about 5% done. 2017-03-11 20:58:38 -05:00
Thomas Harte
c7e973aab4 Extended test set a little, corrected current failures. 2017-03-11 20:51:25 -05:00
Thomas Harte
443d57bc32 Slimmed output and added first six tests. Acid Drop fails since I'm not yet declaring Atari 16k and Atari 32k. 2017-03-11 20:43:19 -05:00
Thomas Harte
57ec756f5b Started speccing out a unit test for Atari ROM analysis. 2017-03-11 20:33:58 -05:00
Thomas Harte
9286a5ba73 Added a new folder to the exclusion list, to contain commercial Atari images, to unit test my paging hardware selection logic. 2017-03-11 19:42:23 -05:00
Thomas Harte
1c9dffe41f Added an earlier exit. 2017-03-11 19:38:05 -05:00
Thomas Harte
8c7f724ce4 Merge pull request #106 from TomHarte/Tigervision
Adds detection and emulation of the Tigervision paging scheme
2017-03-11 18:18:29 -05:00
Thomas Harte
b193248056 Ensured that queue is not touched at all outside of the critical section. 2017-03-11 18:17:09 -05:00
Thomas Harte
f0d944847b Fixed setting of the second 1kb. 2017-03-11 18:16:29 -05:00
Thomas Harte
a72d70e707 Enabled code coverage calculation for unit tests. 2017-03-11 17:44:56 -05:00
Thomas Harte
add14fb43a Made an attempt to implement Tigervision paging. 2017-03-11 17:44:35 -05:00
Thomas Harte
38ce4dc56c Fixed potential deadlock, if a delegate decided to dealloc the queue as a result of its prompting. 2017-03-11 17:44:02 -05:00
Thomas Harte
bce5abd33b Made an attempt to spot Tigervision paging requests. 2017-03-11 13:12:23 -05:00
Thomas Harte
3f36eeb071 Merge pull request #105 from TomHarte/ParkerBros
Introduces detection and emulation of the Parker Bros paging scheme
2017-03-11 13:05:17 -05:00
Thomas Harte
33bda2d40c Switched to image inspection for RAM guesses rather than disassembly. Which fixes the other Parker Bros titles. 2017-03-11 13:04:23 -05:00
Thomas Harte
2b5e3a600e Made a first attempt at implementing the Parker Bros pager within the emulation. 2017-03-11 12:43:12 -05:00
Thomas Harte
8dbf9fd302 Started adding an attempt to distinguish between Atari and Parker Bros paging schemes. 2017-03-11 11:41:18 -05:00
Thomas Harte
9c72ce5bd2 Ensured a settling delay is permitted before an NTSC/PAL decision is made. To avoid false switches just due to startup. 2017-03-06 19:37:35 -05:00
Thomas Harte
ec2762509b Merge pull request #104 from TomHarte/SyncDetection
Improves vertical sync recognition
2017-03-06 19:18:52 -05:00
Thomas Harte
e63229a5e5 Pulled vertical sync detection entirely outside the loop, and gave it greater perspective. 2017-03-06 19:15:33 -05:00
Thomas Harte
ad73379d1c Took vertical sync detection logic entirely out of the loop. 2017-03-05 20:17:55 -05:00
Thomas Harte
abd4d2c42a Extended has-RAM test to check all banks. 2017-03-05 11:58:52 -05:00
Thomas Harte
79784a8e57 Followed tip: missiles are locked to the position after four output pixels, not four input pixels. 2017-03-04 22:23:50 -05:00
Thomas Harte
61b8fc1e2f Merge pull request #103 from TomHarte/CRTCounting
Fixes NTSC colour cycle count
2017-03-04 17:32:45 -05:00
Thomas Harte
4751615623 Fixed NTSC colour cycle count, and hence the 2600's reported line lengths and phase offset. 2017-03-04 17:31:39 -05:00
Thomas Harte
cccdc558e7 Merge pull request #102 from TomHarte/6532Accuracy
Improves 6532 counting accuracy
2017-03-04 17:04:43 -05:00
Thomas Harte
d3257c345a Tested against public ROMs and corrected. Also moved the deferred adjustment into a more canonical place. 2017-03-04 17:00:28 -05:00
Thomas Harte
e09b76bf32 Fixed 'same value, then immediate increment, then proper counting increments' behaviour and ensured it takes one cycle to commit a value. Adjusted tests to match. 2017-03-04 15:57:54 -05:00
Thomas Harte
837cccdf83 Switched to deferred updates for the 6532. 2017-03-04 14:58:28 -05:00
Thomas Harte
a3fcd15980 Loosened sync charge level requirement. 2017-03-01 22:16:56 -05:00
Thomas Harte
93d1573481 Added a fix for certain homebrews. 2017-03-01 07:59:25 -05:00
Thomas Harte
893a5dd007 Added an artificial low pass filter, attempting to capture post-digital effects. 2017-02-28 21:29:55 -05:00
Thomas Harte
026b418b4a Ensured filtered 1:1 audio resampling is applied. 2017-02-28 21:27:38 -05:00
Thomas Harte
06dd98b23c Pulled the reset time for horizontal blank extend up to position 224. 2017-02-28 20:28:54 -05:00
Thomas Harte
2a81ae1dec Now required: at least four stores for RAM to be detected. 2017-02-28 20:28:14 -05:00
Thomas Harte
1625b9c7f9 Merge pull request #101 from TomHarte/CommaVid
Adds detection and emulation of the CommaVid paging scheme
2017-02-27 20:53:19 -05:00
Thomas Harte
184c8ae707 Extended to emulate the CommaVid. 2017-02-27 20:50:59 -05:00
Thomas Harte
8f8b103224 Slightly tidier. Also in the interim: confirmed no remaining false positives or negatives from the existing published set. 2017-02-27 08:42:43 -05:00
Thomas Harte
1af415a88e Okay, this is a bit desperate, but worth investigation. 2017-02-27 08:39:53 -05:00
Thomas Harte
fe07cd0248 Made another attempt to distinguish. 2017-02-27 08:06:57 -05:00
Thomas Harte
a3d339092e Corrected callers of the 6502 disassembler. 2017-02-27 07:56:59 -05:00
Thomas Harte
837216ee9a Adjusted semantics to allow for more complicated mappings, e.g. whereby supplied data repeats itself within a range. 2017-02-27 07:49:33 -05:00
Thomas Harte
dcd0c90283 Switched time of best-effort updater delegate setting, to avoid a callback before setupClockRate has happened, and therefore before it's clear what should be going on with audio. 2017-02-26 21:58:59 -05:00
Thomas Harte
b24cd00a39 Switched time of best-effort updater delegate setting, to avoid a callback before setupClockRate has happened, and therefore before it's clear what should be going on with audio. 2017-02-26 21:58:43 -05:00
Thomas Harte
0273860018 Sought to weed out further false positives on CommaVid, partly by introducing a record of internal calls, to pair with the now confusingly named outward_calls. 2017-02-26 21:58:09 -05:00
Thomas Harte
82c089cde4 Factored out paging type detection and, from that, took out 2k cartridge detection. Added an attempt at a CommaVid detector, which does not currently work. 2017-02-26 21:24:54 -05:00
Thomas Harte
997707a45b Merge pull request #100 from TomHarte/SuperChipDetection
Adds detection and emulation of the Super Chip
2017-02-26 18:04:50 -05:00
Thomas Harte
9d7985c1e1 Added Super Chip emulation. 2017-02-26 17:47:29 -05:00
Thomas Harte
8b1ec827e0 Made an initial, very naive attempt to recognise two types of expanded cartridge: those with a superchip and those with a CBS RAM+, in both cases by looking for instructions that appear to write into cartridge space. 2017-02-26 17:11:57 -05:00
Thomas Harte
153525f23d Extended quality of known address read/write/modify mapping, and started recording internal accesses in addition to external. 2017-02-26 17:10:33 -05:00
Thomas Harte
3101dc94a7 Merge pull request #98 from TomHarte/TIAImprovements
Reimplements the Atari TIA, as a discrete component
2017-02-26 15:14:59 -05:00
Thomas Harte
e6a84fd26b Attempted a hardware-correct implementation of missile-to-player latching. This completes the last of the **knowing** inaccuracies. The rest are as-of-yet unwitting. 2017-02-26 15:12:31 -05:00
Thomas Harte
440467ea3e Started communicating which copy is being requested. 2017-02-26 13:39:25 -05:00
Thomas Harte
98376de9ad Started returning 'no effect' for pot ports, rather than doing nothing. Still very much TODO though. 2017-02-25 22:58:58 -05:00
Thomas Harte
e61e355251 Moved to the maximum possibly required queue length of 4. Though the emulated 2600 should never need more than 2 slots as per the current calling pattern, it's not a contractual guarantee. 2017-02-25 17:25:10 -05:00
Thomas Harte
c898c8a99e Ensured the missiles and ball don't attempt to enqueue. Because I don't think they're supposed to. 2017-02-25 17:13:22 -05:00
Thomas Harte
8c9062857c Added a single-slot queue for player objects to defer drawing, thereby deferring pixel lookup. Which I think is correct. Though more slots might be needed. 2017-02-25 17:10:24 -05:00
Thomas Harte
77ed4ddc05 Slightly simplified ready line release logic. 2017-02-23 21:08:32 -05:00
Thomas Harte
82f392fada This should be the other way around. I want whichever is later. 2017-02-22 21:54:49 -05:00
Thomas Harte
2f0c923c29 Switched away from @synchronized as it appears possibly to be the lock used during -dealloc, creating deadlock with the CSAudioQueueDeallocLock. 2017-02-22 21:42:10 -05:00
Thomas Harte
8291a63d5f Fixed loss of audio when switching to PAL. 2017-02-22 21:15:37 -05:00
Thomas Harte
4c947ad553 Attempted to resolve risk of an audio callback being in progress when -dealloc is received. 2017-02-22 21:12:59 -05:00
Thomas Harte
6120dae61a While I'm using the hacky approach to player/missile synchronisation, I need to seed adder. 2017-02-22 07:39:11 -05:00
Thomas Harte
1d03793f22 Fixed potential race condition: ensure the queue is disposed of synchronously because otherwise there'll be a potential dangling reference to self. 2017-02-22 07:35:09 -05:00
Thomas Harte
4f5f191cd6 Fixed: will no longer attempt to output pixels from before the pixel part of a line on which sync was disabled abruptly. 2017-02-22 07:33:36 -05:00
Thomas Harte
21abf4e9fc Enshrined a terminology switch, albeit without any flow change behind it. 2017-02-22 07:29:48 -05:00
Thomas Harte
144d6b70d9 Minor cleaning. 2017-02-22 07:14:30 -05:00
Thomas Harte
b769f22ca0 Switched back to the collision_buffer_ being part of the TIA object, added one more assert. 2017-02-21 22:26:20 -05:00
Thomas Harte
7019d396d0 Threw in some asserts, discovering a bug in missile positioning. 2017-02-21 22:04:27 -05:00
Thomas Harte
f4447fd9cd Attempted to fix failure of sprites properly to wrap when performing motion. 2017-02-21 21:53:09 -05:00
Thomas Harte
36396b3d62 Made a slightly better, albeit still inaccurate, version of missile-player lock.Enough for Combat to do reasonable things. 2017-02-21 20:45:20 -05:00
Thomas Harte
d1dbf8c21f Missile to player lock is supposed to be a toggle; also factored out the commonalities of missile and ball drawing. 2017-02-21 07:58:37 -05:00
Thomas Harte
1bde0fed6f Simplified relationship between Objects and the usage-specific components through inheritance. 2017-02-21 07:37:20 -05:00
Thomas Harte
7ab2358bba Made an attempt to reintroduce missiles. 2017-02-20 22:22:39 -05:00
Thomas Harte
99547181f1 Attempted to template this thing. Without yet a plan in place for pixel lookup timing. 2017-02-20 21:42:59 -05:00
Thomas Harte
2bf784535c Simplified calllng. 2017-02-20 18:04:40 -05:00
Thomas Harte
57f434c199 Reorganised state, with an eye towards unifying object motion and triggers. 2017-02-20 17:58:28 -05:00
Thomas Harte
87afa9140e Took some provision steps towards paging type autodetection and communication. But I think this is a distraction. 2017-02-20 17:44:36 -05:00
Thomas Harte
d19f26887d Performed a very naive shuffling of output builder sets onto the OpenGL queue. Which makes the frequency switcher work properly from it's possibly-contextless thread. 2017-02-20 10:39:31 -05:00
Thomas Harte
6cb95b4fc5 Switched to passing around std::strings rather than char *s, because they should be easier to capture. 2017-02-20 10:35:33 -05:00
Thomas Harte
d979a822ac Introduced a deferred task list for the OpenGL thread. 2017-02-19 21:46:07 -05:00
Thomas Harte
fccdce65b9 Switched to lock guards. 2017-02-19 21:45:28 -05:00
Thomas Harte
99a35266e1 Attempted to bring frequency-switching logic into the cross-platform realm. Which for now creates an issue with the OpenGL context. 2017-02-19 21:20:37 -05:00
Thomas Harte
51bcaea60c Disabled incorrect 'optimisations'. 2017-02-19 12:00:04 -05:00
Thomas Harte
e00339ef0a Attempted to reintroduce the ball. 2017-02-19 08:02:54 -05:00
Thomas Harte
53cd125712 Added stub calls to draw the missiles and ball. 2017-02-19 07:28:24 -05:00
Thomas Harte
04693b067c Fixed failure of the optimised route to pump the pixel clock; removed optimisation entirely for now. 2017-02-18 21:36:48 -05:00
Thomas Harte
cd7876a746 Reintroduced the extra clocking delay. 2017-02-18 20:18:50 -05:00
Thomas Harte
ed5ff49ef5 Fixed vertical delay, retreated from my previous thought about adding the one extra cycle of sprite delay, at least temporarily. 2017-02-16 20:52:01 -05:00
Thomas Harte
8d502a0b03 Decided not to run before I can walk and switched to storing the motion time and next step explicitly per object. 2017-02-16 20:28:37 -05:00
Thomas Harte
5ea232310f Added a check against negative runs. 2017-02-16 18:55:58 -05:00
Thomas Harte
09309aa74f Attempted to prevent extraneous moves. 2017-02-16 18:52:39 -05:00
Thomas Harte
b5357860b9 Made an attempt to split things apart so as to be able to introduce the proper sprite latency. 2017-02-14 20:56:16 -05:00
Thomas Harte
dd17459687 Added my first failing test: delay is incorrect when resetting outside of the play area. 2017-02-12 20:42:49 -05:00
Thomas Harte
cd90118a0f Added two, extraordinarily simple tests. 2017-02-12 20:32:53 -05:00
Thomas Harte
25776de59d I think unit testing this thing is the only way forwards. Started adding appropriate hooks. 2017-02-12 19:55:02 -05:00
Thomas Harte
600bdc9af7 In C++, I think the implicit cast to bool negates the need for any manual collapsing? 2017-02-12 18:18:35 -05:00
Thomas Harte
0c9be2b09e Shunted the collisions buffer onto a separate area of the heap for the time being, as a debugging aid. Also added a few more initial values. 2017-02-12 18:16:50 -05:00
Thomas Harte
df8a5cbe6d Made attempts (i) to respect the delay flag; and (ii) to account for border-region sprite clocking. 2017-02-12 17:35:09 -05:00
Thomas Harte
9ce68c38ae Made an effort to implement proper pixel output for sprites. 2017-02-12 14:01:50 -05:00
Thomas Harte
40954d6a2a Attempted to factor out parts I expect to reuse for missiles and the ball. 2017-02-12 11:31:17 -05:00
Thomas Harte
ac444a3f34 Corrected both position increments and target time calculation. 2017-02-11 21:24:14 -05:00
Thomas Harte
b8abeced6d Made an attempt to introduce the proper eventful loop for player output. With debugging yet to occur. 2017-02-11 21:01:58 -05:00
Thomas Harte
aeff59addc Implemented motion 'correctly', for programs written to do all work outside of the pixel area. 2017-02-11 20:25:49 -05:00
Thomas Harte
7ab6023a0c Ensured no attempt to call strcmp on null if a file name without an extension got into here. 2017-02-11 13:36:36 -05:00
Thomas Harte
97cdfea9e9 Resolved spurious static analyser complaint: input_size and output_size aren't supposed to have defined values if input or output is null. But whatever. 2017-02-11 13:36:09 -05:00
Thomas Harte
aff69dbc34 Resolved spurious static analyser issue; screen mode will always be 0–6 but it doesn't know that. Setting a non-zero divider doesn't feel worth worrying about for a cleaner compile. 2017-02-11 13:35:22 -05:00
Thomas Harte
6381e4e1b0 All that's happened to position is that numbers have been added to it. So it can't be negative, given that it wasn't before. So a regular modulo will do. 2017-02-11 13:34:36 -05:00
Thomas Harte
c8e595d9aa Merge branch 'master' into TIAImprovements 2017-02-11 13:20:52 -05:00
Thomas Harte
8c88fd4261 Merge pull request #99 from TomHarte/StartupRace
Resolves a race condition on machine startup
2017-02-11 13:18:19 -05:00
Thomas Harte
a86a6367b5 Slightly shuffled to avoid a race condition on the best-effort updater. 2017-02-11 13:17:11 -05:00
Thomas Harte
905ed1f87b Switched to the more natural type, which is also signed, making my logic less prone to error. 2017-02-11 13:16:53 -05:00
Thomas Harte
8de6caf6ff Started trying to get into a proper structure here. Chickened out. 2017-02-11 12:59:13 -05:00
Thomas Harte
327c19a222 Slightly shuffled to avoid a race condition on the best-effort updater. 2017-02-11 12:58:47 -05:00
Thomas Harte
40d3f5f7f6 Attempted properly to respect start. 2017-02-11 08:26:09 -05:00
Thomas Harte
64d5712d1d Added an incorrectly-coded version of horizontal move, at least so that I can verify that information is going into the correct slots. 2017-02-10 07:23:43 -05:00
Thomas Harte
3b20d862f0 Made an initial attempt to mark sprite positions. But without hmove implemented, they're all over the place. 2017-02-09 20:53:42 -05:00
Thomas Harte
2e9ef2b0ef Took a shot at reinstating the horizontal blank extend flag. 2017-02-09 18:37:19 -05:00
Thomas Harte
70745286a5 Ensured this array is properly aligned for the uin32_t accesses I intend to make for background drawing. 2017-02-08 20:25:23 -05:00
Thomas Harte
dcb7584060 Added the four-cycle playfield output latency and ensured you can't get smaller-than-usual pixels by rapid register value changing. 2017-02-08 07:30:32 -05:00
Thomas Harte
a477499724 Got a bit more explicit with range returned by get_cycles_until_horizontal_blank and hence attempted a more thorough (/correct) version of WSYNC. 2017-02-07 22:14:45 -05:00
Thomas Harte
944d835eea Switched explicitly to an accumulation model for filling the collision buffer. 2017-02-06 21:59:28 -05:00
Thomas Harte
8f5039130c Changed index naming order to ensure no out-of-bounds accesses. 2017-02-06 21:48:41 -05:00
Thomas Harte
ba165bb70a Made an attempt properly to populate collision registers from the collision buffer. 2017-02-06 21:15:55 -05:00
Thomas Harte
474e2e8d2c Fixed once again to respect mid-line palette changes. 2017-02-06 20:09:12 -05:00
Thomas Harte
8b8eb787df Fixed complete invisibility. 2017-02-06 18:42:58 -05:00
Thomas Harte
66bcdd36f3 Made an attempt to introduce an intermediate buffer that ends up with a bit mask of all graphical components present on it, and to use that to infer collision flags and colours, based on playfield priority and colour palette. Immediately yielding: a blank screen. Good work! 2017-02-06 18:29:00 -05:00
Thomas Harte
fcf8cafb5d Sought to ensure that communicating a colour burst in multiple parts doesn't ruin the phase. 2017-02-06 18:27:44 -05:00
Thomas Harte
6bcf95042c Started trying to be a bit more explicit about usage, and to divide up drawing responsibility. 2017-02-05 17:51:56 -05:00
Thomas Harte
23f3ccd77a Made a further attempt to prevent overwrites. 2017-02-05 17:47:34 -05:00
Thomas Harte
f2437cb257 Added some additional documentation, started making steps towards returning sprites, fixed a counter bug that would exhibit as incorrect sync. 2017-01-31 20:30:32 -05:00
Thomas Harte
abe04334c2 Attempted to retain more player information, and removed the output cursor from class storage as I think it's acceptable as a temporary. 2017-01-30 22:42:27 -05:00
Thomas Harte
8545707b54 Reinstituted the playfield. Probably needs more buffering though. Time to look into delays. 2017-01-30 21:38:58 -05:00
Thomas Harte
2b08758b2b Started capturing playfield/ball and background colours. 2017-01-30 08:08:03 -05:00
Thomas Harte
764b528891 Made a first attempt at switching to a model that respects blank and sync. 2017-01-30 07:19:19 -05:00
Thomas Harte
92754ace7a Some mild fixes get me up to having a rolling screen of vertical lines. Which is what I was hoping for right now! 2017-01-29 22:16:23 -05:00
Thomas Harte
1cc13b2799 Merge branch 'master' into TIAImprovements 2017-01-29 16:13:46 -05:00
Thomas Harte
38f944bc34 This needs to be a memmove as the areas may overlap. 2017-01-29 16:13:33 -05:00
Thomas Harte
427175b9c0 Added an extra flag to avoid potential race condition on is_full_, being reset from the background despite a write area not having been allocated. 2017-01-29 16:13:28 -05:00
Thomas Harte
ebde955356 This needs to be a memmove as the areas may overlap. 2017-01-29 16:12:48 -05:00
Thomas Harte
7fd02e7f4c Added an extra flag to avoid potential race condition on is_full_, being reset from the background despite a write area not having been allocated. 2017-01-29 16:11:29 -05:00
Thomas Harte
d51f185dc7 Made an attempt to reintroduce the basic horizontal loop. 2017-01-29 15:43:57 -05:00
Thomas Harte
2390358c24 Prevented unbounded CPU usage, albeit without yet deciding who has authority for the clock rate. 2017-01-29 14:19:26 -05:00
Thomas Harte
2432a3b4d7 Fixed condition — >= is smarter. 2017-01-29 14:00:01 -05:00
Thomas Harte
9c3597c7e3 Attempted to reintroduce enough logic to handle [most of] line timing, such that WSYNC works. Initial objective is to get back to having a working background. 2017-01-29 13:47:36 -05:00
Thomas Harte
fba6baaa9c Stubbed and disabled to get back to building. 2017-01-28 21:56:01 -05:00
Thomas Harte
a246530953 Supposing the TIA were implemented, this is (more or less) what the Atari 2600 would now look like. 2017-01-28 21:46:40 -05:00
Thomas Harte
0ffded72a6 Created a placeholder class for a factored-out TIA. There's a bit more it'll need to do, like vending (or receiving) a CRT but this is the full hardware stuff, I think. 2017-01-28 16:19:08 -05:00
Thomas Harte
acadfbabec Merge pull request #97 from TomHarte/Icons
Introduces some file association icons
2017-01-27 21:36:13 -05:00
Thomas Harte
9001cc3fc2 Added a cartridge image. 2017-01-27 21:26:11 -05:00
Thomas Harte
015b2b49f9 Introduced an incomplete set of file association icons. 2017-01-26 22:21:55 -05:00
Thomas Harte
92f928ca42 Merge pull request #96 from TomHarte/PhaseAlignedSampling
Optimises existing composite flow
2017-01-25 21:51:11 -05:00
Thomas Harte
6d087ca054 Restored 2600 audio. 2017-01-25 21:29:19 -05:00
Thomas Harte
c2d7e36c8f Ensured logic for whether composite output is in use is consistent. 2017-01-25 21:25:03 -05:00
Thomas Harte
4d6e78e641 Reinstated temporary Oric-related fix. 2017-01-24 22:16:15 -05:00
Thomas Harte
5761c8267b [Re-]Eliminated connection between colour subcarrier frequency and monitor output mode. 2017-01-24 20:48:54 -05:00
Thomas Harte
a66a8c31b2 Merge branch 'master' into PhaseAlignedSampling 2017-01-24 07:29:18 -05:00
Thomas Harte
19e4ee12e1 Merge branch 'PhaseAlignedSampling' of github.com:TomHarte/CLK into PhaseAlignedSampling 2017-01-24 07:29:14 -05:00
Thomas Harte
4871572a33 Optimised images. 2017-01-23 21:28:13 -05:00
Thomas Harte
2e744a95e4 Merge branch 'master' into PhaseAlignedSampling 2017-01-23 21:11:14 -05:00
Thomas Harte
ff87f1390d Merge pull request #95 from TomHarte/ReadmeImages
Added some example composite images
2017-01-23 20:49:16 -05:00
Thomas Harte
76ca30c26d This version works better. 2017-01-23 20:47:48 -05:00
Thomas Harte
7c2685cb34 Made an attempt at reducing displayed image size. 2017-01-23 20:46:35 -05:00
Thomas Harte
8cf25a2d70 Went tabular. 2017-01-23 20:44:42 -05:00
Thomas Harte
8d69dd30f3 Testing a table. 2017-01-23 20:43:43 -05:00
Thomas Harte
ae8068b86f Added Stormlord images. 2017-01-23 20:38:30 -05:00
Thomas Harte
baeb0ee89f Reduced image sizes. 2017-01-23 20:34:15 -05:00
Thomas Harte
c07993bb0a Added more images. 2017-01-23 20:33:00 -05:00
Thomas Harte
7680cbf9c3 Testing this Markdown implementation for image sizing support. 2017-01-23 20:26:57 -05:00
Thomas Harte
4920fe6701 Added a grab of the Repton title screen. 2017-01-23 20:23:49 -05:00
Thomas Harte
55fe0176bd Added a space. Probably need to hold for a better example though. 2017-01-12 22:12:37 -05:00
Thomas Harte
99fcbb55d1 Attempted to improve layout. 2017-01-12 22:11:25 -05:00
Thomas Harte
6f78ecd12b Added a small pictorial example. Hardly the best, but a step in the right direction. 2017-01-12 22:06:45 -05:00
Thomas Harte
ced644b103 It seems likely that an AY divides its clock by 8, not 16. I had conflated wave frequency and counter clock. 2017-01-11 22:03:01 -05:00
Thomas Harte
be1cb2a551 Fixed NTSC phase. 2017-01-11 21:31:24 -05:00
Thomas Harte
b4159295f6 Switched to using quads for intermediate draws. The specific concern is the flexibility offered in the GL spec as to line drawing algorithms. And even if a driver implements exactly to spec then it should omit the final pixel. 2017-01-11 21:18:41 -05:00
Thomas Harte
d0a93409e6 Made an attempt to simplify in-shader phase calculation, now that output position is a direct multiple of phase. 2017-01-11 08:18:00 -05:00
Thomas Harte
4c3669f210 Reduced precision of input phase, but I'm not necessarily persuaded by it as a move. However it's clear that something is off in that whole area. But if phase is locked by output position, do I need to retain this level of complexity? Also ensured that intermediate buffers prior to the final are sampled using the nearest sampling mode, also to reduce precision errors. 2017-01-10 22:08:07 -05:00
Thomas Harte
eeb646868b Switched off filtering, at least temporarily, to try to ensure that sampling is all where it should be. 2017-01-08 19:53:08 -05:00
Thomas Harte
3d789732a2 Switched back to full buffer clearing. Until I can figure out the source of noise. 2017-01-08 19:50:31 -05:00
Thomas Harte
d2a7d39749 Ensured the output lock isn't held while talking to the delegate. 2017-01-08 19:49:21 -05:00
Thomas Harte
9521718120 Colour phase is multiplied by 255, not 256. 2017-01-08 17:21:26 -05:00
Thomas Harte
28909e33ca Eliminated phaseCyclesPerTick as implied. 2017-01-08 16:48:02 -05:00
Thomas Harte
79632b1d34 Instituted de-escalating phase-related extensions, definitively to kill rounding error edges. 2017-01-08 16:24:22 -05:00
Thomas Harte
cf6d03e35c Merge branch 'master' into PhaseAlignedSampling 2017-01-08 14:49:40 -05:00
Thomas Harte
4a4b31a15c Merge pull request #94 from TomHarte/ElectronDisks
Fixes the Electron's ability automatically to launch a disk
2017-01-08 14:48:58 -05:00
Thomas Harte
f3d9aec8fc Fixed Electron's support for automatically booting floppy disks. 2017-01-08 14:47:41 -05:00
Thomas Harte
7ad64ff16b Made further efforts to support throughput via memory barrier. 2017-01-08 14:47:16 -05:00
Thomas Harte
6153ada33b Fixed Electron's support for automatically booting floppy disks. 2017-01-08 14:46:19 -05:00
Thomas Harte
be48c950b4 Started taking steps towards using a texture barrier where possible to reduce all of my framebuffer binds. Some output appears, but it's not correct. 2017-01-08 11:13:20 -05:00
Thomas Harte
0487b8c178 Definitively eliminated the additional y filtering step; if I'm going to work to ensure always four samples per colour cycle, I can put the channel separation coefficients directly into their shaders, cutting down on samples. 2017-01-07 16:02:33 -05:00
Thomas Harte
5740015f56 Temporarily disabled composite processing to show the pure stream. Fixed both automatic calculations of phase — per line and, at input, per pixel. 2017-01-07 12:38:00 -05:00
Thomas Harte
c84004bfa3 Fixed: colour_cycle_numerator_ doesn't need to be multiplied by the time multiplier because it'll get that for free from the calculation of next_run_length. 2017-01-06 21:36:19 -05:00
Thomas Harte
c746a3711f Temporarily disabled my attempt to be clever with bilinear filtering when applying a lowpass filter. Will need to investigate. 2017-01-04 08:06:18 -05:00
Thomas Harte
aa7774a9a6 Experimental: up the chroma accuracy, just let the luma go straight through. Subject to figuring out how I'm still losing so much precision. 2017-01-03 22:41:34 -05:00
Thomas Harte
a836120945 Restored proper colour separation, but somewhere a massive hit in horizontal resolution is happening — much greater than one would expect from the sample size picked. So investigation to come. 2017-01-03 22:32:07 -05:00
Thomas Harte
7d60df9075 Added the option for both intermediate and output shaders to use only a portion of the input/output texture; made an attempt to pick an appropriate proportion in order to align signal sampling with the colour subcarrier. 2017-01-03 22:16:52 -05:00
Thomas Harte
f2b8b26bc4 Started throwing some comments into my shaders. 2017-01-03 21:16:38 -05:00
Thomas Harte
9d60172571 Merge pull request #93 from TomHarte/ReadWriteTrack
Implements the Type 3 WD177x commands
2017-01-01 21:01:56 -05:00
Thomas Harte
eca3995481 Added a CRC check for read address, ensured CRC, lost data and record not found are initially reset. 2017-01-01 21:00:25 -05:00
Thomas Harte
044c920a5b Made it more explicit that there are no unhandled cases. 2017-01-01 20:56:52 -05:00
Thomas Harte
0df9ce5a76 Made an attempt at read address. So superficially that leaves only the force interrupts. 2017-01-01 20:55:09 -05:00
Thomas Harte
f94f34f053 Made an attempt at read track. Which means process_input_bit can't just swallow syncs any more; it now reports them as tokens of type ::Sync. 2017-01-01 20:39:19 -05:00
Thomas Harte
4ad2d2bedd Merge branch 'master' into ReadWriteTrack 2017-01-01 20:04:22 -05:00
Thomas Harte
e28f72d919 Merge pull request #90 from TomHarte/TravisCI
Introduces a shared Xcode scheme plus a first attempt at Travis CI integration
2017-01-01 20:03:26 -05:00
Thomas Harte
c994fa39f6 Ensured spin-up doesn't occur if there's no motor line. 2016-12-31 16:18:30 -05:00
Thomas Harte
1ea4f0d79d Made an attempt to implement 'write track' and ensure that 'write sector' can't end without announcing that it has ended writing. 2016-12-31 16:01:44 -05:00
Thomas Harte
0689df1349 Merge pull request #92 from TomHarte/MFMCleanup
Adds some documentation and tidies some of the new MFM infrastructure
2016-12-31 15:32:11 -05:00
Thomas Harte
b3c33d993a Made an attempt to explain the requirements placed upon Disk subclasses that wish to support writing. 2016-12-31 15:30:48 -05:00
Thomas Harte
8eb21c6702 The "MFM...Byte"s aren't MFM-specific, they're relevant to both FM and MFM encoding. So renamed them. Also slimmed syntax within MFM.cpp mostly where emigration from the Acorn disk analyser had left a residue of lengthy namespace specification. 2016-12-31 15:25:11 -05:00
Thomas Harte
4c62487e6e Merge pull request #91 from TomHarte/TableCRC
Switches to a table-based implementation of CRC generation
2016-12-31 14:16:17 -05:00
Thomas Harte
a147d56ce6 Switched to a table-based implementation of CRC generation, adding construction cost to cheapen running cost. 2016-12-31 14:15:20 -05:00
Thomas Harte
7b696b0962 Switched scheme to shared. 2016-12-31 13:11:07 -05:00
Thomas Harte
57bb771fb7 It looks like spaces are automatically escaped (?) 2016-12-31 13:05:55 -05:00
Thomas Harte
5201a59c44 Attempted to introduce Travis CI. 2016-12-31 13:03:32 -05:00
Thomas Harte
df6e98fa52 Merge pull request #89 from TomHarte/OricDiskWrites
Adds write support for the Oric .DSK file format
2016-12-31 12:53:43 -05:00
Thomas Harte
52b850a3f5 Quick extra: make sure parsed tracks don't overflow the 6400 byte space available in an MFM disk. Which might be better expressed as 6250? 2016-12-31 12:51:52 -05:00
Thomas Harte
cfbab1448c Switched to a track parsing that disallows synchronisation values within sector contents. 2016-12-31 12:23:08 -05:00
Thomas Harte
12549ff412 Might as well get the file offset before entering the critical section; also moved the lock guard down more explicitly to group with the second set of actions. 2016-12-31 11:48:46 -05:00
Thomas Harte
6f0b5427e4 Made an attempt to avoid repetition of sync bytes. 2016-12-31 00:20:00 -05:00
Thomas Harte
0123b37213 Made an attempt to include sync values in the stream and properly to align. 2016-12-31 00:11:31 -05:00
Thomas Harte
ea4d85e1cd The virtual disk constructed is the same across all tracks. So why not just request zero? 2016-12-31 00:10:35 -05:00
Thomas Harte
f217d508b8 Completed first attempt at write support for Oric disk images. 2016-12-30 23:12:46 -05:00
Thomas Harte
1f625fad66 Decided that if this is an [M]FM parsing function then it should be something more intelligent than a mere PLL record. Which I guess conveniently implies Oric DSK-esque behaviour. But properly defined, rather than very vaguely. 2016-12-30 23:10:52 -05:00
Thomas Harte
632b3c63b1 Added the infrastructure necessary for Oric disks to appear writeable to the machine and to receive changed tracks. 2016-12-30 22:51:48 -05:00
Thomas Harte
d581294479 Added get_track to get the PLL output for a complete track. 2016-12-30 19:59:23 -05:00
Thomas Harte
0f399b0a0c Made type conversion explicit. 2016-12-30 19:59:01 -05:00
Thomas Harte
c6fcc40ac5 Merge pull request #88 from TomHarte/WDWrites
Implements write support for the WD.
2016-12-30 18:11:00 -05:00
Thomas Harte
3b29e6a473 Ensured SSD and ADFs are grown if required. 2016-12-30 18:08:12 -05:00
Thomas Harte
07dacff42d Added writing for Acorn ADF disks, plus appropriate TODOs in both similar bits of boilerplate. 2016-12-30 18:03:30 -05:00
Thomas Harte
c85450648f Fix: make sure copies have proper event lengths. Also made it much clearer what's going on with the initial copy to the heap. 2016-12-30 17:55:46 -05:00
Thomas Harte
c740d9655a Fixed: index_count_ may have been left high by a previous call; reset it just in case. 2016-12-30 17:55:06 -05:00
Thomas Harte
d09e7ac1e8 Made an attempt at reacting appropriately if the very first thing that looks like a sector doesn't pan out. 2016-12-30 17:44:35 -05:00
Thomas Harte
5d63556870 Periods need a custom copy constructor too, if they're going to avoid sharing an event_source. 2016-12-30 17:39:52 -05:00
Thomas Harte
e5cc77f22d Added an extra sanity check. 2016-12-30 17:29:51 -05:00
Thomas Harte
81a3cbac45 Ensured a copy is passed for writing back rather than the original. 2016-12-30 17:26:44 -05:00
Thomas Harte
63ff5165a4 After a quick bit of reading, discovered the virtual copy constructor pattern really is only a convention in C++, and conformed to it. Which hopefully gives copyable tracks. 2016-12-30 17:25:39 -05:00
Thomas Harte
71dbd78cf2 If asynchronous background processing is to occur on tracks then, given that they inherently have state, they'll need to be copyable, and ideally 'cheaply' (though it's not too great a priority). So started implementing appropriate copy constructors. Also introduced an extra level of indirection to PCMSegmentEventSource so that it can copy itself without copying the underlying PCMSegment, which is 95% of the heft of a track in all currently-implemented cases. 2016-12-30 14:23:26 -05:00
Thomas Harte
f88f3c65e9 Removed duplicated newline. 2016-12-30 14:21:36 -05:00
Thomas Harte
82bb78fb2d Ensured that get_sector copes even if any invalid sectors are encountered. 2016-12-30 14:21:14 -05:00
Thomas Harte
6fc692cd34 Attempted to switch to an asynchronous means for continuous file updates. Testing with SSD, as usual. 2016-12-29 22:15:58 -05:00
Thomas Harte
bbd94749f4 ... and I guess an instant maximal simplification is also easy if length ends up being 0 2016-12-29 11:02:21 -05:00
Thomas Harte
54900ca3fb Addition and subtraction can end immediately without performing any extra work if the operand is 0 2016-12-29 11:00:47 -05:00
Thomas Harte
a8bc9d830e Removed leftover very temporary debugging aid. 2016-12-28 23:03:05 -05:00
Thomas Harte
b9fad184d7 Added just enough for a complete manual test of writing to a .ADF with the 1770 then getting the correct result parsing it back on the host side in order potentially to update a file.
... which means that now it's time to worry about when and how mounted files should actually update themselves. Which will make for some fun with threading, I dare say.
2016-12-28 23:00:47 -05:00
Thomas Harte
af1b396c9e Found an ugly issue with Storage::Time as implemented (i) to be unsigned; and (ii) automatically to simplify. Will need to fix. Here's a quick workaround for this one segment of code. 2016-12-28 22:57:11 -05:00
Thomas Harte
9cb902cc4f Experimentally marked ADF as writable too, immediately discovering a mistake in the analysing MFM decoder. 2016-12-28 22:34:22 -05:00
Thomas Harte
e4000bd060 Added some even more verbose logging; slightly simplified write loop logic, and decided it's definitely write_byte that's responsible for CRC generator feeding. 2016-12-28 21:24:19 -05:00
Thomas Harte
ce814c9e99 These can be const. 2016-12-28 21:23:22 -05:00
Thomas Harte
bfe6c0a0c1 Ensured that FileHolder gets a writeable file reference if one is possible, and records whether the file in hand is read-only. So now the SSD class can answer honestly. 2016-12-28 20:09:14 -05:00
Thomas Harte
4adcb46665 Fixed FM-mode CRC generation. 2016-12-28 19:51:27 -05:00
Thomas Harte
46a93d2e12 Fixed errors to ensure that FM disks, at least, follow the same CRC generation rules when being built from sectors and when being parsed. 2016-12-28 19:48:46 -05:00
Thomas Harte
1277a67f9a Introduced data_mode_ to replace is_reading_data_, representing that there are now three possible modes. When writing, any input from the read head won't affect the CRC generator. 2016-12-28 19:26:21 -05:00
Thomas Harte
720b1e5802 Attempted to ensure proper CRC generation for FM-format input. 2016-12-28 18:56:53 -05:00
Thomas Harte
8cd1575891 Similar fix to that over in Oric land: ensure a known, effective initial value for the Plus 3's control register. 2016-12-28 18:52:36 -05:00
Thomas Harte
3a9ad3fb08 Fixed Oric .DSK handling, per my latest understanding. Which creates a desire to write shorts directly to the disk surface, so exposed that in the encoder. 2016-12-28 18:50:28 -05:00
Thomas Harte
90151e2094 Fixed to ensure a known initial control register value, which has taken effect. 2016-12-28 18:49:32 -05:00
Thomas Harte
7a627b782d Reintroduced writing of MFM sync marks when writing a sector. 2016-12-28 18:48:50 -05:00
Thomas Harte
a568172758 Made steps towards proper CRC generation. Am currently comparing against Oric disk images, as — amongst other things — they include precomputed CRCs. 2016-12-28 18:29:37 -05:00
Thomas Harte
99993a1b24 Since it's about to become important that objective results match, added a couple of objective-result tests for the CRC generator. 2016-12-27 19:03:46 -05:00
Thomas Harte
9c0f622a2e Started working CRC checking into the 1770. Discovered immediately that my generated CRC does not match that built into the Oric disk images. So mine is pretty-much certainly wrong. An opportunity for learning! 2016-12-26 16:46:26 -05:00
Thomas Harte
0490a47058 Worked on the all-around framework for decoding sectors back from tracks when closing down a file. Hit the wall that the parser is more observant of CRCs than the WD. No, really. So I guess I have to stop avoiding that whole issue. 2016-12-26 14:24:33 -05:00
Thomas Harte
83c433c142 Deviated from the data sheet, which seems likely to be correct. Hence removed a whole load of the temporary logging. 2016-12-26 12:48:49 -05:00
Thomas Harte
742c5df367 With lots of logging arising temporarily, fixed bug whereby conversion to a patched track would lead to holding a track with a distinct measure of time, leading to improperly-placed patches. 2016-12-25 22:00:39 -05:00
Thomas Harte
b538ee5bd8 Fixed discovery of correct active period and setting of track time, when seeking. 2016-12-25 21:32:50 -05:00
Thomas Harte
a6d038cad9 Eliminated special case that doesn't seek properly and isn't needed. Added TODO. 2016-12-25 21:32:14 -05:00
Thomas Harte
4fca30b81f Made the Plus 3 less chatty, documented invalidate_track. 2016-12-25 21:06:58 -05:00
Thomas Harte
26710c988d Modified SSD to ensure a fully-formatted surface is represented even if no track data is in the source file. This corrects the controller's sense of write success. 2016-12-25 20:40:06 -05:00
Thomas Harte
acc35885cd Attempted to reduce track invalidations. 2016-12-25 20:38:25 -05:00
Thomas Harte
c0a1264ab0 Slightly improved legibility. 2016-12-25 20:19:47 -05:00
Thomas Harte
e2b829f68e Made an attempt to write the proper address mark. 2016-12-25 20:15:07 -05:00
Thomas Harte
beaa868079 Factored the MFM parser out into encodings. 2016-12-25 20:00:57 -05:00
Thomas Harte
1349e85d83 [Mostly] fixed track write-back. 2016-12-25 19:19:22 -05:00
Thomas Harte
74e98fd097 Made an attempt to write actual data (albeit that CRC calculation is still missing). 2016-12-25 19:18:45 -05:00
Thomas Harte
007c13ec16 Fixed: cycles_per_bit_ isn't a function of the rotational multiplier, it's absolute. Also made sure that exactly hitting the end of a bit counts. 2016-12-25 16:35:39 -05:00
Thomas Harte
98be6ede45 Shuffled a little to reduce risk of overflow, ensured writing is a loop, still seem to be writing too quickly for some reason. 2016-12-25 16:13:05 -05:00
Thomas Harte
d2ad2c756e Added enough shovelling to write rubbish for an entire sector. 2016-12-25 15:46:49 -05:00
Thomas Harte
ec55a25620 It makes sense to simplify these ahead of time. 2016-12-25 12:32:25 -05:00
Thomas Harte
aceb7e3b6b Started implementing write sector on the 1770, immediately deciding it would be useful to have a callback for end-of-queued-data-written from disk controller. So had a go at implementing that, naively. More investigation required. 2016-12-25 12:31:38 -05:00
Thomas Harte
901f19f89c Added enough stuff that SSDs attached to a 1770 will now reach the entry point for writing. 2016-12-25 09:46:12 -05:00
Thomas Harte
e56beb3e9c Merge pull request #86 from TomHarte/DiskWrites
Implements backing work for in-memory disk writes
2016-12-25 09:37:20 -05:00
Thomas Harte
9d555c4a02 Let's try just declining to pump the PLL while in write mode. Added documentation to explain. 2016-12-25 09:19:18 -05:00
Thomas Harte
b57038edc5 Actually, at least index holes will still be receivable while writing, so this wasn't entirely correct. Probably best to leave it in. 2016-12-25 09:16:09 -05:00
Thomas Harte
d606bd7ce5 Added saturation test, fixed code as indicated. 2016-12-24 23:29:37 -05:00
Thomas Harte
09ff9d6a26 Introduced a couple more floating-point conversion tests, fixed errors uncovered. 2016-12-24 23:21:19 -05:00
Thomas Harte
e25195a718 Added a single test for Storage::Time, discovering that I had the wrong sign on float conversions. 2016-12-24 22:59:01 -05:00
Thomas Harte
af69b21033 This is almost complete, except that it doesn't act appropriately if some bits are written but not enough to cover the entire writing period. 2016-12-24 22:51:26 -05:00
Thomas Harte
f601d796f5 Added documentation. 2016-12-24 22:37:20 -05:00
Thomas Harte
6e94d0c19f Extended Storage::Disk::Disk to permit write-back of modified tracks, exposed some interface via Storage::Disk::Drive. 2016-12-24 22:11:31 -05:00
Thomas Harte
7f303cfceb Continued the baby steps. 2016-12-24 21:54:43 -05:00
Thomas Harte
afc6f4129c Withdrew unused tally. 2016-12-24 21:47:57 -05:00
Thomas Harte
1e416d4af0 Withdrew now-unused and never-implemented API from TimedEventLoop, and the redundant track time count from DiskController. 2016-12-24 21:02:10 -05:00
Thomas Harte
bedea48d03 This is a much better way of dealing with being partway into an incoming event. Subject to eliminating overruns, of course. 2016-12-24 20:54:27 -05:00
Thomas Harte
4cb17143ef Messing around trying to lock down timing precisely. Which includes formal initial conditions. 2016-12-24 15:18:46 -05:00
Thomas Harte
4d4852bb78 Ensured that Times start life in their simplest form. 2016-12-24 15:18:03 -05:00
Thomas Harte
4728bda0a2 Added an additional constructor to make sure that regular ints go to the correct place. 2016-12-24 13:27:57 -05:00
Thomas Harte
1e970a9772 Started stepping slowly towards allowing writing on the disk controller, taking the opportunity to introduce self-simplifying behaviour to Storage::Time. 2016-12-24 13:07:23 -05:00
Thomas Harte
42f25cdffc Merge branch 'master' into DiskWrites 2016-12-22 22:47:19 -05:00
Thomas Harte
393dc5c64f Merge pull request #87 from TomHarte/ElectronVideoFix
Ensures the Electron's video base address is set properly at construction.
2016-12-22 22:46:55 -05:00
Thomas Harte
3805e3d17d Ensured base address is set properly at construction. 2016-12-22 22:46:02 -05:00
Thomas Harte
7028f57336 Simplified a little further. 2016-12-22 18:13:10 -05:00
Thomas Harte
e4e0347638 Attempted to consolidate some of the repetition. 2016-12-21 22:17:00 -05:00
Thomas Harte
72ca06cf8d Added some extra tests, performed some basic tidying. Probably should do more. 2016-12-21 19:54:19 -05:00
Thomas Harte
6a0c7f22ee Added a few more tests. All passing. 2016-12-20 21:46:34 -05:00
Thomas Harte
03579f33f1 Fixed multi-coverage insertion, via an appropriate test. 2016-12-20 21:38:32 -05:00
Thomas Harte
7eca910cc5 Fixed insertion location finding logic, working on the relevant test. 2016-12-20 21:14:05 -05:00
Thomas Harte
c180340474 Added two more passing tests and one that crashes. 2016-12-20 19:25:58 -05:00
Thomas Harte
823ab9bc34 Completed initial non-trivial test, fixing revealed errors. 2016-12-20 19:15:36 -05:00
Thomas Harte
5a508ea0df Attempted properly to cover the exactly-equal starts and ends cases, and to improve meaning. 2016-12-20 18:32:49 -05:00
Thomas Harte
63d861a2f3 Switched from C-in-the-brain manual offset counting to using iterators like an ordinary C++ person. 2016-12-20 18:17:54 -05:00
Thomas Harte
6f17076003 Switched to much more logical shared_ptr ownership of PCMSegmentEventSources by Periods. 2016-12-20 18:13:10 -05:00
Thomas Harte
497b2ae4dd Still by manual inspection: the time for the next event should be provisional until proven acceptable, allowing a proper measurement of time until exiting the period to be taken; also fixed the accumulated period error when seeking back onto the underlying track. 2016-12-20 08:14:16 -05:00
Thomas Harte
6bdde542c5 Edging towards functioning automatic tests, fixed right-period adjustment and slightly decreased searching cost while in the process of adding a test. 2016-12-20 07:52:14 -05:00
Thomas Harte
ec624eaab1 Made an attempt fully to implement PCMPatchedTrack. Which now requires tests. 2016-12-20 07:30:57 -05:00
Thomas Harte
1ef1f6ec69 Attempted to implemnt seek_to and to finish add_segment. Started doing a little of get_next_event but ran out of time for the day. 2016-12-19 21:46:02 -05:00
Thomas Harte
8f937ceac8 Made an attempt to come up with a data structure that actually makes sense (though perhaps this is textbook list rather than vector stuff? I guess it depends on the frequency I expect inserts to occur versus reads) and to implement inserts. Though the Periods aren't yet honoured. 2016-12-19 07:42:43 -05:00
Thomas Harte
1df478d250 Removed dead header file. 2016-12-18 23:04:16 -05:00
Thomas Harte
e081f224b6 Implemented a very basic PCMTrack test, nevertheless revealing an oversight in PCMSegmentEventSource related to improperly counting to the index hole if the final bit is set. Took that as a message that I should comment and document the event source. 2016-12-18 22:53:24 -05:00
Thomas Harte
a6354ebb01 Reimplemented PCMTrack to use PCMSegmentEventSource, eliminating code duplication. 2016-12-18 21:37:05 -05:00
Thomas Harte
f9a5595dad Added seeking tests, correcting such errors as uncovered. 2016-12-18 10:19:24 -05:00
Thomas Harte
3297f6d545 Made an attempt to implement seek_to on PCMSegmentEventSource, taking account of off-by-half counting. 2016-12-17 22:44:33 -05:00
Thomas Harte
3116a2cf4c Realised I was actually testing PCMSegmentEventSource, not PCMSegment; implemented a spread of tests; hence fixed PCMSegmentEventSource. 2016-12-17 21:47:13 -05:00
Thomas Harte
254cc41fd6 Made an attempt to separate and isolate the stuff of creating flux events from a PCMSegment, eventually to factor that out of PCMTrack and make it available also to PCMPatchedTrack. 2016-12-17 21:13:57 -05:00
Thomas Harte
313db75303 Ensured the patchable track owns its underlying track. 2016-12-17 18:17:22 -05:00
Thomas Harte
3017062e89 Maybe TDD is the way to get over my activity block on this thing? Fixed the existing ArrayBuilder tests so that the tests target builds again, added an extremely trivial PCMTrack test, heading towards PCMPatchedTrack tests. 2016-12-17 17:05:49 -05:00
Thomas Harte
f1a08b7ab5 Opted to pass times by reference and added enough to PCMPatchedTrack that it could start being used by the disk controller, albeit that it doesn't work. 2016-12-17 16:26:45 -05:00
Thomas Harte
dc08a23ceb This is going to be a slow walk, I think. This class attempts to be the scratchpad which will hold in-memory track modifications. 2016-12-16 19:20:38 -05:00
Thomas Harte
1e757d1039 Merge branch 'master' into DiskWrites 2016-12-15 19:53:39 -05:00
Thomas Harte
ea1b3d447b Merge pull request #85 from TomHarte/ElectronRefactor
Applies a healthy cleaning to the Electron implementation
2016-12-15 19:53:07 -05:00
Thomas Harte
63107cd492 Tidied, very slightly. 2016-12-15 19:49:25 -05:00
Thomas Harte
a555c5762a Rearranged code, hopefully into a more logical grouping. 2016-12-15 19:47:04 -05:00
Thomas Harte
4a7ddaf2e9 Added documentation and a quick note to self. 2016-12-15 19:43:04 -05:00
Thomas Harte
f61176cd7d Reinstituted something of the don't-do-pixel-work-until-an-affecting-write-occurs optimisation. 2016-12-15 19:20:14 -05:00
Thomas Harte
c1c70a767a Attempted fully to reinstate proper timing. 2016-12-15 18:52:16 -05:00
Thomas Harte
0326316bb8 Reinstated whole-frame counting. Thereby to reinstate proper interrupts. 2016-12-15 18:09:49 -05:00
Thomas Harte
b58b11fc93 Switched to a table-based dispatch of line-by-line actions, primarily to simplify. 2016-12-15 18:07:46 -05:00
Thomas Harte
fd541e1142 An early draft; dealing with the issue that not all cycles are necessarily consumed in a single call. Incomplete; broken. Committing for cross-machine visibility. 2016-12-12 08:01:10 -05:00
Thomas Harte
be7e05e109 Started attempting to move total responsibility for display-related interrupts and RAM timing into the video. 2016-12-11 18:34:49 -05:00
Thomas Harte
c5cf8d9531 Ensured the video subsystem correctly handles requests to run over a frame boundary. 2016-12-11 16:17:51 -05:00
Thomas Harte
52028432e1 Restored some semblance of output. 2016-12-10 22:19:10 -05:00
Thomas Harte
0aae1bd1ef Fixed calculation of termination cycle. 2016-12-10 21:35:41 -05:00
Thomas Harte
c43e481a33 Started factoring video out of the Electron. 2016-12-10 21:07:52 -05:00
Thomas Harte
54b5056c74 Merge branch 'master' into DiskWrites 2016-12-10 19:37:48 -05:00
Thomas Harte
0653770c63 Merge pull request #84 from TomHarte/OricColour
Switches to using the original Oric colour ROM to generate Oric composite values
2016-12-10 19:37:17 -05:00
Thomas Harte
e62be03673 Removed endianness assumption. 2016-12-10 19:10:33 -05:00
Thomas Harte
34d213dec4 Decreased Y resolution, again also hopefully temporarily. 2016-12-10 15:35:38 -05:00
Thomas Harte
81a102d951 Upped intermediate buffer size, at least temporarily, while I look for the source of the interference patterns I'm seeing. 2016-12-10 15:20:10 -05:00
Thomas Harte
a5683dfb21 Removed now untrue comment. 2016-12-10 15:19:48 -05:00
Thomas Harte
0e71802b92 Reduced Oric video to single nibble constants. Removed attempt at asynchronous flush as no longer required. 2016-12-10 14:17:46 -05:00
Thomas Harte
580f347727 Fixed Oric SCART mode by having it change what it's giving to the CRT based on which shader it knows will be active. 2016-12-10 13:55:56 -05:00
Thomas Harte
a549fd1ecc Introduced the ability simply to piggy-back off the CRT's natural phase for the colour burst, thereby eliminating a couple of redundant independent attempts in the Oric and Electron. 2016-12-10 13:42:34 -05:00
Thomas Harte
e359441e2f Added a readme.txt for the omitted Oric ROMs. 2016-12-09 22:18:11 -05:00
Thomas Harte
6cdd41e5a9 Added direct use of the colour ROM, uploading 16 bits per pixel to contain the entire ROM composite wave. 2016-12-09 22:17:10 -05:00
Thomas Harte
3b5962b171 This is an initial attempt at using the actual Oric colour ROM values for composite video generation. 2016-12-09 20:01:27 -05:00
Thomas Harte
c4041b06a8 This'll do as a write interface, won't it? 2016-12-07 22:19:20 -05:00
Thomas Harte
46ebae7e4b Merge pull request #83 from TomHarte/InterruptLine
Corrects interrupt line handling of the WD and Microdisc
2016-12-06 21:21:35 -05:00
Thomas Harte
c304db0f5a Deintegrated the busy flag and the interrupt request line, as the latter is reset by status reads. Which also means I can start reporting the WD INTRQ line status directly from the Microdisc. That appears to be correct, rather than honouring the Microdisc IRQ select there. 2016-12-06 21:16:29 -05:00
Thomas Harte
4d3bdf8c7c Fixed failure to initialise the Microdisc flag if loading a tape. 2016-12-06 20:29:05 -05:00
174 changed files with 8816 additions and 5129 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ DerivedData
# Exclude system ROMs
ROMImages/*
OSBindings/Mac/Clock SignalTests/Atari\ ROMs
# CocoaPods
#

5
.travis.yml Normal file
View File

@@ -0,0 +1,5 @@
language: objective-c
osx_image: xcode8.2
xcode_project: OSBindings/Mac/Clock Signal.xcodeproj
xcode_scheme: Clock Signal
xcode_sdk: macosx10.12

View File

@@ -12,36 +12,35 @@
using namespace WD;
WD1770::Status::Status() :
type(Status::One),
write_protect(false),
record_type(false),
spin_up(false),
record_not_found(false),
crc_error(false),
seek_error(false),
lost_data(false),
data_request(false),
busy(false)
{}
type(Status::One),
write_protect(false),
record_type(false),
spin_up(false),
record_not_found(false),
crc_error(false),
seek_error(false),
lost_data(false),
data_request(false),
interrupt_request(false),
busy(false) {}
WD1770::WD1770(Personality p) :
Storage::Disk::Controller(8000000, 16, 300),
interesting_event_mask_(Event::Command),
resume_point_(0),
delay_time_(0),
index_hole_count_target_(-1),
is_awaiting_marker_value_(false),
is_reading_data_(false),
delegate_(nullptr),
personality_(p),
head_is_loaded_(false)
{
Storage::Disk::Controller(8000000, 16, 300),
crc_generator_(0x1021, 0xffff),
interesting_event_mask_(Event::Command),
resume_point_(0),
delay_time_(0),
index_hole_count_target_(-1),
is_awaiting_marker_value_(false),
data_mode_(DataMode::Scanning),
delegate_(nullptr),
personality_(p),
head_is_loaded_(false) {
set_is_double_density(false);
posit_event(Event::Command);
}
void WD1770::set_is_double_density(bool is_double_density)
{
void WD1770::set_is_double_density(bool is_double_density) {
is_double_density_ = is_double_density;
Storage::Time bit_length;
bit_length.length = 1;
@@ -51,21 +50,15 @@ void WD1770::set_is_double_density(bool is_double_density)
if(!is_double_density) is_awaiting_marker_value_ = false;
}
void WD1770::set_register(int address, uint8_t value)
{
switch(address&3)
{
case 0:
{
if((value&0xf0) == 0xd0)
{
void WD1770::set_register(int address, uint8_t value) {
switch(address&3) {
case 0: {
if((value&0xf0) == 0xd0) {
printf("!!!TODO: force interrupt!!!\n");
update_status([] (Status &status) {
status.type = Status::One;
});
}
else
{
} else {
command_ = value;
posit_event(Event::Command);
}
@@ -73,22 +66,26 @@ void WD1770::set_register(int address, uint8_t value)
break;
case 1: track_ = value; break;
case 2: sector_ = value; break;
case 3: data_ = value; break;
case 3:
data_ = value;
update_status([] (Status &status) {
status.data_request = false;
});
break;
}
}
uint8_t WD1770::get_register(int address)
{
switch(address&3)
{
default:
{
uint8_t WD1770::get_register(int address) {
switch(address&3) {
default: {
update_status([] (Status &status) {
status.interrupt_request = false;
});
uint8_t status =
(status_.write_protect ? Flag::WriteProtect : 0) |
(status_.crc_error ? Flag::CRCError : 0) |
(status_.busy ? Flag::Busy : 0);
switch(status_.type)
{
switch(status_.type) {
case Status::One:
status |=
(get_is_track_zero() ? Flag::TrackZero : 0) |
@@ -106,14 +103,11 @@ uint8_t WD1770::get_register(int address)
break;
}
if(!has_motor_on_line())
{
if(!has_motor_on_line()) {
status |= get_drive_is_ready() ? 0 : Flag::NotReady;
if(status_.type == Status::One)
status |= (head_is_loaded_ ? Flag::HeadLoaded : 0);
}
else
{
} else {
status |= (get_motor_on() ? Flag::MotorOn : 0);
if(status_.type == Status::One)
status |= (status_.spin_up ? Flag::SpinUp : 0);
@@ -130,71 +124,75 @@ uint8_t WD1770::get_register(int address)
}
}
void WD1770::run_for_cycles(unsigned int number_of_cycles)
{
void WD1770::run_for_cycles(unsigned int number_of_cycles) {
Storage::Disk::Controller::run_for_cycles((int)number_of_cycles);
if(delay_time_)
{
if(delay_time_ <= number_of_cycles)
{
if(delay_time_) {
if(delay_time_ <= number_of_cycles) {
delay_time_ = 0;
posit_event(Event::Timer);
}
else
{
} else {
delay_time_ -= number_of_cycles;
}
}
}
void WD1770::process_input_bit(int value, unsigned int cycles_since_index_hole)
{
void WD1770::process_input_bit(int value, unsigned int cycles_since_index_hole) {
if(data_mode_ == DataMode::Writing) return;
shift_register_ = (shift_register_ << 1) | value;
bits_since_token_++;
Token::Type token_type = Token::Byte;
if(!is_reading_data_)
{
if(!is_double_density_)
{
switch(shift_register_ & 0xffff)
{
if(data_mode_ == DataMode::Scanning) {
Token::Type token_type = Token::Byte;
if(!is_double_density_) {
switch(shift_register_ & 0xffff) {
case Storage::Encodings::MFM::FMIndexAddressMark:
token_type = Token::Index;
crc_generator_.reset();
crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IndexAddressByte);
break;
case Storage::Encodings::MFM::FMIDAddressMark:
token_type = Token::ID;
crc_generator_.reset();
crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::IDAddressByte);
break;
case Storage::Encodings::MFM::FMDataAddressMark:
token_type = Token::Data;
crc_generator_.reset();
crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DataAddressByte);
break;
case Storage::Encodings::MFM::FMDeletedDataAddressMark:
token_type = Token::DeletedData;
crc_generator_.reset();
crc_generator_.add(latest_token_.byte_value = Storage::Encodings::MFM::DeletedDataAddressByte);
break;
default:
break;
}
}
else
{
switch(shift_register_ & 0xffff)
{
case Storage::Encodings::MFM::MFMIndexAddressMark:
} else {
switch(shift_register_ & 0xffff) {
case Storage::Encodings::MFM::MFMIndexSync:
bits_since_token_ = 0;
is_awaiting_marker_value_ = true;
return;
case Storage::Encodings::MFM::MFMAddressMark:
token_type = Token::Sync;
latest_token_.byte_value = Storage::Encodings::MFM::MFMIndexSyncByteValue;
break;
case Storage::Encodings::MFM::MFMSync:
bits_since_token_ = 0;
is_awaiting_marker_value_ = true;
return;
crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue);
token_type = Token::Sync;
latest_token_.byte_value = Storage::Encodings::MFM::MFMSyncByteValue;
break;
default:
break;
}
}
if(token_type != Token::Byte)
{
if(token_type != Token::Byte) {
latest_token_.type = token_type;
bits_since_token_ = 0;
posit_event(Event::Token);
@@ -202,8 +200,7 @@ void WD1770::process_input_bit(int value, unsigned int cycles_since_index_hole)
}
}
if(bits_since_token_ == 16)
{
if(bits_since_token_ == 16) {
latest_token_.type = Token::Byte;
latest_token_.byte_value = (uint8_t)(
((shift_register_ & 0x0001) >> 0) |
@@ -216,83 +213,64 @@ void WD1770::process_input_bit(int value, unsigned int cycles_since_index_hole)
((shift_register_ & 0x4000) >> 7));
bits_since_token_ = 0;
if(is_awaiting_marker_value_ && is_double_density_)
{
if(is_awaiting_marker_value_ && is_double_density_) {
is_awaiting_marker_value_ = false;
switch(latest_token_.byte_value)
{
case Storage::Encodings::MFM::MFMIndexAddressByte:
switch(latest_token_.byte_value) {
case Storage::Encodings::MFM::IndexAddressByte:
latest_token_.type = Token::Index;
break;
case Storage::Encodings::MFM::MFMIDAddressByte:
case Storage::Encodings::MFM::IDAddressByte:
latest_token_.type = Token::ID;
break;
case Storage::Encodings::MFM::MFMDataAddressByte:
case Storage::Encodings::MFM::DataAddressByte:
latest_token_.type = Token::Data;
break;
case Storage::Encodings::MFM::MFMDeletedDataAddressByte:
case Storage::Encodings::MFM::DeletedDataAddressByte:
latest_token_.type = Token::DeletedData;
break;
default: break;
}
}
crc_generator_.add(latest_token_.byte_value);
posit_event(Event::Token);
return;
}
}
void WD1770::process_index_hole()
{
void WD1770::process_index_hole() {
index_hole_count_++;
posit_event(Event::IndexHole);
if(index_hole_count_target_ == index_hole_count_)
{
if(index_hole_count_target_ == index_hole_count_) {
posit_event(Event::IndexHoleTarget);
index_hole_count_target_ = -1;
}
// motor power-down
if(index_hole_count_ == 9 && !status_.busy && has_motor_on_line())
{
if(index_hole_count_ == 9 && !status_.busy && has_motor_on_line()) {
set_motor_on(false);
}
// head unload
if(index_hole_count_ == 15 && !status_.busy && has_head_load_line())
{
if(index_hole_count_ == 15 && !status_.busy && has_head_load_line()) {
set_head_load_request(false);
}
}
// +------+----------+-------------------------+
// ! ! ! BITS !
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
// +------+----------+-------------------------+
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
// +------+----------+-------------------------+
void WD1770::process_write_completed() {
posit_event(Event::DataWritten);
}
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = mask; return; case __LINE__:
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; interesting_event_mask_ = Event::Timer; delay_time_ = ms * 8000; if(delay_time_) return; case __LINE__:
#define WAIT_FOR_BYTES(count) resume_point_ = __LINE__; interesting_event_mask_ = Event::Token; distance_into_section_ = 0; return; case __LINE__: if(latest_token_.type == Token::Byte) distance_into_section_++; if(distance_into_section_ < count) { interesting_event_mask_ = Event::Token; return; }
#define BEGIN_SECTION() switch(resume_point_) { default:
#define END_SECTION() 0; }
#define READ_ID() \
if(new_event_type == Event::Token) \
{ \
if(!distance_into_section_ && latest_token_.type == Token::ID) {is_reading_data_ = true; distance_into_section_++; } \
else if(distance_into_section_ && distance_into_section_ < 7 && latest_token_.type == Token::Byte) \
{ \
if(new_event_type == Event::Token) { \
if(!distance_into_section_ && latest_token_.type == Token::ID) {data_mode_ = DataMode::Reading; distance_into_section_++; } \
else if(distance_into_section_ && distance_into_section_ < 7 && latest_token_.type == Token::Byte) { \
header_[distance_into_section_ - 1] = latest_token_.byte_value; \
distance_into_section_++; \
} \
@@ -309,9 +287,24 @@ void WD1770::process_index_hole()
WAIT_FOR_EVENT(Event::IndexHoleTarget); \
status_.spin_up = true;
// +--------+----------+-------------------------+
// ! ! ! BITS !
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
// +--------+----------+-------------------------+
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
// ! 4 ! Forc int ! 1 1 0 1 i3 i2 i1 i0 !
// +--------+----------+-------------------------+
void WD1770::posit_event(Event new_event_type)
{
void WD1770::posit_event(Event new_event_type) {
if(!(interesting_event_mask_ & (int)new_event_type)) return;
interesting_event_mask_ &= ~new_event_type;
@@ -321,17 +314,19 @@ void WD1770::posit_event(Event new_event_type)
// Wait for a new command, branch to the appropriate handler.
wait_for_command:
printf("Idle...\n");
is_reading_data_ = false;
data_mode_ = DataMode::Scanning;
index_hole_count_ = 0;
update_status([] (Status &status) {
status.busy = false;
status.interrupt_request = true;
});
WAIT_FOR_EVENT(Event::Command);
update_status([] (Status &status) {
status.busy = true;
status.interrupt_request = false;
});
printf("Starting %02x\n", command_);
@@ -344,6 +339,17 @@ void WD1770::posit_event(Event new_event_type)
/*
Type 1 entry point.
*/
// +--------+----------+-------------------------+
// ! ! ! BITS !
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
// +--------+----------+-------------------------+
// ! 1 ! Restore ! 0 0 0 0 h v r1 r0 !
// ! 1 ! Seek ! 0 0 0 1 h v r1 r0 !
// ! 1 ! Step ! 0 0 1 u h v r1 r0 !
// ! 1 ! Step-in ! 0 1 0 u h v r1 r0 !
// ! 1 ! Step-out ! 0 1 1 u h v r1 r0 !
// +--------+----------+-------------------------+
begin_type_1:
// Set initial flags, skip spin-up if possible.
update_status([] (Status &status) {
@@ -359,8 +365,7 @@ void WD1770::posit_event(Event new_event_type)
goto begin_type1_load_head;
begin_type1_load_head:
if(!(command_&0x08))
{
if(!(command_&0x08)) {
set_head_load_request(false);
goto test_type1_type;
}
@@ -380,8 +385,7 @@ void WD1770::posit_event(Event new_event_type)
if((command_ >> 5) != 0) goto perform_step_command;
// This is now definitely either a seek or a restore; if it's a restore then set track to 0xff and data to 0x00.
if(!(command_ & 0x10))
{
if(!(command_ & 0x10)) {
track_ = 0xff;
data_ = 0;
}
@@ -394,15 +398,13 @@ void WD1770::posit_event(Event new_event_type)
if(step_direction_) track_++; else track_--;
perform_step:
if(!step_direction_ && get_is_track_zero())
{
if(!step_direction_ && get_is_track_zero()) {
track_ = 0;
goto verify;
}
step(step_direction_ ? 1 : -1);
int time_to_wait;
switch(command_ & 3)
{
switch(command_ & 3) {
default:
case 0: time_to_wait = 6; break;
case 1: time_to_wait = 12; break;
@@ -418,8 +420,7 @@ void WD1770::posit_event(Event new_event_type)
goto perform_step;
verify:
if(!(command_ & 0x04))
{
if(!(command_ & 0x04)) {
goto wait_for_command;
}
@@ -430,19 +431,22 @@ void WD1770::posit_event(Event new_event_type)
WAIT_FOR_EVENT(Event::IndexHole | Event::Token);
READ_ID();
if(index_hole_count_ == 6)
{
if(index_hole_count_ == 6) {
update_status([] (Status &status) {
status.seek_error = true;
});
goto wait_for_command;
}
if(distance_into_section_ == 7)
{
is_reading_data_ = false;
// TODO: CRC check
if(header_[0] == track_)
{
if(distance_into_section_ == 7) {
data_mode_ = DataMode::Scanning;
if(crc_generator_.get_value()) {
update_status([] (Status &status) {
status.crc_error = true;
});
goto verify_read_data;
}
if(header_[0] == track_) {
printf("Reached track %d\n", track_);
update_status([] (Status &status) {
status.crc_error = false;
@@ -458,6 +462,14 @@ void WD1770::posit_event(Event new_event_type)
/*
Type 2 entry point.
*/
// +--------+----------+-------------------------+
// ! ! ! BITS !
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
// +--------+----------+-------------------------+
// ! 2 ! Rd sectr ! 1 0 0 m h E 0 0 !
// ! 2 ! Wt sectr ! 1 0 1 m h E P a0 !
// +--------+----------+-------------------------+
begin_type_2:
update_status([] (Status &status) {
status.type = Status::Two;
@@ -492,8 +504,7 @@ void WD1770::posit_event(Event new_event_type)
WAIT_FOR_TIME(30);
test_type2_write_protection:
if(command_&0x20) // TODO:: && is_write_protected
{
if(command_&0x20 && get_drive_is_read_only()) {
update_status([] (Status &status) {
status.write_protect = true;
});
@@ -504,21 +515,30 @@ void WD1770::posit_event(Event new_event_type)
WAIT_FOR_EVENT(Event::IndexHole | Event::Token);
READ_ID();
if(index_hole_count_ == 5)
{
if(index_hole_count_ == 5) {
printf("Failed to find sector %d\n", sector_);
update_status([] (Status &status) {
status.record_not_found = true;
});
goto wait_for_command;
}
if(distance_into_section_ == 7)
{
is_reading_data_ = false;
if(header_[0] == track_ && header_[2] == sector_ &&
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1]))
{
// TODO: test CRC
if(distance_into_section_ == 7) {
printf("Considering %d/%d\n", header_[0], header_[2]);
data_mode_ = DataMode::Scanning;
if( header_[0] == track_ && header_[2] == sector_ &&
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
printf("Found %d/%d\n", header_[0], header_[2]);
if(crc_generator_.get_value()) {
printf("CRC error; back to searching\n");
update_status([] (Status &status) {
status.crc_error = true;
});
goto type2_get_header;
}
update_status([] (Status &status) {
status.crc_error = false;
});
goto type2_read_or_write_data;
}
distance_into_section_ = 0;
@@ -533,13 +553,12 @@ void WD1770::posit_event(Event new_event_type)
type2_read_data:
WAIT_FOR_EVENT(Event::Token);
// TODO: timeout
if(latest_token_.type == Token::Data || latest_token_.type == Token::DeletedData)
{
if(latest_token_.type == Token::Data || latest_token_.type == Token::DeletedData) {
update_status([this] (Status &status) {
status.record_type = (latest_token_.type == Token::DeletedData);
});
distance_into_section_ = 0;
is_reading_data_ = true;
data_mode_ = DataMode::Reading;
goto type2_read_byte;
}
goto type2_read_data;
@@ -553,8 +572,7 @@ void WD1770::posit_event(Event new_event_type)
status.data_request = true;
});
distance_into_section_++;
if(distance_into_section_ == 128 << header_[3])
{
if(distance_into_section_ == 128 << header_[3]) {
distance_into_section_ = 0;
goto type2_check_crc;
}
@@ -565,11 +583,16 @@ void WD1770::posit_event(Event new_event_type)
if(latest_token_.type != Token::Byte) goto type2_read_byte;
header_[distance_into_section_] = latest_token_.byte_value;
distance_into_section_++;
if(distance_into_section_ == 2)
{
// TODO: check CRC
if(command_ & 0x10)
{
if(distance_into_section_ == 2) {
if(crc_generator_.get_value()) {
printf("CRC error; terminating\n");
update_status([this] (Status &status) {
status.crc_error = true;
});
goto wait_for_command;
}
if(command_ & 0x10) {
sector_++;
goto test_type2_write_protection;
}
@@ -580,22 +603,298 @@ void WD1770::posit_event(Event new_event_type)
type2_write_data:
printf("!!!TODO: data portion of sector!!!\n");
WAIT_FOR_BYTES(2);
update_status([] (Status &status) {
status.data_request = true;
});
WAIT_FOR_BYTES(9);
if(status_.data_request) {
update_status([] (Status &status) {
status.lost_data = true;
});
goto wait_for_command;
}
WAIT_FOR_BYTES(1);
if(is_double_density_) {
WAIT_FOR_BYTES(11);
}
data_mode_ = DataMode::Writing;
begin_writing();
for(int c = 0; c < (is_double_density_ ? 12 : 6); c++) {
write_byte(0);
}
WAIT_FOR_EVENT(Event::DataWritten);
if(is_double_density_) {
crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue);
for(int c = 0; c < 3; c++) write_raw_short(Storage::Encodings::MFM::MFMSync);
write_byte((command_&0x01) ? Storage::Encodings::MFM::DeletedDataAddressByte : Storage::Encodings::MFM::DataAddressByte);
} else {
crc_generator_.reset();
crc_generator_.add((command_&0x01) ? Storage::Encodings::MFM::DeletedDataAddressByte : Storage::Encodings::MFM::DataAddressByte);
write_raw_short((command_&0x01) ? Storage::Encodings::MFM::FMDeletedDataAddressMark : Storage::Encodings::MFM::FMDataAddressMark);
}
WAIT_FOR_EVENT(Event::DataWritten);
distance_into_section_ = 0;
type2_write_loop:
/*
This deviates from the data sheet slightly since that would prima facie request one more byte
of data than is actually written — the last time around the loop it has transferred from the
data register to the data shift register, set data request, written the byte, checked that data
request has been satified, then finally considers whether all bytes are done. Based on both
natural expectations and the way that emulated machines responded, I believe that to be a
documentation error.
*/
write_byte(data_);
distance_into_section_++;
if(distance_into_section_ == 128 << header_[3]) {
goto type2_write_crc;
}
update_status([] (Status &status) {
status.data_request = true;
});
WAIT_FOR_EVENT(Event::DataWritten);
if(status_.data_request) {
end_writing();
update_status([] (Status &status) {
status.lost_data = true;
});
goto wait_for_command;
}
goto type2_write_loop;
type2_write_crc: {
uint16_t crc = crc_generator_.get_value();
write_byte(crc >> 8);
write_byte(crc & 0xff);
}
write_byte(0xff);
WAIT_FOR_EVENT(Event::DataWritten);
end_writing();
if(command_ & 0x10) {
sector_++;
goto test_type2_write_protection;
}
printf("Wrote sector %d\n", sector_);
goto wait_for_command;
/*
Type 3 entry point.
*/
// +--------+----------+-------------------------+
// ! ! ! BITS !
// ! TYPE ! COMMAND ! 7 6 5 4 3 2 1 0 !
// +--------+----------+-------------------------+
// ! 3 ! Rd addr ! 1 1 0 0 h E 0 0 !
// ! 3 ! Rd track ! 1 1 1 0 h E 0 0 !
// ! 3 ! Wt track ! 1 1 1 1 h E P 0 !
// +--------+----------+-------------------------+
begin_type_3:
update_status([] (Status &status) {
status.type = Status::Three;
status.crc_error = false;
status.lost_data = false;
status.record_not_found = false;
});
printf("!!!TODO: type 3 commands!!!\n");
if(!has_motor_on_line() && !has_head_load_line()) goto type3_test_delay;
if(has_motor_on_line()) goto begin_type3_spin_up;
goto begin_type3_load_head;
begin_type3_load_head:
set_head_load_request(true);
if(head_is_loaded_) goto type3_test_delay;
WAIT_FOR_EVENT(Event::HeadLoad);
goto type3_test_delay;
begin_type3_spin_up:
if((command_&0x08) || get_motor_on()) goto type3_test_delay;
SPIN_UP();
type3_test_delay:
if(!(command_&0x04)) goto test_type3_type;
WAIT_FOR_TIME(30);
test_type3_type:
if(!(command_&0x20)) goto begin_read_address;
if(!(command_&0x10)) goto begin_read_track;
goto begin_write_track;
begin_read_address:
index_hole_count_ = 0;
distance_into_section_ = 0;
read_address_get_header:
WAIT_FOR_EVENT(Event::IndexHole | Event::Token);
if(new_event_type == Event::Token) {
if(!distance_into_section_ && latest_token_.type == Token::ID) {data_mode_ = DataMode::Reading; distance_into_section_++; }
else if(distance_into_section_ && distance_into_section_ < 7 && latest_token_.type == Token::Byte) {
if(status_.data_request) {
update_status([] (Status &status) {
status.lost_data = true;
});
goto wait_for_command;
}
header_[distance_into_section_ - 1] = data_ = latest_token_.byte_value;
track_ = header_[0];
update_status([] (Status &status) {
status.data_request = true;
});
distance_into_section_++;
if(distance_into_section_ == 7) {
if(crc_generator_.get_value()) {
update_status([] (Status &status) {
status.crc_error = true;
});
}
goto wait_for_command;
}
}
}
if(index_hole_count_ == 6) {
update_status([] (Status &status) {
status.record_not_found = true;
});
goto wait_for_command;
}
goto read_address_get_header;
begin_read_track:
WAIT_FOR_EVENT(Event::IndexHole);
index_hole_count_ = 0;
read_track_read_byte:
WAIT_FOR_EVENT(Event::Token | Event::IndexHole);
if(index_hole_count_) {
goto wait_for_command;
}
if(status_.data_request) {
update_status([] (Status &status) {
status.lost_data = true;
});
goto wait_for_command;
}
data_ = latest_token_.byte_value;
update_status([] (Status &status) {
status.data_request = true;
});
goto read_track_read_byte;
begin_write_track:
update_status([] (Status &status) {
status.data_request = false;
status.lost_data = false;
});
write_track_test_write_protect:
if(get_drive_is_read_only()) {
update_status([] (Status &status) {
status.write_protect = true;
});
goto wait_for_command;
}
update_status([] (Status &status) {
status.data_request = true;
});
WAIT_FOR_BYTES(3);
if(status_.data_request) {
update_status([] (Status &status) {
status.lost_data = true;
});
goto wait_for_command;
}
WAIT_FOR_EVENT(Event::IndexHoleTarget);
begin_writing();
index_hole_count_ = 0;
write_track_write_loop:
if(is_double_density_) {
switch(data_) {
case 0xf5:
write_raw_short(Storage::Encodings::MFM::MFMSync);
crc_generator_.set_value(Storage::Encodings::MFM::MFMPostSyncCRCValue);
break;
case 0xf6:
write_raw_short(Storage::Encodings::MFM::MFMIndexSync);
break;
case 0xff: {
uint16_t crc = crc_generator_.get_value();
write_byte(crc >> 8);
write_byte(crc & 0xff);
} break;
default:
write_byte(data_);
break;
}
} else {
switch(data_) {
case 0xf8: case 0xf9: case 0xfa: case 0xfb:
case 0xfd: case 0xfe:
// clock is 0xc7 = 1010 0000 0010 1010 = 0xa022
write_raw_short(
(uint16_t)(
0xa022 |
((data_ & 0x80) << 7) |
((data_ & 0x40) << 6) |
((data_ & 0x20) << 5) |
((data_ & 0x10) << 4) |
((data_ & 0x08) << 3) |
((data_ & 0x04) << 2) |
((data_ & 0x02) << 1) |
(data_ & 0x01)
)
);
crc_generator_.reset();
crc_generator_.add(data_);
break;
case 0xfc:
write_raw_short(Storage::Encodings::MFM::FMIndexAddressMark);
break;
case 0xf7: {
uint16_t crc = crc_generator_.get_value();
write_byte(crc >> 8);
write_byte(crc & 0xff);
} break;
default:
write_byte(data_);
break;
}
}
update_status([] (Status &status) {
status.data_request = true;
});
WAIT_FOR_EVENT(Event::DataWritten);
if(status_.data_request) {
update_status([] (Status &status) {
status.lost_data = true;
});
end_writing();
goto wait_for_command;
}
if(index_hole_count_) {
end_writing();
goto wait_for_command;
}
goto write_track_write_loop;
END_SECTION()
}
void WD1770::update_status(std::function<void(Status &)> updater)
{
if(delegate_)
{
void WD1770::update_status(std::function<void(Status &)> updater) {
if(delegate_) {
Status old_status = status_;
updater(status_);
bool did_change =
@@ -608,8 +907,29 @@ void WD1770::update_status(std::function<void(Status &)> updater)
void WD1770::set_head_load_request(bool head_load) {}
void WD1770::set_head_loaded(bool head_loaded)
{
void WD1770::set_head_loaded(bool head_loaded) {
head_is_loaded_ = head_loaded;
if(head_loaded) posit_event(Event::HeadLoad);
}
void WD1770::write_bit(int bit) {
if(is_double_density_) {
Controller::write_bit(!bit && !last_bit_);
Controller::write_bit(!!bit);
last_bit_ = bit;
} else {
Controller::write_bit(true);
Controller::write_bit(!!bit);
}
}
void WD1770::write_byte(uint8_t byte) {
for(int c = 0; c < 8; c++) write_bit((byte << c)&0x80);
crc_generator_.add(byte);
}
void WD1770::write_raw_short(uint16_t value) {
for(int c = 0; c < 16; c++) {
Controller::write_bit(!!((value << c)&0x8000));
}
}

View File

@@ -10,6 +10,7 @@
#define _770_hpp
#include "../../Storage/Disk/DiskController.hpp"
#include "../../NumberTheory/CRC.hpp"
namespace WD {
@@ -46,8 +47,8 @@ class WD1770: public Storage::Disk::Controller {
Busy = 0x01
};
inline bool get_interrupt_request_line() { return !status_.busy; }
inline bool get_data_request_line() { return status_.data_request; }
inline bool get_interrupt_request_line() { return status_.interrupt_request; }
inline bool get_data_request_line() { return status_.data_request; }
class Delegate {
public:
virtual void wd1770_did_change_output(WD1770 *wd1770) = 0;
@@ -73,6 +74,7 @@ class WD1770: public Storage::Disk::Controller {
bool seek_error;
bool lost_data;
bool data_request;
bool interrupt_request;
bool busy;
enum {
One, Two, Three
@@ -93,12 +95,16 @@ class WD1770: public Storage::Disk::Controller {
void update_status(std::function<void(Status &)> updater);
// Tokeniser
bool is_reading_data_;
enum DataMode {
Scanning,
Reading,
Writing
} data_mode_;
bool is_double_density_;
int shift_register_;
struct Token {
enum Type {
Index, ID, Data, DeletedData, Byte
Index, ID, Data, DeletedData, Sync, Byte
} type;
uint8_t byte_value;
} latest_token_;
@@ -109,18 +115,28 @@ class WD1770: public Storage::Disk::Controller {
Token = (1 << 1), // Indicates recognition of a new token in the flux stream. Interrogate latest_token_ for details.
IndexHole = (1 << 2), // Indicates the passing of a physical index hole.
HeadLoad = (1 << 3), // Indicates the head has been loaded (1973 only).
DataWritten = (1 << 4), // Indicates that all queued bits have been written
Timer = (1 << 4), // Indicates that the delay_time_-powered timer has timed out.
IndexHoleTarget = (1 << 5) // Indicates that index_hole_count_ has reached index_hole_count_target_.
Timer = (1 << 5), // Indicates that the delay_time_-powered timer has timed out.
IndexHoleTarget = (1 << 6) // Indicates that index_hole_count_ has reached index_hole_count_target_.
};
void posit_event(Event type);
int interesting_event_mask_;
int resume_point_;
int delay_time_;
// Output
int last_bit_;
void write_bit(int bit);
void write_byte(uint8_t byte);
void write_raw_short(uint16_t value);
// ID buffer
uint8_t header_[6];
// CRC generator
NumberTheory::CRC16 crc_generator_;
// 1793 head-loading logic
bool head_is_loaded_;
@@ -130,6 +146,7 @@ class WD1770: public Storage::Disk::Controller {
// Storage::Disk::Controller
virtual void process_input_bit(int value, unsigned int cycles_since_index_hole);
virtual void process_index_hole();
virtual void process_write_completed();
};
}

View File

@@ -50,12 +50,10 @@ template <class T> class MOS6522 {
};
/*! Sets a register value. */
inline void set_register(int address, uint8_t value)
{
inline void set_register(int address, uint8_t value) {
address &= 0xf;
// printf("6522 [%s]: %0x <- %02x\n", typeid(*this).name(), address, value);
switch(address)
{
switch(address) {
case 0x0:
registers_.output[1] = value;
static_cast<T *>(this)->set_port_output(Port::B, value, registers_.data_direction[1]); // TODO: handshake
@@ -88,8 +86,7 @@ template <class T> class MOS6522 {
case 0x5: case 0x7:
registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | (uint16_t)(value << 8);
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
if(address == 0x05)
{
if(address == 0x05) {
registers_.next_timer[0] = registers_.timer_latch[0];
timer_is_running_[0] = true;
}
@@ -117,19 +114,15 @@ template <class T> class MOS6522 {
registers_.peripheral_control = value;
// TODO: simplify below; trying to avoid improper logging of unimplemented warnings in input mode
if(value & 0x08)
{
switch(value & 0x0e)
{
if(value & 0x08) {
switch(value & 0x0e) {
default: printf("Unimplemented control line mode %d\n", (value >> 1)&7); break;
case 0x0c: static_cast<T *>(this)->set_control_line_output(Port::A, Line::Two, false); break;
case 0x0e: static_cast<T *>(this)->set_control_line_output(Port::A, Line::Two, true); break;
}
}
if(value & 0x80)
{
switch(value & 0xe0)
{
if(value & 0x80) {
switch(value & 0xe0) {
default: printf("Unimplemented control line mode %d\n", (value >> 5)&7); break;
case 0xc0: static_cast<T *>(this)->set_control_line_output(Port::B, Line::Two, false); break;
case 0xe0: static_cast<T *>(this)->set_control_line_output(Port::B, Line::Two, true); break;
@@ -153,12 +146,10 @@ template <class T> class MOS6522 {
}
/*! Gets a register value. */
inline uint8_t get_register(int address)
{
inline uint8_t get_register(int address) {
address &= 0xf;
// printf("6522 %p: %d\n", this, address);
switch(address)
{
switch(address) {
case 0x0:
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
reevaluate_interrupts();
@@ -200,15 +191,12 @@ template <class T> class MOS6522 {
return 0xff;
}
inline void set_control_line_input(Port port, Line line, bool value)
{
switch(line)
{
inline void set_control_line_input(Port port, Line line, bool value) {
switch(line) {
case Line::One:
if( value != control_inputs_[port].line_one &&
value == !!(registers_.peripheral_control & (port ? 0x10 : 0x01))
)
{
) {
registers_.interrupt_flags |= port ? InterruptFlag::CB1ActiveEdge : InterruptFlag::CA1ActiveEdge;
reevaluate_interrupts();
}
@@ -220,8 +208,7 @@ template <class T> class MOS6522 {
if( value != control_inputs_[port].line_two && // i.e. value has changed ...
!(registers_.peripheral_control & (port ? 0x80 : 0x08)) && // ... and line is input ...
value == !!(registers_.peripheral_control & (port ? 0x40 : 0x04)) // ... and it's either high or low, as required
)
{
) {
registers_.interrupt_flags |= port ? InterruptFlag::CB2ActiveEdge : InterruptFlag::CA2ActiveEdge;
reevaluate_interrupts();
}
@@ -234,8 +221,7 @@ template <class T> class MOS6522 {
registers_.last_timer[0] = registers_.timer[0];\
registers_.last_timer[1] = registers_.timer[1];\
\
if(registers_.timer_needs_reload)\
{\
if(registers_.timer_needs_reload) {\
registers_.timer_needs_reload = false;\
registers_.timer[0] = registers_.timer_latch[0];\
}\
@@ -248,15 +234,13 @@ template <class T> class MOS6522 {
// IRQ is raised on the half cycle after overflow
#define phase1() \
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1])\
{\
if((registers_.timer[1] == 0xffff) && !registers_.last_timer[1] && timer_is_running_[1]) {\
timer_is_running_[1] = false;\
registers_.interrupt_flags |= InterruptFlag::Timer2;\
reevaluate_interrupts();\
}\
\
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0])\
{\
if((registers_.timer[0] == 0xffff) && !registers_.last_timer[0] && timer_is_running_[0]) {\
registers_.interrupt_flags |= InterruptFlag::Timer1;\
reevaluate_interrupts();\
\
@@ -279,28 +263,22 @@ template <class T> class MOS6522 {
Callers should decide whether they are going to use @c run_for_half_cycles or @c run_for_cycles, and not
intermingle usage.
*/
inline void run_for_half_cycles(unsigned int number_of_cycles)
{
if(is_phase2_)
{
inline void run_for_half_cycles(unsigned int number_of_cycles) {
if(is_phase2_) {
phase2();
number_of_cycles--;
}
while(number_of_cycles >= 2)
{
while(number_of_cycles >= 2) {
phase1();
phase2();
number_of_cycles -= 2;
}
if(number_of_cycles)
{
if(number_of_cycles) {
phase1();
is_phase2_ = true;
}
else
{
} else {
is_phase2_ = false;
}
}
@@ -311,10 +289,8 @@ template <class T> class MOS6522 {
Callers should decide whether they are going to use @c run_for_half_cycles or @c run_for_cycles, and not
intermingle usage.
*/
inline void run_for_cycles(unsigned int number_of_cycles)
{
while(number_of_cycles--)
{
inline void run_for_cycles(unsigned int number_of_cycles) {
while(number_of_cycles--) {
phase1();
phase2();
}
@@ -324,8 +300,7 @@ template <class T> class MOS6522 {
#undef phase2
/*! @returns @c true if the IRQ line is currently active; @c false otherwise. */
inline bool get_interrupt_line()
{
inline bool get_interrupt_line() {
uint8_t interrupt_status = registers_.interrupt_flags & registers_.interrupt_enable & 0x7f;
return !!interrupt_status;
}
@@ -333,8 +308,7 @@ template <class T> class MOS6522 {
MOS6522() :
timer_is_running_{false, false},
last_posted_interrupt_status_(false),
is_phase2_(false)
{}
is_phase2_(false) {}
private:
// Expected to be overridden
@@ -344,8 +318,7 @@ template <class T> class MOS6522 {
void set_interrupt_status(bool status) {}
// Input/output multiplexer
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output)
{
uint8_t get_port_input(Port port, uint8_t output_mask, uint8_t output) {
uint8_t input = static_cast<T *>(this)->get_port_input(port);
return (input & ~output_mask) | (output & output_mask);
}
@@ -355,11 +328,9 @@ template <class T> class MOS6522 {
// Delegate and communications
bool last_posted_interrupt_status_;
inline void reevaluate_interrupts()
{
inline void reevaluate_interrupts() {
bool new_interrupt_status = get_interrupt_line();
if(new_interrupt_status != last_posted_interrupt_status_)
{
if(new_interrupt_status != last_posted_interrupt_status_) {
last_posted_interrupt_status_ = new_interrupt_status;
static_cast<T *>(this)->set_interrupt_status(new_interrupt_status);
}
@@ -404,13 +375,11 @@ class MOS6522IRQDelegate {
virtual void mos6522_did_change_interrupt_status(void *mos6522) = 0;
};
inline void set_interrupt_delegate(Delegate *delegate)
{
inline void set_interrupt_delegate(Delegate *delegate) {
delegate_ = delegate;
}
inline void set_interrupt_status(bool new_status)
{
inline void set_interrupt_status(bool new_status) {
if(delegate_) delegate_->mos6522_did_change_interrupt_status(this);
}

View File

@@ -30,8 +30,7 @@ template <class T> class MOS6532 {
inline void set_ram(uint16_t address, uint8_t value) { ram_[address&0x7f] = value; }
inline uint8_t get_ram(uint16_t address) { return ram_[address & 0x7f]; }
inline void set_register(int address, uint8_t value)
{
inline void set_register(int address, uint8_t value) {
const uint8_t decodedAddress = address & 0x07;
switch(decodedAddress) {
// Port output
@@ -48,16 +47,13 @@ template <class T> class MOS6532 {
// The timer and edge detect control
case 0x04: case 0x05: case 0x06: case 0x07:
if(address & 0x10)
{
if(address & 0x10) {
timer_.writtenShift = timer_.activeShift = (decodedAddress - 0x04) * 3 + (decodedAddress / 0x07); // i.e. 0, 3, 6, 10
timer_.value = ((unsigned int)(value) << timer_.activeShift) | ((1 << timer_.activeShift)-1);
timer_.value = ((unsigned int)value << timer_.activeShift) ;
timer_.interrupt_enabled = !!(address&0x08);
interrupt_status_ &= ~InterruptFlag::Timer;
evaluate_interrupts();
}
else
{
} else {
a7_interrupt_.enabled = !!(address&0x2);
a7_interrupt_.active_on_positive = !!(address & 0x01);
}
@@ -65,13 +61,11 @@ template <class T> class MOS6532 {
}
}
inline uint8_t get_register(int address)
{
inline uint8_t get_register(int address) {
const uint8_t decodedAddress = address & 0x7;
switch(decodedAddress) {
// Port input
case 0x00: case 0x02:
{
case 0x00: case 0x02: {
const int port = decodedAddress / 2;
uint8_t input = static_cast<T *>(this)->get_port_input(port);
return (input & ~port_[port].output_mask) | (port_[port].output & port_[port].output_mask);
@@ -82,8 +76,7 @@ template <class T> class MOS6532 {
break;
// Timer and interrupt control
case 0x04: case 0x06:
{
case 0x04: case 0x06: {
uint8_t value = (uint8_t)(timer_.value >> timer_.activeShift);
timer_.interrupt_enabled = !!(address&0x08);
interrupt_status_ &= ~InterruptFlag::Timer;
@@ -99,8 +92,7 @@ template <class T> class MOS6532 {
}
break;
case 0x05: case 0x07:
{
case 0x05: case 0x07: {
uint8_t value = interrupt_status_;
interrupt_status_ &= ~InterruptFlag::PA7;
evaluate_interrupts();
@@ -112,14 +104,13 @@ template <class T> class MOS6532 {
return 0xff;
}
inline void run_for_cycles(unsigned int number_of_cycles)
{
inline void run_for_cycles(unsigned int number_of_cycles) {
// permit counting _to_ zero; counting _through_ zero initiates the other behaviour
if(timer_.value >= number_of_cycles) {
timer_.value -= number_of_cycles;
} else {
number_of_cycles -= timer_.value;
timer_.value = 0x100 - number_of_cycles;
timer_.value = (0x100 - number_of_cycles) & 0xff;
timer_.activeShift = 0;
interrupt_status_ |= InterruptFlag::Timer;
evaluate_interrupts();
@@ -130,23 +121,19 @@ template <class T> class MOS6532 {
interrupt_status_(0),
port_{{.output_mask = 0, .output = 0}, {.output_mask = 0, .output = 0}},
a7_interrupt_({.last_port_value = 0, .enabled = false}),
interrupt_line_(false)
{}
interrupt_line_(false),
timer_{.value = (unsigned int)((rand() & 0xff) << 10), .activeShift = 10, .writtenShift = 10, .interrupt_enabled = false} {}
inline void set_port_did_change(int port)
{
if(!port)
{
inline void set_port_did_change(int port) {
if(!port) {
uint8_t new_port_a_value = (get_port_input(0) & ~port_[0].output_mask) | (port_[0].output & port_[0].output_mask);
uint8_t difference = new_port_a_value ^ a7_interrupt_.last_port_value;
a7_interrupt_.last_port_value = new_port_a_value;
if(difference&0x80)
{
if(difference&0x80) {
if(
((new_port_a_value&0x80) && a7_interrupt_.active_on_positive) ||
(!(new_port_a_value&0x80) && !a7_interrupt_.active_on_positive)
)
{
) {
interrupt_status_ |= InterruptFlag::PA7;
evaluate_interrupts();
}
@@ -154,8 +141,7 @@ template <class T> class MOS6532 {
}
}
inline bool get_inerrupt_line()
{
inline bool get_inerrupt_line() {
return interrupt_line_;
}
@@ -190,8 +176,7 @@ template <class T> class MOS6532 {
void set_port_output(int port, uint8_t value, uint8_t output_mask) {}
void set_irq_line(bool new_value) {}
inline void evaluate_interrupts()
{
inline void evaluate_interrupts() {
interrupt_line_ =
((interrupt_status_&InterruptFlag::Timer) && timer_.interrupt_enabled) ||
((interrupt_status_&InterruptFlag::PA7) && a7_interrupt_.enabled);

View File

@@ -14,18 +14,15 @@ Speaker::Speaker() :
volume_(0),
control_registers_{0, 0, 0, 0},
shift_registers_{0, 0, 0, 0},
counters_{2, 1, 0, 0} // create a slight phase offset for the three channels
{}
counters_{2, 1, 0, 0} {} // create a slight phase offset for the three channels
void Speaker::set_volume(uint8_t volume)
{
void Speaker::set_volume(uint8_t volume) {
enqueue([=]() {
volume_ = volume;
});
}
void Speaker::set_control(int channel, uint8_t value)
{
void Speaker::set_control(int channel, uint8_t value) {
enqueue([=]() {
control_registers_[channel] = value;
});
@@ -108,10 +105,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 Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
{
for(unsigned int c = 0; c < number_of_samples; c++)
{
void Speaker::get_samples(unsigned int number_of_samples, int16_t *target) {
for(unsigned int c = 0; c < number_of_samples; c++) {
update(0, 2, shift);
update(1, 1, shift);
update(2, 0, shift);
@@ -128,10 +123,8 @@ void Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
}
}
void Speaker::skip_samples(unsigned int number_of_samples)
{
for(unsigned int c = 0; c < number_of_samples; c++)
{
void Speaker::skip_samples(unsigned int 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);

View File

@@ -43,32 +43,28 @@ class Speaker: public ::Outputs::Filter<Speaker> {
template <class T> class MOS6560 {
public:
MOS6560() :
crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::NTSC60, 1)),
speaker_(new Speaker),
horizontal_counter_(0),
vertical_counter_(0),
cycles_since_speaker_update_(0),
is_odd_frame_(false),
is_odd_line_(false)
{
crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::NTSC60, 2)),
speaker_(new Speaker),
horizontal_counter_(0),
vertical_counter_(0),
cycles_since_speaker_update_(0),
is_odd_frame_(false),
is_odd_line_(false) {
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"float y = float(c >> 4) / 4.0;"
"uint yC = c & 15u;"
"float phaseOffset = 6.283185308 * float(yC) / 16.0;"
"vec2 yc = texture(texID, coordinate).rg / vec2(255.0);"
"float phaseOffset = 6.283185308 * 2.0 * yc.y;"
"float chroma = cos(phase + phaseOffset);"
"return mix(y, step(yC, 14) * chroma, amplitude);"
"return mix(yc.x, step(yc.y, 0.75) * chroma, amplitude);"
"}");
// default to NTSC
set_output_mode(OutputMode::NTSC);
}
void set_clock_rate(double clock_rate)
{
void set_clock_rate(double clock_rate) {
speaker_->set_input_rate((float)(clock_rate / 4.0));
}
@@ -82,26 +78,36 @@ template <class T> class MOS6560 {
/*!
Sets the output mode to either PAL or NTSC.
*/
void set_output_mode(OutputMode output_mode)
{
void set_output_mode(OutputMode output_mode) {
output_mode_ = output_mode;
uint8_t luminances[16] = { // range is 04
0, 4, 1, 3, 2, 2, 1, 3,
2, 1, 2, 1, 2, 3, 2, 3
// Lumunances are encoded trivially: on a 0255 scale.
const uint8_t luminances[16] = {
0, 255, 109, 189,
199, 144, 159, 161,
126, 227, 227, 207,
235, 173, 188, 196
};
uint8_t pal_chrominances[16] = { // range is 015; 15 is a special case meaning "no chrominance"
15, 15, 5, 13, 2, 10, 0, 8,
6, 7, 5, 13, 2, 10, 0, 8,
// Chrominances are encoded such that 0128 is a complete revolution of phase;
// anything above 191 disables the colour subcarrier. Phase is relative to the
// colour burst, so 0 is green.
const uint8_t pal_chrominances[16] = {
255, 255, 40, 112,
8, 88, 120, 56,
40, 48, 40, 112,
8, 88, 120, 56,
};
uint8_t ntsc_chrominances[16] = {
15, 15, 2, 10, 4, 12, 6, 14,
0, 8, 2, 10, 4, 12, 6, 14,
const uint8_t ntsc_chrominances[16] = {
255, 255, 40, 104,
64, 120, 80, 16,
32, 32, 40, 104,
64, 120, 80, 16,
};
uint8_t *chrominances;
const uint8_t *chrominances;
Outputs::CRT::DisplayType display_type;
switch(output_mode)
{
switch(output_mode) {
case OutputMode::PAL:
chrominances = pal_chrominances;
display_type = Outputs::CRT::PAL50;
@@ -122,10 +128,9 @@ template <class T> class MOS6560 {
}
crt_->set_new_display_type((unsigned int)(timing_.cycles_per_line*4), display_type);
// crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.1f, 0.8f, 0.8f));
crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f));
// switch(output_mode)
// {
// switch(output_mode) {
// case OutputMode::PAL:
// crt_->set_visible_area(crt_->get_rect_for_area(16, 237, 15*4, 55*4, 4.0f / 3.0f));
// break;
@@ -134,32 +139,29 @@ template <class T> class MOS6560 {
// break;
// }
for(int c = 0; c < 16; c++)
{
colours_[c] = (uint8_t)((luminances[c] << 4) | chrominances[c]);
for(int c = 0; c < 16; c++) {
uint8_t *colour = (uint8_t *)&colours_[c];
colour[0] = luminances[c];
colour[1] = chrominances[c];
}
}
/*!
Runs for cycles. Derr.
*/
inline void run_for_cycles(unsigned int number_of_cycles)
{
inline void run_for_cycles(unsigned int number_of_cycles) {
// keep track of the amount of time since the speaker was updated; lazy updates are applied
cycles_since_speaker_update_ += number_of_cycles;
while(number_of_cycles--)
{
while(number_of_cycles--) {
// keep an old copy of the vertical count because that test is a cycle later than the actual changes
int previous_vertical_counter = vertical_counter_;
// keep track of internal time relative to this scanline
horizontal_counter_++;
full_frame_counter_++;
if(horizontal_counter_ == timing_.cycles_per_line)
{
if(horizontal_drawing_latch_)
{
if(horizontal_counter_ == timing_.cycles_per_line) {
if(horizontal_drawing_latch_) {
current_character_row_++;
if(
(current_character_row_ == 16) ||
@@ -179,8 +181,7 @@ template <class T> class MOS6560 {
horizontal_drawing_latch_ = false;
vertical_counter_ ++;
if(vertical_counter_ == (registers_.interlaced ? (is_odd_frame_ ? 262 : 263) : timing_.lines_per_progressive_field))
{
if(vertical_counter_ == (registers_.interlaced ? (is_odd_frame_ ? 262 : 263) : timing_.lines_per_progressive_field)) {
vertical_counter_ = 0;
full_frame_counter_ = 0;
@@ -198,11 +199,9 @@ template <class T> class MOS6560 {
horizontal_drawing_latch_ |= vertical_drawing_latch_ && (horizontal_counter_ == registers_.first_column_location);
if(pixel_line_cycle_ >= 0) pixel_line_cycle_++;
switch(pixel_line_cycle_)
{
switch(pixel_line_cycle_) {
case -1:
if(horizontal_drawing_latch_)
{
if(horizontal_drawing_latch_) {
pixel_line_cycle_ = 0;
video_matrix_address_counter_ = base_video_matrix_address_counter_;
}
@@ -213,14 +212,10 @@ template <class T> class MOS6560 {
}
uint16_t fetch_address = 0x1c;
if(column_counter_ >= 0 && column_counter_ < columns_this_line_*2)
{
if(column_counter_&1)
{
if(column_counter_ >= 0 && column_counter_ < columns_this_line_*2) {
if(column_counter_&1) {
fetch_address = registers_.character_cell_start_address + (character_code_*(registers_.tall_characters ? 16 : 8)) + current_character_row_;
}
else
{
} else {
fetch_address = (uint16_t)(registers_.video_matrix_start_address + video_matrix_address_counter_);
video_matrix_address_counter_++;
if(
@@ -244,8 +239,7 @@ template <class T> class MOS6560 {
// 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;
else
{
else {
this_state_ = (column_counter_ >= 0 && column_counter_ < columns_this_line_*2) ? State::Pixels : State::Border;
}
@@ -262,10 +256,8 @@ template <class T> class MOS6560 {
this_state_ = State::Sync;
// update the CRT
if(this_state_ != output_state_)
{
switch(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, 0); break;
case State::Border: output_border(cycles_in_state_ * 4); break;
@@ -275,32 +267,24 @@ template <class T> class MOS6560 {
cycles_in_state_ = 0;
pixel_pointer = nullptr;
if(output_state_ == State::Pixels)
{
pixel_pointer = crt_->allocate_write_area(260);
if(output_state_ == State::Pixels) {
pixel_pointer = (uint16_t *)crt_->allocate_write_area(260);
}
}
cycles_in_state_++;
if(this_state_ == State::Pixels)
{
if(column_counter_&1)
{
if(this_state_ == State::Pixels) {
if(column_counter_&1) {
character_value_ = pixel_data;
if(pixel_pointer)
{
uint8_t cell_colour = colours_[character_colour_ & 0x7];
if(!(character_colour_&0x8))
{
uint8_t colours[2];
if(registers_.invertedCells)
{
if(pixel_pointer) {
uint16_t cell_colour = colours_[character_colour_ & 0x7];
if(!(character_colour_&0x8)) {
uint16_t colours[2];
if(registers_.invertedCells) {
colours[0] = cell_colour;
colours[1] = registers_.backgroundColour;
}
else
{
} else {
colours[0] = registers_.backgroundColour;
colours[1] = cell_colour;
}
@@ -312,10 +296,8 @@ template <class T> class MOS6560 {
pixel_pointer[5] = colours[(character_value_ >> 2)&1];
pixel_pointer[6] = colours[(character_value_ >> 1)&1];
pixel_pointer[7] = colours[(character_value_ >> 0)&1];
}
else
{
uint8_t colours[4] = {registers_.backgroundColour, registers_.borderColour, cell_colour, registers_.auxiliary_colour};
} else {
uint16_t colours[4] = {registers_.backgroundColour, registers_.borderColour, cell_colour, registers_.auxiliary_colour};
pixel_pointer[0] =
pixel_pointer[1] = colours[(character_value_ >> 6)&3];
pixel_pointer[2] =
@@ -325,11 +307,10 @@ template <class T> class MOS6560 {
pixel_pointer[6] =
pixel_pointer[7] = colours[(character_value_ >> 0)&3];
}
pixel_pointer += 8;
}
}
else
{
} else {
character_code_ = pixel_data;
character_colour_ = colour_data;
}
@@ -347,12 +328,10 @@ template <class T> class MOS6560 {
/*!
Writes to a 6560 register.
*/
void set_register(int address, uint8_t value)
{
void set_register(int address, uint8_t value) {
address &= 0xf;
registers_.direct_values[address] = value;
switch(address)
{
switch(address) {
case 0x0:
registers_.interlaced = !!(value&0x80) && timing_.supports_interlacing;
registers_.first_column_location = value & 0x7f;
@@ -391,11 +370,9 @@ template <class T> class MOS6560 {
speaker_->set_volume(value & 0xf);
break;
case 0xf:
{
uint8_t new_border_colour = colours_[value & 0x07];
if(this_state_ == State::Border && new_border_colour != registers_.borderColour)
{
case 0xf: {
uint16_t new_border_colour = colours_[value & 0x07];
if(this_state_ == State::Border && new_border_colour != registers_.borderColour) {
output_border(cycles_in_state_ * 4);
cycles_in_state_ = 0;
}
@@ -415,12 +392,10 @@ template <class T> class MOS6560 {
/*
Reads from a 6560 register.
*/
uint8_t get_register(int address)
{
uint8_t get_register(int address) {
address &= 0xf;
int current_line = (full_frame_counter_ + timing_.line_counter_increment_offset) / timing_.cycles_per_line;
switch(address)
{
switch(address) {
default: return registers_.direct_values[address];
case 0x03: return (uint8_t)(current_line << 7) | (registers_.direct_values[3] & 0x7f);
case 0x04: return (current_line >> 1) & 0xff;
@@ -432,8 +407,7 @@ template <class T> class MOS6560 {
std::shared_ptr<Speaker> speaker_;
unsigned int cycles_since_speaker_update_;
void update_audio()
{
void update_audio() {
speaker_->run_for_cycles(cycles_since_speaker_update_ >> 2);
cycles_since_speaker_update_ &= 3;
}
@@ -444,7 +418,7 @@ template <class T> class MOS6560 {
uint8_t first_column_location, first_row_location;
uint8_t number_of_columns, number_of_rows;
uint16_t character_cell_start_address, video_matrix_start_address;
uint8_t backgroundColour, borderColour, auxiliary_colour;
uint16_t backgroundColour, borderColour, auxiliary_colour;
bool invertedCells;
uint8_t direct_values[16];
@@ -475,12 +449,11 @@ template <class T> class MOS6560 {
bool is_odd_frame_, is_odd_line_;
// lookup table from 6560 colour index to appropriate PAL/NTSC value
uint8_t colours_[16];
uint16_t colours_[16];
uint8_t *pixel_pointer;
void output_border(unsigned int number_of_cycles)
{
uint8_t *colour_pointer = crt_->allocate_write_area(1);
uint16_t *pixel_pointer;
void output_border(unsigned int number_of_cycles) {
uint16_t *colour_pointer = (uint16_t *)crt_->allocate_write_area(1);
if(colour_pointer) *colour_pointer = registers_.borderColour;
crt_->output_level(number_of_cycles);
}

View File

@@ -11,22 +11,18 @@
using namespace GI;
AY38910::AY38910() :
selected_register_(0),
tone_counters_{0, 0, 0}, tone_periods_{0, 0, 0}, tone_outputs_{0, 0, 0},
noise_shift_register_(0xffff), noise_period_(0), noise_counter_(0), noise_output_(0),
envelope_divider_(0), envelope_period_(0), envelope_position_(0),
master_divider_(0),
output_registers_{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
{
selected_register_(0),
tone_counters_{0, 0, 0}, tone_periods_{0, 0, 0}, tone_outputs_{0, 0, 0},
noise_shift_register_(0xffff), noise_period_(0), noise_counter_(0), noise_output_(0),
envelope_divider_(0), envelope_period_(0), envelope_position_(0),
master_divider_(0),
output_registers_{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} {
output_registers_[8] = output_registers_[9] = output_registers_[10] = 0;
// set up envelope lookup tables
for(int c = 0; c < 16; c++)
{
for(int p = 0; p < 32; p++)
{
switch(c)
{
for(int c = 0; c < 16; c++) {
for(int p = 0; p < 32; p++) {
switch(c) {
case 0: case 1: case 2: case 3: case 9:
envelope_shapes_[c][p] = (p < 16) ? (p^0xf) : 0;
envelope_overflow_masks_[c] = 0x1f;
@@ -69,34 +65,28 @@ AY38910::AY38910() :
// set up volume lookup table
float max_volume = 8192;
float root_two = sqrtf(2.0f);
for(int v = 0; v < 16; v++)
{
for(int v = 0; v < 16; v++) {
volumes_[v] = (int)(max_volume / powf(root_two, (float)(v ^ 0xf)));
}
volumes_[0] = 0;
}
void AY38910::set_clock_rate(double clock_rate)
{
void AY38910::set_clock_rate(double clock_rate) {
set_input_rate((float)clock_rate);
}
void AY38910::get_samples(unsigned int number_of_samples, int16_t *target)
{
void AY38910::get_samples(unsigned int number_of_samples, int16_t *target) {
int c = 0;
while((master_divider_&15) && c < number_of_samples)
{
while((master_divider_&7) && c < number_of_samples) {
target[c] = output_volume_;
master_divider_++;
c++;
}
while(c < number_of_samples)
{
while(c < number_of_samples) {
#define step_channel(c) \
if(tone_counters_[c]) tone_counters_[c]--;\
else\
{\
else {\
tone_outputs_[c] ^= 1;\
tone_counters_[c] = tone_periods_[c];\
}
@@ -111,8 +101,7 @@ void AY38910::get_samples(unsigned int number_of_samples, int16_t *target)
// ... 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
{
else {
noise_counter_ = noise_period_;
noise_output_ ^= noise_shift_register_&1;
noise_shift_register_ |= ((noise_shift_register_ ^ (noise_shift_register_ >> 3))&1) << 17;
@@ -122,8 +111,7 @@ void AY38910::get_samples(unsigned int number_of_samples, int16_t *target)
// ... and the envelope generator. Table based for pattern lookup, with a 'refill' step — a way of
// implementing non-repeating patterns by locking them to table position 0x1f.
if(envelope_divider_) envelope_divider_--;
else
{
else {
envelope_divider_ = envelope_period_;
envelope_position_ ++;
if(envelope_position_ == 32) envelope_position_ = envelope_overflow_masks_[output_registers_[13]];
@@ -131,19 +119,17 @@ void AY38910::get_samples(unsigned int number_of_samples, int16_t *target)
evaluate_output_volume();
for(int ic = 0; ic < 16 && c < number_of_samples; ic++)
{
for(int ic = 0; ic < 8 && c < number_of_samples; ic++) {
target[c] = output_volume_;
c++;
master_divider_++;
}
}
master_divider_ &= 15;
master_divider_ &= 7;
}
void AY38910::evaluate_output_volume()
{
void AY38910::evaluate_output_volume() {
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_];
// The output level for a channel is:
@@ -180,24 +166,19 @@ void AY38910::evaluate_output_volume()
);
}
void AY38910::select_register(uint8_t r)
{
void AY38910::select_register(uint8_t r) {
selected_register_ = r & 0xf;
}
void AY38910::set_register_value(uint8_t value)
{
void AY38910::set_register_value(uint8_t value) {
registers_[selected_register_] = value;
if(selected_register_ < 14)
{
if(selected_register_ < 14) {
int selected_register = selected_register_;
enqueue([=] () {
uint8_t masked_value = value;
switch(selected_register)
{
switch(selected_register) {
case 0: case 2: case 4:
case 1: case 3: case 5:
{
case 1: case 3: case 5: {
int channel = selected_register >> 1;
if(selected_register & 1)
@@ -234,8 +215,7 @@ void AY38910::set_register_value(uint8_t value)
}
}
uint8_t AY38910::get_register_value()
{
uint8_t AY38910::get_register_value() {
// This table ensures that bits that aren't defined within the AY are returned as 1s
// when read. I can't find documentation on this and don't have a machine to test, so
// this is provisionally a guess. TODO: investigate.
@@ -247,26 +227,21 @@ uint8_t AY38910::get_register_value()
return registers_[selected_register_] | register_masks[selected_register_];
}
uint8_t AY38910::get_port_output(bool port_b)
{
uint8_t AY38910::get_port_output(bool port_b) {
return registers_[port_b ? 15 : 14];
}
void AY38910::set_data_input(uint8_t r)
{
void AY38910::set_data_input(uint8_t r) {
data_input_ = r;
}
uint8_t AY38910::get_data_output()
{
uint8_t AY38910::get_data_output() {
return data_output_;
}
void AY38910::set_control_lines(ControlLines control_lines)
{
void AY38910::set_control_lines(ControlLines control_lines) {
ControlState new_state;
switch((int)control_lines)
{
switch((int)control_lines) {
default: new_state = Inactive; break;
case (int)(BCDIR | BC2 | BC1):
@@ -277,11 +252,9 @@ void AY38910::set_control_lines(ControlLines control_lines)
case (int)(BCDIR | BC2): new_state = Write; break;
}
if(new_state != control_state_)
{
if(new_state != control_state_) {
control_state_ = new_state;
switch(new_state)
{
switch(new_state) {
default: break;
case LatchAddress: select_register(data_input_); break;
case Write: set_register_value(data_input_); break;

View File

@@ -19,26 +19,21 @@ AsyncTaskQueue::AsyncTaskQueue()
serial_dispatch_queue_ = dispatch_queue_create("com.thomasharte.clocksignal.asyntaskqueue", DISPATCH_QUEUE_SERIAL);
#else
thread_.reset(new std::thread([this]() {
while(!should_destruct_)
{
while(!should_destruct_) {
std::function<void(void)> next_function;
// Take lock, check for a new task
std::unique_lock<std::mutex> lock(queue_mutex_);
if(!pending_tasks_.empty())
{
if(!pending_tasks_.empty()) {
next_function = pending_tasks_.front();
pending_tasks_.pop_front();
}
if(next_function)
{
if(next_function) {
// If there is a task, release lock and perform it
lock.unlock();
next_function();
}
else
{
} else {
// If there isn't a task, atomically block on the processing condition and release the lock
// until there's something pending (and then release it again via scope)
processing_condition_.wait(lock);
@@ -48,8 +43,7 @@ AsyncTaskQueue::AsyncTaskQueue()
#endif
}
AsyncTaskQueue::~AsyncTaskQueue()
{
AsyncTaskQueue::~AsyncTaskQueue() {
#ifdef __APPLE__
dispatch_release(serial_dispatch_queue_);
#else
@@ -60,8 +54,7 @@ AsyncTaskQueue::~AsyncTaskQueue()
#endif
}
void AsyncTaskQueue::enqueue(std::function<void(void)> function)
{
void AsyncTaskQueue::enqueue(std::function<void(void)> function) {
#ifdef __APPLE__
dispatch_async(serial_dispatch_queue_, ^{function();});
#else
@@ -71,8 +64,7 @@ void AsyncTaskQueue::enqueue(std::function<void(void)> function)
#endif
}
void AsyncTaskQueue::flush()
{
void AsyncTaskQueue::flush() {
#ifdef __APPLE__
dispatch_sync(serial_dispatch_queue_, ^{});
#else

View File

@@ -10,772 +10,148 @@
#include <algorithm>
#include <stdio.h>
#include "Cartridges/CartridgeAtari8k.hpp"
#include "Cartridges/CartridgeAtari16k.hpp"
#include "Cartridges/CartridgeAtari32k.hpp"
#include "Cartridges/CartridgeActivisionStack.hpp"
#include "Cartridges/CartridgeCBSRAMPlus.hpp"
#include "Cartridges/CartridgeCommaVid.hpp"
#include "Cartridges/CartridgeMegaBoy.hpp"
#include "Cartridges/CartridgeMNetwork.hpp"
#include "Cartridges/CartridgeParkerBros.hpp"
#include "Cartridges/CartridgePitfall2.hpp"
#include "Cartridges/CartridgeTigervision.hpp"
#include "Cartridges/CartridgeUnpaged.hpp"
using namespace Atari2600;
namespace {
static const unsigned int horizontalTimerPeriod = 228;
static const double NTSC_clock_rate = 1194720;
static const double PAL_clock_rate = 1182298;
}
Machine::Machine() :
horizontal_timer_(0),
last_output_state_duration_(0),
last_output_state_(OutputState::Sync),
rom_(nullptr),
tia_input_value_{0xff, 0xff},
upcoming_events_pointer_(0),
object_counter_pointer_(0),
state_by_time_(state_by_extend_time_[0]),
cycles_since_speaker_update_(0),
is_pal_region_(false)
{
memset(collisions_, 0xff, sizeof(collisions_));
setup_reported_collisions();
for(int vbextend = 0; vbextend < 2; vbextend++)
{
for(int c = 0; c < 57; c++)
{
OutputState state;
// determine which output state will be active in four cycles from now
switch(c)
{
case 0: case 1: case 2: case 3: state = OutputState::Blank; break;
case 4: case 5: case 6: case 7: state = OutputState::Sync; break;
case 8: case 9: case 10: case 11: state = OutputState::ColourBurst; break;
case 12: case 13: case 14:
case 15: case 16: state = OutputState::Blank; break;
case 17: case 18: state = vbextend ? OutputState::Blank : OutputState::Pixel; break;
default: state = OutputState::Pixel; break;
}
state_by_extend_time_[vbextend][c] = state;
}
}
frame_record_pointer_(0),
is_ntsc_(true) {
set_clock_rate(NTSC_clock_rate);
}
void Machine::setup_output(float aspect_ratio)
{
speaker_.reset(new Speaker);
crt_.reset(new Outputs::CRT::CRT(228, 1, 263, Outputs::CRT::ColourSpace::YIQ, 228, 1, 1));
crt_->set_output_device(Outputs::CRT::Television);
// this is the NTSC phase offset function; see below for PAL
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"float phaseOffset = 6.283185308 * float(iPhase - 1u) / 13.0;"
"return mix(float(y) / 14.0, step(1, iPhase) * cos(phase + phaseOffset), amplitude);"
"}");
speaker_->set_input_rate((float)(get_clock_rate() / 38.0));
void Machine::setup_output(float aspect_ratio) {
bus_->tia_.reset(new TIA);
bus_->speaker_.reset(new Speaker);
bus_->speaker_->set_input_rate((float)(get_clock_rate() / (double)CPUTicksPerAudioTick));
bus_->tia_->get_crt()->set_delegate(this);
}
void Machine::switch_region()
{
// the PAL function
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"uint direction = iPhase & 1u;"
"float phaseOffset = float(7u - direction) + (float(direction) - 0.5) * 2.0 * float(iPhase >> 1);"
"phaseOffset *= 6.283185308 / 12.0;"
"return mix(float(y) / 14.0, step(4, (iPhase + 2u) & 15u) * cos(phase + phaseOffset), amplitude);"
"}");
crt_->set_new_timing(228, 312, Outputs::CRT::ColourSpace::YUV, 228, 1);
is_pal_region_ = true;
speaker_->set_input_rate((float)(get_clock_rate() / 38.0));
set_clock_rate(PAL_clock_rate);
void Machine::close_output() {
bus_.reset();
}
void Machine::close_output()
{
crt_ = nullptr;
}
Machine::~Machine()
{
delete[] rom_;
Machine::~Machine() {
close_output();
}
void Machine::update_timers(int mask)
{
unsigned int upcoming_pointer_plus_4 = (upcoming_events_pointer_ + 4)%number_of_upcoming_events;
object_counter_pointer_ = (object_counter_pointer_ + 1)%number_of_recorded_counters;
ObjectCounter *oneClockAgo = object_counter_[(object_counter_pointer_ - 1 + number_of_recorded_counters)%number_of_recorded_counters];
ObjectCounter *twoClocksAgo = object_counter_[(object_counter_pointer_ - 2 + number_of_recorded_counters)%number_of_recorded_counters];
ObjectCounter *now = object_counter_[object_counter_pointer_];
// grab the background now, for application in four clocks
if(mask & (1 << 5) && !(horizontal_timer_&3))
{
unsigned int offset = 4 + horizontal_timer_ - (horizontalTimerPeriod - 160);
upcoming_events_[upcoming_pointer_plus_4].updates |= Event::Action::Playfield;
upcoming_events_[upcoming_pointer_plus_4].playfield_pixel = playfield_[(offset >> 2)%40];
}
if(mask & (1 << 4))
{
// the ball becomes visible whenever it hits zero, regardless of whether its status
// is the result of a counter rollover or a programmatic reset, and there's a four
// clock delay on that triggering the start signal
now[4].count = (oneClockAgo[4].count + 1)%160;
now[4].pixel = oneClockAgo[4].pixel + 1;
if(!now[4].count) now[4].pixel = 0;
}
else
{
now[4] = oneClockAgo[4];
}
// check for player and missle triggers
for(int c = 0; c < 4; c++)
{
if(mask & (1 << c))
{
// update the count
now[c].count = (oneClockAgo[c].count + 1)%160;
uint8_t repeatMask = player_and_missile_size_[c&1] & 7;
ObjectCounter *rollover;
ObjectCounter *equality;
if(c < 2)
{
// update the pixel
now[c].broad_pixel = oneClockAgo[c].broad_pixel + 1;
switch(repeatMask)
{
default: now[c].pixel = oneClockAgo[c].pixel + 1; break;
case 5: now[c].pixel = oneClockAgo[c].pixel + (now[c].broad_pixel&1); break;
case 7: now[c].pixel = oneClockAgo[c].pixel + (((now[c].broad_pixel | (now[c].broad_pixel >> 1))^1)&1); break;
}
// check for a rollover six clocks ago or equality five clocks ago
rollover = twoClocksAgo;
equality = oneClockAgo;
}
else
{
// update the pixel
now[c].pixel = oneClockAgo[c].pixel + 1;
// check for a rollover five clocks ago or equality four clocks ago
rollover = oneClockAgo;
equality = now;
}
if(
(rollover[c].count == 159) ||
(has_second_copy_[c&1] && equality[c].count == 16) ||
(has_third_copy_[c&1] && equality[c].count == 32) ||
(has_fourth_copy_[c&1] && equality[c].count == 64)
)
{
now[c].pixel = 0;
now[c].broad_pixel = 0;
}
}
else
{
now[c] = oneClockAgo[c];
}
}
}
uint8_t Machine::get_output_pixel()
{
ObjectCounter *now = object_counter_[object_counter_pointer_];
// get the playfield pixel
unsigned int offset = horizontal_timer_ - (horizontalTimerPeriod - 160);
uint8_t playfieldColour = ((playfield_control_&6) == 2) ? player_colour_[offset / 80] : playfield_colour_;
// ball pixel
uint8_t ballPixel = 0;
if(now[4].pixel < ball_size_) {
ballPixel = ball_graphics_enable_[ball_graphics_selector_];
}
// determine the player and missile pixels
uint8_t playerPixels[2] = { 0, 0 };
uint8_t missilePixels[2] = { 0, 0 };
for(int c = 0; c < 2; c++)
{
if(player_graphics_[c] && now[c].pixel < 8) {
playerPixels[c] = (player_graphics_[player_graphics_selector_[c]][c] >> (now[c].pixel ^ player_reflection_mask_[c])) & 1;
}
if(!missile_graphics_reset_[c] && now[c+2].pixel < missile_size_[c]) {
missilePixels[c] = missile_graphics_enable_[c];
}
}
// accumulate collisions
int pixel_mask = playerPixels[0] | (playerPixels[1] << 1) | (missilePixels[0] << 2) | (missilePixels[1] << 3) | (ballPixel << 4) | (playfield_output_ << 5);
collisions_[0] |= reported_collisions_[pixel_mask][0];
collisions_[1] |= reported_collisions_[pixel_mask][1];
collisions_[2] |= reported_collisions_[pixel_mask][2];
collisions_[3] |= reported_collisions_[pixel_mask][3];
collisions_[4] |= reported_collisions_[pixel_mask][4];
collisions_[5] |= reported_collisions_[pixel_mask][5];
collisions_[6] |= reported_collisions_[pixel_mask][6];
collisions_[7] |= reported_collisions_[pixel_mask][7];
// apply appropriate priority to pick a colour
uint8_t playfield_pixel = playfield_output_ | ballPixel;
uint8_t outputColour = playfield_pixel ? playfieldColour : background_colour_;
if(!(playfield_control_&0x04) || !playfield_pixel) {
if(playerPixels[1] || missilePixels[1]) outputColour = player_colour_[1];
if(playerPixels[0] || missilePixels[0]) outputColour = player_colour_[0];
}
// return colour
return outputColour;
}
void Machine::setup_reported_collisions()
{
for(int c = 0; c < 64; c++)
{
memset(reported_collisions_[c], 0, 8);
int playerPixels[2] = { c&1, (c >> 1)&1 };
int missilePixels[2] = { (c >> 2)&1, (c >> 3)&1 };
int ballPixel = (c >> 4)&1;
int playfield_pixel = (c >> 5)&1;
if(playerPixels[0] | playerPixels[1]) {
reported_collisions_[c][0] |= ((missilePixels[0] & playerPixels[1]) << 7) | ((missilePixels[0] & playerPixels[0]) << 6);
reported_collisions_[c][1] |= ((missilePixels[1] & playerPixels[0]) << 7) | ((missilePixels[1] & playerPixels[1]) << 6);
reported_collisions_[c][2] |= ((playfield_pixel & playerPixels[0]) << 7) | ((ballPixel & playerPixels[0]) << 6);
reported_collisions_[c][3] |= ((playfield_pixel & playerPixels[1]) << 7) | ((ballPixel & playerPixels[1]) << 6);
reported_collisions_[c][7] |= ((playerPixels[0] & playerPixels[1]) << 7);
}
if(playfield_pixel | ballPixel) {
reported_collisions_[c][4] |= ((playfield_pixel & missilePixels[0]) << 7) | ((ballPixel & missilePixels[0]) << 6);
reported_collisions_[c][5] |= ((playfield_pixel & missilePixels[1]) << 7) | ((ballPixel & missilePixels[1]) << 6);
reported_collisions_[c][6] |= ((playfield_pixel & ballPixel) << 7);
}
if(missilePixels[0] & missilePixels[1])
reported_collisions_[c][7] |= (1 << 6);
}
}
void Machine::output_pixels(unsigned int count)
{
while(count--)
{
if(upcoming_events_[upcoming_events_pointer_].updates)
{
// apply any queued changes and flush the record
if(upcoming_events_[upcoming_events_pointer_].updates & Event::Action::HMoveSetup)
{
// schedule an extended left border
state_by_time_ = state_by_extend_time_[1];
// clear any ongoing moves
if(hmove_flags_)
{
for(int c = 0; c < number_of_upcoming_events; c++)
{
upcoming_events_[c].updates &= ~(Event::Action::HMoveCompare | Event::Action::HMoveDecrement);
}
}
// schedule new moves
hmove_flags_ = 0x1f;
hmove_counter_ = 15;
// follow-through into a compare immediately
upcoming_events_[upcoming_events_pointer_].updates |= Event::Action::HMoveCompare;
}
if(upcoming_events_[upcoming_events_pointer_].updates & Event::Action::HMoveCompare)
{
for(int c = 0; c < 5; c++)
{
if(((object_motions_[c] >> 4)^hmove_counter_) == 7)
{
hmove_flags_ &= ~(1 << c);
}
}
if(hmove_flags_)
{
if(hmove_counter_) hmove_counter_--;
upcoming_events_[(upcoming_events_pointer_+4)%number_of_upcoming_events].updates |= Event::Action::HMoveCompare;
upcoming_events_[(upcoming_events_pointer_+2)%number_of_upcoming_events].updates |= Event::Action::HMoveDecrement;
}
}
if(upcoming_events_[upcoming_events_pointer_].updates & Event::Action::HMoveDecrement)
{
update_timers(hmove_flags_);
}
if(upcoming_events_[upcoming_events_pointer_].updates & Event::Action::ResetCounter)
{
object_counter_[object_counter_pointer_][upcoming_events_[upcoming_events_pointer_].counter].count = 0;
}
// zero out current update event
upcoming_events_[upcoming_events_pointer_].updates = 0;
}
// progress to next event
upcoming_events_pointer_ = (upcoming_events_pointer_ + 1)%number_of_upcoming_events;
// determine which output state is currently active
OutputState primary_state = state_by_time_[horizontal_timer_ >> 2];
OutputState effective_state = primary_state;
// update pixel timers
if(primary_state == OutputState::Pixel) update_timers(~0);
// update the background chain
if(horizontal_timer_ >= 64 && horizontal_timer_ <= 160+64 && !(horizontal_timer_&3))
{
playfield_output_ = next_playfield_output_;
next_playfield_output_ = playfield_[(horizontal_timer_ - 64) >> 2];
}
// if vsync is enabled, output the opposite of the automatic hsync output;
// also honour the vertical blank flag
if(vsync_enabled_) {
effective_state = (effective_state = OutputState::Sync) ? OutputState::Blank : OutputState::Sync;
} else if(vblank_enabled_ && effective_state == OutputState::Pixel) {
effective_state = OutputState::Blank;
}
// decide what that means needs to be communicated to the CRT
last_output_state_duration_++;
if(effective_state != last_output_state_) {
switch(last_output_state_) {
case OutputState::Blank: crt_->output_blank(last_output_state_duration_); break;
case OutputState::Sync: crt_->output_sync(last_output_state_duration_); break;
case OutputState::ColourBurst: crt_->output_colour_burst(last_output_state_duration_, 96, 0); break;
case OutputState::Pixel: crt_->output_data(last_output_state_duration_, 1); break;
}
last_output_state_duration_ = 0;
last_output_state_ = effective_state;
if(effective_state == OutputState::Pixel) {
output_buffer_ = crt_->allocate_write_area(160);
} else {
output_buffer_ = nullptr;
}
}
// decide on a pixel colour if that's what's happening
if(effective_state == OutputState::Pixel)
{
uint8_t colour = get_output_pixel();
if(output_buffer_)
{
*output_buffer_ = colour;
output_buffer_++;
}
}
// advance horizontal timer, perform reset actions if desired
horizontal_timer_ = (horizontal_timer_ + 1) % horizontalTimerPeriod;
if(!horizontal_timer_)
{
// switch back to a normal length left border
state_by_time_ = state_by_extend_time_[0];
set_ready_line(false);
}
}
}
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value)
{
uint8_t returnValue = 0xff;
unsigned int cycles_run_for = 3;
// this occurs as a feedback loop — the 2600 requests ready, then performs the cycles_run_for
// leap to the end of ready only once ready is signalled — because on a 6502 ready doesn't take
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// skips to the end of the line.
if(operation == CPU6502::BusOperation::Ready) {
unsigned int distance_to_end_of_ready = horizontalTimerPeriod - horizontal_timer_;
cycles_run_for = distance_to_end_of_ready;
}
output_pixels(cycles_run_for);
cycles_since_speaker_update_ += cycles_run_for;
if(operation != CPU6502::BusOperation::Ready) {
// check for a paging access
if(rom_size_ > 4096 && ((address & 0x1f00) == 0x1f00)) {
uint8_t *base_ptr = rom_pages_[0];
uint8_t first_paging_register = (uint8_t)(0xf8 - (rom_size_ >> 14)*2);
const uint8_t paging_register = address&0xff;
if(paging_register >= first_paging_register) {
const uint16_t selected_page = paging_register - first_paging_register;
if(selected_page * 4096 < rom_size_) {
base_ptr = &rom_[selected_page * 4096];
}
}
if(base_ptr != rom_pages_[0]) {
rom_pages_[0] = base_ptr;
rom_pages_[1] = base_ptr + 1024;
rom_pages_[2] = base_ptr + 2048;
rom_pages_[3] = base_ptr + 3072;
}
}
// check for a ROM read
if((address&0x1000) && isReadOperation(operation)) {
returnValue &= rom_pages_[(address >> 10)&3][address&1023];
}
// check for a RAM access
if((address&0x1280) == 0x80) {
if(isReadOperation(operation)) {
returnValue &= mos6532_.get_ram(address);
} else {
mos6532_.set_ram(address, *value);
}
}
// check for a TIA access
if(!(address&0x1080)) {
if(isReadOperation(operation)) {
const uint16_t decodedAddress = address & 0xf;
switch(decodedAddress) {
case 0x00: // missile 0 / player collisions
case 0x01: // missile 1 / player collisions
case 0x02: // player 0 / playfield / ball collisions
case 0x03: // player 1 / playfield / ball collisions
case 0x04: // missile 0 / playfield / ball collisions
case 0x05: // missile 1 / playfield / ball collisions
case 0x06: // ball / playfield collisions
case 0x07: // player / player, missile / missile collisions
returnValue &= collisions_[decodedAddress];
break;
case 0x08:
case 0x09:
case 0x0a:
case 0x0b:
// TODO: pot ports
break;
case 0x0c:
case 0x0d:
returnValue &= tia_input_value_[decodedAddress - 0x0c];
break;
}
} else {
const uint16_t decodedAddress = address & 0x3f;
switch(decodedAddress) {
case 0x00:
vsync_enabled_ = !!(*value & 0x02);
break;
case 0x01: vblank_enabled_ = !!(*value & 0x02); break;
case 0x02:
if(horizontal_timer_) set_ready_line(true);
break;
case 0x03:
// Reset is delayed by four cycles.
horizontal_timer_ = horizontalTimerPeriod - 4;
// TODO: audio will now be out of synchronisation — fix
break;
case 0x04:
case 0x05: {
int entry = decodedAddress - 0x04;
player_and_missile_size_[entry] = *value;
missile_size_[entry] = 1 << ((*value >> 4)&3);
uint8_t repeatMask = (*value)&7;
has_second_copy_[entry] = (repeatMask == 1) || (repeatMask == 3);
has_third_copy_[entry] = (repeatMask == 2) || (repeatMask == 3) || (repeatMask == 6);
has_fourth_copy_[entry] = (repeatMask == 4) || (repeatMask == 6);
} break;
case 0x06:
case 0x07: player_colour_[decodedAddress - 0x06] = *value; break;
case 0x08: playfield_colour_ = *value; break;
case 0x09: background_colour_ = *value; break;
case 0x0a: {
uint8_t old_playfield_control = playfield_control_;
playfield_control_ = *value;
ball_size_ = 1 << ((playfield_control_ >> 4)&3);
// did the mirroring bit change?
if((playfield_control_^old_playfield_control)&1) {
if(playfield_control_&1) {
for(int c = 0; c < 20; c++) playfield_[c+20] = playfield_[19-c];
} else {
memcpy(&playfield_[20], playfield_, 20);
}
}
} break;
case 0x0b:
case 0x0c: player_reflection_mask_[decodedAddress - 0x0b] = (*value)&8 ? 0 : 7; break;
case 0x0d:
playfield_[0] = ((*value) >> 4)&1;
playfield_[1] = ((*value) >> 5)&1;
playfield_[2] = ((*value) >> 6)&1;
playfield_[3] = (*value) >> 7;
if(playfield_control_&1) {
for(int c = 0; c < 4; c++) playfield_[39-c] = playfield_[c];
} else {
memcpy(&playfield_[20], playfield_, 4);
}
break;
case 0x0e:
playfield_[4] = (*value) >> 7;
playfield_[5] = ((*value) >> 6)&1;
playfield_[6] = ((*value) >> 5)&1;
playfield_[7] = ((*value) >> 4)&1;
playfield_[8] = ((*value) >> 3)&1;
playfield_[9] = ((*value) >> 2)&1;
playfield_[10] = ((*value) >> 1)&1;
playfield_[11] = (*value)&1;
if(playfield_control_&1) {
for(int c = 0; c < 8; c++) playfield_[35-c] = playfield_[c+4];
} else {
memcpy(&playfield_[24], &playfield_[4], 8);
}
break;
case 0x0f:
playfield_[19] = (*value) >> 7;
playfield_[18] = ((*value) >> 6)&1;
playfield_[17] = ((*value) >> 5)&1;
playfield_[16] = ((*value) >> 4)&1;
playfield_[15] = ((*value) >> 3)&1;
playfield_[14] = ((*value) >> 2)&1;
playfield_[13] = ((*value) >> 1)&1;
playfield_[12] = (*value)&1;
if(playfield_control_&1) {
for(int c = 0; c < 8; c++) playfield_[27-c] = playfield_[c+12];
} else {
memcpy(&playfield_[32], &playfield_[12], 8);
}
break;
case 0x10: case 0x11: case 0x12: case 0x13:
case 0x14:
upcoming_events_[(upcoming_events_pointer_ + 4)%number_of_upcoming_events].updates |= Event::Action::ResetCounter;
upcoming_events_[(upcoming_events_pointer_ + 4)%number_of_upcoming_events].counter = decodedAddress - 0x10;
break;
case 0x15: case 0x16:
update_audio();
speaker_->set_control(decodedAddress - 0x15, *value);
break;
case 0x17: case 0x18:
update_audio();
speaker_->set_divider(decodedAddress - 0x17, *value);
break;
case 0x19: case 0x1a:
update_audio();
speaker_->set_volume(decodedAddress - 0x19, *value);
break;
case 0x1c:
ball_graphics_enable_[1] = ball_graphics_enable_[0];
case 0x1b: {
int index = decodedAddress - 0x1b;
player_graphics_[0][index] = *value;
player_graphics_[1][index^1] = player_graphics_[0][index^1];
} break;
case 0x1d:
case 0x1e:
missile_graphics_enable_[decodedAddress - 0x1d] = ((*value) >> 1)&1;
// printf("e:%02x <- %c\n", decodedAddress - 0x1d, ((*value)&1) ? 'E' : '-');
break;
case 0x1f:
ball_graphics_enable_[0] = ((*value) >> 1)&1;
break;
case 0x20:
case 0x21:
case 0x22:
case 0x23:
case 0x24:
object_motions_[decodedAddress - 0x20] = *value;
break;
case 0x25: player_graphics_selector_[0] = (*value)&1; break;
case 0x26: player_graphics_selector_[1] = (*value)&1; break;
case 0x27: ball_graphics_selector_ = (*value)&1; break;
case 0x28:
case 0x29:
{
// TODO: this should properly mean setting a flag and propagating later, I think?
int index = decodedAddress - 0x28;
if(!(*value&0x02) && missile_graphics_reset_[index])
{
object_counter_[object_counter_pointer_][index + 2].count = object_counter_[object_counter_pointer_][index].count;
uint8_t repeatMask = player_and_missile_size_[index] & 7;
int extra_offset;
switch(repeatMask)
{
default: extra_offset = 3; break;
case 5: extra_offset = 6; break;
case 7: extra_offset = 10; break;
}
object_counter_[object_counter_pointer_][index + 2].count = (object_counter_[object_counter_pointer_][index + 2].count + extra_offset)%160;
}
missile_graphics_reset_[index] = !!((*value) & 0x02);
// printf("r:%02x <- %c\n", decodedAddress - 0x28, ((*value)&2) ? 'R' : '-');
}
break;
case 0x2a: {
// justification for +5: "we need to wait at least 71 [clocks] before the HMOVE operation is complete";
// which will take 16*4 + 2 = 66 cycles from the first compare, implying the first compare must be
// in five cycles from now
// int start_pause = ((horizontal_timer_ + 3)&3) + 4;
upcoming_events_[(upcoming_events_pointer_ + 5)%number_of_upcoming_events].updates |= Event::Action::HMoveSetup;
} break;
case 0x2b:
object_motions_[0] =
object_motions_[1] =
object_motions_[2] =
object_motions_[3] =
object_motions_[4] = 0;
break;
case 0x2c:
collisions_[0] = collisions_[1] = collisions_[2] =
collisions_[3] = collisions_[4] = collisions_[5] = 0x3f;
collisions_[6] = 0x7f;
collisions_[7] = 0x3f;
break;
}
}
}
// check for a PIA access
if((address&0x1280) == 0x280) {
if(isReadOperation(operation)) {
returnValue &= mos6532_.get_register(address);
} else {
mos6532_.set_register(address, *value);
}
}
if(isReadOperation(operation)) {
*value = returnValue;
}
}
mos6532_.run_for_cycles(cycles_run_for / 3);
return cycles_run_for / 3;
}
void Machine::set_digital_input(Atari2600DigitalInput input, bool state)
{
void Machine::set_digital_input(Atari2600DigitalInput input, bool state) {
switch (input) {
case Atari2600DigitalInputJoy1Up: mos6532_.update_port_input(0, 0x10, state); break;
case Atari2600DigitalInputJoy1Down: mos6532_.update_port_input(0, 0x20, state); break;
case Atari2600DigitalInputJoy1Left: mos6532_.update_port_input(0, 0x40, state); break;
case Atari2600DigitalInputJoy1Right: mos6532_.update_port_input(0, 0x80, state); break;
case Atari2600DigitalInputJoy1Up: bus_->mos6532_.update_port_input(0, 0x10, state); break;
case Atari2600DigitalInputJoy1Down: bus_->mos6532_.update_port_input(0, 0x20, state); break;
case Atari2600DigitalInputJoy1Left: bus_->mos6532_.update_port_input(0, 0x40, state); break;
case Atari2600DigitalInputJoy1Right: bus_->mos6532_.update_port_input(0, 0x80, state); break;
case Atari2600DigitalInputJoy2Up: mos6532_.update_port_input(0, 0x01, state); break;
case Atari2600DigitalInputJoy2Down: mos6532_.update_port_input(0, 0x02, state); break;
case Atari2600DigitalInputJoy2Left: mos6532_.update_port_input(0, 0x04, state); break;
case Atari2600DigitalInputJoy2Right: mos6532_.update_port_input(0, 0x08, state); break;
case Atari2600DigitalInputJoy2Up: bus_->mos6532_.update_port_input(0, 0x01, state); break;
case Atari2600DigitalInputJoy2Down: bus_->mos6532_.update_port_input(0, 0x02, state); break;
case Atari2600DigitalInputJoy2Left: bus_->mos6532_.update_port_input(0, 0x04, state); break;
case Atari2600DigitalInputJoy2Right: bus_->mos6532_.update_port_input(0, 0x08, state); break;
// TODO: latching
case Atari2600DigitalInputJoy1Fire: if(state) tia_input_value_[0] &= ~0x80; else tia_input_value_[0] |= 0x80; break;
case Atari2600DigitalInputJoy2Fire: if(state) tia_input_value_[1] &= ~0x80; else tia_input_value_[1] |= 0x80; break;
case Atari2600DigitalInputJoy1Fire: if(state) bus_->tia_input_value_[0] &= ~0x80; else bus_->tia_input_value_[0] |= 0x80; break;
case Atari2600DigitalInputJoy2Fire: if(state) bus_->tia_input_value_[1] &= ~0x80; else bus_->tia_input_value_[1] |= 0x80; break;
default: break;
}
}
void Machine::set_switch_is_enabled(Atari2600Switch input, bool state)
{
void Machine::set_switch_is_enabled(Atari2600Switch input, bool state) {
switch(input) {
case Atari2600SwitchReset: mos6532_.update_port_input(1, 0x01, state); break;
case Atari2600SwitchSelect: mos6532_.update_port_input(1, 0x02, state); break;
case Atari2600SwitchColour: mos6532_.update_port_input(1, 0x08, state); break;
case Atari2600SwitchLeftPlayerDifficulty: mos6532_.update_port_input(1, 0x40, state); break;
case Atari2600SwitchRightPlayerDifficulty: mos6532_.update_port_input(1, 0x80, state); break;
case Atari2600SwitchReset: bus_->mos6532_.update_port_input(1, 0x01, state); break;
case Atari2600SwitchSelect: bus_->mos6532_.update_port_input(1, 0x02, state); break;
case Atari2600SwitchColour: bus_->mos6532_.update_port_input(1, 0x08, state); break;
case Atari2600SwitchLeftPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x40, state); break;
case Atari2600SwitchRightPlayerDifficulty: bus_->mos6532_.update_port_input(1, 0x80, state); break;
}
}
void Machine::configure_as_target(const StaticAnalyser::Target &target)
{
if(!target.cartridges.front()->get_segments().size()) return;
Storage::Cartridge::Cartridge::Segment segment = target.cartridges.front()->get_segments().front();
size_t length = segment.data.size();
void Machine::configure_as_target(const StaticAnalyser::Target &target) {
const std::vector<uint8_t> &rom = target.cartridges.front()->get_segments().front().data;
switch(target.atari.paging_model) {
case StaticAnalyser::Atari2600PagingModel::ActivisionStack: bus_.reset(new CartridgeActivisionStack(rom)); break;
case StaticAnalyser::Atari2600PagingModel::CBSRamPlus: bus_.reset(new CartridgeCBSRAMPlus(rom)); break;
case StaticAnalyser::Atari2600PagingModel::CommaVid: bus_.reset(new CartridgeCommaVid(rom)); break;
case StaticAnalyser::Atari2600PagingModel::MegaBoy: bus_.reset(new CartridgeMegaBoy(rom)); break;
case StaticAnalyser::Atari2600PagingModel::MNetwork: bus_.reset(new CartridgeMNetwork(rom)); break;
case StaticAnalyser::Atari2600PagingModel::None: bus_.reset(new CartridgeUnpaged(rom)); break;
case StaticAnalyser::Atari2600PagingModel::ParkerBros: bus_.reset(new CartridgeParkerBros(rom)); break;
case StaticAnalyser::Atari2600PagingModel::Pitfall2: bus_.reset(new CartridgePitfall2(rom)); break;
case StaticAnalyser::Atari2600PagingModel::Tigervision: bus_.reset(new CartridgeTigervision(rom)); break;
rom_size_ = 1024;
while(rom_size_ < length && rom_size_ < 32768) rom_size_ <<= 1;
delete[] rom_;
rom_ = new uint8_t[rom_size_];
size_t offset = 0;
const size_t copy_step = std::min(rom_size_, length);
while(offset < rom_size_)
{
size_t copy_length = std::min(copy_step, rom_size_ - offset);
memcpy(&rom_[offset], &segment.data[0], copy_length);
offset += copy_length;
case StaticAnalyser::Atari2600PagingModel::Atari8k:
if(target.atari.uses_superchip) {
bus_.reset(new CartridgeAtari8kSuperChip(rom));
} else {
bus_.reset(new CartridgeAtari8k(rom));
}
break;
case StaticAnalyser::Atari2600PagingModel::Atari16k:
if(target.atari.uses_superchip) {
bus_.reset(new CartridgeAtari16kSuperChip(rom));
} else {
bus_.reset(new CartridgeAtari16k(rom));
}
break;
case StaticAnalyser::Atari2600PagingModel::Atari32k:
if(target.atari.uses_superchip) {
bus_.reset(new CartridgeAtari32kSuperChip(rom));
} else {
bus_.reset(new CartridgeAtari32k(rom));
}
break;
}
size_t romMask = rom_size_ - 1;
rom_pages_[0] = rom_;
rom_pages_[1] = &rom_[1024 & romMask];
rom_pages_[2] = &rom_[2048 & romMask];
rom_pages_[3] = &rom_[3072 & romMask];
}
#pragma mark - Audio
#pragma mark - CRT delegate
void Machine::update_audio()
{
unsigned int audio_cycles = cycles_since_speaker_update_ / 114;
void Machine::crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) {
const size_t number_of_frame_records = sizeof(frame_records_) / sizeof(frame_records_[0]);
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_frames = number_of_frames;
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
frame_record_pointer_ ++;
speaker_->run_for_cycles(audio_cycles);
cycles_since_speaker_update_ %= 114;
if(frame_record_pointer_ >= 6) {
unsigned int total_number_of_frames = 0;
unsigned int total_number_of_unexpected_vertical_syncs = 0;
for(size_t c = 0; c < number_of_frame_records; c++) {
total_number_of_frames += frame_records_[c].number_of_frames;
total_number_of_unexpected_vertical_syncs += frame_records_[c].number_of_unexpected_vertical_syncs;
}
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
for(size_t c = 0; c < number_of_frame_records; c++) {
frame_records_[c].number_of_frames = 0;
frame_records_[c].number_of_unexpected_vertical_syncs = 0;
}
is_ntsc_ ^= true;
double clock_rate;
if(is_ntsc_) {
clock_rate = NTSC_clock_rate;
bus_->tia_->set_output_mode(TIA::OutputMode::NTSC);
} else {
clock_rate = PAL_clock_rate;
bus_->tia_->set_output_mode(TIA::OutputMode::PAL);
}
bus_->speaker_->set_input_rate((float)(clock_rate / (double)CPUTicksPerAudioTick));
bus_->speaker_->set_high_frequency_cut_off((float)(clock_rate / ((double)CPUTicksPerAudioTick * 2.0)));
set_clock_rate(clock_rate);
}
}
}
void Machine::synchronise()
{
update_audio();
}

View File

@@ -13,21 +13,20 @@
#include "../../Processors/6502/CPU6502.hpp"
#include "../CRTMachine.hpp"
#include "Bus.hpp"
#include "PIA.hpp"
#include "Speaker.hpp"
#include "TIA.hpp"
#include "../ConfigurationTarget.hpp"
#include "Atari2600Inputs.h"
namespace Atari2600 {
const unsigned int number_of_upcoming_events = 6;
const unsigned int number_of_recorded_counters = 7;
class Machine:
public CPU6502::Processor<Machine>,
public CRTMachine::Machine,
public ConfigurationTarget::Machine {
public ConfigurationTarget::Machine,
public Outputs::CRT::Delegate {
public:
Machine();
@@ -38,137 +37,31 @@ class Machine:
void set_digital_input(Atari2600DigitalInput input, bool state);
void set_switch_is_enabled(Atari2600Switch input, bool state);
// to satisfy CPU6502::Processor
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
void synchronise();
void set_reset_line(bool state) { bus_->set_reset_line(state); }
// to satisfy CRTMachine::Machine
virtual void setup_output(float aspect_ratio);
virtual void close_output();
virtual std::shared_ptr<Outputs::CRT::CRT> get_crt() { return crt_; }
virtual std::shared_ptr<Outputs::Speaker> get_speaker() { return speaker_; }
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
// TODO: different rate for PAL
virtual std::shared_ptr<Outputs::CRT::CRT> get_crt() { return bus_->tia_->get_crt(); }
virtual std::shared_ptr<Outputs::Speaker> get_speaker() { return bus_->speaker_; }
virtual void run_for_cycles(int number_of_cycles) { bus_->run_for_cycles(number_of_cycles); }
// to satisfy Outputs::CRT::Delegate
virtual void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs);
private:
uint8_t *rom_, *rom_pages_[4];
size_t rom_size_;
// the bus
std::unique_ptr<Bus> bus_;
// the RIOT
PIA mos6532_;
// output frame rate tracker
struct FrameRecord {
unsigned int number_of_frames;
unsigned int number_of_unexpected_vertical_syncs;
// playfield registers
uint8_t playfield_control_;
uint8_t playfield_colour_;
uint8_t background_colour_;
uint8_t playfield_[41];
// ... and derivatives
int ball_size_, missile_size_[2];
// delayed clock events
enum OutputState {
Sync,
Blank,
ColourBurst,
Pixel
};
struct Event {
enum Action {
Playfield = 1 << 0,
ResetCounter = 1 << 1,
HMoveSetup = 1 << 2,
HMoveCompare = 1 << 3,
HMoveDecrement = 1 << 4,
};
int updates;
OutputState state;
uint8_t playfield_pixel;
int counter;
Event() : updates(0), playfield_pixel(0) {}
} upcoming_events_[number_of_upcoming_events];
unsigned int upcoming_events_pointer_;
// object counters
struct ObjectCounter {
int count; // the counter value, multiplied by four, counting phase
int pixel; // for non-sprite objects, a count of cycles since the last counter reset; for sprite objects a count of pixels so far elapsed
int broad_pixel; // for sprite objects, a count of cycles since the last counter reset; otherwise unused
ObjectCounter() : count(0), pixel(0), broad_pixel(0) {}
} object_counter_[number_of_recorded_counters][5];
unsigned int object_counter_pointer_;
// the latched playfield output
uint8_t playfield_output_, next_playfield_output_;
// player registers
uint8_t player_colour_[2];
uint8_t player_reflection_mask_[2];
uint8_t player_graphics_[2][2];
uint8_t player_graphics_selector_[2];
// object flags
bool has_second_copy_[2];
bool has_third_copy_[2];
bool has_fourth_copy_[2];
uint8_t object_motions_[5]; // the value stored to this counter's motion register
// player + missile registers
uint8_t player_and_missile_size_[2];
// missile registers
uint8_t missile_graphics_enable_[2];
bool missile_graphics_reset_[2];
// ball registers
uint8_t ball_graphics_enable_[2];
uint8_t ball_graphics_selector_;
// graphics output
unsigned int horizontal_timer_;
bool vsync_enabled_, vblank_enabled_;
// horizontal motion control
uint8_t hmove_counter_;
uint8_t hmove_flags_;
// joystick state
uint8_t tia_input_value_[2];
// collisions
uint8_t collisions_[8];
void output_pixels(unsigned int count);
uint8_t get_output_pixel();
void update_timers(int mask);
// outputs
std::shared_ptr<Outputs::CRT::CRT> crt_;
std::shared_ptr<Speaker> speaker_;
// current mode
bool is_pal_region_;
// speaker backlog accumlation counter
unsigned int cycles_since_speaker_update_;
void update_audio();
// latched output state
unsigned int last_output_state_duration_;
OutputState state_by_extend_time_[2][57];
OutputState *state_by_time_;
OutputState last_output_state_;
uint8_t *output_buffer_;
// lookup table for collision reporting
uint8_t reported_collisions_[64][8];
void setup_reported_collisions();
FrameRecord() : number_of_frames(0), number_of_unexpected_vertical_syncs(0) {}
} frame_records_[4];
unsigned int frame_record_pointer_;
bool is_ntsc_;
};
}

View File

@@ -0,0 +1,64 @@
//
// Bus.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_Bus_hpp
#define Atari2600_Bus_hpp
#include "Atari2600.hpp"
#include "PIA.hpp"
#include "Speaker.hpp"
#include "TIA.hpp"
namespace Atari2600 {
class Bus {
public:
Bus() :
tia_input_value_{0xff, 0xff},
cycles_since_speaker_update_(0),
cycles_since_video_update_(0),
cycles_since_6532_update_(0) {}
virtual void run_for_cycles(int number_of_cycles) = 0;
virtual void set_reset_line(bool state) = 0;
// the RIOT, TIA and speaker
PIA mos6532_;
std::shared_ptr<TIA> tia_;
std::shared_ptr<Speaker> speaker_;
// joystick state
uint8_t tia_input_value_[2];
protected:
// speaker backlog accumlation counter
unsigned int cycles_since_speaker_update_;
inline void update_audio() {
unsigned int audio_cycles = cycles_since_speaker_update_ / (CPUTicksPerAudioTick * 3);
cycles_since_speaker_update_ %= (CPUTicksPerAudioTick * 3);
speaker_->run_for_cycles(audio_cycles);
}
// video backlog accumulation counter
unsigned int cycles_since_video_update_;
inline void update_video() {
tia_->run_for_cycles((int)cycles_since_video_update_);
cycles_since_video_update_ = 0;
}
// RIOT backlog accumulation counter
unsigned int cycles_since_6532_update_;
inline void update_6532() {
mos6532_.run_for_cycles(cycles_since_6532_update_);
cycles_since_6532_update_ = 0;
}
};
}
#endif /* Atari2600_Bus_hpp */

View File

@@ -0,0 +1,176 @@
//
// Cartridge.h
// Clock Signal
//
// Created by Thomas Harte on 17/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_Cartridge_hpp
#define Atari2600_Cartridge_hpp
#include "../../../Processors/6502/CPU6502.hpp"
#include "../Bus.hpp"
namespace Atari2600 {
template<class T> class Cartridge:
public CPU6502::Processor<Cartridge<T>>,
public Bus {
public:
Cartridge(const std::vector<uint8_t> &rom) :
rom_(rom) {}
void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Cartridge<T>>::run_for_cycles(number_of_cycles); }
void set_reset_line(bool state) { CPU6502::Processor<Cartridge<T>>::set_reset_line(state); }
void advance_cycles(unsigned int cycles) {}
// to satisfy CPU6502::Processor
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
uint8_t returnValue = 0xff;
unsigned int cycles_run_for = 3;
// this occurs as a feedback loop — the 2600 requests ready, then performs the cycles_run_for
// leap to the end of ready only once ready is signalled — because on a 6502 ready doesn't take
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// skips to the end of the line.
if(operation == CPU6502::BusOperation::Ready)
cycles_run_for = (unsigned int)tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_);
cycles_since_speaker_update_ += cycles_run_for;
cycles_since_video_update_ += cycles_run_for;
cycles_since_6532_update_ += (cycles_run_for / 3);
static_cast<T *>(this)->advance_cycles(cycles_run_for / 3);
if(operation != CPU6502::BusOperation::Ready) {
// give the cartridge a chance to respond to the bus access
static_cast<T *>(this)->perform_bus_operation(operation, address, value);
// check for a RIOT RAM access
if((address&0x1280) == 0x80) {
if(isReadOperation(operation)) {
returnValue &= mos6532_.get_ram(address);
} else {
mos6532_.set_ram(address, *value);
}
}
// check for a TIA access
if(!(address&0x1080)) {
if(isReadOperation(operation)) {
const uint16_t decodedAddress = address & 0xf;
switch(decodedAddress) {
case 0x00: // missile 0 / player collisions
case 0x01: // missile 1 / player collisions
case 0x02: // player 0 / playfield / ball collisions
case 0x03: // player 1 / playfield / ball collisions
case 0x04: // missile 0 / playfield / ball collisions
case 0x05: // missile 1 / playfield / ball collisions
case 0x06: // ball / playfield collisions
case 0x07: // player / player, missile / missile collisions
returnValue &= tia_->get_collision_flags(decodedAddress);
break;
case 0x08:
case 0x09:
case 0x0a:
case 0x0b:
// TODO: pot ports
returnValue &= 0;
break;
case 0x0c:
case 0x0d:
returnValue &= tia_input_value_[decodedAddress - 0x0c];
break;
}
} else {
const uint16_t decodedAddress = address & 0x3f;
switch(decodedAddress) {
case 0x00: update_video(); tia_->set_sync(*value & 0x02); break;
case 0x01: update_video(); tia_->set_blank(*value & 0x02); break;
case 0x02: CPU6502::Processor<Cartridge<T>>::set_ready_line(true); break;
case 0x03: update_video(); tia_->reset_horizontal_counter(); break;
// TODO: audio will now be out of synchronisation — fix
case 0x04:
case 0x05: update_video(); tia_->set_player_number_and_size(decodedAddress - 0x04, *value); break;
case 0x06:
case 0x07: update_video(); tia_->set_player_missile_colour(decodedAddress - 0x06, *value); break;
case 0x08: update_video(); tia_->set_playfield_ball_colour(*value); break;
case 0x09: update_video(); tia_->set_background_colour(*value); break;
case 0x0a: update_video(); tia_->set_playfield_control_and_ball_size(*value); break;
case 0x0b:
case 0x0c: update_video(); tia_->set_player_reflected(decodedAddress - 0x0b, !((*value)&8)); break;
case 0x0d:
case 0x0e:
case 0x0f: update_video(); tia_->set_playfield(decodedAddress - 0x0d, *value); break;
case 0x10:
case 0x11: update_video(); tia_->set_player_position(decodedAddress - 0x10); break;
case 0x12:
case 0x13: update_video(); tia_->set_missile_position(decodedAddress - 0x12); break;
case 0x14: update_video(); tia_->set_ball_position(); break;
case 0x1b:
case 0x1c: update_video(); tia_->set_player_graphic(decodedAddress - 0x1b, *value); break;
case 0x1d:
case 0x1e: update_video(); tia_->set_missile_enable(decodedAddress - 0x1d, (*value)&2); break;
case 0x1f: update_video(); tia_->set_ball_enable((*value)&2); break;
case 0x20:
case 0x21: update_video(); tia_->set_player_motion(decodedAddress - 0x20, *value); break;
case 0x22:
case 0x23: update_video(); tia_->set_missile_motion(decodedAddress - 0x22, *value); break;
case 0x24: update_video(); tia_->set_ball_motion(*value); break;
case 0x25:
case 0x26: tia_->set_player_delay(decodedAddress - 0x25, (*value)&1); break;
case 0x27: tia_->set_ball_delay((*value)&1); break;
case 0x28:
case 0x29: update_video(); tia_->set_missile_position_to_player(decodedAddress - 0x28, (*value)&2); break;
case 0x2a: update_video(); tia_->move(); break;
case 0x2b: update_video(); tia_->clear_motion(); break;
case 0x2c: update_video(); tia_->clear_collision_flags(); break;
case 0x15:
case 0x16: update_audio(); speaker_->set_control(decodedAddress - 0x15, *value); break;
case 0x17:
case 0x18: update_audio(); speaker_->set_divider(decodedAddress - 0x17, *value); break;
case 0x19:
case 0x1a: update_audio(); speaker_->set_volume(decodedAddress - 0x19, *value); break;
}
}
}
// check for a PIA access
if((address&0x1280) == 0x280) {
update_6532();
if(isReadOperation(operation)) {
returnValue &= mos6532_.get_register(address);
} else {
mos6532_.set_register(address, *value);
}
}
if(isReadOperation(operation)) {
*value &= returnValue;
}
}
if(!tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_)) CPU6502::Processor<Cartridge<T>>::set_ready_line(false);
return cycles_run_for / 3;
}
void synchronise() {
update_audio();
update_video();
speaker_->flush();
}
protected:
std::vector<uint8_t> rom_;
};
}
#endif /* Atari2600_Cartridge_hpp */

View File

@@ -0,0 +1,50 @@
//
// CartridgeActivisionStack.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeActivisionStack_hpp
#define Atari2600_CartridgeActivisionStack_hpp
namespace Atari2600 {
class CartridgeActivisionStack: public Cartridge<CartridgeActivisionStack> {
public:
CartridgeActivisionStack(const std::vector<uint8_t> &rom) :
Cartridge(rom),
last_opcode_(0x00) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
if(!(address & 0x1000)) return;
// This is a bit of a hack; a real cartridge can't see either the sync or read lines, and can't see
// address line 13. Instead it looks for a pattern in recent address accesses that would imply an
// RST or JSR.
if(operation == CPU6502::BusOperation::ReadOpcode && (last_opcode_ == 0x20 || last_opcode_ == 0x60)) {
if(address & 0x2000) {
rom_ptr_ = rom_.data();
} else {
rom_ptr_ = rom_.data() + 4096;
}
}
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
if(operation == CPU6502::BusOperation::ReadOpcode) last_opcode_ = *value;
}
private:
uint8_t *rom_ptr_;
uint8_t last_opcode_;
};
}
#endif /* Atari2600_CartridgeActivisionStack_hpp */

View File

@@ -0,0 +1,66 @@
//
// CartridgeAtari8k.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeAtari16k_hpp
#define Atari2600_CartridgeAtari16k_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeAtari16k: public Cartridge<CartridgeAtari16k> {
public:
CartridgeAtari16k(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1ff6 && address <= 0x1ff9) rom_ptr_ = rom_.data() + (address - 0x1ff6) * 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
}
private:
uint8_t *rom_ptr_;
};
class CartridgeAtari16kSuperChip: public Cartridge<CartridgeAtari16kSuperChip> {
public:
CartridgeAtari16kSuperChip(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1ff6 && address <= 0x1ff9) rom_ptr_ = rom_.data() + (address - 0x1ff6) * 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
if(address < 0x1080) ram_[address & 0x7f] = *value;
else if(address < 0x1100 && isReadOperation(operation)) *value = ram_[address & 0x7f];
}
private:
uint8_t *rom_ptr_;
uint8_t ram_[128];
};
}
#endif /* Atari2600_CartridgeAtari16k_hpp */

View File

@@ -0,0 +1,66 @@
//
// CartridgeAtari8k.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeAtari32k_hpp
#define Atari2600_CartridgeAtari32k_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeAtari32k: public Cartridge<CartridgeAtari32k> {
public:
CartridgeAtari32k(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1ff4 && address <= 0x1ffb) rom_ptr_ = rom_.data() + (address - 0x1ff4) * 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
}
private:
uint8_t *rom_ptr_;
};
class CartridgeAtari32kSuperChip: public Cartridge<CartridgeAtari32kSuperChip> {
public:
CartridgeAtari32kSuperChip(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1ff4 && address <= 0x1ffb) rom_ptr_ = rom_.data() + (address - 0x1ff4) * 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
if(address < 0x1080) ram_[address & 0x7f] = *value;
else if(address < 0x1100 && isReadOperation(operation)) *value = ram_[address & 0x7f];
}
private:
uint8_t *rom_ptr_;
uint8_t ram_[128];
};
}
#endif /* Atari2600_CartridgeAtari32k_hpp */

View File

@@ -0,0 +1,68 @@
//
// CartridgeAtari8k.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeAtari8k_hpp
#define Atari2600_CartridgeAtari8k_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeAtari8k: public Cartridge<CartridgeAtari8k> {
public:
CartridgeAtari8k(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address == 0x1ff8) rom_ptr_ = rom_.data();
else if(address == 0x1ff9) rom_ptr_ = rom_.data() + 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
}
private:
uint8_t *rom_ptr_;
};
class CartridgeAtari8kSuperChip: public Cartridge<CartridgeAtari8kSuperChip> {
public:
CartridgeAtari8kSuperChip(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address == 0x1ff8) rom_ptr_ = rom_.data();
if(address == 0x1ff9) rom_ptr_ = rom_.data() + 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
if(address < 0x1080) ram_[address & 0x7f] = *value;
else if(address < 0x1100 && isReadOperation(operation)) *value = ram_[address & 0x7f];
}
private:
uint8_t *rom_ptr_;
uint8_t ram_[128];
};
}
#endif /* Atari2600_CartridgeAtari8k_hpp */

View File

@@ -0,0 +1,44 @@
//
// CartridgeCBSRAMPlus.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeCBSRAMPlus_hpp
#define Atari2600_CartridgeCBSRAMPlus_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeCBSRAMPlus: public Cartridge<CartridgeCBSRAMPlus> {
public:
CartridgeCBSRAMPlus(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1ff8 && address <= 0x1ffa) rom_ptr_ = rom_.data() + (address - 0x1ff8) * 4096;
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
if(address < 0x1100) ram_[address & 0xff] = *value;
else if(address < 0x1200 && isReadOperation(operation)) *value = ram_[address & 0xff];
}
private:
uint8_t *rom_ptr_;
uint8_t ram_[256];
};
}
#endif /* Atari2600_CartridgeCBSRAMPlus_hpp */

View File

@@ -0,0 +1,42 @@
//
// CartridgeCommaVid.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeCommaVid_hpp
#define Atari2600_CartridgeCommaVid_hpp
namespace Atari2600 {
class CartridgeCommaVid: public Cartridge<CartridgeCommaVid> {
public:
CartridgeCommaVid(const std::vector<uint8_t> &rom) :
Cartridge(rom) {}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
if(!(address & 0x1000)) return;
address &= 0x1fff;
if(address < 0x1400) {
if(isReadOperation(operation)) *value = ram_[address & 1023];
return;
}
if(address < 0x1800) {
ram_[address & 1023] = *value;
return;
}
if(isReadOperation(operation)) *value = rom_[address & 2047];
}
private:
uint8_t ram_[1024];
};
}
#endif /* Atari2600_CartridgeCommaVid_hpp */

View File

@@ -0,0 +1,68 @@
//
// CartridgeMNetwork.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeMNetwork_hpp
#define Atari2600_CartridgeMNetwork_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeMNetwork: public Cartridge<CartridgeMNetwork> {
public:
CartridgeMNetwork(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_[0] = rom_.data() + rom_.size() - 4096;
rom_ptr_[1] = rom_ptr_[0] + 2048;
high_ram_ptr_ = high_ram_;
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1fe0 && address <= 0x1fe6) {
rom_ptr_[0] = rom_.data() + (address - 0x1fe0) * 2048;
} else if(address == 0x1fe7) {
rom_ptr_[0] = nullptr;
} else if(address >= 0x1ff8 && address <= 0x1ffb) {
int offset = (address - 0x1ff8) * 256;
high_ram_ptr_ = &high_ram_[offset];
}
if(address & 0x800) {
if(address < 0x1900) {
high_ram_ptr_[address & 255] = *value;
} else if(address < 0x1a00) {
if(isReadOperation(operation)) *value = high_ram_ptr_[address & 255];
} else {
if(isReadOperation(operation)) *value = rom_ptr_[1][address & 2047];
}
} else {
if(rom_ptr_[0]) {
if(isReadOperation(operation)) *value = rom_ptr_[0][address & 2047];
} else {
if(address < 0x1400) {
low_ram_[address & 1023] = *value;
} else {
if(isReadOperation(operation)) *value = low_ram_[address & 1023];
}
}
}
}
private:
uint8_t *rom_ptr_[2];
uint8_t *high_ram_ptr_;
uint8_t low_ram_[1024], high_ram_[1024];
};
}
#endif /* Atari2600_CartridgeMNetwork_hpp */

View File

@@ -0,0 +1,45 @@
//
// CartridgeMegaBoy.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeMegaBoy_hpp
#define Atari2600_CartridgeMegaBoy_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeMegaBoy: public Cartridge<CartridgeMegaBoy> {
public:
CartridgeMegaBoy(const std::vector<uint8_t> &rom) :
Cartridge(rom),
current_page_(0) {
rom_ptr_ = rom_.data();
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address == 0x1ff0) {
current_page_ = (current_page_ + 1) & 15;
rom_ptr_ = rom_.data() + current_page_ * 4096;
}
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
}
private:
uint8_t *rom_ptr_;
uint8_t current_page_;
};
}
#endif /* CartridgeMegaBoy_h */

View File

@@ -0,0 +1,46 @@
//
// CartridgeParkerBros.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeParkerBros_hpp
#define Atari2600_CartridgeParkerBros_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeParkerBros: public Cartridge<CartridgeParkerBros> {
public:
CartridgeParkerBros(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_[0] = rom_.data() + 4096;
rom_ptr_[1] = rom_ptr_[0] + 1024;
rom_ptr_[2] = rom_ptr_[1] + 1024;
rom_ptr_[3] = rom_ptr_[2] + 1024;
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
if(address >= 0x1fe0 && address < 0x1ff8) {
int slot = (address >> 3)&3;
rom_ptr_[slot] = rom_.data() + ((address & 7) * 1024);
}
if(isReadOperation(operation)) {
*value = rom_ptr_[(address >> 10)&3][address & 1023];
}
}
private:
uint8_t *rom_ptr_[4];
};
}
#endif /* Atari2600_CartridgeParkerBros_hpp */

View File

@@ -0,0 +1,134 @@
//
// CartridgePitfall2.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgePitfall2_hpp
#define Atari2600_CartridgePitfall2_hpp
namespace Atari2600 {
class CartridgePitfall2: public Cartridge<CartridgePitfall2> {
public:
CartridgePitfall2(const std::vector<uint8_t> &rom) :
Cartridge(rom),
random_number_generator_(0),
featcher_address_{0, 0, 0, 0, 0, 0, 0, 0},
mask_{0, 0, 0, 0, 0, 0, 0, 0},
cycles_since_audio_update_(0) {
rom_ptr_ = rom_.data();
}
void advance_cycles(unsigned int cycles) {
cycles_since_audio_update_ += cycles;
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
address &= 0x1fff;
if(!(address & 0x1000)) return;
switch(address) {
#pragma mark - Reads
// The random number generator
case 0x1000: case 0x1001: case 0x1002: case 0x1003: case 0x1004:
if(isReadOperation(operation)) {
*value = random_number_generator_;
}
random_number_generator_ = (uint8_t)(
(random_number_generator_ << 1) |
(~( (random_number_generator_ >> 7) ^
(random_number_generator_ >> 5) ^
(random_number_generator_ >> 4) ^
(random_number_generator_ >> 3)
) & 1));
break;
case 0x1005: case 0x1006: case 0x1007:
*value = update_audio();
break;
case 0x1008: case 0x1009: case 0x100a: case 0x100b: case 0x100c: case 0x100d: case 0x100e: case 0x100f:
*value = rom_[8192 + address_for_counter(address & 7)];
break;
case 0x1010: case 0x1011: case 0x1012: case 0x1013: case 0x1014: case 0x1015: case 0x1016: case 0x1017:
*value = rom_[8192 + address_for_counter(address & 7)] & mask_[address & 7];
break;
#pragma mark - Writes
case 0x1040: case 0x1041: case 0x1042: case 0x1043: case 0x1044: case 0x1045: case 0x1046: case 0x1047:
top_[address & 7] = *value;
break;
case 0x1048: case 0x1049: case 0x104a: case 0x104b: case 0x104c: case 0x104d: case 0x104e: case 0x104f:
bottom_[address & 7] = *value;
break;
case 0x1050: case 0x1051: case 0x1052: case 0x1053: case 0x1054: case 0x1055: case 0x1056: case 0x1057:
featcher_address_[address & 7] = (featcher_address_[address & 7] & 0xff00) | *value;
mask_[address & 7] = 0x00;
break;
case 0x1058: case 0x1059: case 0x105a: case 0x105b: case 0x105c: case 0x105d: case 0x105e: case 0x105f:
featcher_address_[address & 7] = (featcher_address_[address & 7] & 0x00ff) | (uint16_t)(*value << 8);
break;
case 0x1070: case 0x1071: case 0x1072: case 0x1073: case 0x1074: case 0x1075: case 0x1076: case 0x1077:
random_number_generator_ = 0;
break;
#pragma mark - Paging
case 0x1ff8: rom_ptr_ = rom_.data(); break;
case 0x1ff9: rom_ptr_ = rom_.data() + 4096; break;
#pragma mark - Business as usual
default:
if(isReadOperation(operation)) {
*value = rom_ptr_[address & 4095];
}
break;
}
}
private:
inline uint16_t address_for_counter(int counter) {
uint16_t fetch_address = (featcher_address_[counter] & 2047) ^ 2047;
if((featcher_address_[counter] & 0xff) == top_[counter]) mask_[counter] = 0xff;
if((featcher_address_[counter] & 0xff) == bottom_[counter]) mask_[counter] = 0x00;
featcher_address_[counter]--;
return fetch_address;
}
inline uint8_t update_audio() {
const unsigned int clock_divisor = 57;
unsigned int cycles_to_run_for = cycles_since_audio_update_ / clock_divisor;
cycles_since_audio_update_ %= clock_divisor;
int table_position = 0;
for(int c = 0; c < 3; c++) {
audio_channel_[c] = (audio_channel_[c] + cycles_to_run_for) % (1 + top_[5 + c]);
if((featcher_address_[5 + c] & 0x1000) && ((top_[5 + c] - audio_channel_[c]) > bottom_[5 + c])) {
table_position |= 0x4 >> c;
}
}
static uint8_t level_table[8] = { 0x0, 0x4, 0x5, 0x9, 0x6, 0xa, 0xb, 0xf };
return level_table[table_position];
}
uint16_t featcher_address_[8];
uint8_t top_[8], bottom_[8], mask_[8];
uint8_t music_mode_[3];
uint8_t random_number_generator_;
uint8_t *rom_ptr_;
uint8_t audio_channel_[3];
unsigned int cycles_since_audio_update_;
};
}
#endif /* Atari2600_CartridgePitfall2_hpp */

View File

@@ -0,0 +1,40 @@
//
// CartridgeTigervision.h
// Clock Signal
//
// Created by Thomas Harte on 18/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeTigervision_hpp
#define Atari2600_CartridgeTigervision_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeTigervision: public Cartridge<CartridgeTigervision> {
public:
CartridgeTigervision(const std::vector<uint8_t> &rom) :
Cartridge(rom) {
rom_ptr_[0] = rom_.data() + rom_.size() - 4096;
rom_ptr_[1] = rom_ptr_[0] + 2048;
}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
if((address&0x1fff) == 0x3f) {
int offset = ((*value) * 2048) & (rom_.size() - 1);
rom_ptr_[0] = rom_.data() + offset;
return;
} else if((address&0x1000) && isReadOperation(operation)) {
*value = rom_ptr_[(address >> 11)&1][address & 2047];
}
}
private:
uint8_t *rom_ptr_[2];
};
}
#endif /* Atari2600_CartridgeTigervision_hpp */

View File

@@ -0,0 +1,30 @@
//
// CartridgeUnpaged.h
// Clock Signal
//
// Created by Thomas Harte on 17/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Atari2600_CartridgeUnpaged_hpp
#define Atari2600_CartridgeUnpaged_hpp
#include "Cartridge.hpp"
namespace Atari2600 {
class CartridgeUnpaged: public Cartridge<CartridgeUnpaged> {
public:
CartridgeUnpaged(const std::vector<uint8_t> &rom) :
Cartridge(rom) {}
void perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
if(isReadOperation(operation) && (address & 0x1000)) {
*value = rom_[address & (rom_.size() - 1)];
}
}
};
}
#endif /* Atari2600_CartridgeUnpaged_hpp */

View File

@@ -15,13 +15,11 @@ namespace Atari2600 {
class PIA: public MOS::MOS6532<PIA> {
public:
inline uint8_t get_port_input(int port)
{
inline uint8_t get_port_input(int port) {
return port_values_[port];
}
inline void update_port_input(int port, uint8_t mask, bool set)
{
inline void update_port_input(int port, uint8_t mask, bool set) {
if(set) port_values_[port] &= ~mask; else port_values_[port] |= mask;
set_port_did_change(port);
}

View File

@@ -16,23 +16,20 @@ Atari2600::Speaker::Speaker() :
poly9_counter_{0x1ff, 0x1ff}
{}
void Atari2600::Speaker::set_volume(int channel, uint8_t volume)
{
void Atari2600::Speaker::set_volume(int channel, uint8_t volume) {
enqueue([=]() {
volume_[channel] = volume & 0xf;
});
}
void Atari2600::Speaker::set_divider(int channel, uint8_t divider)
{
void Atari2600::Speaker::set_divider(int channel, uint8_t divider) {
enqueue([=]() {
divider_[channel] = divider & 0x1f;
divider_counter_[channel] = 0;
});
}
void Atari2600::Speaker::set_control(int channel, uint8_t control)
{
void Atari2600::Speaker::set_control(int channel, uint8_t control) {
enqueue([=]() {
control_[channel] = control & 0xf;
});
@@ -42,41 +39,37 @@ void Atari2600::Speaker::set_control(int channel, uint8_t control)
#define advance_poly5(c) poly5_counter_[channel] = (poly5_counter_[channel] >> 1) | (((poly5_counter_[channel] << 4) ^ (poly5_counter_[channel] << 2))&0x010)
#define advance_poly9(c) poly9_counter_[channel] = (poly9_counter_[channel] >> 1) | (((poly9_counter_[channel] << 4) ^ (poly9_counter_[channel] << 8))&0x100)
void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
{
for(unsigned int c = 0; c < number_of_samples; c++)
{
void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *target) {
for(unsigned int c = 0; c < number_of_samples; c++) {
target[c] = 0;
for(int channel = 0; channel < 2; channel++)
{
for(int channel = 0; channel < 2; channel++) {
divider_counter_[channel] ++;
int divider_value = divider_counter_[channel] / (38 / CPUTicksPerAudioTick);
int level = 0;
switch(control_[channel])
{
switch(control_[channel]) {
case 0x0: case 0xb: // constant 1
level = 1;
break;
case 0x4: case 0x5: // div2 tone
level = (divider_counter_[channel] / (divider_[channel]+1))&1;
level = (divider_value / (divider_[channel]+1))&1;
break;
case 0xc: case 0xd: // div6 tone
level = (divider_counter_[channel] / ((divider_[channel]+1)*3))&1;
level = (divider_value / ((divider_[channel]+1)*3))&1;
break;
case 0x6: case 0xa: // div31 tone
level = (divider_counter_[channel] / (divider_[channel]+1))%30 <= 18;
level = (divider_value / (divider_[channel]+1))%30 <= 18;
break;
case 0xe: // div93 tone
level = (divider_counter_[channel] / ((divider_[channel]+1)*3))%30 <= 18;
level = (divider_value / ((divider_[channel]+1)*3))%30 <= 18;
break;
case 0x1: // 4-bit poly
level = poly4_counter_[channel]&1;
if(divider_counter_[channel] == divider_[channel]+1)
{
if(divider_value == divider_[channel]+1) {
divider_counter_[channel] = 0;
advance_poly4(channel);
}
@@ -84,18 +77,15 @@ void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *ta
case 0x2: // 4-bit poly div31
level = poly4_counter_[channel]&1;
if(divider_counter_[channel]%(30*(divider_[channel]+1)) == 18)
{
if(divider_value%(30*(divider_[channel]+1)) == 18) {
advance_poly4(channel);
}
break;
case 0x3: // 5/4-bit poly
level = output_state_[channel];
if(divider_counter_[channel] == divider_[channel]+1)
{
if(poly5_counter_[channel]&1)
{
if(divider_value == divider_[channel]+1) {
if(poly5_counter_[channel]&1) {
output_state_[channel] = poly4_counter_[channel]&1;
advance_poly4(channel);
}
@@ -105,8 +95,7 @@ void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *ta
case 0x7: case 0x9: // 5-bit poly
level = poly5_counter_[channel]&1;
if(divider_counter_[channel] == divider_[channel]+1)
{
if(divider_value == divider_[channel]+1) {
divider_counter_[channel] = 0;
advance_poly5(channel);
}
@@ -114,8 +103,7 @@ void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *ta
case 0xf: // 5-bit poly div6
level = poly5_counter_[channel]&1;
if(divider_counter_[channel] == (divider_[channel]+1)*3)
{
if(divider_value == (divider_[channel]+1)*3) {
divider_counter_[channel] = 0;
advance_poly5(channel);
}
@@ -123,8 +111,7 @@ void Atari2600::Speaker::get_samples(unsigned int number_of_samples, int16_t *ta
case 0x8: // 9-bit poly
level = poly9_counter_[channel]&1;
if(divider_counter_[channel] == divider_[channel]+1)
{
if(divider_value == divider_[channel]+1) {
divider_counter_[channel] = 0;
advance_poly9(channel);
}

View File

@@ -13,6 +13,10 @@
namespace Atari2600 {
// This should be a divisor of 38; audio counters are updated every 38 cycles — though lesser dividers
// will give greater resolution to changes in audio state. 1, 2 and 19 are the only divisors of 38.
const int CPUTicksPerAudioTick = 2;
class Speaker: public ::Outputs::Filter<Speaker> {
public:
Speaker();

679
Machines/Atari2600/TIA.cpp Normal file
View File

@@ -0,0 +1,679 @@
//
// TIA.cpp
// Clock Signal
//
// Created by Thomas Harte on 28/01/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "TIA.hpp"
#include <cassert>
using namespace Atari2600;
namespace {
const int cycles_per_line = 228;
const int first_pixel_cycle = 68;
const int sync_flag = 0x1;
const int blank_flag = 0x2;
uint8_t reverse_table[256];
}
TIA::TIA(bool create_crt) :
horizontal_counter_(0),
pixels_start_location_(0),
output_mode_(0),
pixel_target_(nullptr),
background_{0, 0},
background_half_mask_(0),
horizontal_blank_extend_(false),
collision_flags_(0)
{
if(create_crt) {
crt_.reset(new Outputs::CRT::CRT(cycles_per_line * 2 - 1, 1, Outputs::CRT::DisplayType::NTSC60, 1));
crt_->set_output_device(Outputs::CRT::Television);
set_output_mode(OutputMode::NTSC);
}
for(int c = 0; c < 256; c++) {
reverse_table[c] = (uint8_t)(
((c & 0x01) << 7) | ((c & 0x02) << 5) | ((c & 0x04) << 3) | ((c & 0x08) << 1) |
((c & 0x10) >> 1) | ((c & 0x20) >> 3) | ((c & 0x40) >> 5) | ((c & 0x80) >> 7)
);
}
for(int c = 0; c < 64; c++) {
bool has_playfield = c & (int)(CollisionType::Playfield);
bool has_ball = c & (int)(CollisionType::Ball);
bool has_player0 = c & (int)(CollisionType::Player0);
bool has_player1 = c & (int)(CollisionType::Player1);
bool has_missile0 = c & (int)(CollisionType::Missile0);
bool has_missile1 = c & (int)(CollisionType::Missile1);
uint8_t collision_registers[8];
collision_registers[0] = ((has_missile0 && has_player1) ? 0x80 : 0x00) | ((has_missile0 && has_player0) ? 0x40 : 0x00);
collision_registers[1] = ((has_missile1 && has_player0) ? 0x80 : 0x00) | ((has_missile1 && has_player1) ? 0x40 : 0x00);
collision_registers[2] = ((has_playfield && has_player0) ? 0x80 : 0x00) | ((has_ball && has_player0) ? 0x40 : 0x00);
collision_registers[3] = ((has_playfield && has_player1) ? 0x80 : 0x00) | ((has_ball && has_player1) ? 0x40 : 0x00);
collision_registers[4] = ((has_playfield && has_missile0) ? 0x80 : 0x00) | ((has_ball && has_missile0) ? 0x40 : 0x00);
collision_registers[5] = ((has_playfield && has_missile1) ? 0x80 : 0x00) | ((has_ball && has_missile1) ? 0x40 : 0x00);
collision_registers[6] = ((has_playfield && has_ball) ? 0x80 : 0x00);
collision_registers[7] = ((has_player0 && has_player1) ? 0x80 : 0x00) | ((has_missile0 && has_missile1) ? 0x40 : 0x00);
collision_flags_by_buffer_vaules_[c] =
(collision_registers[0] >> 6) |
(collision_registers[1] >> 4) |
(collision_registers[2] >> 2) |
(collision_registers[3] >> 0) |
(collision_registers[4] << 2) |
(collision_registers[5] << 4) |
(collision_registers[6] << 6) |
(collision_registers[7] << 8);
// all priority modes show the background if nothing else is present
colour_mask_by_mode_collision_flags_[(int)ColourMode::Standard][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::OnTop][c] = (uint8_t)ColourIndex::Background;
// test 1 for standard priority: if there is a playfield or ball pixel, plot that colour
if(has_playfield || has_ball) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::Standard][c] = (uint8_t)ColourIndex::PlayfieldBall;
}
// test 1 for score mode: if there is a ball pixel, plot that colour
if(has_ball) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][c] = (uint8_t)ColourIndex::PlayfieldBall;
}
// test 1 for on-top mode, test 2 for everbody else: if there is a player 1 or missile 1 pixel, plot that colour
if(has_player1 || has_missile1) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::Standard][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::OnTop][c] = (uint8_t)ColourIndex::PlayerMissile1;
}
// in the right-hand side of score mode, the playfield has the same priority as player 1
if(has_playfield) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][c] = (uint8_t)ColourIndex::PlayerMissile1;
}
// next test for everybody: if there is a player 0 or missile 0 pixel, plot that colour instead
if(has_player0 || has_missile0) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::Standard][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][c] =
colour_mask_by_mode_collision_flags_[(int)ColourMode::OnTop][c] = (uint8_t)ColourIndex::PlayerMissile0;
}
// if this is the left-hand side of score mode, the playfield has the same priority as player 0
if(has_playfield) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][c] = (uint8_t)ColourIndex::PlayerMissile0;
}
// a final test for 'on top' priority mode: if the playfield or ball are visible, prefer that colour to all others
if(has_playfield || has_ball) {
colour_mask_by_mode_collision_flags_[(int)ColourMode::OnTop][c] = (uint8_t)ColourIndex::PlayfieldBall;
}
}
}
TIA::TIA() : TIA(true) {}
TIA::TIA(std::function<void(uint8_t *output_buffer)> line_end_function) : TIA(false) {
line_end_function_ = line_end_function;
}
void TIA::set_output_mode(Atari2600::TIA::OutputMode output_mode) {
Outputs::CRT::DisplayType display_type;
if(output_mode == OutputMode::NTSC) {
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"float phaseOffset = 6.283185308 * float(iPhase) / 13.0 + 5.074880441076923;"
"return mix(float(y) / 14.0, step(1, iPhase) * cos(phase + phaseOffset), amplitude);"
"}");
display_type = Outputs::CRT::DisplayType::NTSC60;
} else {
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"uint direction = iPhase & 1u;"
"float phaseOffset = float(7u - direction) + (float(direction) - 0.5) * 2.0 * float(iPhase >> 1);"
"phaseOffset *= 6.283185308 / 12.0;"
"return mix(float(y) / 14.0, step(4, (iPhase + 2u) & 15u) * cos(phase + phaseOffset), amplitude);"
"}");
display_type = Outputs::CRT::DisplayType::PAL50;
}
// line number of cycles in a line of video is one less than twice the number of clock cycles per line; the Atari
// outputs 228 colour cycles of material per line when an NTSC line 227.5. Since all clock numbers will be doubled
// later, cycles_per_line * 2 - 1 is therefore the real length of an NTSC line, even though we're going to supply
// cycles_per_line * 2 cycles of information from one sync edge to the next
crt_->set_new_display_type(cycles_per_line * 2 - 1, display_type);
/* speaker_->set_input_rate((float)(get_clock_rate() / 38.0));*/
}
void TIA::run_for_cycles(int number_of_cycles)
{
// if part way through a line, definitely perform a partial, at most up to the end of the line
if(horizontal_counter_) {
int cycles = std::min(number_of_cycles, cycles_per_line - horizontal_counter_);
output_for_cycles(cycles);
number_of_cycles -= cycles;
}
// output full lines for as long as possible
while(number_of_cycles >= cycles_per_line) {
output_line();
number_of_cycles -= cycles_per_line;
}
// partly start a new line if necessary
if(number_of_cycles) {
output_for_cycles(number_of_cycles);
}
}
void TIA::set_sync(bool sync) {
output_mode_ = (output_mode_ & ~sync_flag) | (sync ? sync_flag : 0);
}
void TIA::set_blank(bool blank) {
output_mode_ = (output_mode_ & ~blank_flag) | (blank ? blank_flag : 0);
}
void TIA::reset_horizontal_counter() {
}
int TIA::get_cycles_until_horizontal_blank(unsigned int from_offset) {
return (cycles_per_line - (horizontal_counter_ + (int)from_offset) % cycles_per_line) % cycles_per_line;
}
void TIA::set_background_colour(uint8_t colour) {
colour_palette_[(int)ColourIndex::Background] = colour;
}
void TIA::set_playfield(uint16_t offset, uint8_t value) {
assert(offset >= 0 && offset < 3);
switch(offset) {
case 0:
background_[1] = (background_[1] & 0x0ffff) | ((uint32_t)reverse_table[value & 0xf0] << 16);
background_[0] = (background_[0] & 0xffff0) | (uint32_t)(value >> 4);
break;
case 1:
background_[1] = (background_[1] & 0xf00ff) | ((uint32_t)value << 8);
background_[0] = (background_[0] & 0xff00f) | ((uint32_t)reverse_table[value] << 4);
break;
case 2:
background_[1] = (background_[1] & 0xfff00) | reverse_table[value];
background_[0] = (background_[0] & 0x00fff) | ((uint32_t)value << 12);
break;
}
}
void TIA::set_playfield_control_and_ball_size(uint8_t value) {
background_half_mask_ = value & 1;
switch(value & 6) {
case 0:
playfield_priority_ = PlayfieldPriority::Standard;
break;
case 2:
playfield_priority_ = PlayfieldPriority::Score;
break;
case 4:
case 6:
playfield_priority_ = PlayfieldPriority::OnTop;
break;
}
ball_.size = 1 << ((value >> 4)&3);
}
void TIA::set_playfield_ball_colour(uint8_t colour) {
colour_palette_[(int)ColourIndex::PlayfieldBall] = colour;
}
void TIA::set_player_number_and_size(int player, uint8_t value) {
assert(player >= 0 && player < 2);
int size = 0;
switch(value & 7) {
case 0: case 1: case 2: case 3: case 4:
player_[player].copy_flags = value & 7;
break;
case 5:
size = 1;
player_[player].copy_flags = 0;
break;
case 6:
player_[player].copy_flags = 6;
break;
case 7:
size = 2;
player_[player].copy_flags = 0;
break;
}
missile_[player].size = 1 << ((value >> 4)&3);
missile_[player].copy_flags = player_[player].copy_flags;
player_[player].adder = 4 >> size;
}
void TIA::set_player_graphic(int player, uint8_t value) {
assert(player >= 0 && player < 2);
player_[player].graphic[1] = value;
player_[player^1].graphic[0] = player_[player^1].graphic[1];
if(player) ball_.enabled[0] = ball_.enabled[1];
}
void TIA::set_player_reflected(int player, bool reflected) {
assert(player >= 0 && player < 2);
player_[player].reverse_mask = reflected ? 7 : 0;
}
void TIA::set_player_delay(int player, bool delay) {
assert(player >= 0 && player < 2);
player_[player].graphic_index = delay ? 0 : 1;
}
void TIA::set_player_position(int player) {
assert(player >= 0 && player < 2);
// players have an extra clock of delay before output and don't display upon reset;
// both aims are achieved by setting to -1 because: (i) it causes the clock to be
// one behind its real hardware value, creating the extra delay; and (ii) the player
// code is written to start a draw upon wraparound from 159 to 0, so -1 is the
// correct option rather than 159.
player_[player].position = -1;
}
void TIA::set_player_motion(int player, uint8_t motion) {
assert(player >= 0 && player < 2);
player_[player].motion = (motion >> 4)&0xf;
}
void TIA::set_player_missile_colour(int player, uint8_t colour) {
assert(player >= 0 && player < 2);
colour_palette_[(int)ColourIndex::PlayerMissile0 + player] = colour;
}
void TIA::set_missile_enable(int missile, bool enabled) {
assert(missile >= 0 && missile < 2);
missile_[missile].enabled = enabled;
}
void TIA::set_missile_position(int missile) {
assert(missile >= 0 && missile < 2);
missile_[missile].position = 0;
}
void TIA::set_missile_position_to_player(int missile, bool lock) {
assert(missile >= 0 && missile < 2);
missile_[missile].locked_to_player = lock;
player_[missile].latched_pixel4_time = -1;
}
void TIA::set_missile_motion(int missile, uint8_t motion) {
assert(missile >= 0 && missile < 2);
missile_[missile].motion = (motion >> 4)&0xf;
}
void TIA::set_ball_enable(bool enabled) {
ball_.enabled[1] = enabled;
}
void TIA::set_ball_delay(bool delay) {
ball_.enabled_index = delay ? 0 : 1;
}
void TIA::set_ball_position() {
ball_.position = 0;
// setting the ball position also triggers a draw
ball_.reset_pixels(0);
}
void TIA::set_ball_motion(uint8_t motion) {
ball_.motion = (motion >> 4) & 0xf;
}
void TIA::move() {
horizontal_blank_extend_ = true;
player_[0].is_moving = player_[1].is_moving = missile_[0].is_moving = missile_[1].is_moving = ball_.is_moving = true;
player_[0].motion_step = player_[1].motion_step = missile_[0].motion_step = missile_[1].motion_step = ball_.motion_step = 15;
player_[0].motion_time = player_[1].motion_time = missile_[0].motion_time = missile_[1].motion_time = ball_.motion_time = (horizontal_counter_ + 3) & ~3;
}
void TIA::clear_motion() {
player_[0].motion = player_[1].motion = missile_[0].motion = missile_[1].motion = ball_.motion = 0;
}
uint8_t TIA::get_collision_flags(int offset) {
return (uint8_t)((collision_flags_ >> (offset << 1)) << 6) & 0xc0;
}
void TIA::clear_collision_flags() {
collision_flags_ = 0;
}
void TIA::output_for_cycles(int number_of_cycles) {
/*
Line timing is oriented around 0 being the start of the right-hand side vertical blank;
a wsync synchronises the CPU to horizontal_counter_ = 0. All timing below is in terms of the
NTSC colour clock.
Therefore, each line is composed of:
16 cycles: blank ; -> 16
16 cycles: sync ; -> 32
16 cycles: colour burst ; -> 48
20 cycles: blank ; -> 68
8 cycles: blank or pixels, depending on whether the blank extend bit is set
152 cycles: pixels
*/
int output_cursor = horizontal_counter_;
horizontal_counter_ += number_of_cycles;
bool is_reset = output_cursor < 224 && horizontal_counter_ >= 224;
if(!output_cursor) {
if(line_end_function_) line_end_function_(collision_buffer_);
memset(collision_buffer_, 0, sizeof(collision_buffer_));
ball_.motion_time %= 228;
player_[0].motion_time %= 228;
player_[1].motion_time %= 228;
missile_[0].motion_time %= 228;
missile_[1].motion_time %= 228;
}
// accumulate an OR'd version of the output into the collision buffer
int latent_start = output_cursor + 4;
int latent_end = horizontal_counter_ + 4;
draw_playfield(latent_start, latent_end);
draw_object<Player>(player_[0], (uint8_t)CollisionType::Player0, output_cursor, horizontal_counter_);
draw_object<Player>(player_[1], (uint8_t)CollisionType::Player1, output_cursor, horizontal_counter_);
draw_missile(missile_[0], player_[0], (uint8_t)CollisionType::Missile0, output_cursor, horizontal_counter_);
draw_missile(missile_[1], player_[1], (uint8_t)CollisionType::Missile1, output_cursor, horizontal_counter_);
draw_object<Ball>(ball_, (uint8_t)CollisionType::Ball, output_cursor, horizontal_counter_);
// convert to television signals
#define Period(function, target) \
if(output_cursor < target) { \
if(horizontal_counter_ <= target) { \
if(crt_) crt_->function((unsigned int)((horizontal_counter_ - output_cursor) * 2)); \
horizontal_counter_ %= cycles_per_line; \
return; \
} else { \
if(crt_) crt_->function((unsigned int)((target - output_cursor) * 2)); \
output_cursor = target; \
} \
}
switch(output_mode_) {
default:
Period(output_blank, 16)
Period(output_sync, 32)
Period(output_default_colour_burst, 48)
Period(output_blank, 68)
break;
case sync_flag:
case sync_flag | blank_flag:
Period(output_sync, 16)
Period(output_blank, 32)
Period(output_default_colour_burst, 48)
Period(output_sync, 228)
break;
}
#undef Period
if(output_mode_ & blank_flag) {
if(pixel_target_) {
output_pixels(pixels_start_location_, output_cursor);
if(crt_) crt_->output_data((unsigned int)(output_cursor - pixels_start_location_) * 2, 2);
pixel_target_ = nullptr;
pixels_start_location_ = 0;
}
int duration = std::min(228, horizontal_counter_) - output_cursor;
if(crt_) crt_->output_blank((unsigned int)(duration * 2));
} else {
if(!pixels_start_location_ && crt_) {
pixels_start_location_ = output_cursor;
pixel_target_ = crt_->allocate_write_area(160);
}
// convert that into pixels
if(pixel_target_) output_pixels(output_cursor, horizontal_counter_);
// accumulate collision flags
while(output_cursor < horizontal_counter_) {
collision_flags_ |= collision_flags_by_buffer_vaules_[collision_buffer_[output_cursor - first_pixel_cycle]];
output_cursor++;
}
if(horizontal_counter_ == cycles_per_line && crt_) {
crt_->output_data((unsigned int)(output_cursor - pixels_start_location_) * 2, 2);
pixel_target_ = nullptr;
pixels_start_location_ = 0;
}
}
if(is_reset) horizontal_blank_extend_ = false;
horizontal_counter_ %= cycles_per_line;
}
void TIA::output_pixels(int start, int end) {
start = std::max(start, pixels_start_location_);
int target_position = start - pixels_start_location_;
if(start < first_pixel_cycle+8 && horizontal_blank_extend_) {
while(start < end && start < first_pixel_cycle+8) {
pixel_target_[target_position] = 0;
start++;
target_position++;
}
}
if(playfield_priority_ == PlayfieldPriority::Score) {
while(start < end && start < first_pixel_cycle + 80) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreLeft][buffer_value]];
start++;
target_position++;
}
while(start < end) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[(int)ColourMode::ScoreRight][buffer_value]];
start++;
target_position++;
}
} else {
int table_index = (int)((playfield_priority_ == PlayfieldPriority::Standard) ? ColourMode::Standard : ColourMode::OnTop);
while(start < end) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[table_index][buffer_value]];
start++;
target_position++;
}
}
}
void TIA::output_line() {
switch(output_mode_) {
default:
// TODO: optimise special case
output_for_cycles(cycles_per_line);
break;
case sync_flag:
case sync_flag | blank_flag:
if(crt_) {
crt_->output_sync(32);
crt_->output_blank(32);
crt_->output_sync(392);
}
horizontal_blank_extend_ = false;
break;
case blank_flag:
if(crt_) {
crt_->output_blank(32);
crt_->output_sync(32);
crt_->output_default_colour_burst(32);
crt_->output_blank(360);
}
horizontal_blank_extend_ = false;
break;
}
}
#pragma mark - Playfield output
void TIA::draw_playfield(int start, int end) {
// don't do anything if this window ends too early
if(end < first_pixel_cycle) return;
// clip to drawable bounds
start = std::max(start, first_pixel_cycle);
end = std::min(end, 228);
// proceed along four-pixel boundaries, plotting four pixels at a time
int aligned_position = (start + 3)&~3;
while(aligned_position < end) {
int offset = (aligned_position - first_pixel_cycle) >> 2;
uint32_t value = ((background_[(offset/20)&background_half_mask_] >> (offset%20))&1) * 0x01010101;
*(uint32_t *)&collision_buffer_[aligned_position - first_pixel_cycle] |= value;
aligned_position += 4;
}
}
#pragma mark - Motion
template<class T> void TIA::perform_motion_step(T &object) {
if((object.motion_step ^ (object.motion ^ 8)) == 0xf) {
object.is_moving = false;
} else {
if(object.position == 159) object.reset_pixels(0);
else if(object.position == 15 && object.copy_flags&1) object.reset_pixels(1);
else if(object.position == 31 && object.copy_flags&2) object.reset_pixels(2);
else if(object.position == 63 && object.copy_flags&4) object.reset_pixels(3);
else object.skip_pixels(1, object.motion_time);
object.position = (object.position + 1) % 160;
object.motion_step --;
object.motion_time += 4;
}
}
template<class T> void TIA::perform_border_motion(T &object, int start, int end) {
while(object.is_moving && object.motion_time < end)
perform_motion_step<T>(object);
}
template<class T> void TIA::draw_object(T &object, const uint8_t collision_identity, int start, int end) {
int first_pixel = first_pixel_cycle - 4 + (horizontal_blank_extend_ ? 8 : 0);
object.dequeue_pixels(collision_buffer_, collision_identity, end - first_pixel_cycle);
// movement works across the entire screen, so do work that falls outside of the pixel area
if(start < first_pixel) {
perform_border_motion<T>(object, start, std::min(end, first_pixel));
}
// don't continue to do any drawing if this window ends too early
if(end < first_pixel) return;
if(start < first_pixel) start = first_pixel;
if(start >= end) return;
// perform the visible part of the line, if any
if(start < 224) {
draw_object_visible<T>(object, collision_identity, start - first_pixel_cycle + 4, std::min(end - first_pixel_cycle + 4, 160), end - first_pixel_cycle);
}
// move further if required
if(object.is_moving && end >= 224 && object.motion_time < end) {
perform_motion_step<T>(object);
}
}
template<class T> void TIA::draw_object_visible(T &object, const uint8_t collision_identity, int start, int end, int time_now) {
// perform a miniature event loop on (i) triggering draws; (ii) drawing; and (iii) motion
int next_motion_time = object.motion_time - first_pixel_cycle + 4;
while(start < end) {
int next_event_time = end;
// is the next event a movement tick?
if(object.is_moving && next_motion_time < next_event_time) {
next_event_time = next_motion_time;
}
// is the next event a graphics trigger?
int next_copy = 160;
int next_copy_id = 0;
if(object.copy_flags) {
if(object.position < 16 && object.copy_flags&1) {
next_copy = 16;
next_copy_id = 1;
} else if(object.position < 32 && object.copy_flags&2) {
next_copy = 32;
next_copy_id = 2;
} else if(object.position < 64 && object.copy_flags&4) {
next_copy = 64;
next_copy_id = 3;
}
}
int next_copy_time = start + next_copy - object.position;
if(next_copy_time < next_event_time) next_event_time = next_copy_time;
// the decision is to progress by length
const int length = next_event_time - start;
// enqueue a future intention to draw pixels if spitting them out now would violate accuracy;
// otherwise draw them now
if(object.enqueues && next_event_time > time_now) {
if(start < time_now) {
object.output_pixels(&collision_buffer_[start], time_now - start, collision_identity, start + first_pixel_cycle - 4);
object.enqueue_pixels(time_now, next_event_time, time_now + first_pixel_cycle - 4);
} else {
object.enqueue_pixels(start, next_event_time, start + first_pixel_cycle - 4);
}
} else {
object.output_pixels(&collision_buffer_[start], length, collision_identity, start + first_pixel_cycle - 4);
}
// the next interesting event is after next_event_time cycles, so progress
object.position = (object.position + length) % 160;
start = next_event_time;
// if the event is a motion tick, apply; if it's a draw trigger, trigger a draw
if(object.is_moving && start == next_motion_time) {
perform_motion_step(object);
next_motion_time += 4;
} else if(start == next_copy_time) {
object.reset_pixels(next_copy_id);
}
}
}
#pragma mark - Missile drawing
void TIA::draw_missile(Missile &missile, Player &player, const uint8_t collision_identity, int start, int end) {
if(!missile.locked_to_player || player.latched_pixel4_time < 0) {
draw_object<Missile>(missile, collision_identity, start, end);
} else {
draw_object<Missile>(missile, collision_identity, start, player.latched_pixel4_time);
missile.position = 0;
draw_object<Missile>(missile, collision_identity, player.latched_pixel4_time, end);
player.latched_pixel4_time = -1;
}
}

333
Machines/Atari2600/TIA.hpp Normal file
View File

@@ -0,0 +1,333 @@
//
// TIA.hpp
// Clock Signal
//
// Created by Thomas Harte on 28/01/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef TIA_hpp
#define TIA_hpp
#include <cstdint>
#include "../CRTMachine.hpp"
namespace Atari2600 {
class TIA {
public:
TIA();
// The supplied hook is for unit testing only; if instantiated with a line_end_function then it will
// be called with the latest collision buffer upon the conclusion of each line. What's a collision
// buffer? It's an implementation detail. If you're not writing a unit test, leave it alone.
TIA(std::function<void(uint8_t *output_buffer)> line_end_function);
enum class OutputMode {
NTSC, PAL
};
/*!
Advances the TIA by @c number_of_cycles cycles. Any queued setters take effect in the
first cycle performed.
*/
void run_for_cycles(int number_of_cycles);
void set_output_mode(OutputMode output_mode);
void set_sync(bool sync);
void set_blank(bool blank);
void reset_horizontal_counter(); // Reset is delayed by four cycles.
/*!
@returns the number of cycles between (current TIA time) + from_offset to the current or
next horizontal blanking period. Returns numbers in the range [0, 227].
*/
int get_cycles_until_horizontal_blank(unsigned int from_offset);
void set_background_colour(uint8_t colour);
void set_playfield(uint16_t offset, uint8_t value);
void set_playfield_control_and_ball_size(uint8_t value);
void set_playfield_ball_colour(uint8_t colour);
void set_player_number_and_size(int player, uint8_t value);
void set_player_graphic(int player, uint8_t value);
void set_player_reflected(int player, bool reflected);
void set_player_delay(int player, bool delay);
void set_player_position(int player);
void set_player_motion(int player, uint8_t motion);
void set_player_missile_colour(int player, uint8_t colour);
void set_missile_enable(int missile, bool enabled);
void set_missile_position(int missile);
void set_missile_position_to_player(int missile, bool lock);
void set_missile_motion(int missile, uint8_t motion);
void set_ball_enable(bool enabled);
void set_ball_delay(bool delay);
void set_ball_position();
void set_ball_motion(uint8_t motion);
void move();
void clear_motion();
uint8_t get_collision_flags(int offset);
void clear_collision_flags();
virtual std::shared_ptr<Outputs::CRT::CRT> get_crt() { return crt_; }
private:
TIA(bool create_crt);
std::shared_ptr<Outputs::CRT::CRT> crt_;
std::function<void(uint8_t *output_buffer)> line_end_function_;
// the master counter; counts from 0 to 228 with all visible pixels being in the final 160
int horizontal_counter_;
// contains flags to indicate whether sync or blank are currently active
int output_mode_;
// keeps track of the target pixel buffer for this line and when it was acquired, and a corresponding collision buffer
alignas(alignof(uint32_t)) uint8_t collision_buffer_[160];
enum class CollisionType : uint8_t {
Playfield = (1 << 0),
Ball = (1 << 1),
Player0 = (1 << 2),
Player1 = (1 << 3),
Missile0 = (1 << 4),
Missile1 = (1 << 5)
};
int collision_flags_;
int collision_flags_by_buffer_vaules_[64];
// colour mapping tables
enum class ColourMode {
Standard = 0,
ScoreLeft,
ScoreRight,
OnTop
};
uint8_t colour_mask_by_mode_collision_flags_[4][64]; // maps from [ColourMode][CollisionMark] to colour_pallete_ entry
enum class ColourIndex {
Background = 0,
PlayfieldBall,
PlayerMissile0,
PlayerMissile1
};
uint8_t colour_palette_[4];
// playfield state
int background_half_mask_;
enum class PlayfieldPriority {
Standard,
Score,
OnTop
} playfield_priority_;
uint32_t background_[2]; // contains two 20-bit bitfields representing the background state;
// at index 0 is the left-hand side of the playfield with bit 0 being
// the first bit to display, bit 1 the second, etc. Index 1 contains
// a mirror image of index 0. If the playfield is being displayed in
// mirroring mode, background_[0] will be output on the left and
// background_[1] on the right; otherwise background_[0] will be
// output twice.
// objects
template<class T> struct Object {
// the two programmer-set values
int position;
int motion;
// motion_step_ is the current motion counter value; motion_time_ is the next time it will fire
int motion_step;
int motion_time;
// indicates whether this object is currently undergoing motion
bool is_moving;
Object() : position(0), motion(0), motion_step(0), motion_time(0), is_moving(false) {};
};
// player state
struct Player: public Object<Player> {
Player() :
adder(4),
copy_flags(0),
graphic{0, 0},
reverse_mask(false),
graphic_index(0),
pixel_position(32),
pixel_counter(0),
latched_pixel4_time(-1),
copy_index_(0),
queue_read_pointer_(0),
queue_write_pointer_(0) {}
int adder;
int copy_flags; // a bit field, corresponding to the first few values of NUSIZ
uint8_t graphic[2]; // the player graphic; 1 = new, 0 = current
int reverse_mask; // 7 for a reflected player, 0 for normal
int graphic_index;
int pixel_position, pixel_counter;
int latched_pixel4_time;
const bool enqueues = true;
inline void skip_pixels(const int count, int from_horizontal_counter) {
int old_pixel_counter = pixel_counter;
pixel_position = std::min(32, pixel_position + count * adder);
pixel_counter += count;
if(!copy_index_ && old_pixel_counter < 4 && pixel_counter >= 4) {
latched_pixel4_time = from_horizontal_counter + 4 - old_pixel_counter;
}
}
inline void reset_pixels(int copy) {
pixel_position = pixel_counter = 0;
copy_index_ = copy;
}
inline void output_pixels(uint8_t *const target, const int count, const uint8_t collision_identity, int from_horizontal_counter) {
output_pixels(target, count, collision_identity, pixel_position, adder, reverse_mask);
skip_pixels(count, from_horizontal_counter);
}
void dequeue_pixels(uint8_t *const target, const uint8_t collision_identity, const int time_now) {
while(queue_read_pointer_ != queue_write_pointer_) {
uint8_t *const start_ptr = &target[queue_[queue_read_pointer_].start];
if(queue_[queue_read_pointer_].end > time_now) {
const int length = time_now - queue_[queue_read_pointer_].start;
output_pixels(start_ptr, length, collision_identity, queue_[queue_read_pointer_].pixel_position, queue_[queue_read_pointer_].adder, queue_[queue_read_pointer_].reverse_mask);
queue_[queue_read_pointer_].pixel_position += length * queue_[queue_read_pointer_].adder;
queue_[queue_read_pointer_].start = time_now;
return;
} else {
output_pixels(start_ptr, queue_[queue_read_pointer_].end - queue_[queue_read_pointer_].start, collision_identity, queue_[queue_read_pointer_].pixel_position, queue_[queue_read_pointer_].adder, queue_[queue_read_pointer_].reverse_mask);
}
queue_read_pointer_ = (queue_read_pointer_ + 1)&3;
}
}
void enqueue_pixels(const int start, const int end, int from_horizontal_counter) {
queue_[queue_write_pointer_].start = start;
queue_[queue_write_pointer_].end = end;
queue_[queue_write_pointer_].pixel_position = pixel_position;
queue_[queue_write_pointer_].adder = adder;
queue_[queue_write_pointer_].reverse_mask = reverse_mask;
queue_write_pointer_ = (queue_write_pointer_ + 1)&3;
skip_pixels(end - start, from_horizontal_counter);
}
private:
int copy_index_;
struct QueuedPixels {
int start, end;
int pixel_position;
int adder;
int reverse_mask;
QueuedPixels() : start(0), end(0), pixel_position(0), adder(0), reverse_mask(false) {}
} queue_[4];
int queue_read_pointer_, queue_write_pointer_;
inline void output_pixels(uint8_t *const target, const int count, const uint8_t collision_identity, int pixel_position, int adder, int reverse_mask) {
if(pixel_position == 32 || !graphic[graphic_index]) return;
int output_cursor = 0;
while(pixel_position < 32 && output_cursor < count) {
int shift = (pixel_position >> 2) ^ reverse_mask;
target[output_cursor] |= ((graphic[graphic_index] >> shift)&1) * collision_identity;
output_cursor++;
pixel_position += adder;
}
}
} player_[2];
// common actor for things that appear as a horizontal run of pixels
struct HorizontalRun: public Object<HorizontalRun> {
int pixel_position;
int size;
const bool enqueues = false;
inline void skip_pixels(const int count, int from_horizontal_counter) {
pixel_position = std::max(0, pixel_position - count);
}
inline void reset_pixels(int copy) {
pixel_position = size;
}
inline void output_pixels(uint8_t *const target, const int count, const uint8_t collision_identity, int from_horizontal_counter) {
int output_cursor = 0;
while(pixel_position && output_cursor < count)
{
target[output_cursor] |= collision_identity;
output_cursor++;
pixel_position--;
}
}
void dequeue_pixels(uint8_t *const target, const uint8_t collision_identity, const int time_now) {}
void enqueue_pixels(const int start, const int end, int from_horizontal_counter) {}
HorizontalRun() : pixel_position(0), size(1) {}
};
// missile state
struct Missile: public HorizontalRun {
bool enabled;
bool locked_to_player;
int copy_flags;
inline void output_pixels(uint8_t *const target, const int count, const uint8_t collision_identity, int from_horizontal_counter) {
if(!pixel_position) return;
if(enabled && !locked_to_player) {
HorizontalRun::output_pixels(target, count, collision_identity, from_horizontal_counter);
} else {
skip_pixels(count, from_horizontal_counter);
}
}
Missile() : enabled(false), copy_flags(0) {}
} missile_[2];
// ball state
struct Ball: public HorizontalRun {
bool enabled[2];
int enabled_index;
const int copy_flags = 0;
inline void output_pixels(uint8_t *const target, const int count, const uint8_t collision_identity, int from_horizontal_counter) {
if(!pixel_position) return;
if(enabled[enabled_index]) {
HorizontalRun::output_pixels(target, count, collision_identity, from_horizontal_counter);
} else {
skip_pixels(count, from_horizontal_counter);
}
}
Ball() : enabled{false, false}, enabled_index(0) {}
} ball_;
// motion
bool horizontal_blank_extend_;
template<class T> void perform_border_motion(T &object, int start, int end);
template<class T> void perform_motion_step(T &object);
// drawing methods and state
void draw_missile(Missile &, Player &, const uint8_t collision_identity, int start, int end);
template<class T> void draw_object(T &, const uint8_t collision_identity, int start, int end);
template<class T> void draw_object_visible(T &, const uint8_t collision_identity, int start, int end, int time_now);
inline void draw_playfield(int start, int end);
inline void output_for_cycles(int number_of_cycles);
inline void output_line();
int pixels_start_location_;
uint8_t *pixel_target_;
inline void output_pixels(int start, int end);
};
}
#endif /* TIA_hpp */

View File

@@ -13,11 +13,10 @@
using namespace Commodore::C1540;
Machine::Machine() :
shift_register_(0),
Storage::Disk::Controller(1000000, 4, 300),
serial_port_(new SerialPort),
serial_port_VIA_(new SerialPortVIA)
{
shift_register_(0),
Storage::Disk::Controller(1000000, 4, 300),
serial_port_(new SerialPort),
serial_port_VIA_(new SerialPortVIA) {
// attach the serial port to its VIA and vice versa
serial_port_->set_serial_port_via(serial_port_VIA_);
serial_port_VIA_->set_serial_port(serial_port_);
@@ -31,13 +30,11 @@ Machine::Machine() :
set_expected_bit_length(Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(3));
}
void Machine::set_serial_bus(std::shared_ptr<::Commodore::Serial::Bus> serial_bus)
{
void Machine::set_serial_bus(std::shared_ptr<::Commodore::Serial::Bus> serial_bus) {
Commodore::Serial::AttachPortAndBus(serial_port_, serial_bus);
}
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value)
{
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
/*
Memory map (given that I'm unsure yet on any potential mirroring):
@@ -46,27 +43,20 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
0x1c000x1c0f the drive VIA
0xc0000xffff ROM
*/
if(address < 0x800)
{
if(address < 0x800) {
if(isReadOperation(operation))
*value = ram_[address];
else
ram_[address] = *value;
}
else if(address >= 0xc000)
{
} else if(address >= 0xc000) {
if(isReadOperation(operation))
*value = rom_[address & 0x3fff];
}
else if(address >= 0x1800 && address <= 0x180f)
{
} else if(address >= 0x1800 && address <= 0x180f) {
if(isReadOperation(operation))
*value = serial_port_VIA_->get_register(address);
else
serial_port_VIA_->set_register(address, *value);
}
else if(address >= 0x1c00 && address <= 0x1c0f)
{
} else if(address >= 0x1c00 && address <= 0x1c0f) {
if(isReadOperation(operation))
*value = drive_VIA_.get_register(address);
else
@@ -79,20 +69,17 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
return 1;
}
void Machine::set_rom(const uint8_t *rom)
{
void Machine::set_rom(const uint8_t *rom) {
memcpy(rom_, rom, sizeof(rom_));
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk)
{
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
std::shared_ptr<Storage::Disk::Drive> drive(new Storage::Disk::Drive);
drive->set_disk(disk);
set_drive(drive);
}
void Machine::run_for_cycles(int number_of_cycles)
{
void Machine::run_for_cycles(int number_of_cycles) {
CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles);
set_motor_on(drive_VIA_.get_motor_enabled());
if(drive_VIA_.get_motor_enabled()) // TODO: motor speed up/down
@@ -101,38 +88,30 @@ void Machine::run_for_cycles(int number_of_cycles)
#pragma mark - 6522 delegate
void Machine::mos6522_did_change_interrupt_status(void *mos6522)
{
void Machine::mos6522_did_change_interrupt_status(void *mos6522) {
// both VIAs are connected to the IRQ line
set_irq_line(serial_port_VIA_->get_interrupt_line() || drive_VIA_.get_interrupt_line());
}
#pragma mark - Disk drive
void Machine::process_input_bit(int value, unsigned int cycles_since_index_hole)
{
void Machine::process_input_bit(int value, unsigned int cycles_since_index_hole) {
shift_register_ = (shift_register_ << 1) | value;
if((shift_register_ & 0x3ff) == 0x3ff)
{
if((shift_register_ & 0x3ff) == 0x3ff) {
drive_VIA_.set_sync_detected(true);
bit_window_offset_ = -1; // i.e. this bit isn't the first within a data window, but the next might be
}
else
{
} else {
drive_VIA_.set_sync_detected(false);
}
bit_window_offset_++;
if(bit_window_offset_ == 8)
{
if(bit_window_offset_ == 8) {
drive_VIA_.set_data_input((uint8_t)shift_register_);
bit_window_offset_ = 0;
if(drive_VIA_.get_should_set_overflow())
{
if(drive_VIA_.get_should_set_overflow()) {
set_overflow_line(true);
}
}
else
set_overflow_line(false);
else set_overflow_line(false);
}
// the 1540 does not recognise index holes
@@ -140,32 +119,26 @@ void Machine::process_index_hole() {}
#pragma mak - Drive VIA delegate
void Machine::drive_via_did_step_head(void *driveVIA, int direction)
{
void Machine::drive_via_did_step_head(void *driveVIA, int direction) {
step(direction);
}
void Machine::drive_via_did_set_data_density(void *driveVIA, int density)
{
void Machine::drive_via_did_set_data_density(void *driveVIA, int density) {
set_expected_bit_length(Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone((unsigned int)density));
}
#pragma mark - SerialPortVIA
SerialPortVIA::SerialPortVIA() :
port_b_(0x00), attention_acknowledge_level_(false), attention_level_input_(true), data_level_output_(false)
{}
port_b_(0x00), attention_acknowledge_level_(false), attention_level_input_(true), data_level_output_(false) {}
uint8_t SerialPortVIA::get_port_input(Port port)
{
uint8_t SerialPortVIA::get_port_input(Port port) {
if(port) return port_b_;
return 0xff;
}
void SerialPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask)
{
if(port)
{
void SerialPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask) {
if(port) {
std::shared_ptr<::Commodore::Serial::Port> serialPort = serial_port_.lock();
if(serialPort) {
attention_acknowledge_level_ = !(value&0x10);
@@ -177,10 +150,8 @@ void SerialPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask)
}
}
void SerialPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool value)
{
switch(line)
{
void SerialPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool value) {
switch(line) {
default: break;
case ::Commodore::Serial::Line::Data: port_b_ = (port_b_ & ~0x01) | (value ? 0x00 : 0x01); break;
case ::Commodore::Serial::Line::Clock: port_b_ = (port_b_ & ~0x04) | (value ? 0x00 : 0x04); break;
@@ -193,16 +164,13 @@ void SerialPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool v
}
}
void SerialPortVIA::set_serial_port(const std::shared_ptr<::Commodore::Serial::Port> &serialPort)
{
void SerialPortVIA::set_serial_port(const std::shared_ptr<::Commodore::Serial::Port> &serialPort) {
serial_port_ = serialPort;
}
void SerialPortVIA::update_data_line()
{
void SerialPortVIA::update_data_line() {
std::shared_ptr<::Commodore::Serial::Port> serialPort = serial_port_.lock();
if(serialPort)
{
if(serialPort) {
// "ATN (Attention) is an input on pin 3 of P2 and P3 that is sensed at PB7 and CA1 of UC3 after being inverted by UA1"
serialPort->set_output(::Commodore::Serial::Line::Data,
(::Commodore::Serial::LineLevel)(!data_level_output_ && (attention_level_input_ != attention_acknowledge_level_)));
@@ -211,8 +179,7 @@ void SerialPortVIA::update_data_line()
#pragma mark - DriveVIA
void DriveVIA::set_delegate(Delegate *delegate)
{
void DriveVIA::set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
@@ -246,22 +213,19 @@ void DriveVIA::set_control_line_output(Port port, Line line, bool value) {
}
void DriveVIA::set_port_output(Port port, uint8_t value, uint8_t direction_mask) {
if(port)
{
if(port) {
// record drive motor state
drive_motor_ = !!(value&4);
// check for a head step
int step_difference = ((value&3) - (previous_port_b_output_&3))&3;
if(step_difference)
{
if(step_difference) {
if(delegate_) delegate_->drive_via_did_step_head(this, (step_difference == 1) ? 1 : -1);
}
// check for a change in density
int density_difference = (previous_port_b_output_^value) & (3 << 5);
if(density_difference && delegate_)
{
if(density_difference && delegate_) {
delegate_->drive_via_did_set_data_density(this, (value >> 5)&3);
}

View File

@@ -10,10 +10,8 @@
using namespace Commodore::Serial;
const char *::Commodore::Serial::StringForLine(Line line)
{
switch(line)
{
const char *::Commodore::Serial::StringForLine(Line line) {
switch(line) {
case ServiceRequest: return "Service request";
case Attention: return "Attention";
case Clock: return "Clock";
@@ -22,17 +20,14 @@ const char *::Commodore::Serial::StringForLine(Line line)
}
}
void ::Commodore::Serial::AttachPortAndBus(std::shared_ptr<Port> port, std::shared_ptr<Bus> bus)
{
void ::Commodore::Serial::AttachPortAndBus(std::shared_ptr<Port> port, std::shared_ptr<Bus> bus) {
port->set_serial_bus(bus);
bus->add_port(port);
}
void Bus::add_port(std::shared_ptr<Port> port)
{
void Bus::add_port(std::shared_ptr<Port> port) {
ports_.push_back(port);
for(int line = (int)ServiceRequest; line <= (int)Reset; line++)
{
for(int line = (int)ServiceRequest; line <= (int)Reset; line++) {
// the addition of a new device may change the line output...
set_line_output_did_change((Line)line);
@@ -41,29 +36,23 @@ void Bus::add_port(std::shared_ptr<Port> port)
}
}
void Bus::set_line_output_did_change(Line line)
{
void Bus::set_line_output_did_change(Line line) {
// i.e. I believe these lines to be open collector
LineLevel new_line_level = High;
for(std::weak_ptr<Port> port : ports_)
{
for(std::weak_ptr<Port> port : ports_) {
std::shared_ptr<Port> locked_port = port.lock();
if(locked_port)
{
if(locked_port) {
new_line_level = (LineLevel)((bool)new_line_level & (bool)locked_port->get_output(line));
}
}
// post an update only if one occurred
if(new_line_level != line_levels_[line])
{
if(new_line_level != line_levels_[line]) {
line_levels_[line] = new_line_level;
for(std::weak_ptr<Port> port : ports_)
{
for(std::weak_ptr<Port> port : ports_) {
std::shared_ptr<Port> locked_port = port.lock();
if(locked_port)
{
if(locked_port) {
locked_port->set_input(line, new_line_level);
}
}
@@ -72,19 +61,14 @@ void Bus::set_line_output_did_change(Line line)
#pragma mark - The debug port
void DebugPort::set_input(Line line, LineLevel value)
{
void DebugPort::set_input(Line line, LineLevel value) {
input_levels_[line] = value;
printf("[Bus] %s is %s\n", StringForLine(line), value ? "high" : "low");
if(!incoming_count_)
{
if(!incoming_count_) {
incoming_count_ = (!input_levels_[Line::Clock] && !input_levels_[Line::Data]) ? 8 : 0;
}
else
{
if(line == Line::Clock && value)
{
} else {
if(line == Line::Clock && value) {
incoming_byte_ = (incoming_byte_ >> 1) | (input_levels_[Line::Data] ? 0x80 : 0x00);
}
incoming_count_--;

View File

@@ -78,8 +78,7 @@ namespace Serial {
Sets the current level of an output line on this serial port.
*/
void set_output(Line line, LineLevel level) {
if(line_levels_[line] != level)
{
if(line_levels_[line] != level) {
line_levels_[line] = level;
std::shared_ptr<Bus> bus = serial_bus_.lock();
if(bus) bus->set_line_output_did_change(line);

View File

@@ -8,8 +8,7 @@
#include "Vic20.hpp"
uint16_t *Commodore::Vic20::Machine::sequence_for_character(Utility::Typer *typer, char character)
{
uint16_t *Commodore::Vic20::Machine::sequence_for_character(Utility::Typer *typer, char character) {
#define KEYS(...) {__VA_ARGS__, TerminateSequence}
#define SHIFT(...) {KeyLShift, __VA_ARGS__, TerminateSequence}
#define X {NotMapped}

View File

@@ -10,20 +10,21 @@
#include <algorithm>
#include "../../../Storage/Tape/Formats/TapePRG.hpp"
#include "../../../Storage/Tape/Parsers/Commodore.hpp"
#include "../../../StaticAnalyser/StaticAnalyser.hpp"
using namespace Commodore::Vic20;
Machine::Machine() :
rom_(nullptr),
is_running_at_zero_cost_(false),
tape_(1022727)
{
// create 6522s, serial port and bus
user_port_via_.reset(new UserPortVIA);
keyboard_via_.reset(new KeyboardVIA);
serial_port_.reset(new SerialPort);
serial_bus_.reset(new ::Commodore::Serial::Bus);
rom_(nullptr),
is_running_at_zero_cost_(false),
tape_(new Storage::Tape::BinaryTapePlayer(1022727)),
user_port_via_(new UserPortVIA),
keyboard_via_(new KeyboardVIA),
serial_port_(new SerialPort),
serial_bus_(new ::Commodore::Serial::Bus) {
// communicate the tape to the user-port VIA
user_port_via_->set_tape(tape_);
// wire up the serial bus and serial port
Commodore::Serial::AttachPortAndBus(serial_port_, serial_bus_);
@@ -36,7 +37,7 @@ Machine::Machine() :
// wire up the 6522s, tape and machine
user_port_via_->set_interrupt_delegate(this);
keyboard_via_->set_interrupt_delegate(this);
tape_.set_delegate(this);
tape_->set_delegate(this);
// establish the memory maps
set_memory_size(MemorySize::Default);
@@ -48,13 +49,11 @@ Machine::Machine() :
// serial_bus_->add_port(_debugPort);
}
void Machine::set_memory_size(MemorySize size)
{
void Machine::set_memory_size(MemorySize size) {
memset(processor_read_memory_map_, 0, sizeof(processor_read_memory_map_));
memset(processor_write_memory_map_, 0, sizeof(processor_write_memory_map_));
switch(size)
{
switch(size) {
default: break;
case ThreeKB:
write_to_map(processor_read_memory_map_, expansion_ram_, 0x0000, 0x1000);
@@ -79,52 +78,33 @@ void Machine::set_memory_size(MemorySize size)
write_to_map(processor_write_memory_map_, colour_memory_, 0x9400, sizeof(colour_memory_));
// install the inserted ROM if there is one
if(rom_)
{
if(rom_) {
write_to_map(processor_read_memory_map_, rom_, rom_address_, rom_length_);
}
}
void Machine::write_to_map(uint8_t **map, uint8_t *area, uint16_t address, uint16_t length)
{
void Machine::write_to_map(uint8_t **map, uint8_t *area, uint16_t address, uint16_t length) {
address >>= 10;
length >>= 10;
while(length--)
{
while(length--) {
map[address] = area;
area += 0x400;
address++;
}
}
Machine::~Machine()
{
Machine::~Machine() {
delete[] rom_;
}
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value)
{
// static int logCount = 0;
// if(operation == CPU6502::BusOperation::ReadOpcode && address == 0xf957) logCount = 500;
// if(operation == CPU6502::BusOperation::ReadOpcode && logCount) {
// logCount--;
// printf("%04x\n", address);
// }
// if(operation == CPU6502::BusOperation::Write && (address >= 0x033C && address < 0x033C + 192))
// {
// printf("\n[%04x] <- %02x\n", address, *value);
// }
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
// run the phase-1 part of this cycle, in which the VIC accesses memory
if(!is_running_at_zero_cost_) mos6560_->run_for_cycles(1);
// run the phase-2 part of the cycle, which is whatever the 6502 said it should be
if(isReadOperation(operation))
{
if(isReadOperation(operation)) {
uint8_t result = processor_read_memory_map_[address >> 10] ? processor_read_memory_map_[address >> 10][address & 0x3ff] : 0xff;
if((address&0xfc00) == 0x9000)
{
if((address&0xfc00) == 0x9000) {
if((address&0xff00) == 0x9000) result &= mos6560_->get_register(address);
if((address&0xfc10) == 0x9010) result &= user_port_via_->get_register(address);
if((address&0xfc20) == 0x9020) result &= keyboard_via_->get_register(address);
@@ -135,22 +115,64 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
// PC hits the start of the loop that just waits for an interesting tape interrupt to have
// occurred then skip both 6522s and the tape ahead to the next interrupt without any further
// CPU or 6560 costs.
if(use_fast_tape_hack_ && tape_.has_tape() && address == 0xf92f && operation == CPU6502::BusOperation::ReadOpcode)
{
while(!user_port_via_->get_interrupt_line() && !keyboard_via_->get_interrupt_line() && !tape_.get_tape()->is_at_end())
{
user_port_via_->run_for_cycles(1);
keyboard_via_->run_for_cycles(1);
tape_.run_for_cycles(1);
if(use_fast_tape_hack_ && tape_->has_tape() && operation == CPU6502::BusOperation::ReadOpcode) {
if(address == 0xf7b2) {
// Address 0xf7b2 contains a JSR to 0xf8c0 that will fill the tape buffer with the next header.
// So cancel that via a double NOP and fill in the next header programmatically.
Storage::Tape::Commodore::Parser parser;
std::unique_ptr<Storage::Tape::Commodore::Header> header = parser.get_next_header(tape_->get_tape());
// serialise to wherever b2:b3 points
uint16_t tape_buffer_pointer = (uint16_t)user_basic_memory_[0xb2] | (uint16_t)(user_basic_memory_[0xb3] << 8);
if(header) {
header->serialise(&user_basic_memory_[tape_buffer_pointer], 0x8000 - tape_buffer_pointer);
} else {
// no header found, so store end-of-tape
user_basic_memory_[tape_buffer_pointer] = 0x05; // i.e. end of tape
}
// clear status and the verify flag
user_basic_memory_[0x90] = 0;
user_basic_memory_[0x93] = 0;
*value = 0x0c; // i.e. NOP abs
} else if(address == 0xf90b) {
uint8_t x = (uint8_t)get_value_of_register(CPU6502::Register::X);
if(x == 0xe) {
Storage::Tape::Commodore::Parser parser;
std::unique_ptr<Storage::Tape::Commodore::Data> data = parser.get_next_data(tape_->get_tape());
uint16_t start_address, end_address;
start_address = (uint16_t)(user_basic_memory_[0xc1] | (user_basic_memory_[0xc2] << 8));
end_address = (uint16_t)(user_basic_memory_[0xae] | (user_basic_memory_[0xaf] << 8));
// perform a via-processor_write_memory_map_ memcpy
uint8_t *data_ptr = data->data.data();
size_t data_left = data->data.size();
while(data_left && start_address != end_address) {
uint8_t *page = processor_write_memory_map_[start_address >> 10];
if(page) page[start_address & 0x3ff] = *data_ptr;
data_ptr++;
start_address++;
data_left--;
}
// set tape status, carry and flag
user_basic_memory_[0x90] |= 0x40;
uint8_t flags = (uint8_t)get_value_of_register(CPU6502::Register::Flags);
flags &= ~(uint8_t)(CPU6502::Flag::Carry | CPU6502::Flag::Interrupt);
set_value_of_register(CPU6502::Register::Flags, flags);
// to ensure that execution proceeds to 0xfccf, pretend a NOP was here and
// ensure that the PC leaps to 0xfccf
set_value_of_register(CPU6502::Register::ProgramCounter, 0xfccf);
*value = 0xea; // i.e. NOP implied
}
}
}
}
else
{
} else {
uint8_t *ram = processor_write_memory_map_[address >> 10];
if(ram) ram[address & 0x3ff] = *value;
if((address&0xfc00) == 0x9000)
{
if((address&0xfc00) == 0x9000) {
if((address&0xff00) == 0x9000) mos6560_->set_register(address, *value);
if((address&0xfc10) == 0x9010) user_port_via_->set_register(address, *value);
if((address&0xfc20) == 0x9020) keyboard_via_->set_register(address, *value);
@@ -159,75 +181,40 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
user_port_via_->run_for_cycles(1);
keyboard_via_->run_for_cycles(1);
if(typer_ && operation == CPU6502::BusOperation::ReadOpcode && address == 0xEB1E)
{
if(!typer_->type_next_character())
{
if(typer_ && operation == CPU6502::BusOperation::ReadOpcode && address == 0xEB1E) {
if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
}
}
tape_.run_for_cycles(1);
tape_->run_for_cycles(1);
if(c1540_) c1540_->run_for_cycles(1);
// If using fast tape then:
// if the PC hits 0xf98e, the ROM's tape loading routine, then begin zero cost processing;
// if the PC heads into RAM
//
// Where 'zero cost processing' is taken to be taking the 6560 off the bus (because I know it's
// expensive, and not relevant) then running the tape, the CPU and both 6522s as usual but not
// counting cycles towards the processing budget. So the limit is the host machine.
//
// Note the additional test above for PC hitting 0xf92f, which is a loop in the ROM that waits
// for an interesting interrupt. Up there the fast tape hack goes even further in also cutting
// the CPU out of the action.
if(use_fast_tape_hack_ && tape_.has_tape())
{
if(address == 0xf98e && operation == CPU6502::BusOperation::ReadOpcode)
{
is_running_at_zero_cost_ = true;
set_clock_is_unlimited(true);
}
if(
(address < 0xe000 && operation == CPU6502::BusOperation::ReadOpcode) ||
tape_.get_tape()->is_at_end()
)
{
is_running_at_zero_cost_ = false;
set_clock_is_unlimited(false);
}
}
return 1;
}
#pragma mark - 6522 delegate
void Machine::mos6522_did_change_interrupt_status(void *mos6522)
{
void Machine::mos6522_did_change_interrupt_status(void *mos6522) {
set_nmi_line(user_port_via_->get_interrupt_line());
set_irq_line(keyboard_via_->get_interrupt_line());
}
#pragma mark - Setup
void Machine::set_region(Commodore::Vic20::Region region)
{
void Machine::set_region(Commodore::Vic20::Region region) {
region_ = region;
switch(region)
{
switch(region) {
case PAL:
set_clock_rate(1108404);
if(mos6560_)
{
if(mos6560_) {
mos6560_->set_output_mode(MOS::MOS6560<Commodore::Vic20::Vic6560>::OutputMode::PAL);
mos6560_->set_clock_rate(1108404);
}
break;
case NTSC:
set_clock_rate(1022727);
if(mos6560_)
{
if(mos6560_) {
mos6560_->set_output_mode(MOS::MOS6560<Commodore::Vic20::Vic6560>::OutputMode::NTSC);
mos6560_->set_clock_rate(1022727);
}
@@ -235,8 +222,7 @@ void Machine::set_region(Commodore::Vic20::Region region)
}
}
void Machine::setup_output(float aspect_ratio)
{
void Machine::setup_output(float aspect_ratio) {
mos6560_.reset(new Vic6560());
mos6560_->get_speaker()->set_high_frequency_cut_off(1600); // There is a 1.6Khz low-pass filter in the Vic-20.
set_region(region_);
@@ -248,17 +234,14 @@ void Machine::setup_output(float aspect_ratio)
mos6560_->colour_memory = colour_memory_;
}
void Machine::close_output()
{
void Machine::close_output() {
mos6560_ = nullptr;
}
void Machine::set_rom(ROMSlot slot, size_t length, const uint8_t *data)
{
void Machine::set_rom(ROMSlot slot, size_t length, const uint8_t *data) {
uint8_t *target = nullptr;
size_t max_length = 0x2000;
switch(slot)
{
switch(slot) {
case Kernel: target = kernel_rom_; break;
case Characters: target = character_rom_; max_length = 0x1000; break;
case BASIC: target = basic_rom_; break;
@@ -269,45 +252,20 @@ void Machine::set_rom(ROMSlot slot, size_t length, const uint8_t *data)
return;
}
if(target)
{
if(target) {
size_t length_to_copy = std::min(max_length, length);
memcpy(target, data, length_to_copy);
}
}
//void Machine::set_prg(const char *file_name, size_t length, const uint8_t *data)
//{
// if(length > 2)
// {
// _rom_address = (uint16_t)(data[0] | (data[1] << 8));
// _rom_length = (uint16_t)(length - 2);
//
// // install in the ROM area if this looks like a ROM; otherwise put on tape and throw into that mechanism
// if(_rom_address == 0xa000)
// {
// _rom = new uint8_t[0x2000];
// memcpy(_rom, &data[2], length - 2);
// write_to_map(processor_read_memory_map_, _rom, _rom_address, 0x2000);
// }
// else
// {
// set_tape(std::shared_ptr<Storage::Tape::Tape>(new Storage::Tape::PRG(file_name)));
// }
// }
//}
#pragma mar - Tape
void Machine::configure_as_target(const StaticAnalyser::Target &target)
{
if(target.tapes.size())
{
tape_.set_tape(target.tapes.front());
void Machine::configure_as_target(const StaticAnalyser::Target &target) {
if(target.tapes.size()) {
tape_->set_tape(target.tapes.front());
}
if(target.disks.size())
{
if(target.disks.size()) {
// construct the 1540
c1540_.reset(new ::Commodore::C1540::Machine);
@@ -321,8 +279,7 @@ void Machine::configure_as_target(const StaticAnalyser::Target &target)
install_disk_rom();
}
if(target.cartridges.size())
{
if(target.cartridges.size()) {
rom_address_ = 0xa000;
std::vector<uint8_t> rom_image = target.cartridges.front()->get_segments().front().data;
rom_length_ = (uint16_t)(rom_image.size());
@@ -332,39 +289,31 @@ void Machine::configure_as_target(const StaticAnalyser::Target &target)
write_to_map(processor_read_memory_map_, rom_, rom_address_, 0x2000);
}
if(should_automatically_load_media_)
{
if(target.loadingCommand.length()) // TODO: and automatic loading option enabled
{
set_typer_for_string(target.loadingCommand.c_str());
}
if(target.loadingCommand.length()) {
set_typer_for_string(target.loadingCommand.c_str());
}
switch(target.vic20.memory_model)
{
case StaticAnalyser::Vic20MemoryModel::Unexpanded:
set_memory_size(Default);
break;
case StaticAnalyser::Vic20MemoryModel::EightKB:
set_memory_size(ThreeKB);
break;
case StaticAnalyser::Vic20MemoryModel::ThirtyTwoKB:
set_memory_size(ThirtyTwoKB);
break;
}
switch(target.vic20.memory_model) {
case StaticAnalyser::Vic20MemoryModel::Unexpanded:
set_memory_size(Default);
break;
case StaticAnalyser::Vic20MemoryModel::EightKB:
set_memory_size(ThreeKB);
break;
case StaticAnalyser::Vic20MemoryModel::ThirtyTwoKB:
set_memory_size(ThirtyTwoKB);
break;
}
}
void Machine::tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape)
{
void Machine::tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) {
keyboard_via_->set_control_line_input(KeyboardVIA::Port::A, KeyboardVIA::Line::One, tape->get_input());
}
#pragma mark - Disc
void Machine::install_disk_rom()
{
if(drive_rom_ && c1540_)
{
void Machine::install_disk_rom() {
if(drive_rom_ && c1540_) {
c1540_->set_rom(drive_rom_.get());
c1540_->run_for_cycles(2000000);
drive_rom_.reset();
@@ -373,45 +322,36 @@ void Machine::install_disk_rom()
#pragma mark - UserPortVIA
uint8_t UserPortVIA::get_port_input(Port port)
{
if(!port)
{
return port_a_; // TODO: bit 6 should be high if there is no tape, low otherwise
uint8_t UserPortVIA::get_port_input(Port port) {
if(!port) {
return port_a_ | (tape_->has_tape() ? 0x00 : 0x40);
}
return 0xff;
}
void UserPortVIA::set_control_line_output(Port port, Line line, bool value)
{
// if(port == Port::A && line == Line::Two) {
// printf("Tape motor %s\n", value ? "on" : "off");
// }
void UserPortVIA::set_control_line_output(Port port, Line line, bool value) {
if(port == Port::A && line == Line::Two) {
tape_->set_motor_control(!value);
}
}
void UserPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool value)
{
switch(line)
{
void UserPortVIA::set_serial_line_state(::Commodore::Serial::Line line, bool value) {
switch(line) {
default: break;
case ::Commodore::Serial::Line::Data: port_a_ = (port_a_ & ~0x02) | (value ? 0x02 : 0x00); break;
case ::Commodore::Serial::Line::Clock: port_a_ = (port_a_ & ~0x01) | (value ? 0x01 : 0x00); break;
}
}
void UserPortVIA::set_joystick_state(JoystickInput input, bool value)
{
if(input != JoystickInput::Right)
{
void UserPortVIA::set_joystick_state(JoystickInput input, bool value) {
if(input != JoystickInput::Right) {
port_a_ = (port_a_ & ~input) | (value ? 0 : input);
}
}
void UserPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask)
{
void UserPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask) {
// Line 7 of port A is inverted and output as serial ATN
if(!port)
{
if(!port) {
std::shared_ptr<::Commodore::Serial::Port> serialPort = serial_port_.lock();
if(serialPort)
serialPort->set_output(::Commodore::Serial::Line::Attention, (::Commodore::Serial::LineLevel)!(value&0x80));
@@ -420,38 +360,35 @@ void UserPortVIA::set_port_output(Port port, uint8_t value, uint8_t mask)
UserPortVIA::UserPortVIA() : port_a_(0xbf) {}
void UserPortVIA::set_serial_port(std::shared_ptr<::Commodore::Serial::Port> serialPort)
{
void UserPortVIA::set_serial_port(std::shared_ptr<::Commodore::Serial::Port> serialPort) {
serial_port_ = serialPort;
}
void UserPortVIA::set_tape(std::shared_ptr<Storage::Tape::BinaryTapePlayer> tape) {
tape_ = tape;
}
#pragma mark - KeyboardVIA
KeyboardVIA::KeyboardVIA() : port_b_(0xff)
{
KeyboardVIA::KeyboardVIA() : port_b_(0xff) {
clear_all_keys();
}
void KeyboardVIA::set_key_state(uint16_t key, bool isPressed)
{
void KeyboardVIA::set_key_state(uint16_t key, bool isPressed) {
if(isPressed)
columns_[key & 7] &= ~(key >> 3);
else
columns_[key & 7] |= (key >> 3);
}
void KeyboardVIA::clear_all_keys()
{
void KeyboardVIA::clear_all_keys() {
memset(columns_, 0xff, sizeof(columns_));
}
uint8_t KeyboardVIA::get_port_input(Port port)
{
if(!port)
{
uint8_t KeyboardVIA::get_port_input(Port port) {
if(!port) {
uint8_t result = 0xff;
for(int c = 0; c < 8; c++)
{
for(int c = 0; c < 8; c++) {
if(!(activation_mask_&(1 << c)))
result &= columns_[c];
}
@@ -461,19 +398,15 @@ uint8_t KeyboardVIA::get_port_input(Port port)
return port_b_;
}
void KeyboardVIA::set_port_output(Port port, uint8_t value, uint8_t mask)
{
void KeyboardVIA::set_port_output(Port port, uint8_t value, uint8_t mask) {
if(port)
activation_mask_ = (value & mask) | (~mask);
}
void KeyboardVIA::set_control_line_output(Port port, Line line, bool value)
{
if(line == Line::Two)
{
void KeyboardVIA::set_control_line_output(Port port, Line line, bool value) {
if(line == Line::Two) {
std::shared_ptr<::Commodore::Serial::Port> serialPort = serial_port_.lock();
if(serialPort)
{
if(serialPort) {
// CB2 is inverted to become serial data; CA2 is inverted to become serial clock
if(port == Port::A)
serialPort->set_output(::Commodore::Serial::Line::Clock, (::Commodore::Serial::LineLevel)!value);
@@ -483,28 +416,23 @@ void KeyboardVIA::set_control_line_output(Port port, Line line, bool value)
}
}
void KeyboardVIA::set_joystick_state(JoystickInput input, bool value)
{
if(input == JoystickInput::Right)
{
void KeyboardVIA::set_joystick_state(JoystickInput input, bool value) {
if(input == JoystickInput::Right) {
port_b_ = (port_b_ & ~input) | (value ? 0 : input);
}
}
void KeyboardVIA::set_serial_port(std::shared_ptr<::Commodore::Serial::Port> serialPort)
{
void KeyboardVIA::set_serial_port(std::shared_ptr<::Commodore::Serial::Port> serialPort) {
serial_port_ = serialPort;
}
#pragma mark - SerialPort
void SerialPort::set_input(::Commodore::Serial::Line line, ::Commodore::Serial::LineLevel level)
{
void SerialPort::set_input(::Commodore::Serial::Line line, ::Commodore::Serial::LineLevel level) {
std::shared_ptr<UserPortVIA> userPortVIA = user_port_via_.lock();
if(userPortVIA) userPortVIA->set_serial_line_state(line, (bool)level);
}
void SerialPort::set_user_port_via(std::shared_ptr<UserPortVIA> userPortVIA)
{
void SerialPort::set_user_port_via(std::shared_ptr<UserPortVIA> userPortVIA) {
user_port_via_ = userPortVIA;
}

View File

@@ -87,10 +87,12 @@ class UserPortVIA: public MOS::MOS6522<UserPortVIA>, public MOS::MOS6522IRQDeleg
void set_port_output(Port port, uint8_t value, uint8_t mask);
void set_serial_port(std::shared_ptr<::Commodore::Serial::Port> serialPort);
void set_tape(std::shared_ptr<Storage::Tape::BinaryTapePlayer> tape);
private:
uint8_t port_a_;
std::weak_ptr<::Commodore::Serial::Port> serial_port_;
std::shared_ptr<Storage::Tape::BinaryTapePlayer> tape_;
};
class KeyboardVIA: public MOS::MOS6522<KeyboardVIA>, public MOS::MOS6522IRQDelegate {
@@ -129,8 +131,7 @@ class SerialPort : public ::Commodore::Serial::Port {
class Vic6560: public MOS::MOS6560<Vic6560> {
public:
inline void perform_read(uint16_t address, uint8_t *pixel_data, uint8_t *colour_data)
{
inline void perform_read(uint16_t address, uint8_t *pixel_data, uint8_t *colour_data) {
*pixel_data = video_memory_map[address >> 10] ? video_memory_map[address >> 10][address & 0x3ff] : 0xff; // TODO
*colour_data = colour_memory[address & 0x03ff];
}
@@ -165,7 +166,6 @@ class Machine:
void set_region(Region region);
inline void set_use_fast_tape_hack(bool activate) { use_fast_tape_hack_ = activate; }
inline void set_should_automatically_load_media(bool activate) { should_automatically_load_media_ = activate; }
// to satisfy CPU6502::Processor
unsigned int perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value);
@@ -214,15 +214,13 @@ class Machine:
std::shared_ptr<::Commodore::Serial::Bus> serial_bus_;
// Tape
Storage::Tape::BinaryTapePlayer tape_;
bool use_fast_tape_hack_, should_automatically_load_media_;
std::shared_ptr<Storage::Tape::BinaryTapePlayer> tape_;
bool use_fast_tape_hack_;
bool is_running_at_zero_cost_;
// Disk
std::shared_ptr<::Commodore::C1540::Machine> c1540_;
void install_disk_rom();
// Autoload string
};
}

View File

@@ -19,7 +19,7 @@ namespace ConfigurationTarget {
*/
class Machine {
public:
virtual void configure_as_target(const StaticAnalyser::Target &target) =0;
virtual void configure_as_target(const StaticAnalyser::Target &target) = 0;
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,12 @@
#include "../ConfigurationTarget.hpp"
#include "../CRTMachine.hpp"
#include "../Typer.hpp"
#include "Interrupts.hpp"
#include "Plus3.hpp"
#include "Speaker.hpp"
#include "Tape.hpp"
#include "Interrupts.hpp"
#include "Video.hpp"
#include <cstdint>
#include <vector>
@@ -92,8 +94,8 @@ class Machine:
// to satisfy CRTMachine::Machine
virtual void setup_output(float aspect_ratio);
virtual void close_output();
virtual std::shared_ptr<Outputs::CRT::CRT> get_crt() { return crt_; }
virtual std::shared_ptr<Outputs::Speaker> get_speaker() { return speaker_; }
virtual std::shared_ptr<Outputs::CRT::CRT> get_crt();
virtual std::shared_ptr<Outputs::Speaker> get_speaker();
virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles); }
// to satisfy Tape::Delegate
@@ -105,13 +107,10 @@ class Machine:
uint16_t *sequence_for_character(Utility::Typer *typer, char character);
private:
inline void update_display();
inline void start_pixel_line();
inline void end_pixel_line();
inline void output_pixels(unsigned int number_of_cycles);
inline void queue_next_display_interrupt();
inline void update_audio();
inline void signal_interrupt(Interrupt interrupt);
inline void clear_interrupt(Interrupt interrupt);
inline void evaluate_interrupts();
@@ -122,38 +121,20 @@ class Machine:
uint8_t os_[16384], ram_[32768];
std::vector<uint8_t> dfs_, adfs_;
// Things affected by registers, explicitly or otherwise.
uint8_t interrupt_status_, interrupt_control_;
uint8_t palette_[16];
uint8_t key_states_[14];
// Paging
ROMSlot active_rom_;
bool keyboard_is_active_, basic_is_active_;
uint8_t screen_mode_;
uint16_t screen_mode_base_address_;
uint16_t start_screen_address_;
// Interrupt and keyboard state
uint8_t interrupt_status_, interrupt_control_;
uint8_t key_states_[14];
// Counters related to simultaneous subsystems
unsigned int frame_cycles_, display_output_position_;
unsigned int audio_output_position_, audio_output_position_error_;
uint8_t phase_;
struct {
uint16_t forty1bpp[256];
uint8_t forty2bpp[256];
uint32_t eighty1bpp[256];
uint16_t eighty2bpp[256];
uint8_t eighty4bpp[256];
} palette_tables_;
// Display generation.
uint16_t start_line_address_, current_screen_address_;
int current_pixel_line_, current_pixel_column_, current_character_row_;
uint8_t last_pixel_byte_;
bool is_blank_line_;
// CRT output
uint8_t *current_output_target_, *initial_output_target_;
unsigned int current_output_divider_;
unsigned int cycles_since_display_update_;
unsigned int cycles_since_audio_update_;
int cycles_until_display_interrupt_;
Interrupt next_display_interrupt_;
VideoOutput::Range video_access_range_;
// Tape
Tape tape_;
@@ -163,9 +144,10 @@ class Machine:
// Disk
std::unique_ptr<Plus3> plus3_;
bool is_holding_shift_;
int shift_restart_counter_;
// Outputs
std::shared_ptr<Outputs::CRT::CRT> crt_;
std::unique_ptr<VideoOutput> video_output_;
std::shared_ptr<Speaker> speaker_;
bool speaker_is_enabled_;
};

View File

@@ -10,32 +10,41 @@
using namespace Electron;
Plus3::Plus3() : WD1770(P1770) {}
Plus3::Plus3() : WD1770(P1770), last_control_(0) {
set_control_register(last_control_, 0xff);
}
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive)
{
if(!drives_[drive])
{
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
if(!drives_[drive]) {
drives_[drive].reset(new Storage::Disk::Drive);
if(drive == selected_drive_) set_drive(drives_[drive]);
}
drives_[drive]->set_disk(disk);
}
void Plus3::set_control_register(uint8_t control)
{
// TODO:
void Plus3::set_control_register(uint8_t control) {
// bit 0 => enable or disable drive 1
// bit 1 => enable or disable drive 2
// bit 2 => side select
// bit 3 => single density select
switch(control&3)
{
case 0: selected_drive_ = -1; set_drive(nullptr); break;
default: selected_drive_ = 0; set_drive(drives_[0]); break;
case 2: selected_drive_ = 1; set_drive(drives_[1]); break;
}
if(drives_[0]) drives_[0]->set_head((control & 0x04) ? 1 : 0);
if(drives_[1]) drives_[1]->set_head((control & 0x04) ? 1 : 0);
set_is_double_density(!(control & 0x08));
uint8_t changes = control ^ last_control_;
last_control_ = control;
set_control_register(control, changes);
}
void Plus3::set_control_register(uint8_t control, uint8_t changes) {
if(changes&3) {
switch(control&3) {
case 0: selected_drive_ = -1; set_drive(nullptr); break;
default: selected_drive_ = 0; set_drive(drives_[0]); break;
case 2: selected_drive_ = 1; set_drive(drives_[1]); break;
}
}
if(changes & 0x04) {
invalidate_track();
if(drives_[0]) drives_[0]->set_head((control & 0x04) ? 1 : 0);
if(drives_[1]) drives_[1]->set_head((control & 0x04) ? 1 : 0);
}
if(changes & 0x08) set_is_double_density(!(control & 0x08));
}

View File

@@ -21,8 +21,10 @@ class Plus3 : public WD::WD1770 {
void set_control_register(uint8_t control);
private:
void set_control_register(uint8_t control, uint8_t changes);
std::shared_ptr<Storage::Disk::Drive> drives_[2];
int selected_drive_;
uint8_t last_control_;
};
}

View File

@@ -10,37 +10,29 @@
using namespace Electron;
void Speaker::get_samples(unsigned int number_of_samples, int16_t *target)
{
if(is_enabled_)
{
while(number_of_samples--)
{
void Speaker::get_samples(unsigned int number_of_samples, int16_t *target) {
if(is_enabled_) {
while(number_of_samples--) {
*target = (int16_t)((counter_ / (divider_+1)) * 8192);
target++;
counter_ = (counter_ + 1) % ((divider_+1) * 2);
}
}
else
{
} else {
memset(target, 0, sizeof(int16_t) * number_of_samples);
}
}
void Speaker::skip_samples(unsigned int number_of_samples)
{
void Speaker::skip_samples(unsigned int number_of_samples) {
counter_ = (counter_ + number_of_samples) % ((divider_+1) * 2);
}
void Speaker::set_divider(uint8_t divider)
{
void Speaker::set_divider(uint8_t divider) {
enqueue([=]() {
divider_ = divider * 32 / clock_rate_divider;
});
}
void Speaker::set_is_enabled(bool is_enabled)
{
void Speaker::set_is_enabled(bool is_enabled) {
enqueue([=]() {
is_enabled_ = is_enabled;
counter_ = 0;

View File

@@ -11,25 +11,21 @@
using namespace Electron;
Tape::Tape() :
TapePlayer(2000000),
is_running_(false),
data_register_(0),
delegate_(nullptr),
output_({.bits_remaining_until_empty = 0, .cycles_into_pulse = 0}),
last_posted_interrupt_status_(0),
interrupt_status_(0)
{}
TapePlayer(2000000),
is_running_(false),
data_register_(0),
delegate_(nullptr),
output_({.bits_remaining_until_empty = 0, .cycles_into_pulse = 0}),
last_posted_interrupt_status_(0),
interrupt_status_(0) {}
void Tape::push_tape_bit(uint16_t bit)
{
void Tape::push_tape_bit(uint16_t bit) {
data_register_ = (uint16_t)((data_register_ >> 1) | (bit << 10));
if(input_.minimum_bits_until_full) input_.minimum_bits_until_full--;
if(input_.minimum_bits_until_full == 8) interrupt_status_ &= ~Interrupt::ReceiveDataFull;
if(!input_.minimum_bits_until_full)
{
if((data_register_&0x3) == 0x1)
{
if(!input_.minimum_bits_until_full) {
if((data_register_&0x3) == 0x1) {
interrupt_status_ |= Interrupt::ReceiveDataFull;
if(is_in_input_mode_) input_.minimum_bits_until_full = 9;
}
@@ -44,66 +40,53 @@ void Tape::push_tape_bit(uint16_t bit)
evaluate_interrupts();
}
void Tape::evaluate_interrupts()
{
if(last_posted_interrupt_status_ != interrupt_status_)
{
void Tape::evaluate_interrupts() {
if(last_posted_interrupt_status_ != interrupt_status_) {
last_posted_interrupt_status_ = interrupt_status_;
if(delegate_) delegate_->tape_did_change_interrupt_status(this);
}
}
void Tape::clear_interrupts(uint8_t interrupts)
{
void Tape::clear_interrupts(uint8_t interrupts) {
interrupt_status_ &= ~interrupts;
evaluate_interrupts();
}
void Tape::set_is_in_input_mode(bool is_in_input_mode)
{
void Tape::set_is_in_input_mode(bool is_in_input_mode) {
is_in_input_mode_ = is_in_input_mode;
}
void Tape::set_counter(uint8_t value)
{
void Tape::set_counter(uint8_t value) {
output_.cycles_into_pulse = 0;
output_.bits_remaining_until_empty = 0;
}
void Tape::set_data_register(uint8_t value)
{
void Tape::set_data_register(uint8_t value) {
data_register_ = (uint16_t)((value << 2) | 1);
output_.bits_remaining_until_empty = 9;
}
uint8_t Tape::get_data_register()
{
uint8_t Tape::get_data_register() {
return (uint8_t)(data_register_ >> 2);
}
void Tape::process_input_pulse(Storage::Tape::Tape::Pulse pulse)
{
void Tape::process_input_pulse(Storage::Tape::Tape::Pulse pulse) {
crossings_[0] = crossings_[1];
crossings_[1] = crossings_[2];
crossings_[2] = crossings_[3];
crossings_[3] = Tape::Unrecognised;
if(pulse.type != Storage::Tape::Tape::Pulse::Zero)
{
if(pulse.type != Storage::Tape::Tape::Pulse::Zero) {
float pulse_length = (float)pulse.length.length / (float)pulse.length.clock_rate;
if(pulse_length >= 0.35 / 2400.0 && pulse_length < 0.7 / 2400.0) crossings_[3] = Tape::Short;
if(pulse_length >= 0.35 / 1200.0 && pulse_length < 0.7 / 1200.0) crossings_[3] = Tape::Long;
}
if(crossings_[0] == Tape::Long && crossings_[1] == Tape::Long)
{
if(crossings_[0] == Tape::Long && crossings_[1] == Tape::Long) {
push_tape_bit(0);
crossings_[0] = crossings_[1] = Tape::Recognised;
}
else
{
if(crossings_[0] == Tape::Short && crossings_[1] == Tape::Short && crossings_[2] == Tape::Short && crossings_[3] == Tape::Short)
{
} else {
if(crossings_[0] == Tape::Short && crossings_[1] == Tape::Short && crossings_[2] == Tape::Short && crossings_[3] == Tape::Short) {
push_tape_bit(1);
crossings_[0] = crossings_[1] =
crossings_[2] = crossings_[3] = Tape::Recognised;
@@ -111,23 +94,16 @@ void Tape::process_input_pulse(Storage::Tape::Tape::Pulse pulse)
}
}
void Tape::run_for_cycles(unsigned int number_of_cycles)
{
if(is_enabled_)
{
if(is_in_input_mode_)
{
if(is_running_)
{
void Tape::run_for_cycles(unsigned int number_of_cycles) {
if(is_enabled_) {
if(is_in_input_mode_) {
if(is_running_) {
TapePlayer::run_for_cycles((int)number_of_cycles);
}
}
else
{
} else {
output_.cycles_into_pulse += number_of_cycles;
while(output_.cycles_into_pulse > 1664) // 1664 = the closest you can get to 1200 baud if you're looking for something
{ // that divides the 125,000Hz clock that the sound divider runs off.
output_.cycles_into_pulse -= 1664;
while(output_.cycles_into_pulse > 1664) { // 1664 = the closest you can get to 1200 baud if you're looking for something
output_.cycles_into_pulse -= 1664; // that divides the 125,000Hz clock that the sound divider runs off.
push_tape_bit(1);
}
}

View File

@@ -8,18 +8,15 @@
#include "Electron.hpp"
int Electron::Machine::get_typer_delay()
{
int Electron::Machine::get_typer_delay() {
return get_is_resetting() ? 625*25*128 : 0; // wait one second if resetting
}
int Electron::Machine::get_typer_frequency()
{
int Electron::Machine::get_typer_frequency() {
return 625*128*2; // accept a new character every two frames
}
uint16_t *Electron::Machine::sequence_for_character(Utility::Typer *typer, char character)
{
uint16_t *Electron::Machine::sequence_for_character(Utility::Typer *typer, char character) {
#define KEYS(...) {__VA_ARGS__, TerminateSequence}
#define SHIFT(...) {KeyShift, __VA_ARGS__, TerminateSequence}
#define CTRL(...) {KeyControl, __VA_ARGS__, TerminateSequence}

453
Machines/Electron/Video.cpp Normal file
View File

@@ -0,0 +1,453 @@
//
// Video.cpp
// Clock Signal
//
// Created by Thomas Harte on 10/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "Video.hpp"
using namespace Electron;
#define graphics_line(v) ((((v) >> 7) - first_graphics_line + field_divider_line) % field_divider_line)
#define graphics_column(v) ((((v) & 127) - first_graphics_cycle + 128) & 127)
namespace {
static const int cycles_per_line = 128;
static const int lines_per_frame = 625;
static const int cycles_per_frame = lines_per_frame * cycles_per_line;
static const int crt_cycles_multiplier = 8;
static const int crt_cycles_per_line = crt_cycles_multiplier * cycles_per_line;
static const int field_divider_line = 312; // i.e. the line, simultaneous with which, the first field's sync ends. So if
// the first line with pixels in field 1 is the 20th in the frame, the first line
// with pixels in field 2 will be 20+field_divider_line
static const int first_graphics_line = 31;
static const int first_graphics_cycle = 33;
static const int display_end_interrupt_line = 256;
static const int real_time_clock_interrupt_1 = 16704;
static const int real_time_clock_interrupt_2 = 56704;
static const int display_end_interrupt_1 = (first_graphics_line + display_end_interrupt_line)*cycles_per_line;
static const int display_end_interrupt_2 = (first_graphics_line + field_divider_line + display_end_interrupt_line)*cycles_per_line;
}
#pragma mark - Lifecycle
VideoOutput::VideoOutput(uint8_t *memory) :
ram_(memory),
current_pixel_line_(-1),
output_position_(0),
screen_mode_(6),
screen_map_pointer_(0),
cycles_into_draw_action_(0) {
memset(palette_, 0xf, sizeof(palette_));
setup_screen_map();
setup_base_address();
crt_.reset(new Outputs::CRT::CRT(crt_cycles_per_line, 8, Outputs::CRT::DisplayType::PAL50, 1));
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
"{"
"uint texValue = texture(sampler, coordinate).r;"
"texValue >>= 4 - (int(icoordinate.x * 8) & 4);"
"return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));"
"}");
// TODO: as implied below, I've introduced a clock's latency into the graphics pipeline somehow. Investigate.
crt_->set_visible_area(crt_->get_rect_for_area(first_graphics_line - 3, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f));
}
#pragma mark - CRT getter
std::shared_ptr<Outputs::CRT::CRT> VideoOutput::get_crt() {
return crt_;
}
#pragma mark - Display update methods
void VideoOutput::start_pixel_line() {
current_pixel_line_ = (current_pixel_line_+1)&255;
if(!current_pixel_line_) {
start_line_address_ = start_screen_address_;
current_character_row_ = 0;
is_blank_line_ = false;
} else {
bool mode_has_blank_lines = (screen_mode_ == 6) || (screen_mode_ == 3);
is_blank_line_ = (mode_has_blank_lines && ((current_character_row_ > 7 && current_character_row_ < 10) || (current_pixel_line_ > 249)));
if(!is_blank_line_) {
start_line_address_++;
if(current_character_row_ > 7) {
start_line_address_ += ((screen_mode_ < 4) ? 80 : 40) * 8 - 8;
current_character_row_ = 0;
}
}
}
current_screen_address_ = start_line_address_;
current_pixel_column_ = 0;
initial_output_target_ = current_output_target_ = nullptr;
}
void VideoOutput::end_pixel_line() {
if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_);
current_character_row_++;
}
void VideoOutput::output_pixels(unsigned int number_of_cycles) {
if(!number_of_cycles) return;
if(is_blank_line_) {
crt_->output_blank(number_of_cycles * crt_cycles_multiplier);
} else {
unsigned int divider = 1;
switch(screen_mode_) {
case 0: case 3: divider = 2; break;
case 1: case 4: case 6: divider = 4; break;
case 2: case 5: divider = 8; break;
}
if(!initial_output_target_ || divider != current_output_divider_) {
if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_);
current_output_divider_ = divider;
initial_output_target_ = current_output_target_ = crt_->allocate_write_area(640 / current_output_divider_);
}
#define get_pixel() \
if(current_screen_address_&32768) {\
current_screen_address_ = (screen_mode_base_address_ + current_screen_address_)&32767;\
}\
last_pixel_byte_ = ram_[current_screen_address_];\
current_screen_address_ = current_screen_address_+8
switch(screen_mode_) {
case 0: case 3:
if(initial_output_target_) {
while(number_of_cycles--) {
get_pixel();
*(uint32_t *)current_output_target_ = palette_tables_.eighty1bpp[last_pixel_byte_];
current_output_target_ += 4;
current_pixel_column_++;
}
} else current_output_target_ += 4*number_of_cycles;
break;
case 1:
if(initial_output_target_) {
while(number_of_cycles--) {
get_pixel();
*(uint16_t *)current_output_target_ = palette_tables_.eighty2bpp[last_pixel_byte_];
current_output_target_ += 2;
current_pixel_column_++;
}
} else current_output_target_ += 2*number_of_cycles;
break;
case 2:
if(initial_output_target_) {
while(number_of_cycles--) {
get_pixel();
*current_output_target_ = palette_tables_.eighty4bpp[last_pixel_byte_];
current_output_target_ += 1;
current_pixel_column_++;
}
} else current_output_target_ += number_of_cycles;
break;
case 4: case 6:
if(initial_output_target_) {
if(current_pixel_column_&1) {
last_pixel_byte_ <<= 4;
*(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_];
current_output_target_ += 2;
number_of_cycles--;
current_pixel_column_++;
}
while(number_of_cycles > 1) {
get_pixel();
*(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_];
current_output_target_ += 2;
last_pixel_byte_ <<= 4;
*(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_];
current_output_target_ += 2;
number_of_cycles -= 2;
current_pixel_column_+=2;
}
if(number_of_cycles) {
get_pixel();
*(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_];
current_output_target_ += 2;
current_pixel_column_++;
}
} else current_output_target_ += 2 * number_of_cycles;
break;
case 5:
if(initial_output_target_) {
if(current_pixel_column_&1) {
last_pixel_byte_ <<= 2;
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
current_output_target_ += 1;
number_of_cycles--;
current_pixel_column_++;
}
while(number_of_cycles > 1) {
get_pixel();
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
current_output_target_ += 1;
last_pixel_byte_ <<= 2;
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
current_output_target_ += 1;
number_of_cycles -= 2;
current_pixel_column_+=2;
}
if(number_of_cycles) {
get_pixel();
*current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_];
current_output_target_ += 1;
current_pixel_column_++;
}
} else current_output_target_ += number_of_cycles;
break;
}
#undef get_pixel
}
}
void VideoOutput::run_for_cycles(int number_of_cycles) {
output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame;
while(number_of_cycles) {
int draw_action_length = screen_map_[screen_map_pointer_].length;
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels((unsigned int)time_left_in_action);
number_of_cycles -= time_left_in_action;
cycles_into_draw_action_ += time_left_in_action;
if(cycles_into_draw_action_ == draw_action_length) {
switch(screen_map_[screen_map_pointer_].type) {
case DrawAction::Sync: crt_->output_sync((unsigned int)(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::ColourBurst: crt_->output_default_colour_burst((unsigned int)(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::Blank: crt_->output_blank((unsigned int)(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::Pixels: end_pixel_line(); break;
}
screen_map_pointer_ = (screen_map_pointer_ + 1) % screen_map_.size();
cycles_into_draw_action_ = 0;
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) start_pixel_line();
}
}
}
#pragma mark - Register hub
void VideoOutput::set_register(int address, uint8_t value) {
switch(address & 0xf) {
case 0x02:
start_screen_address_ = (start_screen_address_ & 0xfe00) | (uint16_t)((value & 0xe0) << 1);
if(!start_screen_address_) start_screen_address_ |= 0x8000;
break;
case 0x03:
start_screen_address_ = (start_screen_address_ & 0x01ff) | (uint16_t)((value & 0x3f) << 9);
if(!start_screen_address_) start_screen_address_ |= 0x8000;
break;
case 0x07: {
// update screen mode
uint8_t new_screen_mode = (value >> 3)&7;
if(new_screen_mode == 7) new_screen_mode = 4;
if(new_screen_mode != screen_mode_) {
screen_mode_ = new_screen_mode;
setup_base_address();
}
}
break;
case 0x08: case 0x09: case 0x0a: case 0x0b:
case 0x0c: case 0x0d: case 0x0e: case 0x0f: {
static const int registers[4][4] = {
{10, 8, 2, 0},
{14, 12, 6, 4},
{15, 13, 7, 5},
{11, 9, 3, 1},
};
const int index = (address >> 1)&3;
const uint8_t colour = ~value;
if(address&1) {
palette_[registers[index][0]] = (palette_[registers[index][0]]&3) | ((colour >> 1)&4);
palette_[registers[index][1]] = (palette_[registers[index][1]]&3) | ((colour >> 0)&4);
palette_[registers[index][2]] = (palette_[registers[index][2]]&3) | ((colour << 1)&4);
palette_[registers[index][3]] = (palette_[registers[index][3]]&3) | ((colour << 2)&4);
palette_[registers[index][2]] = (palette_[registers[index][2]]&5) | ((colour >> 4)&2);
palette_[registers[index][3]] = (palette_[registers[index][3]]&5) | ((colour >> 3)&2);
} else {
palette_[registers[index][0]] = (palette_[registers[index][0]]&6) | ((colour >> 7)&1);
palette_[registers[index][1]] = (palette_[registers[index][1]]&6) | ((colour >> 6)&1);
palette_[registers[index][2]] = (palette_[registers[index][2]]&6) | ((colour >> 5)&1);
palette_[registers[index][3]] = (palette_[registers[index][3]]&6) | ((colour >> 4)&1);
palette_[registers[index][0]] = (palette_[registers[index][0]]&5) | ((colour >> 2)&2);
palette_[registers[index][1]] = (palette_[registers[index][1]]&5) | ((colour >> 1)&2);
}
// regenerate all palette tables for now
#define pack(a, b) (uint8_t)((a << 4) | (b))
for(int byte = 0; byte < 256; byte++) {
uint8_t *target = (uint8_t *)&palette_tables_.forty1bpp[byte];
target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]);
target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]);
target = (uint8_t *)&palette_tables_.eighty2bpp[byte];
target[0] = pack(palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]);
target[1] = pack(palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)], palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]);
target = (uint8_t *)&palette_tables_.eighty1bpp[byte];
target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]);
target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]);
target[2] = pack(palette_[(byte&0x08) >> 0], palette_[(byte&0x04) << 1]);
target[3] = pack(palette_[(byte&0x02) << 2], palette_[(byte&0x01) << 3]);
palette_tables_.forty2bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]);
palette_tables_.eighty4bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)],
palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]);
}
#undef pack
}
break;
}
}
void VideoOutput::setup_base_address() {
switch(screen_mode_) {
case 0: case 1: case 2: screen_mode_base_address_ = 0x3000; break;
case 3: screen_mode_base_address_ = 0x4000; break;
case 4: case 5: screen_mode_base_address_ = 0x5800; break;
case 6: screen_mode_base_address_ = 0x6000; break;
}
}
#pragma mark - Interrupts
VideoOutput::Interrupt VideoOutput::get_next_interrupt() {
VideoOutput::Interrupt interrupt;
if(output_position_ < real_time_clock_interrupt_1) {
interrupt.cycles = real_time_clock_interrupt_1 - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
}
if(output_position_ < display_end_interrupt_1) {
interrupt.cycles = display_end_interrupt_1 - output_position_;
interrupt.interrupt = DisplayEnd;
return interrupt;
}
if(output_position_ < real_time_clock_interrupt_2) {
interrupt.cycles = real_time_clock_interrupt_2 - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
}
if(output_position_ < display_end_interrupt_2) {
interrupt.cycles = display_end_interrupt_2 - output_position_;
interrupt.interrupt = DisplayEnd;
return interrupt;
}
interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_;
interrupt.interrupt = RealTimeClock;
return interrupt;
}
#pragma mark - RAM timing and access information
unsigned int VideoOutput::get_cycles_until_next_ram_availability(int from_time) {
unsigned int result = 0;
int position = output_position_ + from_time;
result += 1 + (position&1);
if(screen_mode_ < 4) {
const int current_column = graphics_column(position + (position&1));
int current_line = graphics_line(position);
if(current_column < 80 && current_line < 256) {
if(screen_mode_ == 3) {
int output_position_line = graphics_line(output_position_);
int implied_row = current_character_row_ + (current_line - output_position_line) % 10;
if(implied_row < 8)
result += (unsigned int)(80 - current_column);
}
else result += (unsigned int)(80 - current_column);
}
}
return result;
}
VideoOutput::Range VideoOutput::get_memory_access_range() {
// This can't be more specific than this without applying a lot more thought because of mixed modes:
// suppose a program runs half the screen in an 80-column mode then switches to 40 columns. Then the
// real end address will be at 128*80 + 128*40 after the original base, subject to wrapping that depends
// on where the overflow occurred. Assuming accesses may run from the lowest possible position through to
// the end of RAM is good enough for 95% of use cases however.
VideoOutput::Range range;
range.low_address = std::min(start_screen_address_, screen_mode_base_address_);
range.high_address = 0x8000;
return range;
}
#pragma mark - The screen map
void VideoOutput::setup_screen_map() {
/*
Odd field: Even field:
|--S--| -S-|
|--S--| |--S--|
|-S-B-| = 3 |--S--| = 2.5
|--B--| |--B--|
|--P--| |--P--|
|--B--| = 312 |--B--| = 312.5
|-B-
*/
for(int c = 0; c < 2; c++) {
if(c&1) {
screen_map_.emplace_back(DrawAction::Sync, (cycles_per_line * 5) >> 1);
screen_map_.emplace_back(DrawAction::Blank, cycles_per_line >> 1);
} else {
screen_map_.emplace_back(DrawAction::Blank, cycles_per_line >> 1);
screen_map_.emplace_back(DrawAction::Sync, (cycles_per_line * 5) >> 1);
}
for(int c = 0; c < first_graphics_line - 3; c++) emplace_blank_line();
for(int c = 0; c < 256; c++) emplace_pixel_line();
for(int c = 256 + first_graphics_line; c < 312; c++) emplace_blank_line();
if(c&1) emplace_blank_line();
}
}
void VideoOutput::emplace_blank_line() {
screen_map_.emplace_back(DrawAction::Sync, 9);
screen_map_.emplace_back(DrawAction::ColourBurst, 24 - 9);
screen_map_.emplace_back(DrawAction::Blank, 128 - 24);
}
void VideoOutput::emplace_pixel_line() {
// output format is:
// 9 cycles: sync
// ... to 24 cycles: colour burst
// ... to first_graphics_cycle: blank
// ... for 80 cycles: pixels
// ... until end of line: blank
screen_map_.emplace_back(DrawAction::Sync, 9);
screen_map_.emplace_back(DrawAction::ColourBurst, 24 - 9);
screen_map_.emplace_back(DrawAction::Blank, first_graphics_cycle - 24);
screen_map_.emplace_back(DrawAction::Pixels, 80);
screen_map_.emplace_back(DrawAction::Blank, 48 - first_graphics_cycle);
}

127
Machines/Electron/Video.hpp Normal file
View File

@@ -0,0 +1,127 @@
//
// Video.hpp
// Clock Signal
//
// Created by Thomas Harte on 10/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#ifndef Machines_Electron_Video_hpp
#define Machines_Electron_Video_hpp
#include "../../Outputs/CRT/CRT.hpp"
#include "Interrupts.hpp"
namespace Electron {
/*!
Implements the Electron's video subsystem plus appropriate signalling.
The Electron has an interlaced fully-bitmapped display with six different output modes,
running either at 40 or 80 columns. Memory is shared between video and CPU; when the video
is accessing it the CPU may not.
*/
class VideoOutput {
public:
/*!
Instantiates a VideoOutput that will read its pixels from @c memory. The pointer supplied
should be to address 0 in the unexpanded Electron's memory map.
*/
VideoOutput(uint8_t *memory);
/// @returns the CRT to which output is being painted.
std::shared_ptr<Outputs::CRT::CRT> get_crt();
/// Produces the next @c number_of_cycles cycles of video output.
void run_for_cycles(int number_of_cycles);
/*!
Writes @c value to the register at @c address. May mutate the results of @c get_next_interrupt,
@c get_cycles_until_next_ram_availability and @c get_memory_access_range.
*/
void set_register(int address, uint8_t value);
/*!
Describes an interrupt the video hardware will generate by its identity and scheduling time.
*/
struct Interrupt {
/// The interrupt that will be signalled.
Electron::Interrupt interrupt;
/// The number of cycles until it is signalled.
int cycles;
};
/*!
@returns the next interrupt that should be generated as a result of the video hardware.
The time until signalling returned is the number of cycles after the final one triggered
by the most recent call to @c run_for_cycles.
This result may be mutated by calls to @c set_register.
*/
Interrupt get_next_interrupt();
/*!
@returns the number of cycles after (final cycle of last run_for_cycles batch + @c from_time)
before the video circuits will allow the CPU to access RAM.
*/
unsigned int get_cycles_until_next_ram_availability(int from_time);
struct Range {
uint16_t low_address, high_address;
};
/*!
@returns the range of addresses that the video might read from.
*/
Range get_memory_access_range();
private:
inline void start_pixel_line();
inline void end_pixel_line();
inline void output_pixels(unsigned int number_of_cycles);
inline void setup_base_address();
int output_position_, unused_cycles_;
uint8_t palette_[16];
uint8_t screen_mode_;
uint16_t screen_mode_base_address_;
uint16_t start_screen_address_;
uint8_t *ram_;
struct {
uint16_t forty1bpp[256];
uint8_t forty2bpp[256];
uint32_t eighty1bpp[256];
uint16_t eighty2bpp[256];
uint8_t eighty4bpp[256];
} palette_tables_;
// Display generation.
uint16_t start_line_address_, current_screen_address_;
int current_pixel_line_, current_pixel_column_, current_character_row_;
uint8_t last_pixel_byte_;
bool is_blank_line_;
// CRT output
uint8_t *current_output_target_, *initial_output_target_;
unsigned int current_output_divider_;
std::shared_ptr<Outputs::CRT::CRT> crt_;
struct DrawAction {
enum Type {
Sync, ColourBurst, Blank, Pixels
} type;
int length;
DrawAction(Type type, int length) : type(type), length(length) {}
};
std::vector<DrawAction> screen_map_;
void setup_screen_map();
void emplace_blank_line();
void emplace_pixel_line();
size_t screen_map_pointer_;
int cycles_into_draw_action_;
};
}
#endif /* Video_hpp */

View File

@@ -10,18 +10,15 @@
#include <cstdlib>
void Memory::Fuzz(uint8_t *buffer, size_t size)
{
void Memory::Fuzz(uint8_t *buffer, size_t size) {
unsigned int divider = ((unsigned int)RAND_MAX + 1) / 256;
unsigned int shift = 1, value = 1;
while(value < divider)
{
while(value < divider) {
value <<= 1;
shift++;
}
for(size_t c = 0; c < size; c++)
{
buffer[c] = (uint8_t)(rand() >> shift);
for(size_t c = 0; c < size; c++) {
buffer[c] = (uint8_t)(std::rand() >> shift);
}
}

View File

@@ -19,110 +19,99 @@ namespace {
}
Microdisc::Microdisc() :
irq_enable_(false),
delegate_(nullptr),
paging_flags_(BASICDisable),
head_load_request_counter_(-1),
WD1770(P1793)
{}
irq_enable_(false),
delegate_(nullptr),
paging_flags_(BASICDisable),
head_load_request_counter_(-1),
WD1770(P1793),
last_control_(0) {
set_control_register(last_control_, 0xff);
}
void Microdisc::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive)
{
if(!drives_[drive])
{
void Microdisc::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
if(!drives_[drive]) {
drives_[drive].reset(new Storage::Disk::Drive);
if(drive == selected_drive_) set_drive(drives_[drive]);
}
drives_[drive]->set_disk(disk);
}
void Microdisc::set_control_register(uint8_t control)
{
printf("control: %d%d%d%d%d%d%d%d\n",
(control >> 7)&1,
(control >> 6)&1,
(control >> 5)&1,
(control >> 4)&1,
(control >> 3)&1,
(control >> 2)&1,
(control >> 1)&1,
(control >> 0)&1);
void Microdisc::set_control_register(uint8_t control) {
uint8_t changes = last_control_ ^ control;
last_control_ = control;
set_control_register(control, changes);
}
void Microdisc::set_control_register(uint8_t control, uint8_t changes) {
// b2: data separator clock rate select (1 = double) [TODO]
// b65: drive select
selected_drive_ = (control >> 5)&3;
set_drive(drives_[selected_drive_]);
if((changes >> 5)&3) {
selected_drive_ = (control >> 5)&3;
set_drive(drives_[selected_drive_]);
}
// b4: side select
unsigned int head = (control & 0x10) ? 1 : 0;
for(int c = 0; c < 4; c++)
{
if(drives_[c]) drives_[c]->set_head(head);
if(changes & 0x10) {
unsigned int head = (control & 0x10) ? 1 : 0;
for(int c = 0; c < 4; c++) {
if(drives_[c]) drives_[c]->set_head(head);
}
}
// b3: double density select (0 = double)
set_is_double_density(!(control & 0x08));
if(changes & 0x08) {
set_is_double_density(!(control & 0x08));
}
// b0: IRQ enable
bool had_irq = get_interrupt_request_line();
irq_enable_ = !!(control & 0x01);
bool has_irq = get_interrupt_request_line();
if(has_irq != had_irq && delegate_)
{
delegate_->wd1770_did_change_output(this);
if(changes & 0x01) {
bool had_irq = get_interrupt_request_line();
irq_enable_ = !!(control & 0x01);
bool has_irq = get_interrupt_request_line();
if(has_irq != had_irq && delegate_) {
delegate_->wd1770_did_change_output(this);
}
}
// b7: EPROM select (0 = select)
// b1: ROM disable (0 = disable)
int new_paging_flags = ((control & 0x02) ? 0 : BASICDisable) | ((control & 0x80) ? MicrodscDisable : 0);
if(new_paging_flags != paging_flags_)
{
paging_flags_ = new_paging_flags;
if(changes & 0x82) {
paging_flags_ = ((control & 0x02) ? 0 : BASICDisable) | ((control & 0x80) ? MicrodscDisable : 0);
if(delegate_) delegate_->microdisc_did_change_paging_flags(this);
}
}
bool Microdisc::get_interrupt_request_line()
{
bool Microdisc::get_interrupt_request_line() {
return irq_enable_ && WD1770::get_interrupt_request_line();
}
uint8_t Microdisc::get_interrupt_request_register()
{
return 0x7f | (get_interrupt_request_line() ? 0x00 : 0x80);
uint8_t Microdisc::get_interrupt_request_register() {
return 0x7f | (WD1770::get_interrupt_request_line() ? 0x00 : 0x80);
}
uint8_t Microdisc::get_data_request_register()
{
uint8_t Microdisc::get_data_request_register() {
return 0x7f | (get_data_request_line() ? 0x00 : 0x80);
}
void Microdisc::set_head_load_request(bool head_load)
{
void Microdisc::set_head_load_request(bool head_load) {
set_motor_on(head_load);
if(head_load)
{
if(head_load) {
head_load_request_counter_ = 0;
}
else
{
} else {
head_load_request_counter_ = head_load_request_counter_target;
set_head_loaded(head_load);
}
}
void Microdisc::run_for_cycles(unsigned int number_of_cycles)
{
if(head_load_request_counter_ < head_load_request_counter_target)
{
void Microdisc::run_for_cycles(unsigned int number_of_cycles) {
if(head_load_request_counter_ < head_load_request_counter_target) {
head_load_request_counter_ += number_of_cycles;
if(head_load_request_counter_ >= head_load_request_counter_target) set_head_loaded(true);
}
WD::WD1770::run_for_cycles(number_of_cycles);
}
bool Microdisc::get_drive_is_ready()
{
bool Microdisc::get_drive_is_ready() {
return true;
}

View File

@@ -39,6 +39,7 @@ class Microdisc: public WD::WD1770 {
inline int get_paging_flags() { return paging_flags_; }
private:
void set_control_register(uint8_t control, uint8_t changes);
void set_head_load_request(bool head_load);
bool get_drive_is_ready();
std::shared_ptr<Storage::Disk::Drive> drives_[4];
@@ -47,6 +48,7 @@ class Microdisc: public WD::WD1770 {
int paging_flags_;
int head_load_request_counter_;
Delegate *delegate_;
uint8_t last_control_;
};
}

View File

@@ -12,15 +12,14 @@
using namespace Oric;
Machine::Machine() :
cycles_since_video_update_(0),
use_fast_tape_hack_(false),
typer_delay_(2500000),
keyboard_read_count_(0),
keyboard_(new Keyboard),
ram_top_(0xbfff),
paged_rom_(rom_),
microdisc_is_enabled_(false)
{
cycles_since_video_update_(0),
use_fast_tape_hack_(false),
typer_delay_(2500000),
keyboard_read_count_(0),
keyboard_(new Keyboard),
ram_top_(0xbfff),
paged_rom_(rom_),
microdisc_is_enabled_(false) {
set_clock_rate(1000000);
via_.set_interrupt_delegate(this);
via_.keyboard = keyboard_;
@@ -29,43 +28,35 @@ Machine::Machine() :
Memory::Fuzz(ram_, sizeof(ram_));
}
void Machine::configure_as_target(const StaticAnalyser::Target &target)
{
if(target.tapes.size())
{
void Machine::configure_as_target(const StaticAnalyser::Target &target) {
if(target.tapes.size()) {
via_.tape->set_tape(target.tapes.front());
}
if(target.loadingCommand.length()) // TODO: and automatic loading option enabled
{
if(target.loadingCommand.length()) { // TODO: and automatic loading option enabled
set_typer_for_string(target.loadingCommand.c_str());
}
if(target.oric.has_microdisc)
{
if(target.oric.has_microdisc) {
microdisc_is_enabled_ = true;
microdisc_did_change_paging_flags(&microdisc_);
microdisc_.set_delegate(this);
}
int drive_index = 0;
for(auto disk : target.disks)
{
for(auto disk : target.disks) {
if(drive_index < 4) microdisc_.set_disk(disk, drive_index);
drive_index++;
}
if(target.oric.use_atmos_rom)
{
if(target.oric.use_atmos_rom) {
memcpy(rom_, basic11_rom_.data(), std::min(basic11_rom_.size(), sizeof(rom_)));
is_using_basic11_ = true;
tape_get_byte_address_ = 0xe6c9;
scan_keyboard_address_ = 0xf495;
tape_speed_address_ = 0x024d;
}
else
{
} else {
memcpy(rom_, basic10_rom_.data(), std::min(basic10_rom_.size(), sizeof(rom_)));
is_using_basic11_ = false;
@@ -75,40 +66,34 @@ void Machine::configure_as_target(const StaticAnalyser::Target &target)
}
}
void Machine::set_rom(ROM rom, const std::vector<uint8_t> &data)
{
switch(rom)
{
void Machine::set_rom(ROM rom, const std::vector<uint8_t> &data) {
switch(rom) {
case BASIC11: basic11_rom_ = std::move(data); break;
case BASIC10: basic10_rom_ = std::move(data); break;
case Microdisc: microdisc_rom_ = std::move(data); break;
case Colour:
colour_rom_ = std::move(data);
if(video_output_) video_output_->set_colour_rom(colour_rom_);
break;
}
}
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value)
{
if(address > ram_top_)
{
unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) {
if(address > ram_top_) {
if(isReadOperation(operation)) *value = paged_rom_[address - ram_top_ - 1];
// 024D = 0 => fast; otherwise slow
// E6C9 = read byte: return byte in A
if(address == tape_get_byte_address_ && paged_rom_ == rom_ && use_fast_tape_hack_ && operation == CPU6502::BusOperation::ReadOpcode && via_.tape->has_tape() && !via_.tape->get_tape()->is_at_end())
{
if(address == tape_get_byte_address_ && paged_rom_ == rom_ && use_fast_tape_hack_ && operation == CPU6502::BusOperation::ReadOpcode && via_.tape->has_tape() && !via_.tape->get_tape()->is_at_end()) {
uint8_t next_byte = via_.tape->get_next_byte(!ram_[tape_speed_address_]);
set_value_of_register(CPU6502::A, next_byte);
set_value_of_register(CPU6502::Flags, next_byte ? 0 : CPU6502::Flag::Zero);
*value = 0x60; // i.e. RTS
}
}
else
{
if((address & 0xff00) == 0x0300)
{
if(microdisc_is_enabled_ && address >= 0x0310)
{
switch(address)
{
} else {
if((address & 0xff00) == 0x0300) {
if(microdisc_is_enabled_ && address >= 0x0310) {
switch(address) {
case 0x0310: case 0x0311: case 0x0312: case 0x0313:
if(isReadOperation(operation)) *value = microdisc_.get_register(address);
else microdisc_.set_register(address, *value);
@@ -121,32 +106,25 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
if(isReadOperation(operation)) *value = microdisc_.get_data_request_register();
break;
}
}
else
{
} else {
if(isReadOperation(operation)) *value = via_.get_register(address);
else via_.set_register(address, *value);
}
}
else
{
} else {
if(isReadOperation(operation))
*value = ram_[address];
else
{
else {
if(address >= 0x9800 && address <= 0xc000) { update_video(); typer_delay_ = 0; }
ram_[address] = *value;
}
}
}
if(typer_ && address == scan_keyboard_address_ && operation == CPU6502::BusOperation::ReadOpcode)
{
if(typer_ && address == scan_keyboard_address_ && operation == CPU6502::BusOperation::ReadOpcode) {
// the Oric 1 misses any key pressed on the very first entry into the read keyboard routine, so don't
// do anything until at least the second, regardless of machine
if(!keyboard_read_count_) keyboard_read_count_++;
else if(!typer_->type_next_character())
{
else if(!typer_->type_next_character()) {
clear_all_keys();
typer_.reset();
}
@@ -158,44 +136,36 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin
return 1;
}
void Machine::synchronise()
{
void Machine::synchronise() {
update_video();
via_.synchronise();
}
void Machine::update_video()
{
void Machine::update_video() {
video_output_->run_for_cycles(cycles_since_video_update_);
cycles_since_video_update_ = 0;
}
void Machine::setup_output(float aspect_ratio)
{
video_output_.reset(new VideoOutput(ram_));
void Machine::setup_output(float aspect_ratio) {
via_.ay8910.reset(new GI::AY38910());
via_.ay8910->set_clock_rate(1000000);
video_output_.reset(new VideoOutput(ram_));
if(!colour_rom_.empty()) video_output_->set_colour_rom(colour_rom_);
}
void Machine::close_output()
{
void Machine::close_output() {
video_output_.reset();
via_.ay8910.reset();
}
void Machine::mos6522_did_change_interrupt_status(void *mos6522)
{
void Machine::mos6522_did_change_interrupt_status(void *mos6522) {
set_interrupt_line();
}
void Machine::set_key_state(uint16_t key, bool isPressed)
{
if(key == KeyNMI)
{
void Machine::set_key_state(uint16_t key, bool isPressed) {
if(key == KeyNMI) {
set_nmi_line(isPressed);
}
else
{
} else {
if(isPressed)
keyboard_->rows[key >> 8] |= (key & 0xff);
else
@@ -203,95 +173,80 @@ void Machine::set_key_state(uint16_t key, bool isPressed)
}
}
void Machine::clear_all_keys()
{
void Machine::clear_all_keys() {
memset(keyboard_->rows, 0, sizeof(keyboard_->rows));
}
void Machine::set_use_fast_tape_hack(bool activate)
{
void Machine::set_use_fast_tape_hack(bool activate) {
use_fast_tape_hack_ = activate;
}
void Machine::tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape_player)
{
void Machine::set_output_device(Outputs::CRT::OutputDevice output_device) {
video_output_->set_output_device(output_device);
}
void Machine::tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape_player) {
// set CB1
via_.set_control_line_input(VIA::Port::B, VIA::Line::One, tape_player->get_input());
}
std::shared_ptr<Outputs::CRT::CRT> Machine::get_crt()
{
std::shared_ptr<Outputs::CRT::CRT> Machine::get_crt() {
return video_output_->get_crt();
}
std::shared_ptr<Outputs::Speaker> Machine::get_speaker()
{
std::shared_ptr<Outputs::Speaker> Machine::get_speaker() {
return via_.ay8910;
}
void Machine::run_for_cycles(int number_of_cycles)
{
void Machine::run_for_cycles(int number_of_cycles) {
CPU6502::Processor<Machine>::run_for_cycles(number_of_cycles);
}
#pragma mark - The 6522
Machine::VIA::VIA() :
MOS::MOS6522<Machine::VIA>(),
cycles_since_ay_update_(0),
tape(new TapePlayer) {}
MOS::MOS6522<Machine::VIA>(),
cycles_since_ay_update_(0),
tape(new TapePlayer) {}
void Machine::VIA::set_control_line_output(Port port, Line line, bool value)
{
if(line)
{
void Machine::VIA::set_control_line_output(Port port, Line line, bool value) {
if(line) {
if(port) ay_bdir_ = value; else ay_bc1_ = value;
update_ay();
}
}
void Machine::VIA::set_port_output(Port port, uint8_t value, uint8_t direction_mask)
{
if(port)
{
void Machine::VIA::set_port_output(Port port, uint8_t value, uint8_t direction_mask) {
if(port) {
keyboard->row = value;
tape->set_motor_control(value & 0x40);
}
else
{
} else {
ay8910->set_data_input(value);
}
}
uint8_t Machine::VIA::get_port_input(Port port)
{
if(port)
{
uint8_t Machine::VIA::get_port_input(Port port) {
if(port) {
uint8_t column = ay8910->get_port_output(false) ^ 0xff;
return (keyboard->rows[keyboard->row & 7] & column) ? 0x08 : 0x00;
}
else
{
} else {
return ay8910->get_data_output();
}
}
void Machine::VIA::synchronise()
{
void Machine::VIA::synchronise() {
ay8910->run_for_cycles(cycles_since_ay_update_);
ay8910->flush();
cycles_since_ay_update_ = 0;
}
void Machine::VIA::run_for_cycles(unsigned int number_of_cycles)
{
void Machine::VIA::run_for_cycles(unsigned int number_of_cycles) {
cycles_since_ay_update_ += number_of_cycles;
MOS::MOS6522<VIA>::run_for_cycles(number_of_cycles);
tape->run_for_cycles((int)number_of_cycles);
}
void Machine::VIA::update_ay()
{
void Machine::VIA::update_ay() {
ay8910->run_for_cycles(cycles_since_ay_update_);
cycles_since_ay_update_ = 0;
ay8910->set_control_lines( (GI::AY38910::ControlLines)((ay_bdir_ ? GI::AY38910::BCDIR : 0) | (ay_bc1_ ? GI::AY38910::BC1 : 0) | GI::AY38910::BC2));
@@ -300,45 +255,34 @@ void Machine::VIA::update_ay()
#pragma mark - TapePlayer
Machine::TapePlayer::TapePlayer() :
Storage::Tape::BinaryTapePlayer(1000000)
{}
Storage::Tape::BinaryTapePlayer(1000000) {}
uint8_t Machine::TapePlayer::get_next_byte(bool fast)
{
uint8_t Machine::TapePlayer::get_next_byte(bool fast) {
return (uint8_t)parser_.get_next_byte(get_tape(), fast);
}
#pragma mark - Microdisc
void Machine::microdisc_did_change_paging_flags(class Microdisc *microdisc)
{
void Machine::microdisc_did_change_paging_flags(class Microdisc *microdisc) {
int flags = microdisc->get_paging_flags();
if(!(flags&Microdisc::PagingFlags::BASICDisable))
{
if(!(flags&Microdisc::PagingFlags::BASICDisable)) {
ram_top_ = 0xbfff;
paged_rom_ = rom_;
}
else
{
if(flags&Microdisc::PagingFlags::MicrodscDisable)
{
} else {
if(flags&Microdisc::PagingFlags::MicrodscDisable) {
ram_top_ = 0xffff;
}
else
{
} else {
ram_top_ = 0xdfff;
paged_rom_ = microdisc_rom_.data();
}
}
}
void Machine::wd1770_did_change_output(WD::WD1770 *wd1770)
{
void Machine::wd1770_did_change_output(WD::WD1770 *wd1770) {
set_interrupt_line();
}
void Machine::set_interrupt_line()
{
void Machine::set_interrupt_line() {
set_irq_line(
via_.get_interrupt_line() ||
(microdisc_is_enabled_ && microdisc_.get_interrupt_request_line()));

View File

@@ -53,7 +53,7 @@ enum Key: uint16_t {
};
enum ROM {
BASIC10, BASIC11, Microdisc
BASIC10, BASIC11, Microdisc, Colour
};
class Machine:
@@ -73,6 +73,7 @@ class Machine:
void clear_all_keys();
void set_use_fast_tape_hack(bool activate);
void set_output_device(Outputs::CRT::OutputDevice output_device);
// to satisfy ConfigurationTarget::Machine
void configure_as_target(const StaticAnalyser::Target &target);
@@ -103,7 +104,7 @@ class Machine:
private:
// RAM and ROM
std::vector<uint8_t> basic11_rom_, basic10_rom_, microdisc_rom_;
std::vector<uint8_t> basic11_rom_, basic10_rom_, microdisc_rom_, colour_rom_;
uint8_t ram_[65536], rom_[16384];
int cycles_since_video_update_;
inline void update_video();

View File

@@ -1,7 +1,6 @@
#include "Oric.hpp"
uint16_t *Oric::Machine::sequence_for_character(Utility::Typer *typer, char character)
{
uint16_t *Oric::Machine::sequence_for_character(Utility::Typer *typer, char character) {
#define KEYS(...) {__VA_ARGS__, TerminateSequence}
#define SHIFT(...) {KeyLeftShift, __VA_ARGS__, TerminateSequence}
#define X {NotMapped}

View File

@@ -20,67 +20,85 @@ namespace {
}
VideoOutput::VideoOutput(uint8_t *memory) :
ram_(memory),
frame_counter_(0), counter_(0),
is_graphics_mode_(false),
character_set_base_address_(0xb400),
phase_(0),
v_sync_start_position_(PAL50VSyncStartPosition), v_sync_end_position_(PAL50VSyncEndPosition),
counter_period_(PAL50Period), next_frame_is_sixty_hertz_(false),
crt_(new Outputs::CRT::CRT(64*6, 6, Outputs::CRT::DisplayType::PAL50, 1))
{
// TODO: this is a copy and paste from the Electron; factor out.
ram_(memory),
frame_counter_(0), counter_(0),
is_graphics_mode_(false),
character_set_base_address_(0xb400),
v_sync_start_position_(PAL50VSyncStartPosition), v_sync_end_position_(PAL50VSyncEndPosition),
counter_period_(PAL50Period), next_frame_is_sixty_hertz_(false),
crt_(new Outputs::CRT::CRT(64*6, 6, Outputs::CRT::DisplayType::PAL50, 2)) {
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)"
"{"
"uint texValue = texture(sampler, coordinate).r;"
"texValue >>= 4 - (int(icoordinate.x * 8) & 4);"
"return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));"
"}");
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate, float phase, float amplitude)"
"{"
"uint texValue = uint(dot(texture(sampler, coordinate).rg, uvec2(1, 256)));"
"uint iPhase = uint((phase + 3.141592654 + 0.39269908175) * 2.0 / 3.141592654) & 3u;"
"texValue = (texValue >> (4u*(3u - iPhase))) & 15u;"
"return (float(texValue) - 4.0) / 20.0;"
"}"
);
crt_->set_composite_function_type(Outputs::CRT::CRT::CompositeSourceType::DiscreteFourSamplesPerCycle, 0.0f);
crt_->set_output_device(Outputs::CRT::Television);
set_output_device(Outputs::CRT::Television);
crt_->set_visible_area(crt_->get_rect_for_area(50, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
}
std::shared_ptr<Outputs::CRT::CRT> VideoOutput::get_crt()
{
void VideoOutput::set_output_device(Outputs::CRT::OutputDevice output_device) {
output_device_ = output_device;
crt_->set_output_device(output_device);
}
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
for(size_t c = 0; c < 8; c++) {
size_t index = (c << 2);
uint16_t rom_value = (uint16_t)(((uint16_t)rom[index] << 8) | (uint16_t)rom[index+1]);
rom_value = (rom_value & 0xff00) | ((rom_value >> 4)&0x000f) | ((rom_value << 4)&0x00f0);
colour_forms_[c] = rom_value;
}
// check for big endianness and byte swap if required
uint16_t test_value = 0x0001;
if(*(uint8_t *)&test_value != 0x01) {
for(size_t c = 0; c < 8; c++) {
colour_forms_[c] = (uint16_t)((colour_forms_[c] >> 8) | (colour_forms_[c] << 8));
}
}
}
std::shared_ptr<Outputs::CRT::CRT> VideoOutput::get_crt() {
return crt_;
}
void VideoOutput::run_for_cycles(int number_of_cycles)
{
void VideoOutput::run_for_cycles(int number_of_cycles) {
// Vertical: 039: pixels; otherwise blank; 4853 sync, 5456 colour burst
// Horizontal: 0223: pixels; otherwise blank; 256259 sync
#define clamp(action) \
if(cycles_run_for <= number_of_cycles) { action; } else cycles_run_for = number_of_cycles;
while(number_of_cycles)
{
while(number_of_cycles) {
int h_counter = counter_ & 63;
int cycles_run_for = 0;
if(counter_ >= v_sync_start_position_ && counter_ < v_sync_end_position_)
{
if(counter_ >= v_sync_start_position_ && counter_ < v_sync_end_position_) {
// this is a sync line
cycles_run_for = v_sync_end_position_ - counter_;
clamp(crt_->output_sync((unsigned int)(v_sync_end_position_ - v_sync_start_position_) * 6));
}
else if(counter_ < 224*64 && h_counter < 40)
{
} else if(counter_ < 224*64 && h_counter < 40) {
// this is a pixel line
if(!h_counter)
{
ink_ = 0xff;
paper_ = 0x00;
if(!h_counter) {
ink_ = 0x7;
paper_ = 0x0;
use_alternative_character_set_ = use_double_height_characters_ = blink_text_ = false;
set_character_set_base_address();
phase_ += 64;
pixel_target_ = crt_->allocate_write_area(120);
pixel_target_ = (uint16_t *)crt_->allocate_write_area(240);
if(!counter_)
{
phase_ += 128; // TODO: incorporate all the lines that were missed
if(!counter_) {
frame_counter_++;
v_sync_start_position_ = next_frame_is_sixty_hertz_ ? PAL60VSyncStartPosition : PAL50VSyncStartPosition;
@@ -95,51 +113,48 @@ void VideoOutput::run_for_cycles(int number_of_cycles)
int character_base_address = 0xbb80 + (counter_ >> 9) * 40;
uint8_t blink_mask = (blink_text_ && (frame_counter_&32)) ? 0x00 : 0xff;
while(columns--)
{
while(columns--) {
uint8_t pixels, control_byte;
if(is_graphics_mode_ && counter_ < 200*64)
{
if(is_graphics_mode_ && counter_ < 200*64) {
control_byte = pixels = ram_[pixel_base_address + h_counter];
}
else
{
} else {
int address = character_base_address + h_counter;
control_byte = ram_[address];
int line = use_double_height_characters_ ? ((counter_ >> 7) & 7) : ((counter_ >> 6) & 7);
pixels = ram_[character_set_base_address_ + (control_byte&127) * 8 + line];
}
uint8_t inverse_mask = (control_byte & 0x80) ? 0x77 : 0x00;
uint8_t inverse_mask = (control_byte & 0x80) ? 0x7 : 0x0;
pixels &= blink_mask;
if(control_byte & 0x60)
{
if(pixel_target_)
{
uint8_t colours[2] = {
(uint8_t)(paper_ ^ inverse_mask),
(uint8_t)(ink_ ^ inverse_mask),
};
pixel_target_[0] = (colours[(pixels >> 4)&1] & 0x0f) | (colours[(pixels >> 5)&1] & 0xf0);
pixel_target_[1] = (colours[(pixels >> 2)&1] & 0x0f) | (colours[(pixels >> 3)&1] & 0xf0);
pixel_target_[2] = (colours[(pixels >> 0)&1] & 0x0f) | (colours[(pixels >> 1)&1] & 0xf0);
if(control_byte & 0x60) {
if(pixel_target_) {
uint16_t colours[2];
if(output_device_ == Outputs::CRT::Monitor) {
colours[0] = (uint8_t)(paper_ ^ inverse_mask);
colours[1] = (uint8_t)(ink_ ^ inverse_mask);
} else {
colours[0] = colour_forms_[paper_ ^ inverse_mask];
colours[1] = colour_forms_[ink_ ^ inverse_mask];
}
pixel_target_[0] = colours[(pixels >> 5)&1];
pixel_target_[1] = colours[(pixels >> 4)&1];
pixel_target_[2] = colours[(pixels >> 3)&1];
pixel_target_[3] = colours[(pixels >> 2)&1];
pixel_target_[4] = colours[(pixels >> 1)&1];
pixel_target_[5] = colours[(pixels >> 0)&1];
}
}
else
{
switch(control_byte & 0x1f)
{
case 0x00: ink_ = 0x00; break;
case 0x01: ink_ = 0x44; break;
case 0x02: ink_ = 0x22; break;
case 0x03: ink_ = 0x66; break;
case 0x04: ink_ = 0x11; break;
case 0x05: ink_ = 0x55; break;
case 0x06: ink_ = 0x33; break;
case 0x07: ink_ = 0x77; break;
} else {
switch(control_byte & 0x1f) {
case 0x00: ink_ = 0x0; break;
case 0x01: ink_ = 0x4; break;
case 0x02: ink_ = 0x2; break;
case 0x03: ink_ = 0x6; break;
case 0x04: ink_ = 0x1; break;
case 0x05: ink_ = 0x5; break;
case 0x06: ink_ = 0x3; break;
case 0x07: ink_ = 0x7; break;
case 0x08: case 0x09: case 0x0a: case 0x0b:
case 0x0c: case 0x0d: case 0x0e: case 0x0f:
@@ -149,14 +164,14 @@ void VideoOutput::run_for_cycles(int number_of_cycles)
set_character_set_base_address();
break;
case 0x10: paper_ = 0x00; break;
case 0x11: paper_ = 0x44; break;
case 0x12: paper_ = 0x22; break;
case 0x13: paper_ = 0x66; break;
case 0x14: paper_ = 0x11; break;
case 0x15: paper_ = 0x55; break;
case 0x16: paper_ = 0x33; break;
case 0x17: paper_ = 0x77; break;
case 0x10: paper_ = 0x0; break;
case 0x11: paper_ = 0x4; break;
case 0x12: paper_ = 0x2; break;
case 0x13: paper_ = 0x6; break;
case 0x14: paper_ = 0x1; break;
case 0x15: paper_ = 0x5; break;
case 0x16: paper_ = 0x3; break;
case 0x17: paper_ = 0x7; break;
case 0x18: case 0x19: case 0x1a: case 0x1b:
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
@@ -166,40 +181,35 @@ void VideoOutput::run_for_cycles(int number_of_cycles)
default: break;
}
if(pixel_target_) pixel_target_[0] = pixel_target_[1] = pixel_target_[2] = (uint8_t)(paper_ ^ inverse_mask);
if(pixel_target_) {
pixel_target_[0] = pixel_target_[1] =
pixel_target_[2] = pixel_target_[3] =
pixel_target_[4] = pixel_target_[5] =
(output_device_ == Outputs::CRT::Monitor) ? paper_ ^ inverse_mask : colour_forms_[paper_ ^ inverse_mask];
}
}
if(pixel_target_) pixel_target_ += 3;
if(pixel_target_) pixel_target_ += 6;
h_counter++;
}
if(h_counter == 40)
{
crt_->output_data(40 * 6, 2);
if(h_counter == 40) {
crt_->output_data(40 * 6, 1);
}
}
else
{
} else {
// this is a blank line (or the equivalent part of a pixel line)
if(h_counter < 48)
{
if(h_counter < 48) {
cycles_run_for = 48 - h_counter;
clamp(
int period = (counter_ < 224*64) ? 8 : 48;
crt_->output_blank((unsigned int)period * 6);
);
}
else if(h_counter < 54)
{
} else if(h_counter < 54) {
cycles_run_for = 54 - h_counter;
clamp(crt_->output_sync(6 * 6));
}
else if(h_counter < 56)
{
} else if(h_counter < 56) {
cycles_run_for = 56 - h_counter;
clamp(crt_->output_colour_burst(2 * 6, phase_, 128));
}
else
{
clamp(crt_->output_default_colour_burst(2 * 6));
} else {
cycles_run_for = 64 - h_counter;
clamp(crt_->output_blank(8 * 6));
}
@@ -210,8 +220,7 @@ void VideoOutput::run_for_cycles(int number_of_cycles)
}
}
void VideoOutput::set_character_set_base_address()
{
void VideoOutput::set_character_set_base_address() {
if(is_graphics_mode_) character_set_base_address_ = use_alternative_character_set_ ? 0x9c00 : 0x9800;
else character_set_base_address_ = use_alternative_character_set_ ? 0xb800 : 0xb400;
}

View File

@@ -18,6 +18,8 @@ class VideoOutput {
VideoOutput(uint8_t *memory);
std::shared_ptr<Outputs::CRT::CRT> get_crt();
void run_for_cycles(int number_of_cycles);
void set_colour_rom(const std::vector<uint8_t> &rom);
void set_output_device(Outputs::CRT::OutputDevice output_device);
private:
uint8_t *ram_;
@@ -27,8 +29,10 @@ class VideoOutput {
int counter_, frame_counter_;
int v_sync_start_position_, v_sync_end_position_, counter_period_;
// Output target
uint8_t *pixel_target_;
// Output target and device
uint16_t *pixel_target_;
uint16_t colour_forms_[8];
Outputs::CRT::OutputDevice output_device_;
// Registers
uint8_t ink_, paper_;
@@ -41,8 +45,6 @@ class VideoOutput {
bool use_alternative_character_set_;
bool use_double_height_characters_;
bool blink_text_;
uint8_t phase_;
};
}

View File

@@ -12,76 +12,61 @@
using namespace Utility;
Typer::Typer(const char *string, int delay, int frequency, Delegate *delegate) :
counter_(-delay), frequency_(frequency), string_pointer_(0), delegate_(delegate), phase_(0)
{
counter_(-delay), frequency_(frequency), string_pointer_(0), delegate_(delegate), phase_(0) {
size_t string_size = strlen(string) + 3;
string_ = (char *)malloc(string_size);
snprintf(string_, strlen(string) + 3, "%c%s%c", Typer::BeginString, string, Typer::EndString);
}
void Typer::update(int duration)
{
if(string_)
{
if(counter_ < 0 && counter_ + duration >= 0)
{
if(!type_next_character())
{
void Typer::update(int duration) {
if(string_) {
if(counter_ < 0 && counter_ + duration >= 0) {
if(!type_next_character()) {
delegate_->typer_reset(this);
}
}
counter_ += duration;
while(string_ && counter_ > frequency_)
{
while(string_ && counter_ > frequency_) {
counter_ -= frequency_;
if(!type_next_character())
{
if(!type_next_character()) {
delegate_->typer_reset(this);
}
}
}
}
bool Typer::type_next_character()
{
bool Typer::type_next_character() {
if(string_ == nullptr) return false;
if(delegate_->typer_set_next_character(this, string_[string_pointer_], phase_))
{
if(delegate_->typer_set_next_character(this, string_[string_pointer_], phase_)) {
phase_ = 0;
if(!string_[string_pointer_])
{
if(!string_[string_pointer_]) {
free(string_);
string_ = nullptr;
return false;
}
string_pointer_++;
}
else
{
} else {
phase_++;
}
return true;
}
Typer::~Typer()
{
Typer::~Typer() {
free(string_);
}
#pragma mark - Delegate
bool Typer::Delegate::typer_set_next_character(Utility::Typer *typer, char character, int phase)
{
bool Typer::Delegate::typer_set_next_character(Utility::Typer *typer, char character, int phase) {
uint16_t *sequence = sequence_for_character(typer, character);
if(!sequence) return true;
if(!phase) clear_all_keys();
else
{
else {
set_key_state(sequence[phase - 1], true);
return sequence[phase] == Typer::Delegate::EndSequence;
}
@@ -89,7 +74,6 @@ bool Typer::Delegate::typer_set_next_character(Utility::Typer *typer, char chara
return false;
}
uint16_t *Typer::Delegate::sequence_for_character(Typer *typer, char character)
{
uint16_t *Typer::Delegate::sequence_for_character(Typer *typer, char character) {
return nullptr;
}

View File

@@ -45,13 +45,11 @@ class Typer {
class TypeRecipient: public Typer::Delegate {
public:
void set_typer_for_string(const char *string)
{
void set_typer_for_string(const char *string) {
typer_.reset(new Typer(string, get_typer_delay(), get_typer_frequency(), this));
}
void typer_reset(Typer *typer)
{
void typer_reset(Typer *typer) {
clear_all_keys();
typer_.reset();
}

View File

@@ -1,11 +0,0 @@
//
// CRC.cpp
// Clock Signal
//
// Created by Thomas Harte on 18/09/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#include "CRC.hpp"
using namespace NumberTheory;

View File

@@ -16,22 +16,27 @@ namespace NumberTheory {
class CRC16 {
public:
CRC16(uint16_t polynomial, uint16_t reset_value) :
reset_value_(reset_value), value_(reset_value), polynomial_(polynomial) {}
inline void reset() { value_ = reset_value_; }
inline void add(uint8_t value) {
// TODO: go table based
value_ ^= (uint16_t)value << 8;
for(int c = 0; c < 8; c++)
{
uint16_t exclusive_or = (value_&0x8000) ? polynomial_ : 0x0000;
value_ = (uint16_t)(value_ << 1) ^ exclusive_or;
reset_value_(reset_value), value_(reset_value) {
for(int c = 0; c < 256; c++) {
uint16_t shift_value = (uint16_t)(c << 8);
for(int b = 0; b < 8; b++) {
uint16_t exclusive_or = (shift_value&0x8000) ? polynomial : 0x0000;
shift_value = (uint16_t)(shift_value << 1) ^ exclusive_or;
}
xor_table[c] = (uint16_t)shift_value;
}
}
inline uint16_t get_value() { return value_; }
inline void reset() { value_ = reset_value_; }
inline void add(uint8_t byte) {
value_ = (uint16_t)((value_ << 8) ^ xor_table[(value_ >> 8) ^ byte]);
}
inline uint16_t get_value() const { return value_; }
inline void set_value(uint16_t value) { value_ = value; }
private:
uint16_t reset_value_, polynomial_;
const uint16_t reset_value_;
uint16_t xor_table[256];
uint16_t value_;
};

View File

@@ -10,14 +10,17 @@
4B049CDD1DA3C82F00322067 /* BCDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B049CDC1DA3C82F00322067 /* BCDTest.swift */; };
4B0BE4281D3481E700D5256B /* DigitalPhaseLockedLoop.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0BE4261D3481E700D5256B /* DigitalPhaseLockedLoop.cpp */; };
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CCC421C62D0B3001CAC5F /* CRT.cpp */; };
4B121F951E05E66800BFDA12 /* PCMPatchedTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */; };
4B121F9B1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */; };
4B14145B1B58879D00E04248 /* CPU6502.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414571B58879D00E04248 /* CPU6502.cpp */; };
4B14145D1B5887A600E04248 /* CPU6502.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414571B58879D00E04248 /* CPU6502.cpp */; };
4B14145E1B5887AA00E04248 /* CPU6502AllRAM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414591B58879D00E04248 /* CPU6502AllRAM.cpp */; };
4B1414601B58885000E04248 /* WolfgangLorenzTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B14145F1B58885000E04248 /* WolfgangLorenzTests.swift */; };
4B1414621B58888700E04248 /* KlausDormannTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1414611B58888700E04248 /* KlausDormannTests.swift */; };
4B1D08061E0F7A1100763741 /* TimeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B1D08051E0F7A1100763741 /* TimeTests.mm */; };
4B1E85751D170228001EF87D /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E85731D170228001EF87D /* Typer.cpp */; };
4B1E85811D176468001EF87D /* 6532Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E85801D176468001EF87D /* 6532Tests.swift */; };
4B2409551C45AB05004DA684 /* Speaker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2409531C45AB05004DA684 /* Speaker.cpp */; };
4B1EDB451E39A0AC009D6819 /* chip.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B1EDB431E39A0AC009D6819 /* chip.png */; };
4B2A332A1DB8544D002876E3 /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A33281DB8544D002876E3 /* MemoryFuzzer.cpp */; };
4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; };
4B2A332F1DB86869002876E3 /* OricOptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A332E1DB86869002876E3 /* OricOptionsPanel.swift */; };
@@ -26,8 +29,10 @@
4B2A53A11D117D36003C6002 /* CSAtari2600.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A539A1D117D36003C6002 /* CSAtari2600.mm */; };
4B2A53A21D117D36003C6002 /* CSElectron.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A539C1D117D36003C6002 /* CSElectron.mm */; };
4B2A53A31D117D36003C6002 /* CSVic20.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A539E1D117D36003C6002 /* CSVic20.mm */; };
4B2AF8691E513FC20027EE29 /* TIATests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2AF8681E513FC20027EE29 /* TIATests.mm */; };
4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; };
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */; };
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B2C45411E3C3896002A2389 /* cartridge.png */; };
4B2E2D9A1C3A06EC00138695 /* Atari2600.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E2D971C3A06EC00138695 /* Atari2600.cpp */; };
4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E2D9B1C3A070400138695 /* Electron.cpp */; };
4B30512D1D989E2200B4FED8 /* Drive.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B30512B1D989E2200B4FED8 /* Drive.cpp */; };
@@ -39,6 +44,7 @@
4B3BA0CF1D318B44005DD7A7 /* MOS6522Bridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0C91D318B44005DD7A7 /* MOS6522Bridge.mm */; };
4B3BA0D01D318B44005DD7A7 /* MOS6532Bridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0CB1D318B44005DD7A7 /* MOS6532Bridge.mm */; };
4B3BA0D11D318B44005DD7A7 /* TestMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0CD1D318B44005DD7A7 /* TestMachine.mm */; };
4B3F1B461E0388D200DB26EE /* PCMPatchedTrack.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */; };
4B44EBF51DC987AF00A7820C /* AllSuiteA.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */; };
4B44EBF71DC9883B00A7820C /* 6502_functional_test.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */; };
4B44EBF91DC9898E00A7820C /* BCDTEST_beeb in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */; };
@@ -62,6 +68,10 @@
4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */; };
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B69FB451C4D950F00B5F0AA /* libz.tbd */; };
4B6C73BD1D387AE500AFCFCA /* DiskController.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */; };
4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7913CA1DFCD80E00175A82 /* Video.cpp */; };
4B79E4441E3AF38600141F11 /* cassette.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B79E4411E3AF38600141F11 /* cassette.png */; };
4B79E4451E3AF38600141F11 /* floppy35.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B79E4421E3AF38600141F11 /* floppy35.png */; };
4B79E4461E3AF38600141F11 /* floppy525.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B79E4431E3AF38600141F11 /* floppy525.png */; };
4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805EE1DCFC99C003085B1 /* Acorn.cpp */; };
4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F21DCFD22A003085B1 /* Commodore.cpp */; };
4B8805F71DCFF6C9003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F51DCFF6C9003085B1 /* Commodore.cpp */; };
@@ -75,6 +85,8 @@
4B8FE2221DA19FB20090D3CE /* MachinePanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8FE2211DA19FB20090D3CE /* MachinePanel.swift */; };
4B8FE2271DA1DE2D0090D3CE /* NSBundle+DataResource.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B8FE2261DA1DE2D0090D3CE /* NSBundle+DataResource.m */; };
4B8FE2291DA1EDDF0090D3CE /* ElectronOptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8FE2281DA1EDDF0090D3CE /* ElectronOptionsPanel.swift */; };
4B924E991E74D22700B76AF1 /* AtariStaticAnalyserTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B924E981E74D22700B76AF1 /* AtariStaticAnalyserTests.mm */; };
4B9252CE1E74D28200B76AF1 /* Atari ROMs in Resources */ = {isa = PBXBuildFile; fileRef = 4B9252CD1E74D28200B76AF1 /* Atari ROMs */; };
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */; };
4B96F7221D75119A0058BB2D /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B96F7201D75119A0058BB2D /* Tape.cpp */; };
4B9CCDA11DA279CA0098B625 /* Vic20OptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9CCDA01DA279CA0098B625 /* Vic20OptionsPanel.swift */; };
@@ -349,6 +361,7 @@
4BB299F71B587D8400A49093 /* txan in Resources */ = {isa = PBXBuildFile; fileRef = 4BB298EB1B587D8400A49093 /* txan */; };
4BB299F81B587D8400A49093 /* txsn in Resources */ = {isa = PBXBuildFile; fileRef = 4BB298EC1B587D8400A49093 /* txsn */; };
4BB299F91B587D8400A49093 /* tyan in Resources */ = {isa = PBXBuildFile; fileRef = 4BB298ED1B587D8400A49093 /* tyan */; };
4BB2A9AF1E13367E001A5C23 /* CRCTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BB2A9AE1E13367E001A5C23 /* CRCTests.mm */; };
4BB697CB1D4B6D3E00248BDF /* TimedEventLoop.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB697C91D4B6D3E00248BDF /* TimedEventLoop.cpp */; };
4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB697CC1D4BA44400248BDF /* CommodoreGCR.cpp */; };
4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EA11B587A5100552FC2 /* AppDelegate.swift */; };
@@ -380,9 +393,12 @@
4BCF1FAB1DADD41B0039D2E7 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA91DADD41B0039D2E7 /* StaticAnalyser.cpp */; };
4BD14B111D74627C0088EAD6 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD14B0F1D74627C0088EAD6 /* StaticAnalyser.cpp */; };
4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD468F51D8DF41D0084958B /* 1770.cpp */; };
4BD4A8CD1E077E8A0020D856 /* PCMSegment.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B121F961E060CF000BFDA12 /* PCMSegment.cpp */; };
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; };
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.m */; };
4BD69F941D98760000243FE1 /* AcornADF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD69F921D98760000243FE1 /* AcornADF.cpp */; };
4BE77A2E1D84ADFB00BC3827 /* File.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BE77A2C1D84ADFB00BC3827 /* File.cpp */; };
4BE7C9181E3D397100A5496D /* TIA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BE7C9161E3D397100A5496D /* TIA.cpp */; };
4BEA525E1DF33323007E74F2 /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BEA525D1DF33323007E74F2 /* Tape.cpp */; };
4BEA52631DF339D7007E74F2 /* Speaker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BEA52611DF339D7007E74F2 /* Speaker.cpp */; };
4BEA52661DF3472B007E74F2 /* Speaker.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BEA52641DF3472B007E74F2 /* Speaker.cpp */; };
@@ -392,10 +408,8 @@
4BEF6AAC1D35D1C400E73575 /* DPLLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEF6AAB1D35D1C400E73575 /* DPLLTests.swift */; };
4BF1354C1D6D2C300054B2EA /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF1354A1D6D2C300054B2EA /* StaticAnalyser.cpp */; };
4BF8295D1D8F048B001BAE39 /* MFM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF8295B1D8F048B001BAE39 /* MFM.cpp */; };
4BF829601D8F3C87001BAE39 /* CRC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF8295E1D8F3C87001BAE39 /* CRC.cpp */; };
4BF829631D8F536B001BAE39 /* SSD.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF829611D8F536B001BAE39 /* SSD.cpp */; };
4BF829661D8F732B001BAE39 /* Disk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF829641D8F732B001BAE39 /* Disk.cpp */; };
4BF829691D8F7361001BAE39 /* File.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BF829671D8F7361001BAE39 /* File.cpp */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -423,6 +437,10 @@
4B0BE4271D3481E700D5256B /* DigitalPhaseLockedLoop.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DigitalPhaseLockedLoop.hpp; sourceTree = "<group>"; };
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRT.cpp; sourceTree = "<group>"; };
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRT.hpp; sourceTree = "<group>"; };
4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMPatchedTrackTests.mm; sourceTree = "<group>"; };
4B121F961E060CF000BFDA12 /* PCMSegment.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMSegment.cpp; sourceTree = "<group>"; };
4B121F971E060CF000BFDA12 /* PCMSegment.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMSegment.hpp; sourceTree = "<group>"; };
4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMSegmentEventSourceTests.mm; sourceTree = "<group>"; };
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ClockSignal-Bridging-Header.h"; sourceTree = "<group>"; };
4B1414571B58879D00E04248 /* CPU6502.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CPU6502.cpp; sourceTree = "<group>"; };
4B1414581B58879D00E04248 /* CPU6502.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CPU6502.hpp; sourceTree = "<group>"; };
@@ -430,11 +448,12 @@
4B14145A1B58879D00E04248 /* CPU6502AllRAM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CPU6502AllRAM.hpp; sourceTree = "<group>"; };
4B14145F1B58885000E04248 /* WolfgangLorenzTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WolfgangLorenzTests.swift; sourceTree = "<group>"; };
4B1414611B58888700E04248 /* KlausDormannTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KlausDormannTests.swift; sourceTree = "<group>"; };
4B1D08051E0F7A1100763741 /* TimeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TimeTests.mm; sourceTree = "<group>"; };
4B1E85731D170228001EF87D /* Typer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Typer.cpp; sourceTree = "<group>"; };
4B1E85741D170228001EF87D /* Typer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Typer.hpp; sourceTree = "<group>"; };
4B1E857B1D174DEC001EF87D /* 6532.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 6532.hpp; sourceTree = "<group>"; };
4B1E85801D176468001EF87D /* 6532Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6532Tests.swift; sourceTree = "<group>"; };
4B2409531C45AB05004DA684 /* Speaker.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Speaker.cpp; path = ../../Outputs/Speaker.cpp; sourceTree = "<group>"; };
4B1EDB431E39A0AC009D6819 /* chip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chip.png; sourceTree = "<group>"; };
4B2409541C45AB05004DA684 /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Speaker.hpp; path = ../../Outputs/Speaker.hpp; sourceTree = "<group>"; };
4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = "<group>"; };
4B2A33281DB8544D002876E3 /* MemoryFuzzer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MemoryFuzzer.cpp; sourceTree = "<group>"; };
@@ -454,10 +473,12 @@
4B2A539C1D117D36003C6002 /* CSElectron.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSElectron.mm; sourceTree = "<group>"; };
4B2A539D1D117D36003C6002 /* CSVic20.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSVic20.h; sourceTree = "<group>"; };
4B2A539E1D117D36003C6002 /* CSVic20.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSVic20.mm; sourceTree = "<group>"; };
4B2AF8681E513FC20027EE29 /* TIATests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TIATests.mm; sourceTree = "<group>"; };
4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TapePRG.cpp; sourceTree = "<group>"; };
4B2BFC5E1D613E0200BA3AA9 /* TapePRG.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TapePRG.hpp; sourceTree = "<group>"; };
4B2BFDB01DAEF5FF001A68B8 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = Oric/Video.cpp; sourceTree = "<group>"; };
4B2BFDB11DAEF5FF001A68B8 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = Oric/Video.hpp; sourceTree = "<group>"; };
4B2C45411E3C3896002A2389 /* cartridge.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cartridge.png; sourceTree = "<group>"; };
4B2E2D971C3A06EC00138695 /* Atari2600.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Atari2600.cpp; sourceTree = "<group>"; };
4B2E2D981C3A06EC00138695 /* Atari2600.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Atari2600.hpp; sourceTree = "<group>"; };
4B2E2D991C3A06EC00138695 /* Atari2600Inputs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Atari2600Inputs.h; sourceTree = "<group>"; };
@@ -481,6 +502,8 @@
4B3BA0CB1D318B44005DD7A7 /* MOS6532Bridge.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MOS6532Bridge.mm; sourceTree = "<group>"; };
4B3BA0CC1D318B44005DD7A7 /* TestMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestMachine.h; sourceTree = "<group>"; };
4B3BA0CD1D318B44005DD7A7 /* TestMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TestMachine.mm; sourceTree = "<group>"; };
4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMPatchedTrack.cpp; sourceTree = "<group>"; };
4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMPatchedTrack.hpp; sourceTree = "<group>"; };
4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = AllSuiteA.bin; path = AllSuiteA/AllSuiteA.bin; sourceTree = "<group>"; };
4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = 6502_functional_test.bin; path = "Klaus Dormann/6502_functional_test.bin"; sourceTree = "<group>"; };
4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */ = {isa = PBXFileReference; lastKnownFileType = file; name = BCDTEST_beeb; path = BCDTest/BCDTEST_beeb; sourceTree = "<group>"; };
@@ -496,7 +519,6 @@
4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; };
4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ArrayBuilder.cpp; sourceTree = "<group>"; };
4B5073061DDD3B9400C48FBD /* ArrayBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ArrayBuilder.hpp; sourceTree = "<group>"; };
4B5073081DDFCFDF00C48FBD /* ArrayBuilderTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ArrayBuilderTests.h; sourceTree = "<group>"; };
4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArrayBuilderTests.mm; sourceTree = "<group>"; };
4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSOpenGLView.h; sourceTree = "<group>"; };
4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSOpenGLView.m; sourceTree = "<group>"; };
@@ -522,6 +544,11 @@
4B69FB451C4D950F00B5F0AA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DiskController.cpp; sourceTree = "<group>"; };
4B6C73BC1D387AE500AFCFCA /* DiskController.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DiskController.hpp; sourceTree = "<group>"; };
4B7913CA1DFCD80E00175A82 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = Electron/Video.cpp; sourceTree = "<group>"; };
4B7913CB1DFCD80E00175A82 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = Electron/Video.hpp; sourceTree = "<group>"; };
4B79E4411E3AF38600141F11 /* cassette.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cassette.png; sourceTree = "<group>"; };
4B79E4421E3AF38600141F11 /* floppy35.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = floppy35.png; sourceTree = "<group>"; };
4B79E4431E3AF38600141F11 /* floppy525.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = floppy525.png; sourceTree = "<group>"; };
4B8805EE1DCFC99C003085B1 /* Acorn.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Acorn.cpp; path = Parsers/Acorn.cpp; sourceTree = "<group>"; };
4B8805EF1DCFC99C003085B1 /* Acorn.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Acorn.hpp; path = Parsers/Acorn.hpp; sourceTree = "<group>"; };
4B8805F21DCFD22A003085B1 /* Commodore.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Commodore.cpp; path = Parsers/Commodore.cpp; sourceTree = "<group>"; };
@@ -542,6 +569,8 @@
4B8FE2251DA1DE2D0090D3CE /* NSBundle+DataResource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSBundle+DataResource.h"; sourceTree = "<group>"; };
4B8FE2261DA1DE2D0090D3CE /* NSBundle+DataResource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSBundle+DataResource.m"; sourceTree = "<group>"; };
4B8FE2281DA1EDDF0090D3CE /* ElectronOptionsPanel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ElectronOptionsPanel.swift; sourceTree = "<group>"; };
4B924E981E74D22700B76AF1 /* AtariStaticAnalyserTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AtariStaticAnalyserTests.mm; sourceTree = "<group>"; };
4B9252CD1E74D28200B76AF1 /* Atari ROMs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Atari ROMs"; sourceTree = "<group>"; };
4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6502TimingTests.swift; sourceTree = "<group>"; };
4B96F7201D75119A0058BB2D /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Tape.cpp; path = ../../StaticAnalyser/Acorn/Tape.cpp; sourceTree = "<group>"; };
4B96F7211D75119A0058BB2D /* Tape.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Tape.hpp; path = ../../StaticAnalyser/Acorn/Tape.hpp; sourceTree = "<group>"; };
@@ -826,6 +855,7 @@
4BB298EB1B587D8400A49093 /* txan */ = {isa = PBXFileReference; lastKnownFileType = file; path = txan; sourceTree = "<group>"; };
4BB298EC1B587D8400A49093 /* txsn */ = {isa = PBXFileReference; lastKnownFileType = file; path = txsn; sourceTree = "<group>"; };
4BB298ED1B587D8400A49093 /* tyan */ = {isa = PBXFileReference; lastKnownFileType = file; path = tyan; sourceTree = "<group>"; };
4BB2A9AE1E13367E001A5C23 /* CRCTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CRCTests.mm; sourceTree = "<group>"; };
4BB697C61D4B558F00248BDF /* Factors.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Factors.hpp; path = ../../NumberTheory/Factors.hpp; sourceTree = "<group>"; };
4BB697C91D4B6D3E00248BDF /* TimedEventLoop.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TimedEventLoop.cpp; sourceTree = "<group>"; };
4BB697CA1D4B6D3E00248BDF /* TimedEventLoop.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TimedEventLoop.hpp; sourceTree = "<group>"; };
@@ -889,12 +919,15 @@
4BD14B101D74627C0088EAD6 /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = StaticAnalyser.hpp; path = ../../StaticAnalyser/Acorn/StaticAnalyser.hpp; sourceTree = "<group>"; };
4BD468F51D8DF41D0084958B /* 1770.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = 1770.cpp; path = 1770/1770.cpp; sourceTree = "<group>"; };
4BD468F61D8DF41D0084958B /* 1770.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 1770.hpp; path = 1770/1770.hpp; sourceTree = "<group>"; };
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = "<group>"; };
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSBestEffortUpdater.h; path = Updater/CSBestEffortUpdater.h; sourceTree = "<group>"; };
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CSBestEffortUpdater.m; path = Updater/CSBestEffortUpdater.m; sourceTree = "<group>"; };
4BD69F921D98760000243FE1 /* AcornADF.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AcornADF.cpp; sourceTree = "<group>"; };
4BD69F931D98760000243FE1 /* AcornADF.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = AcornADF.hpp; sourceTree = "<group>"; };
4BE77A2C1D84ADFB00BC3827 /* File.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = File.cpp; path = ../../StaticAnalyser/Commodore/File.cpp; sourceTree = "<group>"; };
4BE77A2D1D84ADFB00BC3827 /* File.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = File.hpp; path = ../../StaticAnalyser/Commodore/File.hpp; sourceTree = "<group>"; };
4BE7C9161E3D397100A5496D /* TIA.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TIA.cpp; sourceTree = "<group>"; };
4BE7C9171E3D397100A5496D /* TIA.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TIA.hpp; sourceTree = "<group>"; };
4BEA525D1DF33323007E74F2 /* Tape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Tape.cpp; path = Electron/Tape.cpp; sourceTree = "<group>"; };
4BEA525F1DF333D8007E74F2 /* Tape.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Tape.hpp; path = Electron/Tape.hpp; sourceTree = "<group>"; };
4BEA52601DF3343A007E74F2 /* Interrupts.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Interrupts.hpp; path = Electron/Interrupts.hpp; sourceTree = "<group>"; };
@@ -903,6 +936,20 @@
4BEA52641DF3472B007E74F2 /* Speaker.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Speaker.cpp; sourceTree = "<group>"; };
4BEA52651DF3472B007E74F2 /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
4BEA52671DF34909007E74F2 /* PIA.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PIA.hpp; sourceTree = "<group>"; };
4BEAC0811E7E0DF800EE56B2 /* Cartridge.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Cartridge.hpp; sourceTree = "<group>"; };
4BEAC0821E7E0DF800EE56B2 /* CartridgeActivisionStack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeActivisionStack.hpp; sourceTree = "<group>"; };
4BEAC0831E7E0DF800EE56B2 /* CartridgeAtari16k.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeAtari16k.hpp; sourceTree = "<group>"; };
4BEAC0841E7E0DF800EE56B2 /* CartridgeAtari32k.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeAtari32k.hpp; sourceTree = "<group>"; };
4BEAC0851E7E0DF800EE56B2 /* CartridgeAtari8k.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeAtari8k.hpp; sourceTree = "<group>"; };
4BEAC0861E7E0DF800EE56B2 /* CartridgeCBSRAMPlus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeCBSRAMPlus.hpp; sourceTree = "<group>"; };
4BEAC0871E7E0DF800EE56B2 /* CartridgeCommaVid.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeCommaVid.hpp; sourceTree = "<group>"; };
4BEAC0881E7E0DF800EE56B2 /* CartridgeMegaBoy.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeMegaBoy.hpp; sourceTree = "<group>"; };
4BEAC0891E7E0DF800EE56B2 /* CartridgeMNetwork.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeMNetwork.hpp; sourceTree = "<group>"; };
4BEAC08A1E7E0DF800EE56B2 /* CartridgeParkerBros.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeParkerBros.hpp; sourceTree = "<group>"; };
4BEAC08B1E7E0DF800EE56B2 /* CartridgeTigervision.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeTigervision.hpp; sourceTree = "<group>"; };
4BEAC08C1E7E0DF800EE56B2 /* CartridgeUnpaged.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CartridgeUnpaged.hpp; sourceTree = "<group>"; };
4BEAC08D1E7E0E1A00EE56B2 /* Bus.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Bus.hpp; sourceTree = "<group>"; };
4BEAC08E1E7E110500EE56B2 /* CartridgePitfall2.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CartridgePitfall2.hpp; sourceTree = "<group>"; };
4BEE0A6A1D72496600532C7B /* Cartridge.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Cartridge.cpp; sourceTree = "<group>"; };
4BEE0A6B1D72496600532C7B /* Cartridge.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Cartridge.hpp; sourceTree = "<group>"; };
4BEE0A6D1D72496600532C7B /* PRG.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PRG.cpp; sourceTree = "<group>"; };
@@ -914,13 +961,11 @@
4BF1354B1D6D2C300054B2EA /* StaticAnalyser.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = StaticAnalyser.hpp; path = ../../StaticAnalyser/StaticAnalyser.hpp; sourceTree = "<group>"; };
4BF8295B1D8F048B001BAE39 /* MFM.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MFM.cpp; path = Encodings/MFM.cpp; sourceTree = "<group>"; };
4BF8295C1D8F048B001BAE39 /* MFM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = MFM.hpp; path = Encodings/MFM.hpp; sourceTree = "<group>"; };
4BF8295E1D8F3C87001BAE39 /* CRC.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CRC.cpp; path = ../../NumberTheory/CRC.cpp; sourceTree = "<group>"; };
4BF8295F1D8F3C87001BAE39 /* CRC.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CRC.hpp; path = ../../NumberTheory/CRC.hpp; sourceTree = "<group>"; };
4BF829611D8F536B001BAE39 /* SSD.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SSD.cpp; sourceTree = "<group>"; };
4BF829621D8F536B001BAE39 /* SSD.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SSD.hpp; sourceTree = "<group>"; };
4BF829641D8F732B001BAE39 /* Disk.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Disk.cpp; path = ../../StaticAnalyser/Acorn/Disk.cpp; sourceTree = "<group>"; };
4BF829651D8F732B001BAE39 /* Disk.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Disk.hpp; path = ../../StaticAnalyser/Acorn/Disk.hpp; sourceTree = "<group>"; };
4BF829671D8F7361001BAE39 /* File.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = File.cpp; path = ../../StaticAnalyser/Acorn/File.cpp; sourceTree = "<group>"; };
4BF829681D8F7361001BAE39 /* File.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = File.hpp; path = ../../StaticAnalyser/Acorn/File.hpp; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -977,6 +1022,7 @@
4B1414631B588A1100E04248 /* Test Binaries */ = {
isa = PBXGroup;
children = (
4B9252CD1E74D28200B76AF1 /* Atari ROMs */,
4B44EBF61DC9883B00A7820C /* 6502_functional_test.bin */,
4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */,
4B44EBF81DC9898E00A7820C /* BCDTEST_beeb */,
@@ -993,6 +1039,18 @@
path = 6532;
sourceTree = "<group>";
};
4B1EDB411E39A0AC009D6819 /* Icons */ = {
isa = PBXGroup;
children = (
4B2C45411E3C3896002A2389 /* cartridge.png */,
4B79E4411E3AF38600141F11 /* cassette.png */,
4B79E4421E3AF38600141F11 /* floppy35.png */,
4B79E4431E3AF38600141F11 /* floppy525.png */,
4B1EDB431E39A0AC009D6819 /* chip.png */,
);
path = Icons;
sourceTree = "<group>";
};
4B2409591C45DF85004DA684 /* SignalProcessing */ = {
isa = PBXGroup;
children = (
@@ -1054,10 +1112,14 @@
children = (
4B2E2D971C3A06EC00138695 /* Atari2600.cpp */,
4BEA52641DF3472B007E74F2 /* Speaker.cpp */,
4BE7C9161E3D397100A5496D /* TIA.cpp */,
4B2E2D991C3A06EC00138695 /* Atari2600Inputs.h */,
4B2E2D981C3A06EC00138695 /* Atari2600.hpp */,
4BEA52651DF3472B007E74F2 /* Speaker.hpp */,
4BEAC08D1E7E0E1A00EE56B2 /* Bus.hpp */,
4BEA52671DF34909007E74F2 /* PIA.hpp */,
4BEA52651DF3472B007E74F2 /* Speaker.hpp */,
4BE7C9171E3D397100A5496D /* TIA.hpp */,
4BEAC0801E7E0DF800EE56B2 /* Cartridges */,
);
path = Atari2600;
sourceTree = "<group>";
@@ -1070,11 +1132,13 @@
4BEA52611DF339D7007E74F2 /* Speaker.cpp */,
4BEA525D1DF33323007E74F2 /* Tape.cpp */,
4BC8A62B1DCE60E000DAC693 /* Typer.cpp */,
4B7913CA1DFCD80E00175A82 /* Video.cpp */,
4B2E2D9C1C3A070400138695 /* Electron.hpp */,
4BEA52601DF3343A007E74F2 /* Interrupts.hpp */,
4B30512F1D98ACC600B4FED8 /* Plus3.hpp */,
4BEA52621DF339D7007E74F2 /* Speaker.hpp */,
4BEA525F1DF333D8007E74F2 /* Tape.hpp */,
4B7913CB1DFCD80E00175A82 /* Video.hpp */,
);
name = Electron;
sourceTree = "<group>";
@@ -1083,7 +1147,6 @@
isa = PBXGroup;
children = (
4B0CCC411C62D0B3001CAC5F /* CRT */,
4B2409531C45AB05004DA684 /* Speaker.cpp */,
4B2409541C45AB05004DA684 /* Speaker.hpp */,
);
name = Outputs;
@@ -1290,11 +1353,15 @@
4BAB62AB1D3272D200DF5BA0 /* Disk.cpp */,
4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */,
4B30512B1D989E2200B4FED8 /* Drive.cpp */,
4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */,
4B121F961E060CF000BFDA12 /* PCMSegment.cpp */,
4BAB62B61D3302CA00DF5BA0 /* PCMTrack.cpp */,
4B0BE4271D3481E700D5256B /* DigitalPhaseLockedLoop.hpp */,
4BAB62AC1D3272D200DF5BA0 /* Disk.hpp */,
4B6C73BC1D387AE500AFCFCA /* DiskController.hpp */,
4B30512C1D989E2200B4FED8 /* Drive.hpp */,
4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */,
4B121F971E060CF000BFDA12 /* PCMSegment.hpp */,
4BAB62B71D3302CA00DF5BA0 /* PCMTrack.hpp */,
4BB697CF1D4BA44900248BDF /* Encodings */,
4BAB62B21D327F7E00DF5BA0 /* Formats */,
@@ -1595,7 +1662,6 @@
isa = PBXGroup;
children = (
4BB697C61D4B558F00248BDF /* Factors.hpp */,
4BF8295E1D8F3C87001BAE39 /* CRC.cpp */,
4BF8295F1D8F3C87001BAE39 /* CRC.hpp */,
);
name = NumberTheory;
@@ -1668,8 +1734,14 @@
4BB73EB51B587A5100552FC2 /* Clock SignalTests */ = {
isa = PBXGroup;
children = (
4B5073081DDFCFDF00C48FBD /* ArrayBuilderTests.h */,
4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */,
4B924E981E74D22700B76AF1 /* AtariStaticAnalyserTests.mm */,
4BB2A9AE1E13367E001A5C23 /* CRCTests.mm */,
4B121F941E05E66800BFDA12 /* PCMPatchedTrackTests.mm */,
4B121F9A1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm */,
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */,
4B2AF8681E513FC20027EE29 /* TIATests.mm */,
4B1D08051E0F7A1100763741 /* TimeTests.mm */,
4BB73EB81B587A5100552FC2 /* Info.plist */,
4BC9E1ED1D23449A003FCEE4 /* 6502InterruptTests.swift */,
4B92EAC91B7C112B00246143 /* 6502TimingTests.swift */,
@@ -1840,7 +1912,6 @@
children = (
4BF829641D8F732B001BAE39 /* Disk.cpp */,
4BF829651D8F732B001BAE39 /* Disk.hpp */,
4BF829671D8F7361001BAE39 /* File.cpp */,
4BF829681D8F7361001BAE39 /* File.hpp */,
4BD14B0F1D74627C0088EAD6 /* StaticAnalyser.cpp */,
4BD14B101D74627C0088EAD6 /* StaticAnalyser.hpp */,
@@ -1871,11 +1942,32 @@
4BE5F85A1C3E1C2500C43F01 /* Resources */ = {
isa = PBXGroup;
children = (
4B1EDB411E39A0AC009D6819 /* Icons */,
4BC9DF441D044FCA00F44158 /* ROMImages */,
);
path = Resources;
sourceTree = "<group>";
};
4BEAC0801E7E0DF800EE56B2 /* Cartridges */ = {
isa = PBXGroup;
children = (
4BEAC0811E7E0DF800EE56B2 /* Cartridge.hpp */,
4BEAC0821E7E0DF800EE56B2 /* CartridgeActivisionStack.hpp */,
4BEAC0831E7E0DF800EE56B2 /* CartridgeAtari16k.hpp */,
4BEAC0841E7E0DF800EE56B2 /* CartridgeAtari32k.hpp */,
4BEAC0851E7E0DF800EE56B2 /* CartridgeAtari8k.hpp */,
4BEAC0861E7E0DF800EE56B2 /* CartridgeCBSRAMPlus.hpp */,
4BEAC0871E7E0DF800EE56B2 /* CartridgeCommaVid.hpp */,
4BEAC0881E7E0DF800EE56B2 /* CartridgeMegaBoy.hpp */,
4BEAC0891E7E0DF800EE56B2 /* CartridgeMNetwork.hpp */,
4BEAC08A1E7E0DF800EE56B2 /* CartridgeParkerBros.hpp */,
4BEAC08B1E7E0DF800EE56B2 /* CartridgeTigervision.hpp */,
4BEAC08C1E7E0DF800EE56B2 /* CartridgeUnpaged.hpp */,
4BEAC08E1E7E110500EE56B2 /* CartridgePitfall2.hpp */,
);
path = Cartridges;
sourceTree = "<group>";
};
4BEE0A691D72496600532C7B /* Cartridge */ = {
isa = PBXGroup;
children = (
@@ -2024,13 +2116,18 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
4B2C45421E3C3896002A2389 /* cartridge.png in Resources */,
4BB73EA91B587A5100552FC2 /* Assets.xcassets in Resources */,
4B79E4451E3AF38600141F11 /* floppy35.png in Resources */,
4B1EDB451E39A0AC009D6819 /* chip.png in Resources */,
4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */,
4B8FE21B1DA19D5F0090D3CE /* Atari2600Options.xib in Resources */,
4B8FE21C1DA19D5F0090D3CE /* MachineDocument.xib in Resources */,
4B79E4441E3AF38600141F11 /* cassette.png in Resources */,
4B8FE21E1DA19D5F0090D3CE /* Vic20Options.xib in Resources */,
4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */,
4B8FE21D1DA19D5F0090D3CE /* ElectronOptions.xib in Resources */,
4B79E4461E3AF38600141F11 /* floppy525.png in Resources */,
4BC9DF451D044FCA00F44158 /* ROMImages in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -2161,6 +2258,7 @@
4BB299661B587D8400A49093 /* inszx in Resources */,
4BB299101B587D8400A49093 /* asoz in Resources */,
4BB2998B1B587D8400A49093 /* lseiy in Resources */,
4B9252CE1E74D28200B76AF1 /* Atari ROMs in Resources */,
4BB2997D1B587D8400A49093 /* ldxay in Resources */,
4BB299D71B587D8400A49093 /* staax in Resources */,
4BB2990C1B587D8400A49093 /* asoax in Resources */,
@@ -2329,6 +2427,7 @@
4BC9DF4F1D04691600F44158 /* 6560.cpp in Sources */,
4B59199C1DAC6C46005BB85C /* OricTAP.cpp in Sources */,
4BB697CE1D4BA44400248BDF /* CommodoreGCR.cpp in Sources */,
4BD4A8CD1E077E8A0020D856 /* PCMSegment.cpp in Sources */,
4BD14B111D74627C0088EAD6 /* StaticAnalyser.cpp in Sources */,
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */,
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */,
@@ -2337,7 +2436,6 @@
4BC8A62D1DCE60E000DAC693 /* Typer.cpp in Sources */,
4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */,
4BA799951D8B656E0045123D /* StaticAnalyser.cpp in Sources */,
4BF829601D8F3C87001BAE39 /* CRC.cpp in Sources */,
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */,
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */,
@@ -2353,14 +2451,15 @@
4B2A332A1DB8544D002876E3 /* MemoryFuzzer.cpp in Sources */,
4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */,
4B2A332F1DB86869002876E3 /* OricOptionsPanel.swift in Sources */,
4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */,
4B2A53A11D117D36003C6002 /* CSAtari2600.mm in Sources */,
4BF829661D8F732B001BAE39 /* Disk.cpp in Sources */,
4BEA52631DF339D7007E74F2 /* Speaker.cpp in Sources */,
4BC5E4921D7ED365008CF980 /* StaticAnalyser.cpp in Sources */,
4BC830D11D6E7C690000A26F /* Tape.cpp in Sources */,
4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */,
4BF829691D8F7361001BAE39 /* File.cpp in Sources */,
4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */,
4B3F1B461E0388D200DB26EE /* PCMPatchedTrack.cpp in Sources */,
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */,
4BF8295D1D8F048B001BAE39 /* MFM.cpp in Sources */,
4BE77A2E1D84ADFB00BC3827 /* File.cpp in Sources */,
@@ -2368,13 +2467,13 @@
4BAB62B51D327F7E00DF5BA0 /* G64.cpp in Sources */,
4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */,
4BBF99141C8FBA6F0075DAFB /* TextureBuilder.cpp in Sources */,
4B2409551C45AB05004DA684 /* Speaker.cpp in Sources */,
4BCF1FA81DADC5250039D2E7 /* CSOric.mm in Sources */,
4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */,
4B6C73BD1D387AE500AFCFCA /* DiskController.cpp in Sources */,
4B643F3A1D77AD1900D431D6 /* CSStaticAnalyser.mm in Sources */,
4B4DC8281D2C2470003C5BF8 /* C1540.cpp in Sources */,
4B5A12571DD55862007A2231 /* Disassembler6502.cpp in Sources */,
4BE7C9181E3D397100A5496D /* TIA.cpp in Sources */,
4B1E85751D170228001EF87D /* Typer.cpp in Sources */,
4BF829631D8F536B001BAE39 /* SSD.cpp in Sources */,
4B2E2D9D1C3A070400138695 /* Electron.cpp in Sources */,
@@ -2428,19 +2527,26 @@
4B1E85811D176468001EF87D /* 6532Tests.swift in Sources */,
4BC9E1EE1D23449A003FCEE4 /* 6502InterruptTests.swift in Sources */,
4BEF6AAA1D35CE9E00E73575 /* DigitalPhaseLockedLoopBridge.mm in Sources */,
4B924E991E74D22700B76AF1 /* AtariStaticAnalyserTests.mm in Sources */,
4B50730A1DDFCFDF00C48FBD /* ArrayBuilderTests.mm in Sources */,
4B2AF8691E513FC20027EE29 /* TIATests.mm in Sources */,
4B3BA0CE1D318B44005DD7A7 /* C1540Bridge.mm in Sources */,
4B3BA0D11D318B44005DD7A7 /* TestMachine.mm in Sources */,
4B92EACA1B7C112B00246143 /* 6502TimingTests.swift in Sources */,
4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */,
4B121F9B1E06293F00BFDA12 /* PCMSegmentEventSourceTests.mm in Sources */,
4BEF6AAC1D35D1C400E73575 /* DPLLTests.swift in Sources */,
4B3BA0CF1D318B44005DD7A7 /* MOS6522Bridge.mm in Sources */,
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */,
4BB2A9AF1E13367E001A5C23 /* CRCTests.mm in Sources */,
4B3BA0D01D318B44005DD7A7 /* MOS6532Bridge.mm in Sources */,
4B3BA0C31D318AEC005DD7A7 /* C1540Tests.swift in Sources */,
4B1414621B58888700E04248 /* KlausDormannTests.swift in Sources */,
4B1414601B58885000E04248 /* WolfgangLorenzTests.swift in Sources */,
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */,
4B049CDD1DA3C82F00322067 /* BCDTest.swift in Sources */,
4B1D08061E0F7A1100763741 /* TimeTests.mm in Sources */,
4B121F951E05E66800BFDA12 /* PCMPatchedTrackTests.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73EB11B587A5100552FC2"
BuildableName = "Clock SignalTests.xctest"
BlueprintName = "Clock SignalTests"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73EBC1B587A5100552FC2"
BuildableName = "Clock SignalUITests.xctest"
BlueprintName = "Clock SignalUITests"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "NO">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "4BB73E9D1B587A5100552FC2"
BuildableName = "Clock Signal.app"
BlueprintName = "Clock Signal"
ReferencedContainer = "container:Clock Signal.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -12,32 +12,48 @@
#define AudioQueueBufferMaxLength 8192
#define NumberOfStoredAudioQueueBuffer 16
static NSLock *CSAudioQueueDeallocLock;
/*!
Holds a weak reference to a CSAudioQueue. Used to work around an apparent AudioQueue bug.
See -[CSAudioQueue dealloc].
*/
@interface CSWeakAudioQueuePointer: NSObject
@property(nonatomic, weak) CSAudioQueue *queue;
@end
@implementation CSWeakAudioQueuePointer
@end
@implementation CSAudioQueue
{
AudioQueueRef _audioQueue;
AudioQueueBufferRef _storedBuffers[NumberOfStoredAudioQueueBuffer];
NSLock *_storedBuffersLock;
CSWeakAudioQueuePointer *_weakPointer;
}
#pragma mark - AudioQueue callbacks
- (void)audioQueue:(AudioQueueRef)theAudioQueue didCallbackWithBuffer:(AudioQueueBufferRef)buffer
/*!
@returns @c YES if the queue is running dry; @c NO otherwise.
*/
- (BOOL)audioQueue:(AudioQueueRef)theAudioQueue didCallbackWithBuffer:(AudioQueueBufferRef)buffer
{
[self.delegate audioQueueIsRunningDry:self];
@synchronized(self)
[_storedBuffersLock lock];
for(int c = 0; c < NumberOfStoredAudioQueueBuffer; c++)
{
for(int c = 0; c < NumberOfStoredAudioQueueBuffer; c++)
if(!_storedBuffers[c] || buffer->mAudioDataBytesCapacity > _storedBuffers[c]->mAudioDataBytesCapacity)
{
if(!_storedBuffers[c] || buffer->mAudioDataBytesCapacity > _storedBuffers[c]->mAudioDataBytesCapacity)
{
if(_storedBuffers[c]) AudioQueueFreeBuffer(_audioQueue, _storedBuffers[c]);
_storedBuffers[c] = buffer;
return;
}
if(_storedBuffers[c]) AudioQueueFreeBuffer(_audioQueue, _storedBuffers[c]);
_storedBuffers[c] = buffer;
[_storedBuffersLock unlock];
return YES;
}
}
[_storedBuffersLock unlock];
AudioQueueFreeBuffer(_audioQueue, buffer);
return YES;
}
static void audioOutputCallback(
@@ -45,7 +61,17 @@ static void audioOutputCallback(
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer)
{
[(__bridge CSAudioQueue *)inUserData audioQueue:inAQ didCallbackWithBuffer:inBuffer];
// Pull the delegate call for audio queue running dry outside of the locked region, to allow non-deadlocking
// lifecycle -dealloc events to result from it.
if([CSAudioQueueDeallocLock tryLock])
{
CSAudioQueue *queue = ((__bridge CSWeakAudioQueuePointer *)inUserData).queue;
BOOL isRunningDry = NO;
isRunningDry = [queue audioQueue:inAQ didCallbackWithBuffer:inBuffer];
id<CSAudioQueueDelegate> delegate = queue.delegate;
[CSAudioQueueDeallocLock unlock];
if(isRunningDry) [delegate audioQueueIsRunningDry:queue];
}
}
#pragma mark - Standard object lifecycle
@@ -56,6 +82,12 @@ static void audioOutputCallback(
if(self)
{
if(!CSAudioQueueDeallocLock)
{
CSAudioQueueDeallocLock = [[NSLock alloc] init];
}
_storedBuffersLock = [[NSLock alloc] init];
_samplingRate = samplingRate;
// determine preferred buffer sizes
@@ -80,11 +112,13 @@ static void audioOutputCallback(
outputDescription.mReserved = 0;
// create an audio output queue along those lines
// create an audio output queue along those lines; see -dealloc re: the CSWeakAudioQueuePointer
_weakPointer = [[CSWeakAudioQueuePointer alloc] init];
_weakPointer.queue = self;
if(!AudioQueueNewOutput(
&outputDescription,
audioOutputCallback,
(__bridge void *)(self),
(__bridge void *)(_weakPointer),
NULL,
kCFRunLoopCommonModes,
0,
@@ -104,7 +138,31 @@ static void audioOutputCallback(
- (void)dealloc
{
if(_audioQueue) AudioQueueDispose(_audioQueue, NO);
[CSAudioQueueDeallocLock lock];
if(_audioQueue)
{
AudioQueueDispose(_audioQueue, true);
_audioQueue = NULL;
}
[CSAudioQueueDeallocLock unlock];
// Yuck. Horrid hack happening here. At least under macOS v10.12, I am frequently seeing calls to
// my registered audio callback (audioOutputCallback in this case) that occur **after** the call
// to AudioQueueDispose above, even though the second parameter there asks for a synchronous shutdown.
// So this appears to be a bug on Apple's side.
//
// Since the audio callback receives a void * pointer that identifies the class it should branch into,
// it's therefore unsafe to pass 'self'. Instead I pass a CSWeakAudioQueuePointer which points to the actual
// queue. The lifetime of that class is the lifetime of this instance plus 1 second, as effected by the
// artificial dispatch_after below it serves only to keep pointerSaviour alive for an extra second.
//
// Why a second? That's definitely quite a lot longer than any amount of audio that may be queued. So
// probably safe. As and where Apple's audio queue works properly, CSAudioQueueDeallocLock should provide
// absolute safety; elsewhere the CSWeakAudioQueuePointer provides probabilistic.
CSWeakAudioQueuePointer *pointerSaviour = _weakPointer;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[pointerSaviour hash];
});
}
#pragma mark - Audio enqueuer
@@ -113,28 +171,28 @@ static void audioOutputCallback(
{
size_t bufferBytes = lengthInSamples * sizeof(int16_t);
@synchronized(self)
[_storedBuffersLock lock];
for(int c = 0; c < NumberOfStoredAudioQueueBuffer; c++)
{
for(int c = 0; c < NumberOfStoredAudioQueueBuffer; c++)
if(_storedBuffers[c] && _storedBuffers[c]->mAudioDataBytesCapacity >= bufferBytes)
{
if(_storedBuffers[c] && _storedBuffers[c]->mAudioDataBytesCapacity >= bufferBytes)
{
memcpy(_storedBuffers[c]->mAudioData, buffer, bufferBytes);
_storedBuffers[c]->mAudioDataByteSize = (UInt32)bufferBytes;
memcpy(_storedBuffers[c]->mAudioData, buffer, bufferBytes);
_storedBuffers[c]->mAudioDataByteSize = (UInt32)bufferBytes;
AudioQueueEnqueueBuffer(_audioQueue, _storedBuffers[c], 0, NULL);
_storedBuffers[c] = NULL;
return;
}
AudioQueueEnqueueBuffer(_audioQueue, _storedBuffers[c], 0, NULL);
_storedBuffers[c] = NULL;
[_storedBuffersLock unlock];
return;
}
AudioQueueBufferRef newBuffer;
AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer);
memcpy(newBuffer->mAudioData, buffer, bufferBytes);
newBuffer->mAudioDataByteSize = (UInt32)bufferBytes;
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
}
[_storedBuffersLock unlock];
AudioQueueBufferRef newBuffer;
AudioQueueAllocateBuffer(_audioQueue, (UInt32)bufferBytes * 2, &newBuffer);
memcpy(newBuffer->mAudioData, buffer, bufferBytes);
newBuffer->mAudioDataByteSize = (UInt32)bufferBytes;
AudioQueueEnqueueBuffer(_audioQueue, newBuffer, 0, NULL);
}
#pragma mark - Sampling Rate getters

View File

@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11201" systemVersion="16A323" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16E195" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11201"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
@@ -14,14 +15,14 @@
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ota-g7-hOL" customClass="Vic20OptionsPanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="83" y="102" width="200" height="134"/>
<rect key="contentRect" x="83" y="102" width="200" height="112"/>
<rect key="screenRect" x="0.0" y="0.0" width="1366" height="768"/>
<view key="contentView" id="7Pv-WL-2Rq">
<rect key="frame" x="0.0" y="0.0" width="200" height="134"/>
<rect key="frame" x="0.0" y="0.0" width="200" height="112"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="sBT-cU-h7s">
<rect key="frame" x="18" y="98" width="164" height="18"/>
<rect key="frame" x="18" y="76" width="164" height="18"/>
<buttonCell key="cell" type="check" title="Load Tapes Quickly" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="w0l-ha-esm">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@@ -30,35 +31,8 @@
<action selector="setFastLoading:" target="ota-g7-hOL" id="me0-h2-Ga5"/>
</connections>
</button>
<button translatesAutoresizingMaskIntoConstraints="NO" id="lbt-Wo-6fc">
<rect key="frame" x="18" y="78" width="164" height="18"/>
<buttonCell key="cell" type="check" title="Load Automatically" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="jTj-uV-at1">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="setShouldLoadAutomatically:" target="ota-g7-hOL" id="T3i-gO-T1C"/>
</connections>
</button>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0NP-x1-qH2">
<rect key="frame" x="18" y="17" width="165" height="26"/>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="K81-0X-C4f">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="diI-80-lCf">
<items>
<menuItem title="5 kb" id="ze7-6B-ois"/>
<menuItem title="8 kb" id="6C7-Iv-Wvl"/>
<menuItem title="32 kb" id="DOo-f6-OeZ"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="setMemorySize:" target="ota-g7-hOL" id="lep-Qi-00V"/>
</connections>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MlB-rE-TXV" userLabel="Country Selector">
<rect key="frame" x="18" y="48" width="165" height="26"/>
<rect key="frame" x="18" y="46" width="165" height="26"/>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="UIu-uz-pTu">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@@ -76,18 +50,32 @@
<action selector="setCountry:" target="ota-g7-hOL" id="YIc-QB-R1S"/>
</connections>
</popUpButton>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="0NP-x1-qH2">
<rect key="frame" x="18" y="17" width="165" height="26"/>
<popUpButtonCell key="cell" type="push" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" id="K81-0X-C4f">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="diI-80-lCf">
<items>
<menuItem title="5 kb" id="ze7-6B-ois"/>
<menuItem title="8 kb" id="6C7-Iv-Wvl"/>
<menuItem title="32 kb" id="DOo-f6-OeZ"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="setMemorySize:" target="ota-g7-hOL" id="lep-Qi-00V"/>
</connections>
</popUpButton>
</subviews>
<constraints>
<constraint firstItem="MlB-rE-TXV" firstAttribute="top" secondItem="sBT-cU-h7s" secondAttribute="bottom" constant="8" id="0kc-u0-05p"/>
<constraint firstAttribute="trailing" secondItem="sBT-cU-h7s" secondAttribute="trailing" constant="20" id="79b-2A-2c7"/>
<constraint firstItem="0NP-x1-qH2" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="7EF-L9-lIu"/>
<constraint firstItem="MlB-rE-TXV" firstAttribute="top" secondItem="lbt-Wo-6fc" secondAttribute="bottom" constant="8" id="DIc-Sm-VlA"/>
<constraint firstAttribute="bottom" secondItem="0NP-x1-qH2" secondAttribute="bottom" constant="20" id="Dtd-kf-4oU"/>
<constraint firstItem="sBT-cU-h7s" firstAttribute="top" secondItem="7Pv-WL-2Rq" secondAttribute="top" constant="20" id="E5m-wo-X92"/>
<constraint firstItem="0NP-x1-qH2" firstAttribute="top" secondItem="MlB-rE-TXV" secondAttribute="bottom" constant="10" id="NbW-5e-wGB"/>
<constraint firstItem="lbt-Wo-6fc" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="cID-bi-rVP"/>
<constraint firstItem="lbt-Wo-6fc" firstAttribute="top" secondItem="sBT-cU-h7s" secondAttribute="bottom" constant="6" id="ciY-E8-07P"/>
<constraint firstItem="0NP-x1-qH2" firstAttribute="top" secondItem="MlB-rE-TXV" secondAttribute="bottom" constant="8" id="NbW-5e-wGB"/>
<constraint firstAttribute="trailing" secondItem="0NP-x1-qH2" secondAttribute="trailing" constant="20" id="ero-D6-tJj"/>
<constraint firstAttribute="trailing" secondItem="lbt-Wo-6fc" secondAttribute="trailing" constant="20" id="gMU-gX-3Sg"/>
<constraint firstItem="sBT-cU-h7s" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="nDy-Xc-Ug9"/>
<constraint firstItem="MlB-rE-TXV" firstAttribute="leading" secondItem="7Pv-WL-2Rq" secondAttribute="leading" constant="20" id="qb4-Lp-ZMc"/>
<constraint firstAttribute="trailing" secondItem="MlB-rE-TXV" secondAttribute="trailing" constant="20" id="v18-62-uee"/>
@@ -96,10 +84,9 @@
<connections>
<outlet property="countryButton" destination="MlB-rE-TXV" id="Duc-AC-ZRO"/>
<outlet property="fastLoadingButton" destination="sBT-cU-h7s" id="uWa-EB-mbd"/>
<outlet property="loadAutomaticallyButton" destination="lbt-Wo-6fc" id="dv0-u8-BTc"/>
<outlet property="memorySizeButton" destination="0NP-x1-qH2" id="qYy-3f-o94"/>
</connections>
<point key="canvasLocation" x="-2" y="32"/>
<point key="canvasLocation" x="-2" y="21"/>
</window>
</objects>
</document>

View File

@@ -31,24 +31,14 @@ class MachineDocument:
return NSSize(width: 4.0, height: 3.0)
}
@IBOutlet weak var openGLView: CSOpenGLView! {
didSet {
openGLView.delegate = self
openGLView.responderDelegate = self
}
}
@IBOutlet weak var openGLView: CSOpenGLView!
@IBOutlet var optionsPanel: MachinePanel!
@IBAction func showOptions(_ sender: AnyObject!) {
optionsPanel?.setIsVisible(true)
}
fileprivate var audioQueue: CSAudioQueue! = nil
fileprivate lazy var bestEffortUpdater: CSBestEffortUpdater = {
let updater = CSBestEffortUpdater()
updater.delegate = self
return updater
}()
fileprivate var bestEffortUpdater: CSBestEffortUpdater!
override var windowNibName: String? {
return "MachineDocument"
@@ -64,12 +54,22 @@ class MachineDocument:
self.machine.setView(self.openGLView, aspectRatio: Float(displayAspectRatio.width / displayAspectRatio.height))
})
setupClockRate()
self.machine.delegate = self
self.bestEffortUpdater = CSBestEffortUpdater()
// callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
// hence the full setup of the best-effort updater prior to setting self as a delegate
self.openGLView.delegate = self
self.openGLView.responderDelegate = self
setupClockRate()
self.optionsPanel?.establishStoredOptions()
// bring OpenGL view-holding window on top of the options panel
self.openGLView.window!.makeKeyAndOrderFront(self)
// start accepting best effort updates
self.bestEffortUpdater.delegate = self
}
func machineDidChangeClockRate(_ machine: CSMachine!) {

View File

@@ -13,18 +13,6 @@ class Vic20OptionsPanel: MachinePanel {
}
}
// MARK: automatic loading tick box
@IBOutlet var loadAutomaticallyButton: NSButton?
var autoloadingUserDefaultsKey: String {
get { return prefixedUserDefaultsKey("autoload") }
}
@IBAction func setShouldLoadAutomatically(_ sender: NSButton!) {
let loadAutomatically = sender.state == NSOnState
vic20.shouldLoadAutomatically = loadAutomatically
UserDefaults.standard.set(loadAutomatically, forKey: self.autoloadingUserDefaultsKey)
}
// MARK: country selector
@IBOutlet var countryButton: NSPopUpButton?
var countryUserDefaultsKey: String {
@@ -85,28 +73,21 @@ class Vic20OptionsPanel: MachinePanel {
let standardUserDefaults = UserDefaults.standard
standardUserDefaults.register(defaults: [
self.autoloadingUserDefaultsKey: true,
self.memorySizeUserDefaultsKey: 5,
self.countryUserDefaultsKey: 1
])
let loadAutomatically = standardUserDefaults.bool(forKey: self.autoloadingUserDefaultsKey)
vic20.shouldLoadAutomatically = loadAutomatically
self.loadAutomaticallyButton?.state = loadAutomatically ? NSOnState : NSOffState
if !loadAutomatically {
let memorySize = standardUserDefaults.integer(forKey: self.memorySizeUserDefaultsKey)
var indexToSelect: Int?
switch memorySize {
case 32: indexToSelect = 2
case 8: indexToSelect = 1
default: indexToSelect = 0
}
if let indexToSelect = indexToSelect {
self.memorySizeButton?.selectItem(at: indexToSelect)
setMemorySize(indexToSelect)
}
}
// let memorySize = standardUserDefaults.integer(forKey: self.memorySizeUserDefaultsKey)
// var indexToSelect: Int?
// switch memorySize {
// case 32: indexToSelect = 2
// case 8: indexToSelect = 1
// default: indexToSelect = 0
// }
// if let indexToSelect = indexToSelect {
// self.memorySizeButton?.selectItem(at: indexToSelect)
// setMemorySize(indexToSelect)
// }
// TODO: this should be part of the configuration
let country = standardUserDefaults.integer(forKey: self.countryUserDefaultsKey)

View File

@@ -13,7 +13,7 @@
<string>bin</string>
</array>
<key>CFBundleTypeIconFile</key>
<string></string>
<string>cartridge</string>
<key>CFBundleTypeName</key>
<string>Atari 2600 Cartridge</string>
<key>CFBundleTypeOSTypes</key>
@@ -27,27 +27,13 @@
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>uef</string>
</array>
<key>CFBundleTypeName</key>
<string>Electron/BBC Tape Image</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSItemContentTypes</key>
<array/>
<key>LSTypeIsPackage</key>
<integer>0</integer>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).MachineDocument</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>rom</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>chip</string>
<key>CFBundleTypeName</key>
<string>ROM Image</string>
<key>CFBundleTypeRole</key>
@@ -65,6 +51,8 @@
<string>uef</string>
<string>uef.gz</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>cassette</string>
<key>CFBundleTypeName</key>
<string>Electron/BBC UEF Image</string>
<key>CFBundleTypeRole</key>
@@ -79,6 +67,8 @@
<array>
<string>prg</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy525</string>
<key>CFBundleTypeName</key>
<string>Commodore Program</string>
<key>CFBundleTypeRole</key>
@@ -93,6 +83,8 @@
<array>
<string>tap</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>cassette</string>
<key>CFBundleTypeName</key>
<string>Tape Image</string>
<key>CFBundleTypeRole</key>
@@ -107,6 +99,8 @@
<array>
<string>g64</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy525</string>
<key>CFBundleTypeName</key>
<string>Commodore Disk</string>
<key>CFBundleTypeRole</key>
@@ -121,6 +115,8 @@
<array>
<string>d64</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy525</string>
<key>CFBundleTypeName</key>
<string>Commodore 1540/1 Disk</string>
<key>CFBundleTypeRole</key>
@@ -139,6 +135,8 @@
<string>adl</string>
<string>adm</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy35</string>
<key>CFBundleTypeName</key>
<string>Electron/BBC Disk Image</string>
<key>CFBundleTypeRole</key>
@@ -151,6 +149,8 @@
<array>
<string>dsk</string>
</array>
<key>CFBundleTypeIconFile</key>
<string>floppy35</string>
<key>CFBundleTypeName</key>
<string>Disk Image</string>
<key>CFBundleTypeRole</key>

View File

@@ -11,42 +11,8 @@
#include "Atari2600.hpp"
#import "CSMachine+Subclassing.h"
@interface CSAtari2600 ()
- (void)crt:(Outputs::CRT::CRT *)crt didEndBatchOfFrames:(unsigned int)numberOfFrames withUnexpectedVerticalSyncs:(unsigned int)numberOfUnexpectedSyncs;
@end
struct CRTDelegate: public Outputs::CRT::Delegate {
__weak CSAtari2600 *atari2600;
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) {
[atari2600 crt:crt didEndBatchOfFrames:number_of_frames withUnexpectedVerticalSyncs:number_of_unexpected_vertical_syncs];
}
};
@implementation CSAtari2600 {
Atari2600::Machine _atari2600;
CRTDelegate _crtDelegate;
int _frameCount;
int _hitCount;
BOOL _didDecideRegion;
int _batchesReceived;
}
- (void)crt:(Outputs::CRT::CRT *)crt didEndBatchOfFrames:(unsigned int)numberOfFrames withUnexpectedVerticalSyncs:(unsigned int)numberOfUnexpectedSyncs {
if(!_didDecideRegion)
{
_batchesReceived++;
if(_batchesReceived == 2)
{
_didDecideRegion = YES;
if(numberOfUnexpectedSyncs >= numberOfFrames >> 1)
{
[self.view performWithGLContext:^{
_atari2600.switch_region();
}];
}
}
}
}
- (void)setDirection:(CSJoystickDirection)direction onPad:(NSUInteger)pad isPressed:(BOOL)isPressed {
@@ -78,8 +44,6 @@ struct CRTDelegate: public Outputs::CRT::Delegate {
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
@synchronized(self) {
[super setupOutputWithAspectRatio:aspectRatio];
_atari2600.get_crt()->set_delegate(&_crtDelegate);
_crtDelegate.atari2600 = self;
}
}

View File

@@ -27,10 +27,12 @@
{
NSData *basic10 = [self rom:@"basic10"];
NSData *basic11 = [self rom:@"basic11"];
NSData *colour = [self rom:@"colour"];
NSData *microdisc = [self rom:@"microdisc"];
if(basic10) _oric.set_rom(Oric::BASIC10, basic10.stdVector8);
if(basic11) _oric.set_rom(Oric::BASIC11, basic11.stdVector8);
if(colour) _oric.set_rom(Oric::Colour, colour.stdVector8);
if(microdisc) _oric.set_rom(Oric::Microdisc, microdisc.stdVector8);
}
return self;
@@ -150,7 +152,7 @@
- (void)setUseCompositeOutput:(BOOL)useCompositeOutput {
@synchronized(self) {
_useCompositeOutput = useCompositeOutput;
_oric.get_crt()->set_output_device(useCompositeOutput ? Outputs::CRT::Television : Outputs::CRT::Monitor);
_oric.set_output_device(useCompositeOutput ? Outputs::CRT::Television : Outputs::CRT::Monitor);
}
}

View File

@@ -29,7 +29,6 @@ typedef NS_ENUM(NSInteger, CSVic20MemorySize)
@interface CSVic20 : CSMachine <CSKeyboardMachine, CSFastLoading>
@property (nonatomic, assign) BOOL useFastLoadingHack;
@property (nonatomic, assign) BOOL shouldLoadAutomatically;
@property (nonatomic, assign) CSVic20Country country;
@property (nonatomic, assign) CSVic20MemorySize memorySize;

View File

@@ -178,13 +178,6 @@ using namespace Commodore::Vic20;
}
}
- (void)setShouldLoadAutomatically:(BOOL)shouldLoadAutomatically {
_shouldLoadAutomatically = shouldLoadAutomatically;
@synchronized(self) {
_vic20.set_should_automatically_load_media(shouldLoadAutomatically ? true : false);
}
}
- (void)setCountry:(CSVic20Country)country {
_country = country;
NSString *charactersROM, *kernelROM;

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

View File

@@ -8,10 +8,12 @@
#import "CSBestEffortUpdater.h"
#include <stdatomic.h>
@implementation CSBestEffortUpdater
{
// these are inherently handled only by thread-safe constructions
uint32_t _updateIsOngoing;
atomic_flag _updateIsOngoing;
dispatch_queue_t _serialDispatchQueue;
// these are permitted for modification on _serialDispatchQueue only
@@ -25,16 +27,18 @@
if(self = [super init])
{
_serialDispatchQueue = dispatch_queue_create("Best Effort Updater", DISPATCH_QUEUE_SERIAL);
// This is a workaround for assigning the correct initial value within Objective-C's form.
atomic_flag initialFlagValue = ATOMIC_FLAG_INIT;
_updateIsOngoing = initialFlagValue;
}
return self;
}
- (void)update
{
const uint32_t processingMask = 0x01;
// Always post an -openGLView:didUpdateToTime: if a previous one isn't still ongoing. This is the hook upon which the substantial processing occurs.
if(!OSAtomicTestAndSet(processingMask, &_updateIsOngoing))
if(!atomic_flag_test_and_set(&_updateIsOngoing))
{
dispatch_async(_serialDispatchQueue, ^{
NSTimeInterval timeInterval = [NSDate timeIntervalSinceReferenceDate];
@@ -52,7 +56,7 @@
}
_previousTimeInterval = timeInterval;
_hasSkipped = NO;
OSAtomicTestAndClear(processingMask, &_updateIsOngoing);
atomic_flag_clear(&_updateIsOngoing);
});
}
else

View File

@@ -53,7 +53,7 @@
*/
@interface CSOpenGLView : NSOpenGLView
@property (nonatomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
/*!

View File

@@ -21,14 +21,25 @@ class MOS6532Tests: XCTestCase {
with6532 {
// set a count of 128 at single-clock intervals
$0.setValue(128, forRegister:0x14)
XCTAssertEqual($0.value(forRegister: 4), 128)
// run for one clock and the count should now be 127
// run for one more clock and the count should now be 127
$0.run(forCycles: 1)
XCTAssert($0.value(forRegister: 4) == 127, "A single tick should decrease the counter once")
XCTAssertEqual($0.value(forRegister: 4), 127)
// run for a further 200 clock counts; timer should reach -73 = 183
$0.run(forCycles: 200)
XCTAssert($0.value(forRegister: 4) == 183, "Timer should underflow and keep counting")
// run for 127 clocks and the timer should be zero, but the timer flag will not yet be set
$0.run(forCycles: 127)
XCTAssertEqual($0.value(forRegister: 5) & 0x80, 0)
XCTAssertEqual($0.value(forRegister: 4), 0)
// after one more cycle the counter should be 255 and the timer flag will now be set
$0.run(forCycles: 1)
XCTAssertEqual($0.value(forRegister: 5) & 0x80, 0x80)
XCTAssertEqual($0.value(forRegister: 4), 255)
// run for a further 55 clock counts; timer should reach -200
$0.run(forCycles: 55)
XCTAssertEqual($0.value(forRegister: 4), 200)
}
}
@@ -37,26 +48,40 @@ class MOS6532Tests: XCTestCase {
with6532 {
// set a count of 28 at eight-clock intervals
$0.setValue(28, forRegister:0x15)
XCTAssertEqual($0.value(forRegister: 4), 28)
// run for seven clock and the count should still be 28
$0.run(forCycles: 7)
XCTAssert($0.value(forRegister: 4) == 28, "The timer should remain unchanged for seven clocks")
// run for a further clock and the count should now be 27
// one further cycle and the timer should hit 27
$0.run(forCycles: 1)
XCTAssert($0.value(forRegister: 4) == 27, "The timer should have decremented once after 8 cycles")
XCTAssertEqual($0.value(forRegister: 4), 27)
// run for a further 7 + 27*8 + 5 = 228 clock counts; timer should reach -5 = 0xfb
$0.run(forCycles: 228)
XCTAssert($0.value(forRegister: 4) == 0xfb, "Timer should underflow and start counting at single-clock pace")
// run for seven clock and the count should still be 27
$0.run(forCycles: 7)
XCTAssertEqual($0.value(forRegister: 4), 27)
// run for a further clock and the count should now be 26
$0.run(forCycles: 1)
XCTAssertEqual($0.value(forRegister: 4), 26)
// run for another 26 * 8 = 208 cycles and the count should hit zero without setting the timer flag, and
// stay there for seven more cycles
$0.run(forCycles: 208)
for _ in 0 ..< 8 {
XCTAssertEqual($0.value(forRegister: 5) & 0x80, 0)
XCTAssertEqual($0.value(forRegister: 4), 0)
$0.run(forCycles: 1)
}
// run six more, and the timer should reach 249, with the interrupt flag set
$0.run(forCycles: 6)
XCTAssertEqual($0.value(forRegister: 5) & 0x80, 0x80)
XCTAssertEqual($0.value(forRegister: 4), 249)
// timer should now resume dividing by eight
$0.run(forCycles: 7)
XCTAssert($0.value(forRegister: 4) == 0xfb, "Timer should remain unchanged for seven cycles")
XCTAssertEqual($0.value(forRegister: 4), 249)
// timer should now resume dividing by eight
$0.run(forCycles: 1)
XCTAssert($0.value(forRegister: 4) == 0xfa, "Timer should decrement after eighth cycle")
XCTAssertEqual($0.value(forRegister: 4), 248)
}
}
@@ -64,8 +89,6 @@ class MOS6532Tests: XCTestCase {
with6532 {
// set a count of 1 at single-clock intervals
$0.setValue(1, forRegister:0x1c)
// run for one clock and the count should now be zero
$0.run(forCycles: 1)
// interrupt shouldn't be signalled yet, bit should not be set

View File

@@ -1,13 +0,0 @@
//
// ArrayBuilderTests.h
// Clock Signal
//
// Created by Thomas Harte on 19/11/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
@interface ArrayBuilderTests : XCTestCase
@end

View File

@@ -6,7 +6,8 @@
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import "ArrayBuilderTests.h"
#import <XCTest/XCTest.h>
#include "ArrayBuilder.hpp"
static NSData *inputData, *outputData;
@@ -17,6 +18,9 @@ static void setData(bool is_input, uint8_t *data, size_t size)
if(is_input) inputData = dataObject; else outputData = dataObject;
}
@interface ArrayBuilderTests : XCTestCase
@end
@implementation ArrayBuilderTests
+ (void)setUp
@@ -43,6 +47,11 @@ static void setData(bool is_input, uint8_t *data, size_t size)
}
}
- (std::function<void(uint8_t *input, size_t input_size, uint8_t *output, size_t output_size)>)emptyFlushFunction
{
return [=] (uint8_t *input, size_t input_size, uint8_t *output, size_t output_size) {};
}
- (void)testSingleWriteSingleFlush
{
Outputs::CRT::ArrayBuilder arrayBuilder(200, 100, setData);
@@ -53,7 +62,7 @@ static void setData(bool is_input, uint8_t *data, size_t size)
for(int c = 0; c < 5; c++) input[c] = c;
for(int c = 0; c < 3; c++) output[c] = c + 0x80;
arrayBuilder.flush();
arrayBuilder.flush(self.emptyFlushFunction);
arrayBuilder.submit();
[self assertMonotonicForInputSize:5 outputSize:3];
@@ -77,7 +86,7 @@ static void setData(bool is_input, uint8_t *data, size_t size)
for(int c = 0; c < 2; c++) input[c] = c+2;
for(int c = 0; c < 2; c++) output[c] = c+2 + 0x80;
arrayBuilder.flush();
arrayBuilder.flush(self.emptyFlushFunction);
arrayBuilder.submit();
[self assertMonotonicForInputSize:4 outputSize:4];
@@ -98,7 +107,7 @@ static void setData(bool is_input, uint8_t *data, size_t size)
XCTAssert(inputData.length == 0, @"No input data should have been received; %lu bytes were received", (unsigned long)inputData.length);
XCTAssert(outputData.length == 0, @"No output data should have been received; %lu bytes were received", (unsigned long)outputData.length);
arrayBuilder.flush();
arrayBuilder.flush(self.emptyFlushFunction);
arrayBuilder.submit();
XCTAssert(inputData.length == 25, @"All input data should have been received; %lu bytes were received", (unsigned long)inputData.length);
@@ -112,7 +121,7 @@ static void setData(bool is_input, uint8_t *data, size_t size)
arrayBuilder.get_input_storage(5);
arrayBuilder.get_output_storage(5);
arrayBuilder.flush();
arrayBuilder.flush(self.emptyFlushFunction);
uint8_t *input = arrayBuilder.get_input_storage(5);
uint8_t *output = arrayBuilder.get_output_storage(5);
@@ -122,7 +131,7 @@ static void setData(bool is_input, uint8_t *data, size_t size)
for(int c = 0; c < 5; c++) input[c] = c;
for(int c = 0; c < 5; c++) output[c] = c + 0x80;
arrayBuilder.flush();
arrayBuilder.flush(self.emptyFlushFunction);
arrayBuilder.submit();
[self assertMonotonicForInputSize:5 outputSize:5];

View File

@@ -0,0 +1,3 @@
This folder is intended to contain commercial Atari ROM images; these are used to unit test the Atari static analyser.
Those tested are (i) everything presently available on AtariAge, and (ii) a selection of things from Pouët.

View File

@@ -0,0 +1,614 @@
//
// AtariStaticAnalyserTests.m
// Clock Signal
//
// Created by Thomas Harte on 11/03/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#import <CommonCrypto/CommonDigest.h>
#include "../../../StaticAnalyser/StaticAnalyser.hpp"
@interface AtariROMRecord : NSObject
@property(nonatomic, readonly) StaticAnalyser::Atari2600PagingModel pagingModel;
@property(nonatomic, readonly) BOOL usesSuperchip;
+ (instancetype)recordWithPagingModel:(StaticAnalyser::Atari2600PagingModel)pagingModel usesSuperchip:(BOOL)usesSuperchip;
@end
@implementation AtariROMRecord
+ (instancetype)recordWithPagingModel:(StaticAnalyser::Atari2600PagingModel)pagingModel usesSuperchip:(BOOL)usesSuperchip
{
AtariROMRecord *record = [[AtariROMRecord alloc] init];
record->_pagingModel = pagingModel;
record->_usesSuperchip = usesSuperchip;
return record;
}
@end
#define Record(sha, model, uses) sha : [AtariROMRecord recordWithPagingModel:StaticAnalyser::Atari2600PagingModel::model usesSuperchip:uses],
static NSDictionary<NSString *, AtariROMRecord *> *romRecordsBySHA1 = @{
Record(@"58dbcbdffbe80be97746e94a0a75614e64458fdc", None, NO) // 4kraVCS
Record(@"9967a76efb68017f793188f691159f04e6bb4447", None, NO) // 'X'Mission
Record(@"21d983f2f52b84c22ecae84b0943678ae2c31c10", None, NO) // 3d Tic-Tac-Toe
Record(@"d7c62df8300a68b21ce672cfaa4d0f2f4b3d0ce1", Atari16k, NO) // Acid Drop
Record(@"924ca836aa08eeffc141d487ac6b9b761b2f8ed5", None, NO) // Action Force
Record(@"e07e48d463d30321239a8acc00c490f27f1f7422", None, NO) // Adventure
Record(@"03a495c7bfa0671e24aa4d9460d232731f68cb43", None, NO) // Adventures of Tron
Record(@"6e420544bf91f603639188824a2b570738bb7e02", None, NO) // Adventures On GX12
Record(@"3b02e7dacb418c44d0d3dc77d60a9663b90b0fbc", None, NO) // Air Raid
Record(@"29f5c73d1fe806a4284547274dd73f9972a7ed70", None, NO) // Air Raiders
Record(@"af5b9f33ccb7778b42957da4f20f2bc000992366", None, NO) // Air-Sea Battle
Record(@"0376c242819b785310b8af43c03b1d1156bd5f02", None, NO) // Airlock
Record(@"fb870ec3d51468fa4cf40e0efae9617e60c1c91c", None, NO) // AKA Space Adventure
Record(@"01d99bf307262825db58631e8002dd008a42cb1e", None, NO) // Alien
Record(@"a1f660827ce291f19719a5672f2c5d277d903b03", Atari8k, NO) // Alpha Beam with Ernie
Record(@"b89a5ac6593e83fbebee1fe7d4cec81a7032c544", None, NO) // Amidar
Record(@"ac58ac94ceab78725a1182cc7b907376c011b0c8", None, NO) // Angriff der Luftflotten
Record(@"7d132ab776ff755b86bf4f204165aa54e9e1f1cf", Atari8k, NO) // Aquaventure
Record(@"9b6a54969240baf64928118741c3affee148d721", None, NO) // Armor Ambush
Record(@"8c249e9eaa83fc6be16039f05ec304efdf987beb", Atari8k, NO) // Artillery Duel
Record(@"0c03eba97df5178eec5d4d0aea4a6fe2f961c88f", None, NO) // Assault
Record(@"1a094f92e46a8127d9c29889b5389865561c0a6f", Atari8k, NO) // Asterix (NTSC)
Record(@"f14408429a911854ec76a191ad64231cc2ed7d11", Atari8k, NO) // Asterix (PAL)
Record(@"8f4a00cb4ab6a6f809be0e055d97e8fe17f19e7d", None, NO) // Asteroid Fire
Record(@"8423f99092b454aed89f89f5d7da658caf7af016", Atari8k, NO) // Asteroids
Record(@"b850bd72d18906d9684e1c7251cb699588cbcf64", None, NO) // Astroblast
Record(@"d1563c24208766cf8d28de7af995021a9f89d7e1", None, NO) // Atari Video Cube
Record(@"f4e838de9159c149ac080ab85e4f830d5b299963", None, NO) // Atlantis II
Record(@"c6b1dcdb2f024ab682316db45763bacc6949c33c", None, NO) // Atlantis
Record(@"75e7efa861f7e7d8e367c09bf7c0cc351b472f03", None, NO) // Bachelor Party
Record(@"b88ca823aaa10a7a4a3d325023881b2de969c156", None, NO) // Bachelorette Party
Record(@"9b1da7fbd0bf6fcadf1b60c11eeb31b6a61a03c3", None, NO) // Backgammon
Record(@"80d4020575b14e130f28146bf45921e001f9f649", None, NO) // Bank Heist
Record(@"372663097b419ced64f44ef743fe8d0af4317f46", None, NO) // Barnstorming
Record(@"d0bdd609ebc6e69fb351ba469ff322406bcbab50", None, NO) // Base Attack
Record(@"6c56fad688b2e9bb783f8a5a2360c80ad2338e47", None, NO) // Basic Programming
Record(@"bffe99454cb055552e5d612f0dba25470137328d", None, NO) // Basketball
Record(@"e4134a3b4a065c856802bc935c12fa7e9868110a", Atari8k, NO) // Battlezone
Record(@"47619edb352f7f955f811cbb03a00746c8e099b1", Atari8k, NO) // Beamrider
Record(@"fad0c97331a525a4aeba67987552ba324629a7a0", None, NO) // Beany Bopper
Record(@"e2c29d0a73a4575028b62dca745476a17f07c8f0", None, NO) // Beat 'Em & Eat 'Em
Record(@"c3afd7909b72b49ca7d4485465b622d5e55f8913", Atari8k, NO) // Berenstain Bears
Record(@"fcad0e5130de24f06b98fb86a7c3214841ca42e2", None, NO) // Bermuda Triangle
Record(@"08bcbc8954473e8f0242b881315b0af4466998ae", None, NO) // Berzerk
Record(@"5e4517db83c061926130ab65975e3b83d9401cc9", Atari8k, NO) // Big Bird's Egg Catch
Record(@"512e4d047f1f813bc805c8d2a5f7cbdb34b9ea46", None, NO) // bin00016
Record(@"f6a41507b8cf890ab7c59bb1424f0500534385ce", Atari8k, NO) // Bionic Breakthrough
Record(@"edfd905a34870196f8acb2a9cd41f79f4326f88d", None, NO) // Blackjack
Record(@"0fadef01ce28192880f745b23a5fbb64c5a96efe", Atari8k, NO) // Blueprint
Record(@"ff25ed062dcc430448b358d2ac745787410e1169", Atari16k, NO) // BMX Air Master
Record(@"50e26688fdd3eadcfa83240616267a8f60216c25", None, NO) // Bobby is Going Home
Record(@"282cad17482f5f87805065d1a62e49e662d5b4bb", None, NO) // Bogey Blaster
Record(@"d106bb41a38ed222dead608d839e8a3f0d0ecc18", None, NO) // Boing!
Record(@"cf6ce244b3edaad7ad5e9ca5f01668135c2f93d0", None, NO) // Bowling
Record(@"14b9cd91188c7fb0d4566442d639870f8d6f174d", None, NO) // Boxing
Record(@"238915cafd26f69bc8a3b9aa7d880dde59f6f12d", None, NO) // Brain Games
Record(@"8d473b87b70e26890268e6c417c0bb7f01e402eb", None, NO) // Breakout
Record(@"2873eb6effd35003d13e2f8f997b76dbc85d0f64", None, NO) // Bridge
Record(@"a65dea2d9790f3eb308c048a01566e35e8c24549", Atari8k, NO) // Buck Rogers — Planet of Zoom
Record(@"9c0e13af336a986c271fe828fafdca250afba647", Atari8k, NO) // Bugs Bunny
Record(@"67387d0d3d48a44800c44860bf15339a81f41aa9", None, NO) // Bugs
Record(@"1819ef408c1216c83dcfeceec28d13f6ea5ca477", MNetwork, NO) // Bump 'n' Jump
Record(@"6c199782c79686dc0cbce6d5fe805f276a86a3f5", None, NO) // Bumper Bash
Record(@"49e01b8048ae344cb65838f6b1c1de0e1f416f29", MNetwork, NO) // BurgerTime
Record(@"b233c37aa5164a54e2e7cc3dc621b331ddc6e55b", None, NO) // Burning Desire
Record(@"3f1f17cf620f462355009f5302cddffa730fa2fa", None, NO) // Cakewalk
Record(@"609c20365c3a71ce45cb277c66ec3ce6b2c50980", Atari16k, NO) // California Games
Record(@"b89443a0029e765c2716774fe2582be37650115c", None, NO) // Canyon Bomber
Record(@"e1acf7a845b56e4b3d18192a75a81c7afa6f341a", None, NO) // Carnival
Record(@"54ed2864f58ef3768579ec96cca445ee62078521", None, NO) // cart
Record(@"08598101e38756916613f37581ef1b61c719016f", None, NO) // Casino
Record(@"e979de719cecab2115affd9c0552c6c596b1999a", None, NO) // Cat Trax
Record(@"6adf70e0b7b5dab74cf4778f56000de7605e8713", None, NO) // Cathouse Blues
Record(@"0b5914bc1526a9beaf54d7fd11408175cd8fcc72", Atari8k, NO) // Centipede
Record(@"b2b1bd165b3c10cde5316ed0f9f05a509aac828d", None, NO) // Challenge (Zellers)
Record(@"ac9b0c62ba0ca7a975d08fabbbc7c7448ecdf18d", None, NO) // Challenge of… Nexar
Record(@"e81b5e49cfbb283edba2c8f21f31a8148d8645a1", None, NO) // Challenge
Record(@"872b2f9aa7edbcbb2368de0db3696c90998ff016", None, NO) // Chase the Chuckwagon
Record(@"39b5bb27a6c4cb6532bd9d4cc520415c59dac653", None, NO) // Checkers
Record(@"0b1bb76769ae3f8b4936f0f95f4941d276791bde", None, NO) // China Syndrome
Record(@"51a53bbfdbcc22925515ae0af79df434df6ee68a", None, NO) // Chopper Command
Record(@"8a91ecdbd8bf9d412da051c3422abb004eab8603", None, NO) // Circus
Record(@"3f56d1a376702b64b3992b2d5652a3842c56ffad", None, NO) // Coco Nuts
Record(@"137bd3d3f36e2549c6e1cc3a60f2a7574f767775", None, NO) // Codebreaker
Record(@"53c324ae736afa92a83d619b04e4fe72182281a6", None, NO) // Color Bar Generator
Record(@"66014de1f8e9f39483ee3f97ca0d97d026ffc3bb", Atari8k, NO) // Combat Two
Record(@"ce7580059e8b41cb4a1e734c9b35ce3774bf777a", None, NO) // Combat
Record(@"8dad05085657e95e567f47836502be515b42f66b", None, NO) // Commando Raid
Record(@"68a7cb3ff847cd987a551f3dd9cda5f90ce0a3bf", Atari16k, NO) // Commando
Record(@"dbc0c0451dee44425810e04df8f1d26d1c2d3993", None, NO) // Computer Chess
Record(@"5512a0ed4306edc007a78bb52dbcf492adf798ec", None, NO) // Confrontation
Record(@"3a77db43b6583e8689435f0f14aa04b9e57bdded", Atari8k, NO) // Congo Bongo
Record(@"f4a62ba0ff59803c5f40d59eeed1e126fe37979b", Atari8k, NO) // Cookie Monster Munch
Record(@"187983fd14d37498437d0ef8f3fbd05675feb6ae", None, NO) // Cosmic Ark
Record(@"3717c97bbb0f547e4389db8fc954d1bad992444c", None, NO) // Cosmic Commuter
Record(@"8b9dfef6c6757a6a59e01d783b751e4ab9541d9e", None, NO) // Cosmic Corridor
Record(@"22ff281b1e698e8a5d7a6f6173c86c46d3cd8561", None, NO) // Cosmic Creeps
Record(@"c01354760f2ca8d6e4d01b230f31611973c6ae2d", None, NO) // Cosmic Swarm
Record(@"6ee0a26af4643ff250198dfc1c2b7c6568b4f207", None, NO) // Crackpots
Record(@"73d68f32d1fb73883ceb183d5150bff5f1065de4", None, NO) // Crash Dive
Record(@"70e723aa67d68f8549d9bd8f96d8b1262cbdac3c", Atari8k, NO) // Crazy Climber
Record(@"dd385886fdd20727c060bad6c92541938661e2b4", None, NO) // Crazy Valet
Record(@"b1b3d8d6afe94b73a43c36668cc756c5b6fdc1c3", None, NO) // Cross Force
Record(@"5da3d089ccda960ce244adb855975877c670e615", Atari16k, NO) // Crossbow
Record(@"a57062f44e7ac793d4c39d1350521dc5bc2a665f", None, NO) // Crypts of Chaos
Record(@"2e4ee5ee040b08be1fe568602d1859664e607efb", Atari16k, YES) // Crystal Castles
Record(@"0dd72a3461b4167f2d68c93511ed4985d97e6adc", None, NO) // Cubicolor
Record(@"4b3d02b59e17520b4d60236568d5cb50a4e6aeb3", None, NO) // Custer's Revenge
Record(@"07e94a7d357e859dcff77180981713ce8119324e", None, NO) // Dancing Plate
Record(@"0ae2fc87f87a5cc199c3b9a17444bf3c2f6a829b", None, NO) // Dark Cavern
Record(@"fbb4814973fcb4e101521515e04daa6424c45f5c", Atari16k, YES) // Dark Chambers
Record(@"c5af53c4b64c3db552d4717b8583d6fe8d3e7952", Atari8k, NO) // Dark Mage
Record(@"68de291d5e9cbebfed72d2f9039e60581b6dbdc5", None, NO) // Deadly Duck
Record(@"2ad9db4b5aec2da36ecc3178599b02619c3c462e", ParkerBros, NO) // Death Star Battle
Record(@"5f710a1148740760b4ebcc42861a1f9c3384799e", None, NO) // Death Trap
Record(@"717656f561823edaa69240471c3106963f5c307e", ActivisionStack, NO)// Decathlon
Record(@"d7b506b84f28e1b917a2978753d5a40eb197537a", Atari8k, YES) // Defender 2
Record(@"79facc1bf70e642685057999f5c2b8e94b102439", None, NO) // Defender
Record(@"5aae618292a728b55ad7f00242d870736b5356d3", None, NO) // Demolition Herby
Record(@"a580d886c191a069f6b9036c3f879e83e09500c2", None, NO) // Demon Attack
Record(@"b45582de81c48b04c2bb758d69021e8088c70ce7", None, NO) // Demons to Diamonds
Record(@"ccea2d5095441d7e1b1468e3879a6ab556dc8b7a", Atari16k, YES) // Desert Falcon
Record(@"538108ca9821265e23f06fa7672965631bdf8175", None, NO) // Diagnostic Test Cartridge 2.6
Record(@"81022ef30e682183d51b18bff145ce425c6f924e", None, NO) // Dice Puzzle
Record(@"79e746524520da546249149c33614fc23a4f2a51", Atari16k, YES) // Dig Dug
Record(@"7485cf55201ef98ded201aec73c4141f9f74f863", None, NO) // Dishaster
Record(@"157117df23cb5229386d06bbdb3af20a208722e0", None, NO) // Dodge 'Em
Record(@"e3985d759f8a8f4705f543ce7eb5e93bf63722b5", None, NO) // Dolphin
Record(@"4606c0751f560200aede6598ec9c8e6249a105f5", Atari8k, NO) // Donald Duck's Speedboat
Record(@"359e662da02bf0a2184472e25d05bc597b6c497a", None, NO) // Donkey Kong (1983) (CBS Electronics) (PAL) [!]
Record(@"98f98ac0728c68de66afda6500cafbdffe8ab50a", Atari8k, NO) // Donkey Kong Junior
Record(@"6e6e37ec8d66aea1c13ed444863e3db91497aa35", None, NO) // Donkey Kong
Record(@"251e02ac583d84eb43f1451d55b62c7c70e9644f", Atari16k, NO) // Double Dragon
Record(@"8e2ea320b23994dc87abe69d61249489f3a0fccc", Atari16k, NO) // Double Dunk
Record(@"b446381fe480156077b0b3c51747d156e5dde89f", None, NO) // Dragon Treasures
Record(@"944c52de85464070a946813b050518977750e939", None, NO) // Dragster
Record(@"9caf114c9582d5e0c14396b13d2bd1a89cad90b1", None, NO) // Dukes of Hazzard (1980)
Record(@"c061d753435dcb7275a8764f4ad003b05fa100ed", Atari16k, NO) // Dukes of Hazzard (1983)
Record(@"d16eba13ab1313f375e86b488181567f846f1dc4", Atari8k, NO) // Dumbo's Flying Circus
Record(@"9e34f9ca51573c92918720f8a259b9449a0cd65e", Atari8k, NO) // E.T. — The Extra-Terrestrial
Record(@"68cbfadf097ae2d1e838f315c7cc7b70bbf2ccc8", None, NO) // Eggomania
Record(@"bab872ee41695cefe41d88e4932132eca6c4e69c", Atari8k, YES) // Elevator Action
Record(@"475fc2b23c0ee273388539a4eeafa34f8f8d3fd8", None, NO) // Eli's Ladder
Record(@"3983e109fc0b38c0b559a09a001f3e5f2bb1dc2a", Atari8k, NO) // Elk Attack
Record(@"205af4051ea39fb5a038a8545c78bff91df321b7", None, NO) // Encounter at L-5
Record(@"82e9b2dd6d99f15381506a76ef958a1773a7ba21", None, NO) // Enduro
Record(@"7905aee90a6dd64d9538e0b8e772f833ba9feb83", None, NO) // Entombed
Record(@"27d925d482553deff23f0889b3051091977d6920", Tigervision, NO) // Espial
Record(@"c17801c0190ade27f438e2aa98dde81b3ae66267", None, NO) // Exocet
Record(@"6297dd336a6343f98cd142d1d3d76ce84770a488", None, NO) // Fantastic Voyage
Record(@"a5614c751f29118ddb3dec9794612b98a0f00b98", None, NO) // Fast Eddie
Record(@"c62a70645939480b184e3b2e378ec4bcbd484bc7", None, NO) // Fast Food
Record(@"d0bb58ea1fc37e929e5f7cdead037bb14a166451", Atari32k, YES) // Fatal Run
Record(@"686427cc47b69980d292d04597270347942773ff", Atari8k, NO) // Fathom
Record(@"684275b22f2bac7d577cf48cf42fa14fa6f69678", Atari16k, NO) // Fighter Pilot
Record(@"ba9a8ccfeb552dd756c660ea843a39619d3c77e9", None, NO) // Final Approach
Record(@"f76cc14afd7aef367c5a5defbd84f3bbb2f98ba3", None, NO) // Fire Fighter
Record(@"df5420eb0f71e681e7222ede8e211a7601e7a327", None, NO) // Fire Fly
Record(@"531e995aef6cd47b0efea72ae3e56aeee449d798", None, NO) // Fishing Derby
Record(@"ac05f05f3365f5e348e1e618410065a1c2a88ee4", None, NO) // Flag Capture
Record(@"c6fe4ce24bc1ebd538258d98cfe829963323acca", None, NO) // Football
Record(@"c6023bf73818c78b2e477a9c6dac411cdbf9c0aa", None, NO) // Frankenstein's Monster
Record(@"91cc7e5cd6c0d4a6f42ed66353b7ee7bb972fa3f", None, NO) // Freeway
Record(@"de6fc1b51d41b34dcda92f579b2aa4df8eccf586", Atari8k, NO) // Frog Pond
Record(@"f344d5a8dc895c5a2ae0288f3c6cb66650e49167", None, NO) // Frogflys
Record(@"6b9e591cc53844795725fc66c564f0364d1fbe40", ParkerBros, NO) // Frogger II
Record(@"e859b935a36494f3c4b4bf5547392600fb9c96f0", None, NO) // Frogger
Record(@"cf32bfcd7f2c3b7d2a6ad2f298aea2dfad8242e7", Atari8k, NO) // Front Line
Record(@"b9e60437e7691d5ef9002cfc7d15ae95f1c03a12", None, NO) // Frostbite
Record(@"5cc4010eb2858afe8ce77f53a89d37c7584e15b4", None, NO) // Fun with Numbers
Record(@"9cfb6288a5c2dae63ee6f5e9325200ccd21a3055", None, NO) // G.I. Joe — Cobra Strike
Record(@"8e708e0067d3302327900fa322aeb8e2df2022d7", Atari8k, NO) // Galaxian Enhanced Graphics
Record(@"b081b327ac32d951c36cb4b3ff812be95685d52f", Atari8k, NO) // Galaxian
Record(@"8cf49d43bd62308df788cfacbfcd80e9226c7590", None, NO) // Gangster Alley
Record(@"bc0d1edc251d8d4db3d5234ec83dee171642a547", Atari16k, NO) // Garfield
Record(@"0da1f2de5a9b5a6604ccdb0f30b9da4e5f799b40", None, NO) // Gas Hog
Record(@"73adae38d86d50360b1a247244df05892e33da46", None, NO) // Gauntlet
Record(@"3b1fb93342c7f014a28dddf6f16895d11ac7d6f0", None, NO) // General Re-Treat
Record(@"4b533776dcd9d538f9206ad1e28b30116d08df1e", Atari8k, NO) // Ghost Manor
Record(@"1bcf03e1129015a46ad7028e0e74253653944e86", Atari16k, NO) // Ghostbusters II (alternate)
Record(@"e032876305647a95b622e5c4971f7096ef72acdb", Atari16k, NO) // Ghostbusters II
Record(@"5ed0b2cb346d20720e3c526da331551aa16a23a4", Atari8k, NO) // Ghostbusters
Record(@"b64ed2d5a2f8fdac4ff0ce56939ba72e343fec33", None, NO) // Gigolo
Record(@"3a3d7206afee36786026d6287fe956c2ebc80ea7", None, NO) // Glacier Patrol
Record(@"7bca0f7a0f992782e4e4c90772bac976ca963a6d", None, NO) // Glib
Record(@"f78c478aacf6536522e8d37a3888a975e1a383cd", None, NO) // Go Go Home Monster (2)
Record(@"4c41379f0dd9880384fcbb46bad9fbaaf109a477", None, NO) // Go Go Home Monster
Record(@"a25d52770408314dec6f41aaf5f9f0a2a3e2c18f", None, NO) // Golf
Record(@"97fb489ba4ce0f8a306563063563617321352cfb", None, NO) // Gopher
Record(@"35f8341c73c7e6e896cb065977427b3f98ae9f08", None, NO) // Gorf
Record(@"24fab817728216582b6d95558c361ace66abf96f", None, NO) // Grand Prix
Record(@"a372d4dd3d95b3866553cae2336e4565e00cc25b", Atari8k, NO) // Gravitar
Record(@"7a027329309e018b0d51adcb6ae13c9d13e54f4a", Atari8k, NO) // Gremlins
Record(@"c90acaee066f97efc6a520deb7fa3e5760a471fa", Atari8k, NO) // Grover's Music Maker
Record(@"7d30ff565ad7b2a3143d049c5b39e4a6ac3f9cd5", None, NO) // Guardian
Record(@"45f3f98735798e19427a9100a9000d97917b932f", None, NO) // Gunfight (NTSC)
Record(@"7ac6356224cc718ee5731d1ce14aea6fb2335439", None, NO) // Gunfight (PAL)
Record(@"4bd87ba8b3b6d7850e3ea41b4d494c3b12659f27", ParkerBros, NO) // Gyruss
Record(@"282f94817401e3725c622b73a0c05685ce761783", Atari8k, NO) // H.E.R.O.
Record(@"4c72cec151f219866bf870fa7ac749a19ca501c9", None, NO) // Halloween
Record(@"561bccf508e162bc70c42d85c170cf0d1d4691a3", None, NO) // Hangman
Record(@"d6db71da02ae194140bf812be34d6e8a6785d138", None, NO) // Harbor Escape
Record(@"1476c869619075b551b20f2c7f95b11e0d16aec1", None, NO) // Haunted House
Record(@"8196209ef7048c5494dbdc932adbf1c7abf79f4e", Atari8k, NO) // Holey Moley
Record(@"f362d2b3a50e5ae3c2b412b6c08ecdcfee47a688", None, NO) // Home Run
Record(@"d4b0b2aa379893356c72414ee0065a3a91cf9f97", None, NO) // Human Cannonball
Record(@"a6e42c63138a2fd527cdbe9b7e60f5feabdd55c8", None, NO) // Hunt & Score — Memory Match
Record(@"f7e782214b5f9227e34c00f590be50534f1fda91", None, NO) // I Want my Mommy
Record(@"21de0f034e5dad03fa91eb7ae6cc081c142be35c", None, NO) // Ice Hockey
Record(@"e5b9c3a3638bd42f96a26b651463da96a9432315", Atari16k, NO) // Ikari Warriors
Record(@"620ab88d63cdd3f8ce67deac00a22257c7205c8b", None, NO) // Indy 500
Record(@"922cd171ef132bf6c5bed00ad01410ada4b20729", None, NO) // Infiltrate
Record(@"19fc37f2a24e31a59a17f9cbf3cc03416a8bab9a", None, NO) // Invaders
Record(@"2bbc124cead9aa49b364268735dad8cb1eb6594f", ParkerBros, NO) // James Bond 007
Record(@"ea5c827052886908c0deaa0a03d6f8da8e4f298d", None, NO) // Jammed Demo
Record(@"af4d6867a8bc4818fc6bb701a765a3c907feb628", None, NO) // Jaw Breaker
Record(@"36b9edc7150311203f375c1be10d0510efde6476", None, NO) // Jedi Arena
Record(@"0d94c1caacb862d9e0b4c2dda121cd4d74a1cced", None, NO) // John K Harvey's Equalizer
Record(@"928eaa424b36d98078f9251d67fb13a8fddfafbd", None, NO) // Journey Escape
Record(@"cb94dc316cba282a0036871db2417257e960786b", Atari8k, NO) // Joust
Record(@"cd2cf245d6e924ff2100cc93d20223c4a231e160", Atari16k, YES) // Jr. Pac-Man
Record(@"9a0ee845d9928d4db003b07b927bb2c1f628e725", None, NO) // Jungle Fever
Record(@"83a32a2d686355438c915540cfe0bb13b76c1113", Atari8k, NO) // Jungle Hunt
Record(@"ce8ac88b799c282567495ce509402a5a4c2c4d82", None, NO) // Kabobber
Record(@"40d4df4f8e4a69a299ae7678c17e72bedeb70105", None, NO) // Kaboom!
Record(@"a82aaeef44ad88de605c50d23fb4f6cec73f3ab4", None, NO) // Kamikaze Saucers
Record(@"01fd30311e028944eafb6d14bb001035f816ced7", Atari8k, NO) // Kangaroo
Record(@"c0db7d295e2ce5e00e00b8a83075b1103688ea15", None, NO) // Karate
Record(@"3eefc193dec3b242bcfd43f5a4d9f023e55378a4", None, NO) // Keystone Kapers
Record(@"839e13ffedcbed22d51a24c001900c3474a078f2", None, NO) // King Kong
Record(@"3162259c6dbfbb57a2ea41d849155702151ee39b", Atari16k, YES) // Klax
Record(@"759597d1d779cfdfd7aa30fd28a59acc58ca2533", None, NO) // Knight on the Town
Record(@"2f550743e237f6dc8c75c389a01b02e9a396fdad", None, NO) // Kool-Aid Man
Record(@"4bdf1cf73316bdb0002606facf11b6ddcb287207", Atari8k, NO) // Krull
Record(@"1637b6b9cd1a918339ec054cf95b924e7ce4789a", Atari8k, NO) // Kung Fu Superkicks
Record(@"3b93a34ba2a6b7db387ea588c48d939eee5d71a1", Atari8k, NO) // Kung-Fu Master
Record(@"6d59dfea26b7a06545a817f03f62a59be8993587", None, NO) // Lady in Wading
Record(@"ea8ecc2f6818e1c9479f55c0a3356edcf7a4d657", None, NO) // Laser Blast
Record(@"cdf55b73b4322428a001e545019eaa591d3479cf", None, NO) // Laser Gates
Record(@"afab795719386a776b5fb2165fc84f4858e16e05", None, NO) // Laser Volley
Record(@"fe208ad775cbf9523e7a99632b9f10f2c9c7aa87", None, NO) // Lochjaw
Record(@"fc3d75d46d917457aa1701bf47844817d0ba96c3", None, NO) // Lock 'n' Chase
Record(@"f92b0b83db3cd840d16ee2726011f5f0144103d5", None, NO) // London Blitz
Record(@"ef02fdb94ac092247bfcd5f556e01a68c06a4832", ParkerBros, NO) // Lord of the Rings
Record(@"e8492fe9d62750df682358fe59a4d4272655eb96", None, NO) // Lost Luggage
Record(@"dcd96913a1c840c8b57848986242eeb928bfd2ff", None, NO) // M*A*S*H
Record(@"d6e2b7765a9d30f91c9b2b8d0adf61ec5dc2b30a", None, NO) // M.A.D.
Record(@"4c66b84ab0d25e46729bbcf23f985d59ca8520ad", CommaVid, NO) // MagiCard
Record(@"cdc7e65d965a7a00adda1e8bedfbe6200e349497", None, NO) // Malagai
Record(@"ee8f9bf7cdb55f25f4d99e1a23f4c90106fadc39", None, NO) // Mangia
Record(@"249a11bb4872a24f22dff1027ff256c1408140c2", None, NO) // Marauder
Record(@"dd9e94ca96c75a212f1414aa511fd99ecdadaf44", None, NO) // Marine Wars
Record(@"49425ff154b92ca048abb4ce5e8d485c24935035", Atari8k, NO) // Mario Bros.
Record(@"fbe7a78764407743b43a91136903ede65306f4e7", None, NO) // Master Builder
Record(@"6db8fa65755db86438ada3d90f4c39cc288dcf84", MNetwork, NO) // Masters of the Universe
Record(@"18fac606400c08a0469aebd9b071ae3aec2a3cf2", None, NO) // Math Gran Prix
Record(@"aba25089d87cd6fee8d206b880baa5d938aae255", None, NO) // Maze Craze
Record(@"0ae118373c7bda97da2f8d9c113e1e09ea7e49e1", None, NO) // Mega Force
Record(@"46977baf0e1ee6124b524258879c46f80d624fae", MegaBoy, NO) // MegaBoy
Record(@"9c5748b38661dbadcbc9cd1ec6a6b0c550b0e3da", None, NO) // MegaMania
Record(@"debb1572eadb20beb0e4cd2df8396def8eb02098", None, NO) // Meltdown
Record(@"7fcf95459ea597a332bf5b6f56c8f891307b45b4", Atari16k, NO) // Midnight Magic
Record(@"0616f0dde6d697816dda92ed9e5a4c3d77a39408", Atari16k, YES) // Millipede
Record(@"5edbf8a24fcba9763983befe20e2311f61b986d4", Tigervision, NO) // Miner 2049er Volume 2
Record(@"0e56b48e88f69d405eabf544e57663bd180b3b1e", Tigervision, NO) // Miner 2049er
Record(@"34773998d7740e1e8c206b3b22a19e282ca132e1", None, NO) // Mines of Minos
Record(@"be24b42e3744a81fb217c86c4ed5ce51bff28e65", None, NO) // Miniature Golf
Record(@"f721d1f750e19b9e1788eed5e3872923ab46a91d", Atari8k, NO) // Miss Piggy's Wedding
Record(@"faa06bb0643dbf556b13591c31917d277a83110b", None, NO) // Missile Command
Record(@"224e7a310afdb91c6915743e72b7b53b38eb5754", None, NO) // Missile Control
Record(@"999dc390a7a3f7be7c88022506c70bd4208b26d8", None, NO) // Mission 3,000 AD
Record(@"93520821ce406a7aa6cc30472f76bca543805fd4", None, NO) // Mission Survive
Record(@"0b74a90a22a7a16f9c2131fabd76b7742de0473e", None, NO) // Mogul Maniac
Record(@"81a4d56820b1e00130e368a3532c409929aff5fb", Atari8k, NO) // Monstercise
Record(@"7dfeb1a8ec863c1e0f297113a1cc4185c215e81c", ParkerBros, NO) // Montezuma's Revenge
Record(@"dce778f397a325113f035722b7769492645d69eb", Atari8k, NO) // Moon Patrol
Record(@"05ab04dc30eae31b98ebf6f43fec6793a53e0a23", Atari8k, NO) // Moonsweeper
Record(@"c4d495d42ea5bd354af04e1f2b68cce0fb43175d", Atari8k, NO) // Motocross Racer
Record(@"ece97bda734faffcf847a8bcdfa474789c377d8d", Atari16k, NO) // MotoRodeo
Record(@"ef4112e86d6a3e8f7b8e482d294a5917f162b38c", CBSRamPlus, NO) // Mountain King
Record(@"cf6347dedcfec213c28dd92111ec6f41e74b6f64", None, NO) // Mouse Trap
Record(@"e4c912199779bba25f1b9950007f14dca3d19c84", Atari8k, NO) // Mr Do!
Record(@"330c2c67399e07c40f4101f9e18670fef070475e", ParkerBros, NO) // Mr. Do!'s Castle
Record(@"62b933cdd8844bb1816ce57889203954fe782603", Atari8k, NO) // Ms. Pac-Man
Record(@"b2df23b1bf6df9d253ad0705592d3fce352a837b", Atari8k, NO) // My Golf
Record(@"2b4a0535ca83b963906eb0a5d60ce0e21f07905d", None, NO) // Name This Game
Record(@"372771aeb4e2fb2cd1dead5497e3821e4236d5fc", None, NO) // Night Driver
Record(@"26e1309bc848cf5880b831d7566488ec5b3db58c", None, NO) // Night Stalker (2)
Record(@"281ff7e55c27656522b144b84cba08eb148e2f0a", None, NO) // Night Stalker
Record(@"e2e8750b8856dd44d914c43a7d277188cc148e5c", None, NO) // No Escape!
Record(@"771a2a87b3b457c0b83f556ce00d1e9c54caeabc", None, NO) // Nothing
Record(@"be3f4beeb322cddc7223d6d77e17302aa811e43a", Atari8k, NO) // Obelix
Record(@"1f8d06b99db94b0aa8ca320c7cb212639ac9591f", None, NO) // Ocean City
Record(@"3dcfe93399044148561586056288c6f8e5c96e2b", Atari16k, YES) // Off the Wall
Record(@"7ad74a7c36318f1304f5dc454401cf257fa60d7a", None, NO) // Off Your Rocker
Record(@"ea674cf2c90d407b8f8b96eac692690b602b73f9", None, NO) // Oink!
Record(@"7bd1cbddefcf3bd24da570be015234d0c444a7e5", None, NO) // Okie Dokie
Record(@"dcaab259e7617c7ac7d349893451896a9ca0e292", CBSRamPlus, NO) // Omega Race
Record(@"cd968c2a983f9adf4d8d8d56823923b31c33980f", None, NO) // Open Sesame
Record(@"7905709fcc85cbcfc28ca2ed543ffa737a5483ae", Atari8k, NO) // Oscar's Trash Race
Record(@"cbecf1a32d9366a3dd4ad643916cd59cdc820a8b", None, NO) // Othello
Record(@"344d6942723513c376a7a844779804e10f357b85", None, NO) // Out of Control
Record(@"f8eeaaf4635ac39b4bdf7ded1348bce46313ef9f", None, NO) // Outlaw
Record(@"412e8a2438379878ee4de5c6bf4e5a9ee2707c8b", None, NO) // Oystron
Record(@"923c969d31cef450075932436f03f1404e1cab0e", None, NO) // Pac-Kong
Record(@"0940fea7f04cdb6d4b90c5ad1a7e344e68f6dbb1", None, NO) // Pac-Man
Record(@"b529c1664ed1abc8f5f962a1fed65c0e4440219c", None, NO) // Peek-A-Boo
Record(@"832283530f5dee332f29cf8c4854dd554f2030a0", None, NO) // Pele's Soccer
Record(@"461c2ea3e4d24f86ec02215c1f4743d250796c11", Atari8k, NO) // Pengo (prototype)
Record(@"89b991a7a251f78f422bcdf9cf7d4475fdf33e97", Atari8k, NO) // Pengo
Record(@"c5317035e73f60e959c123d89600c81b7c45701f", None, NO) // Pepsi Invaders
Record(@"19c3ad034466c0433501a415a996ed7155d6063a", Atari16k, NO) // Pete Rose Baseball
Record(@"959aca4b44269b1e5ac58791fc3c7c461a6a4a17", None, NO) // Phantom Tank
Record(@"b299df2792c5cca73118925dff85695b73a16228", None, NO) // Philly Flasher
Record(@"010d51e3f522ba60f021d56819437d7c85897cdd", Atari8k, NO) // Phoenix
Record(@"a5917537cf1093aa350903d85d9e271e8a11d2cf", Atari16k, NO) // Pick 'n' Pile
Record(@"483fc907471c5c358fb3e624097861a2fc9c1e45", None, NO) // Picnic
Record(@"57774193081acea010bd935a0449bc8f53157128", None, NO) // Piece o' Cake
Record(@"d08b30ca2e5e351cac3bd3fb760b87a1a30aa300", Atari8k, NO) // Pigs in Space
Record(@"920cfbd517764ad3fa6a7425c031bd72dc7d927c", Pitfall2, NO) // Pitfall II
Record(@"d0ec08b88d032627701ad72337524d91b26c656b", None, NO) // Pitfall! (PAL)
Record(@"8d525480445d48cc48460dc666ebad78c8fb7b73", None, NO) // Pitfall! (NTSC)
Record(@"dcca30e4ae58c85a070f0c6cfaa4d27be2970d61", None, NO) // Planet of the Apes
Record(@"ccfcbf52815a441158977292b719f7c5ed80c515", None, NO) // Planet Patrol
Record(@"103398dd35ebd39450c5cac760fa332aac3f9458", None, NO) // Plaque Attack
Record(@"2410931a8a18b915993b6982fbabab0f437967a4", Tigervision, NO) // Polaris
Record(@"9d334da07352a9399cbbd9b41c6923232d0cdcd3", Atari8k, NO) // Pole Position
Record(@"c0af0188028cd899c49ba18f52bd1678e573bff2", None, NO) // Polo
Record(@"954d2980ea8f8d9a76921612c378889f24c35639", None, NO) // Pompeii
Record(@"b7a002025c24ab2ec4a03f62212db7b96c0e5ffd", None, NO) // Pooyan
Record(@"1772a22df3e9a1f3842387ac63eeddff7f04b01c", ParkerBros, NO) // Popeye
Record(@"70afc2cc870be546dc976fa0c6811f7e01ebc471", Atari8k, NO) // Porky's
Record(@"8b001373be485060f88182e9a7afcf55b4d07a57", Atari8k, NO) // Pressure Cooker
Record(@"1ea6bea907a6b5607c76f222730f812a99cd1015", Atari8k, NO) // Private Eye
Record(@"feb6bd37e5d722bd080433587972b980afff5fa5", None, NO) // Pumruckl I
Record(@"a61be3702437b5d16e19c0d2cd92393515d42f23", ParkerBros, NO) // Q-Bert's Qubes
Record(@"f3ef9787b4287a32e4d9ac7b9c3358edc16315b2", None, NO) // Q-Bert
Record(@"1e634a8733cbc50462d363562b80013343d2fac3", Atari8k, NO) // Quadrun
Record(@"d83c740d2968343e6401828d62f58be6aea8e858", Atari8k, NO) // Quest for Quintana Roo
Record(@"33a47f79610c4525802c9881f67ad1f3f8c1b55d", None, NO) // Quick Step!
Record(@"7bf945ea667e683ec24a4ed779e88bbe55dc4b26", Atari8k, NO) // Rabbit Transit
Record(@"4af6008152f1d38626d84016a7ef753406b48b46", None, NO) // Racquetball
Record(@"33f016c941fab01e1e2d0d7ba7930e3bcd8feaa3", Atari16k, YES) // Radar Lock
Record(@"5f1f2b5b407b0624b59409e02060a3a9e8eed8fc", None, NO) // Radar
Record(@"a79f6e0f4fd76878e5c3ba6b52d17e88acdbe9f6", None, NO) // Raft Rider
Record(@"7ae70783969709318e56f189cf03da92320a6aba", Atari8k, NO) // Raiders of the Lost Ark
Record(@"fd0a69c06eb3f7c9328951c890644f93c4bad6ad", None, NO) // Ram It
Record(@"7bb7df255829d5fbbee0d944915e50f89a5e7075", Atari16k, NO) // Rampage!
Record(@"5adf9b530321472380ebceb2539de2ffbb0310bc", None, NO) // Reactor
Record(@"ace97b89b8b6ab947434dbfd263951c6c0b349ac", Atari8k, NO) // RealSports Baseball
Record(@"bc2e6bdaa950bc06be040899dfeb9ad0938f4e98", Atari8k, NO) // RealSports Basketball
Record(@"22dedbfce6cc9055a6c4caec013ca80200e51971", Atari16k, NO) // RealSports Boxing
Record(@"200d04c1e7f41a5a3730287ed0c3f9293628f195", Atari8k, NO) // RealSports Football
Record(@"e3d964d918b7f2c420776acd3370ec1ee62744ea", Atari8k, NO) // RealSports Soccer
Record(@"702c1c7d985d0d22f935265bd284d1ed50df2527", Atari8k, NO) // RealSports Tennis
Record(@"2025e1d868595fad36e5d9e7384ffd24c206208d", None, NO) // RealSports Volleyball
Record(@"94e94810bf6c72eee49157f9218c3c170b65c836", None, NO) // Rescue Terra I
Record(@"f8a9dd46f9bad232f74d1ee2671ccb26ea1b3029", None, NO) // Revenge of the Beefsteak Tomatoes
Record(@"acb2430b4e6c72ce13f321d9d3a38986dc4768ef", None, NO) // Riddle of the Sphinx
Record(@"6715493dce54b22362741229078815b3360988ae", Tigervision, NO) // River Patrol
Record(@"40329780402f8247f294fe884ffc56cc3da0c62d", None, NO) // River Raid copy
Record(@"a08c3eae3368334c937a5e03329782e95f7b57c7", Atari16k, NO) // River Raid II
Record(@"325a2374800b2cb78ab7ff9e4017759865109d7d", None, NO) // River Raid
Record(@"7f9c2321c9f22cf2cdbcf1b3f0e563a1c53f68ca", Atari8k, NO) // Robin Hood
Record(@"f45dfcd6db0dae5458e1c0ae8eeaa75b553cdfec", Atari16k, NO) // Road Runner
Record(@"21a3ee57cb622f410ffd51986ab80acadb8d44b7", ActivisionStack, NO)// Robot Tank
Record(@"0abf0a292d4a24df5a5ebe19a9729f3a8f883c8b", Atari8k, NO) // Roc 'n Rope
Record(@"fd243c480e769b20b7bf3e74bcd86e4ac99dab19", None, NO) // Room of Doom
Record(@"85752ac6eb7045a9083425cd166609882a1c2c58", Atari8k, NO) // Saboteur
Record(@"ecd8ef49ae23ddd3e10ec60839b95c8e7764ea27", Atari16k, YES) // Save Mary!
Record(@"e566d7b1f4eb6c2b110eb4fc676eb0ce9e90fe1e", None, NO) // SCIScide 1.32
Record(@"8e9b320d8966315a8b07f1babc0ba2662f761102", None, NO) // SCSIcide 1.30
Record(@"e8f5ae861ca1f410c6a9af116a96ed65d9a3abb2", None, NO) // Scuba Diver
Record(@"e5558ae30acc1fa5b4ffe782ae480622586a32ca", None, NO) // Sea Hunt
Record(@"4ec6982b2da25b29840428fd993a391e63f53730", None, NO) // Seahawk
Record(@"7324a1ebc695a477c8884718ffcad27732a98ab0", None, NO) // Seaquest
Record(@"1914f57ab0a6f221f2ad344b244a3cdd7b1d991a", None, NO) // Secret Agent
Record(@"af11f1666d345267196a1c35223727e2ef93483a", Atari16k, YES) // Secret Quest
Record(@"6518078b3786ac26f75067f0646aef4e83f2db15", None, NO) // Self_Portrait_by_Tjoppen
Record(@"fcf5f8a7d6e59a339c2002e3d4084d87deb670fe", Atari16k, NO) // Sentinel
Record(@"6e91759756c34f40a2c26936df6c0ca1a3850e80", None, NO) // Shark Attack
Record(@"cfb4b41e318c7cd0070e75e412f67c973e124d8e", None, NO) // Shootin' Gallery
Record(@"6e6daa34878d3e331c630359c7125a4ffba1b22d", Atari16k, YES) // Shooting Arcade
Record(@"08952192ea6bf0ef94373520a7e855f58bae6179", None, NO) // Shuttle Orbiter
Record(@"242fc23def80da96da22c2c7238d48635489abb0", Atari8k, NO) // Sinistar
Record(@"e9fa52f8e7f747cd9685ddb18bdeed2f66255324", Atari8k, NO) // Sir Lancelot
Record(@"a26fe0b5a43fe8116ab0ae6656d6b11644d871ec", Atari8k, NO) // Skate Boardin'
Record(@"5ea6d2eb27c76e85f477ba6c799deb7c416ebbc3", None, NO) // Skeet Shoot
Record(@"6581846f983b50cffb75d1c1b902238ba7dd4e92", None, NO) // Skiing
Record(@"4dde18d4abc139562fdd7a9d2fd49a1f00a9e64a", None, NO) // Sky Diver
Record(@"105f722dcf9a89b683c10ddd7f684c5966c8e1db", None, NO) // Sky Jinks
Record(@"fc5f1e30db3b2469c9701dadfa95f3268fd1e4cb", Atari8k, NO) // Sky Patrol
Record(@"ef0a7ecfe8f3b5d1e67a736552a0cdc472803be9", None, NO) // Sky Skipper
Record(@"7239d1c64f3dfc2a1613be325cce13803dd2baa5", None, NO) // Slot Machine
Record(@"a2b13017d759346174e3d8dd53b6347222d3b85d", None, NO) // Slot Racers
Record(@"530c7883fed4c5b9d78e35d48770b56e328999a3", Atari8k, NO) // Smurfs: Rescue in Gargamel's Castle
Record(@"c0ae3965fcfab0294f770af0af57d7d1adc17750", Atari8k, NO) // Smurfs Save the Day
Record(@"e7bf450cf3a3f40de9d24d89968a4bc89b53cb18", None, NO) // Snail Against Squirrel
Record(@"843e3c2fc71af2db3f2ae98eb350fde26334cfd1", None, NO) // Sneak 'n Peak
Record(@"972bc0a77e76f3e4e1270ec1c2fc395e9826bc07", Atari8k, NO) // Snoopy and the Red Baron
Record(@"9d725002e94b04e29d8cbce3c71d3bb2a84352fa", None, NO) // Soccer
Record(@"09ea74f14db8d21ea785d0c8209ed670e4ce88be", Atari8k, NO) // Solar Fox
Record(@"ec65ef9e47239a7d15db9bca7e625b166e8ac242", None, NO) // Solar Storm
Record(@"33b16fbc95c2cdc52d84d98ca471f10dae3f9dbf", Atari16k, NO) // Solaris
Record(@"ae3009e921f23254bb71f67c8cb2d7d6de2845a5", Atari8k, NO) // Sorceror's Apprentice
Record(@"70e912379060d834aa9fb2baa2e6a438f3b5d3b6", None, NO) // Sorceror
Record(@"560563613bc309a532d611f11a1cf2b9af1e2f16", None, NO) // Space Attack
Record(@"26c6c47e9b654e81f47875c5fcb4e6212125f329", None, NO) // Space Canyon
Record(@"b757b883ee114054c650027f3b9a8f15548cbf32", None, NO) // Space Cavern
Record(@"31d9668fe5812c3d2e076987ca327ac6b2e280bf", None, NO) // Space Invaders
Record(@"5bdd8af54020fa43065750bd4239a497695d403b", None, NO) // Space Jockey
Record(@"bcec5a66f8dff1a751769626b0fce305fab44ca2", Atari8k, NO) // Space Shuttle
Record(@"ce4432bb48921a3565d996b80b65fdf73bbfc39b", None, NO) // Space Tunnel
Record(@"23510ba617431097668eaf104aa1e36233173093", None, NO) // Space War
Record(@"b356294e35827bf81add95fee5453b0ca0f497ad", None, NO) // Spacechase
Record(@"983b1aff97ab1243e283ba62d3a6a75ad186d225", None, NO) // SpaceMaster X-7
Record(@"06820ad3c957913847f9849d920bc8725f535f11", None, NO) // Spider Fighter
Record(@"5d6f918bba4bd046e85b707da3b7d643cc2e1f1f", None, NO) // Spider Maze
Record(@"60af23a860b33e1a85081b8de779d2ddfe36b19a", None, NO) // Spider Monster
Record(@"912c5f5571ac59a6782da412183cdd6277345816", None, NO) // Spider-Man
Record(@"904118b0c1be484782ec2a60a24436059608b36d", None, NO) // Spiderdroid
Record(@"205241a12778829981e9281d9c6fa137f11e1376", Atari8k, NO) // Spike's Peak
Record(@"165de0ebca628eb1e9f564390c9eedfe289c7a1d", None, NO) // Spitfire Attack
Record(@"6da0aa8aa40cd9c78dc014deb9074529688d91d0", Tigervision, NO) // Springer
Record(@"c0e29b86fc1cc41a1c8afa37572c3c5698ae70b2", Atari16k, YES) // Sprint Master
Record(@"1d0acf064d06a026a04b6028285db78c834e9854", Atari8k, NO) // Spy Hunter
Record(@"033148faebc97d4ed3a86c97fe0cdee21bd261f7", None, NO) // Squeeze Box
Record(@"11a9dd44787f011ec540159248377cb27fb8f7bb", None, NO) // Squoosh
Record(@"46aabde3074acded8890a2efa5586d6b8bd76b5d", None, NO) // Sssnake
Record(@"277184c4e61ced14393049a21a304e941d05993f", None, NO) // Stampede
Record(@"1b95e07437ddc1523d7ec21c460273e91dbf36c7", None, NO) // Star Fox
Record(@"3359aa7a6a5fa25beaa3ae5868d0034d52de9882", None, NO) // Star Gunner
Record(@"e10cce1a438c82bd499e1eb31a3f07d7254198f5", Atari8k, NO) // Star Raiders
Record(@"878e78ed46e29c44949d0904a2198826e412ed81", None, NO) // Star Ship
Record(@"de05d1ca8ad1e7a85df3faf25b1aa90b159afded", None, NO) // Star Strike
Record(@"61a3ebbffa0bfb761295c66e189b62915f4818d9", Atari8k, NO) // Star Trek — Strategic Operations Simulator
Record(@"ccc5b829c4aa71acb7976e741fdbf59c8ef9eb55", None, NO) // Star Voyager
Record(@"c9d201935bbe6373793241ba9c03cc02f1df31c9", ParkerBros, NO) // Star Wars — Ewok Adventure
Record(@"8823fe3d8e3aeadc6b61ca51914e3b15aa13801c", ParkerBros, NO) // Star Wars — The Arcade Game
Record(@"ad5b2c9df558ab23ad2954fe49ed5b37a06009bf", None, NO) // Star Wars — The Empire Strikes Back
Record(@"4f87be0ef16a1d0389226d1fbda9b4c16b06e13e", Atari8k, YES) // Stargate
Record(@"814876ed270114912363e4718a84123dee213b6f", None, NO) // StarMaster
Record(@"e56ef1c0313d6d04e25446c4e34f9bb7eda8efac", None, NO) // Steeplechase copy
Record(@"bd7f0005fa018f13ed7e942c83c1751fb746a317", None, NO) // Steeplechase
Record(@"de2c146fa7a701d6c37728f5415563ce923a3e5d", None, NO) // Stella_Lives!
Record(@"7d97d014c22a2ed3a5bc4b310f5a7be1b1d3520f", None, NO) // Stellar Track
Record(@"9d6decda6e8ab263f7380ff662c814b8cb8caf34", None, NO) // Strategy X
Record(@"7ca8f9cd74cfa505c493757ff37bf127ff467bb4", None, NO) // Strawberry Shortcake — Musical Match-Ups
Record(@"bffb3d41916c83398624151eb00aa2a3acd23ab8", None, NO) // Street Racer
Record(@"2cfe280fdbb6b5c8cda8a4620df12a5154e123be", None, NO) // Stronghold
Record(@"2e19d7e16cf17682b043baaa30e345e6fa4540e5", None, NO) // Stunt Cycle
Record(@"3aec7ea8af72bbe105b9d2903a92f5ad2b37bddb", None, NO) // Stunt Man
Record(@"ccd75f0141b917656ef2b86c068fba3238d18a0c", None, NO) // Sub-Scan
Record(@"b22ba7cbde60a21ecbbe3953cc4a5c0bf007cc26", None, NO) // Submarine Commander
Record(@"2abc6bbcab27985f19e42915530fd556b6b1ae23", Atari8k, NO) // Subterrenea
Record(@"65f4a708e6af565f1f75d0fbdc8942cb149cf299", Atari16k, NO) // Summer Games
Record(@"b066a60ea1df1db0a55271c7608b0e19e4d18a1e", Atari16k, NO) // Super Baseball
Record(@"e380e243c671e954e86aa1a3a0bfeb36d5e0c3e2", None, NO) // Super Breakout
Record(@"dfce4d6436f91d8d385f8b01f0d8e3488400407b", None, NO) // Super Challenge Baseball
Record(@"5c1338ec76828cfa4a85b5bd8db1c00c8095c330", None, NO) // Super Challenge Football
Record(@"bac0a0256509f8fd1feea93d74ba4c7d82c1edc6", ParkerBros, NO) // Super Cobra
Record(@"eaca6b474fd552ab4aaf75526618828165a91934", Atari16k, YES) // Super Football
Record(@"b9dee027c8d7dd2a46be111ab0b8363c1becc081", None, NO) // Superman
Record(@"cf84e21ada55730d689cfac7d26e2295317222bc", Atari8k, NO) // Surf's Up
Record(@"e754c8985ca7f5780c23a856656099b710e89919", None, NO) // Surfer's Paradise
Record(@"b7988373b81992d08056560d15d3e32d9d3888bc", None, NO) // Surround
Record(@"6c993b4c70cfed390f1f436fdbaa1f81495be18e", None, NO) // Survival Run
Record(@"e2b3b43cadf2f2c91c1ec615651ff9b1e295d065", None, NO) // sv2k12
Record(@"0d59545b22e15019a33de16999a57dae1f998283", None, NO) // Swordfight
Record(@"05db3d09fa3dac80c70aae2e39f1ad7c31c62f02", Atari8k, NO) // SwordQuest — EarthWorld
Record(@"5c3cf976edbea5ded66634a284787f965616d97e", Atari8k, NO) // SwordQuest — FireWorld
Record(@"569fcb67ca1674b48e2f3a2e7af7077a374402de", Atari8k, NO) // SwordQuest — WaterWorld
Record(@"55e98fe14b07460734781a6aa2f4f1646830c0af", None, NO) // Tac-Scan
Record(@"13a9d86cbde32a1478ef0c7ef412427b13bd6222", None, NO) // Tanks But No Tanks
Record(@"ee8bc1710a67c33e9f95bb05cc3d8f841093fde2", None, NO) // Tapeworm
Record(@"e986e1818e747beb9b33ce4dff1cdc6b55bdb620", Atari8k, NO) // Tapper
Record(@"bae73700ba6532e9e6415b6471d115bdb7805464", None, NO) // Task Force
Record(@"7aaf6be610ba6ea1205bdd5ed60838ccb8280d57", Atari8k, NO) // Tax Avoiders
Record(@"476f0c565f54accecafd72c63b0464f469ed20ea", Atari8k, NO) // Taz
Record(@"7efc0ebe334dde84e25fa020ecde4fddcbea9e8f", Atari8k, NO) // Telepathy
Record(@"bf4d570c1c738a4d6d00237e25c62e9c3225f98f", Atari8k, NO) // Tempest
Record(@"3d30e7ed617168d852923def2000c9c0a8b728c6", None, NO) // Tennis
Record(@"53413577afe7def1d390e3892c45822405513c07", Atari8k, NO) // The A-Team
Record(@"1f834923eac271bf04c18621ac2aada68d426917", None, NO) // The Earth Dies Screaming
Record(@"5a641caa2ab3c7c0cd5deb027acbc58efccf8d6a", None, NO) // The Music Machine
Record(@"717122a4184bc8db41e65ab7c369c40b21c048d9", None, NO) // The Texas Chainsaw Massacre
Record(@"49bebad3e2eb210591be709a6ec7e4f0864265ab", None, NO) // This Planet Sucks
Record(@"9a52fa88bd7455044f00548e9615452131d1c711", None, NO) // Threshold
Record(@"09608cfaa7c6e9638f12a1cff9dd5036c9effa43", Atari16k, NO) // Thrust
Record(@"3cc8bcc0ff5164303433f469aa4da2eb256d1ad0", None, NO) // Thunderground
Record(@"53ee70d4b35ee3df3ffb95fa360bddb4f2f56ab2", ActivisionStack, NO)// Thwocker
Record(@"387358514964d0b6b55f9431576a59b55869f7ab", Atari8k, NO) // Time Pilot
Record(@"979d9b0b0f32b40c0a0568be65a0bc5ef36ca6d0", Atari8k, NO) // Title Match Pro Wrestling
Record(@"fcd339065a012c9fe47efbec62969cbc32f3fbf0", Atari8k, NO) // Tomarc the Barbarian
Record(@"d82ac7237df54cc8688e3074b58433a7dd6b7d11", ParkerBros, NO) // Tooth Protectors
Record(@"b344b3e042447afbb3e40292dc4ca063d5d1110d", None, NO) // Towering Inferno
Record(@"005a6a53f5a856f0bdbca519af1ef236aaa1494d", Atari16k, NO) // Track and Field
Record(@"9a9917d82362c77b4d396f56966219fc248edf47", None, NO) // Treasure Below
Record(@"86c563db11db9afbffbd73c55e9fae9b2f69be4f", None, NO) // Trick Shot
Record(@"0ec58a3a5a27d1b82a5f9aabab02f9a8387b6956", None, NO) // TRON — Deadly Discs
Record(@"fc1a0b58765a7dcbd8e33562e1074ddd9e0ac624", CBSRamPlus, NO) // Tunnel Runner
Record(@"1162fe46977f01b4d25efab813e0d05ec90aeadc", None, NO) // Turmoil
Record(@"a4d6bac854a70d2c55946932f1511cc62db7d4aa", ParkerBros, NO) // Tutankham
Record(@"bd0ca4884c85db2323f5a4be5266aabb99d84542", None, NO) // TVNoise.bin
Record(@"286106fb973530bc3e2af13240f28c4bcb37e642", None, NO) // Universal Chaos
Record(@"6bde671a50330af154ed15e73fdba3fa55f23d87", Atari8k, NO) // Up 'n Down
Record(@"01475d037cb7a2a892be09d67083102fa9159216", Atari8k, NO) // Vanguard
Record(@"dce98883e813d77e03a5de975d4c52bfb34e7f77", None, NO) // Vault Assault
Record(@"17626ae7bfd10bcde14903040baee0923ecf41dd", None, NO) // Venture II
Record(@"0305dfc99bf192e53452a1e0408ccc148940afcd", None, NO) // Venture
Record(@"babae88a832b76d8c5af6ea63b8f10a0da5bb992", None, NO) // Video Checkers
Record(@"043ef523e4fcb9fc2fc2fda21f15671bf8620fc3", None, NO) // Video Chess
Record(@"1554b146d076b64776bf49136cea01f60eeba4c1", None, NO) // Video Jogger
Record(@"3b18db73933747851eba9a0ffa3c12b9f602a95c", CommaVid, NO) // Video Life
Record(@"1ffe89d79d55adabc0916b95cc37e18619ef7830", None, NO) // Video Olympics
Record(@"2c16c1a6374c8e22275d152d93dd31ffba26271f", None, NO) // Video Pinball
Record(@"dec2a3e9b366dce2b63dc1c13662d3f22420a22e", None, NO) // Video Reflex
Record(@"a345a5696f1d63a879e7bb7e3a74c825e97ef7c3", None, NO) // Video Simon
Record(@"2ebd0f43ee76833f75759ac1bbb45a8e0c3b86e9", None, NO) // Vulture Attack
Record(@"73072295721453065d62d9136343b81310a4d225", None, NO) // Wabbit
Record(@"482bd349222b8c702e125c27fd516e73af13967b", None, NO) // Wall Ball
Record(@"6de42bbc4766b26301e291ba00f7f7a9ac748639", None, NO) // Wall Break
Record(@"2d7563d337cbc0cdf4fc14f69853ab6757697788", None, NO) // Warlords
Record(@"232a370519a7fcce121e15f850d0d3671909f8b8", None, NO) // Warplock
Record(@"e325c5c0501ff527f06e6518526f9eefed309e89", None, NO) // Waring Worms
Record(@"472215fcb46cec905576d539efc8043488efc4ed", None, NO) // Westward Ho
Record(@"16df34446af2e6035ca871a00e1e8a008cfb8df4", Atari8k, NO) // Wing War
Record(@"6850d329e8ab403bdae38850665a2eff91278e92", Atari16k, NO) // Winter Games
Record(@"326e5e63a54ec6a0231fd38e62e352004d4719fe", None, NO) // Wizard of Wor
Record(@"e4b0f68abff3273cdd2b973639d607ae4a700adc", None, NO) // Wizard
Record(@"806c5a8a7b042a1a3ada1b6f29451a3446f93da3", None, NO) // Word Zapper
Record(@"36a1e73eb5aa5c3cd0b01af5117d19b8c36071e4", None, NO) // Worm War 1
Record(@"1c5d151e86c0a0bbdf3b33ef153888c6be78c36b", None, NO) // X-Man
Record(@"160b6e36437ad6acbc2686fbde1002e2fa88c5fb", Atari16k, NO) // Xenophobe
Record(@"73133b81196e5cbc1cec99eefc1223ddb8f4ca83", Atari8k, NO) // Xevious
Record(@"6a1e0142c6886a6589a58e029e5aec6b72f7d27f", None, NO) // Yahtzee
Record(@"e2cd8996c1cf929e29130690024d1ec23d3b0bde", None, NO) // Yars' Revenge
Record(@"58c2f6abc5599cd35c0e722f24bcc128ac8f9a30", Atari8k, NO) // Zaxxon
};
#undef Record
@interface AtariStaticAnalyserTests : XCTestCase
@end
@implementation AtariStaticAnalyserTests
- (void)testROMsOfSize:(NSInteger)size
{
NSString *basePath = [[[NSBundle bundleForClass:[self class]] resourcePath] stringByAppendingPathComponent:@"Atari ROMs"];
for(NSString *testFile in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:basePath error:nil])
{
NSString *fullPath = [basePath stringByAppendingPathComponent:testFile];
// get a SHA1 for the file
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
if(size > 0 && [fileData length] != (NSUInteger)size) continue;
uint8_t sha1Bytes[CC_SHA1_DIGEST_LENGTH];
CC_SHA1([fileData bytes], (CC_LONG)[fileData length], sha1Bytes);
NSMutableString *sha1 = [[NSMutableString alloc] init];
for(int c = 0; c < CC_SHA1_DIGEST_LENGTH; c++) [sha1 appendFormat:@"%02x", sha1Bytes[c]];
// get an analysis of the file
std::list<StaticAnalyser::Target> targets = StaticAnalyser::GetTargets([fullPath UTF8String]);
// grab the ROM record
AtariROMRecord *romRecord = romRecordsBySHA1[sha1];
if(!romRecord) continue;
// assert equality
XCTAssert(targets.front().atari.paging_model == romRecord.pagingModel, @"%@; should be %d, is %d", testFile, romRecord.pagingModel, targets.front().atari.paging_model);
XCTAssert(targets.front().atari.uses_superchip == romRecord.usesSuperchip, @"%@; should be %@", testFile, romRecord.usesSuperchip ? @"true" : @"false");
}
}
- (void)testAtariROMs { [self testROMsOfSize:-1]; } // This will duplicate all tests below, but also catch anything that isn't 2, 4, 8, 12, 16 or 32kb in size.
- (void)test2kROMs { [self testROMsOfSize:2048]; }
- (void)test4kROMs { [self testROMsOfSize:4096]; }
- (void)test8kROMs { [self testROMsOfSize:8192]; }
- (void)test12kROMs { [self testROMsOfSize:12288]; }
- (void)test16kROMs { [self testROMsOfSize:16384]; }
- (void)test32kROMs { [self testROMsOfSize:32768]; }
@end

View File

@@ -0,0 +1,72 @@
//
// CRCTests.m
// Clock Signal
//
// Created by Thomas Harte on 27/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "CRC.hpp"
@interface CRCTests : XCTestCase
@end
@implementation CRCTests
- (NumberTheory::CRC16)mfmCRCGenerator
{
return NumberTheory::CRC16(0x1021, 0xffff);
}
- (uint16_t)crcOfData:(uint8_t *)data length:(size_t)length generator:(NumberTheory::CRC16 &)generator
{
generator.reset();
for(size_t c = 0; c < length; c++)
generator.add(data[c]);
return generator.get_value();
}
- (void)testIDMark
{
uint8_t IDMark[] =
{
0xa1, 0xa1, 0xa1, 0xfe, 0x00, 0x00, 0x01, 0x01
};
uint16_t crc = 0xfa0c;
NumberTheory::CRC16 crcGenerator = self.mfmCRCGenerator;
uint16_t computedCRC = [self crcOfData:IDMark length:sizeof(IDMark) generator:crcGenerator];
XCTAssert(computedCRC == crc, @"Calculated CRC should have been %04x, was %04x", crc, computedCRC);
}
- (void)testData
{
uint8_t sectorData[] =
{
0xa1, 0xa1, 0xa1, 0xfb, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x53, 0x45, 0x44, 0x4f,
0x52, 0x49, 0x43, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x53, 0x45, 0x44, 0x4f, 0x52, 0x49, 0x43, 0x20, 0x56, 0x31, 0x2e, 0x30,
0x30, 0x36, 0x20, 0x30, 0x31, 0x2f, 0x30, 0x31, 0x2f, 0x38, 0x36, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20
};
uint16_t crc = 0x4de7;
NumberTheory::CRC16 crcGenerator = self.mfmCRCGenerator;
uint16_t computedCRC = [self crcOfData:sectorData length:sizeof(sectorData) generator:crcGenerator];
XCTAssert(computedCRC == crc, @"Calculated CRC should have been %04x, was %04x", crc, computedCRC);
}
@end

View File

@@ -0,0 +1,220 @@
//
// PCMPatchedTrackTests.m
// Clock Signal
//
// Created by Thomas Harte on 17/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "PCMTrack.hpp"
#include "PCMPatchedTrack.hpp"
@interface PCMPatchedTrackTests : XCTestCase
@end
@implementation PCMPatchedTrackTests
#pragma mark - Prebuilt tracks
- (std::shared_ptr<Storage::Disk::Track>)togglingTrack
{
Storage::Disk::PCMSegment segment;
segment.data = { 0xff, 0xff, 0xff, 0xff };
segment.number_of_bits = 32;
return std::shared_ptr<Storage::Disk::Track>(new Storage::Disk::PCMTrack(segment));
}
- (std::shared_ptr<Storage::Disk::Track>)patchableTogglingTrack
{
std::shared_ptr<Storage::Disk::Track> track = self.togglingTrack;
return std::shared_ptr<Storage::Disk::Track>(new Storage::Disk::PCMPatchedTrack(track));
}
- (std::shared_ptr<Storage::Disk::Track>)fourSegmentPatchedTrack
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.patchableTogglingTrack;
Storage::Disk::PCMPatchedTrack *patchable = static_cast<Storage::Disk::PCMPatchedTrack *>(patchableTrack.get());
for(int c = 0; c < 4; c++)
{
Storage::Disk::PCMSegment segment;
segment.data = {0xff};
segment.number_of_bits = 8;
segment.length_of_a_bit.length = 1;
segment.length_of_a_bit.clock_rate = 32;
patchable->add_segment(Storage::Time(c, 4), segment);
}
return patchableTrack;
}
#pragma mark -
- (std::vector<Storage::Disk::Track::Event>)eventsFromTrack:(std::shared_ptr<Storage::Disk::Track>)track
{
std::vector<Storage::Disk::Track::Event> events;
while(1)
{
events.push_back(track->get_next_event());
if(events.back().type == Storage::Disk::Track::Event::IndexHole) break;
}
return events;
}
- (Storage::Time)timeForEvents:(const std::vector<Storage::Disk::Track::Event> &)events
{
Storage::Time result(0);
for(auto event : events)
{
result += event.length;
}
return result;
}
- (void)patchTrack:(std::shared_ptr<Storage::Disk::Track>)track withSegment:(Storage::Disk::PCMSegment)segment atTime:(Storage::Time)time
{
Storage::Disk::PCMPatchedTrack *patchable = static_cast<Storage::Disk::PCMPatchedTrack *>(track.get());
patchable->add_segment(time, segment);
}
#pragma mark - Repeating Asserts
- (void)assertOneThirtyTwosForTrack:(std::shared_ptr<Storage::Disk::Track>)track
{
// Confirm that there are now flux transitions (just the first five will do)
// located 1/32nd of a rotation apart.
for(int c = 0; c < 5; c++)
{
Storage::Disk::Track::Event event = track->get_next_event();
XCTAssert(
event.length == (c ? Storage::Time(1, 32) : Storage::Time(1, 64)),
@"flux transitions should be 1/32nd of a track apart");
}
}
- (void)assertEvents:(const std::vector<Storage::Disk::Track::Event> &)events hasEntries:(size_t)numberOfEntries withEntry:(size_t)entry ofLength:(Storage::Time)time
{
XCTAssert(events.size() == numberOfEntries, @"Should be %zu total events", numberOfEntries);
XCTAssert(events[entry].length == time, @"Event %zu should have been %d/%d long, was %d/%d", entry, time.length, time.clock_rate, events[entry].length.length, events[entry].length.clock_rate);
Storage::Time eventTime = [self timeForEvents:events];
XCTAssert(eventTime == Storage::Time(1), @"Total track length should be 1; was %d/%d", eventTime.length, eventTime.clock_rate);
}
#pragma mark - Unpatched tracks
- (void)testUnpatchedRawTrack
{
[self assertOneThirtyTwosForTrack:self.togglingTrack];
}
- (void)testUnpatchedTrack
{
[self assertOneThirtyTwosForTrack:self.patchableTogglingTrack];
}
#pragma mark - Insertions affecting one existing segment
- (void)testSingleSplice
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.patchableTogglingTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(1, 32), 1, {0xff}) atTime:Storage::Time(3, 128)];
std::vector<Storage::Disk::Track::Event> events = [self eventsFromTrack:patchableTrack];
Storage::Time total_length = [self timeForEvents:events];
XCTAssert(events.size() == 33, @"Should still be 33 total events");
XCTAssert(events[0].length == Storage::Time(1, 64), @"First event should be after 1/64 as usual");
XCTAssert(events[1].length == Storage::Time(3, 128), @"Second event should be 3/128 later"); // ... as it was inserted at 3/128 and runs at the same rate as the main data, so first inserted event is at 3/128+1/64-1/64
XCTAssert(events[2].length == Storage::Time(5, 128), @"Should still be 33 total events"); // 1/64 = 2/128 to exit the patch, plus 3/128 to get to the next event, having spliced in 1/128 ahead of the normal clock
XCTAssert(total_length == Storage::Time(1), @"Total track length should still be 1");
}
- (void)testLeftReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.patchableTogglingTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(1, 16), 8, {0x00}) atTime:Storage::Time(0)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:17 withEntry:0 ofLength:Storage::Time(33, 64)];
}
- (void)testRightReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.patchableTogglingTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(1, 16), 8, {0x00}) atTime:Storage::Time(1, 2)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:17 withEntry:16 ofLength:Storage::Time(33, 64)];
}
#pragma mark - Insertions affecting three existing segments
- (void)testMultiSegmentTrack
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:33 withEntry:4 ofLength:Storage::Time(1, 32)];
}
- (void)testMultiTrimBothSideReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(1, 16), 8, {0x00}) atTime:Storage::Time(1, 8)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:17 withEntry:4 ofLength:Storage::Time(17, 32)];
}
- (void)testMultiTrimRightReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(3, 8), 1, {0x00}) atTime:Storage::Time(1, 8)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:21 withEntry:4 ofLength:Storage::Time(13, 32)];
}
- (void)testMultiTrimLeftReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(3, 8), 1, {0x00}) atTime:Storage::Time(1, 4)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:21 withEntry:8 ofLength:Storage::Time(13, 32)];
}
#pragma mark - Insertions affecting two existing segments
- (void)testTwoSegmentOverlap
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(1, 32), 8, {0x00}) atTime:Storage::Time(1, 8)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:25 withEntry:4 ofLength:Storage::Time(9, 32)];
}
- (void)testTwoSegmentRightReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(3, 8), 1, {0x00}) atTime:Storage::Time(1, 8)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:21 withEntry:4 ofLength:Storage::Time(13, 32)];
}
- (void)testTwoSegmentLeftReplace
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.fourSegmentPatchedTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(3, 8), 1, {0x00}) atTime:Storage::Time(0)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:21 withEntry:0 ofLength:Storage::Time(25, 64)];
}
#pragma mark - Wrapping segment
- (void)testWrappingSegment
{
std::shared_ptr<Storage::Disk::Track> patchableTrack = self.patchableTogglingTrack;
[self patchTrack:patchableTrack withSegment:Storage::Disk::PCMSegment(Storage::Time(5, 2), 1, {0x00}) atTime:Storage::Time(0)];
[self assertEvents:[self eventsFromTrack:patchableTrack] hasEntries:1 withEntry:0 ofLength:Storage::Time(1, 1)];
}
@end

View File

@@ -0,0 +1,113 @@
//
// PCMSegmentEventSourceTests.m
// Clock Signal
//
// Created by Thomas Harte on 17/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "PCMSegment.hpp"
@interface PCMSegmentEventSourceTests : XCTestCase
@end
@implementation PCMSegmentEventSourceTests
- (Storage::Disk::PCMSegmentEventSource)segmentSource
{
Storage::Disk::PCMSegment alternatingFFs;
alternatingFFs.data = {0xff, 0x00, 0xff, 0x00};
alternatingFFs.length_of_a_bit.length = 1;
alternatingFFs.length_of_a_bit.clock_rate = 10;
alternatingFFs.number_of_bits = 32;
return Storage::Disk::PCMSegmentEventSource(alternatingFFs);
}
- (void)testCentring
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
[self assertFirstTwoEventLengthsForSource:segmentSource];
}
- (void)assertFirstTwoEventLengthsForSource:(Storage::Disk::PCMSegmentEventSource &)segmentSource
{
Storage::Disk::Track::Event first_event = segmentSource.get_next_event();
Storage::Disk::Track::Event second_event = segmentSource.get_next_event();
first_event.length.simplify();
second_event.length.simplify();
XCTAssertTrue(first_event.length.length == 1 && first_event.length.clock_rate == 20, @"First event should occur half a bit's length in");
XCTAssertTrue(second_event.length.length == 1 && second_event.length.clock_rate == 10, @"Second event should occur a whole bit's length after the first");
}
- (void)testLongerGap
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
// skip first eight flux transitions
for(int c = 0; c < 8; c++) segmentSource.get_next_event();
Storage::Disk::Track::Event next_event = segmentSource.get_next_event();
next_event.length.simplify();
XCTAssertTrue(next_event.length.length == 9 && next_event.length.clock_rate == 10, @"Zero byte should give a nine bit length event gap");
}
- (void)testTermination
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
Storage::Time total_time;
for(int c = 0; c < 16; c++) total_time += segmentSource.get_next_event().length;
Storage::Disk::Track::Event final_event = segmentSource.get_next_event();
total_time += final_event.length;
total_time.simplify();
XCTAssertTrue(final_event.type == Storage::Disk::Track::Event::IndexHole, @"Segment should end with an index hole");
XCTAssertTrue(total_time.length == 16 && total_time.clock_rate == 5, @"Should have taken 32 bit lengths to finish the segment");
}
- (void)testReset
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
for(int c = 0; c < 8; c++) segmentSource.get_next_event();
segmentSource.reset();
[self assertFirstTwoEventLengthsForSource:segmentSource];
}
- (void)testSeekToSecondBit
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
Storage::Time target_time(1, 10);
Storage::Time found_time = segmentSource.seek_to(target_time);
found_time.simplify();
XCTAssertTrue(found_time.length == 1 && found_time.clock_rate == 20, @"A request to seek to 1/10th should have seeked to 1/20th");
Storage::Disk::Track::Event next_event = segmentSource.get_next_event();
next_event.length.simplify();
XCTAssertTrue(next_event.length.length == 1 && next_event.length.clock_rate == 10, @"Next event should be 1/10th later");
}
- (void)testSeekBeyondFinalBit
{
Storage::Disk::PCMSegmentEventSource segmentSource = self.segmentSource;
Storage::Time target_time(24, 10);
Storage::Time found_time = segmentSource.seek_to(target_time);
found_time.simplify();
XCTAssertTrue(found_time.length == 47 && found_time.clock_rate == 20, @"A request to seek to 24/10ths should have seeked to 47/20ths");
Storage::Disk::Track::Event next_event = segmentSource.get_next_event();
next_event.length.simplify();
XCTAssertTrue(next_event.length.length == 17 && next_event.length.clock_rate == 20, @"Next event should be 17/20ths later");
XCTAssertTrue(next_event.type == Storage::Disk::Track::Event::IndexHole, @"End should have been reached");
}
@end

View File

@@ -0,0 +1,54 @@
//
// PCMTrackTests.m
// Clock Signal
//
// Created by Thomas Harte on 18/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "PCMTrack.hpp"
@interface PCMTrackTests : XCTestCase
@end
@implementation PCMTrackTests
- (Storage::Disk::PCMTrack)multiSpeedTrack
{
Storage::Disk::PCMSegment quickSegment, slowSegment;
quickSegment.data = {0xff};
quickSegment.number_of_bits = 8;
quickSegment.length_of_a_bit.length = 1;
quickSegment.length_of_a_bit.clock_rate = 100;
slowSegment.data = {0xff};
slowSegment.number_of_bits = 8;
slowSegment.length_of_a_bit.length = 1;
slowSegment.length_of_a_bit.clock_rate = 3;
return Storage::Disk::PCMTrack({quickSegment, slowSegment});
}
- (void)testMultispeedTrack
{
Storage::Disk::PCMTrack track = self.multiSpeedTrack;
std::vector<Storage::Disk::Track::Event> events;
Storage::Time total_length;
do {
events.push_back(track.get_next_event());
total_length += events.back().length;
} while(events.back().type != Storage::Disk::Track::Event::IndexHole);
XCTAssert(events.size() == 17, "Should have received 17 events; got %lu", events.size());
total_length.simplify();
XCTAssert(total_length.length == 1 && total_length.clock_rate == 1, "Events should have summed to a total time of 1; instead got %u/%u", total_length.length, total_length.clock_rate);
Storage::Time transition_length = events[0].length + events.back().length;
XCTAssert(events[8].length == transition_length, "Time taken in transition between speed zones should be half of a bit length in the first part plus half of a bit length in the second");
}
@end

View File

@@ -0,0 +1,119 @@
//
// TIATests.m
// Clock Signal
//
// Created by Thomas Harte on 12/02/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "TIA.hpp"
static uint8_t *line;
static void receive_line(uint8_t *next_line)
{
line = next_line;
}
@interface TIATests : XCTestCase
@end
@implementation TIATests {
std::unique_ptr<Atari2600::TIA> _tia;
}
- (void)setUp
{
[super setUp];
std::function<void(uint8_t *)> function = receive_line;
_tia.reset(new Atari2600::TIA(function));
line = nullptr;
_tia->set_playfield(0, 0x00);
_tia->set_playfield(1, 0x00);
_tia->set_playfield(2, 0x00);
_tia->set_player_graphic(0, 0x00);
_tia->set_player_graphic(1, 0x00);
_tia->set_ball_enable(false);
_tia->set_missile_enable(0, false);
_tia->set_missile_enable(1, false);
}
- (void)testReflectedPlayfield
{
// set reflected, bit pattern 1000
_tia->set_playfield_control_and_ball_size(1);
_tia->set_playfield(0, 0x10);
_tia->set_playfield(1, 0xf0);
_tia->set_playfield(2, 0x0e);
_tia->run_for_cycles(228);
XCTAssert(line != nullptr, @"228 cycles should have ended the line");
uint8_t expected_line[] = {
1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1
};
XCTAssert(!memcmp(expected_line, line, sizeof(expected_line)));
}
- (void)testRepeatedPlayfield
{
// set reflected, bit pattern 1000
_tia->set_playfield_control_and_ball_size(0);
_tia->set_playfield(0, 0x10);
_tia->set_playfield(1, 0xf0);
_tia->set_playfield(2, 0x0e);
_tia->run_for_cycles(228);
XCTAssert(line != nullptr, @"228 cycles should have ended the line");
uint8_t expected_line[] = {
1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
XCTAssert(!memcmp(expected_line, line, sizeof(expected_line)));
}
- (void)testSinglePlayer
{
// set a player graphic, reset position so that it'll appear from column 1
_tia->set_player_graphic(0, 0xff);
_tia->set_player_position(0);
_tia->run_for_cycles(228);
uint8_t first_expected_line[] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
XCTAssert(line != nullptr, @"228 cycles should have ended the line");
XCTAssert(!memcmp(first_expected_line, line, sizeof(first_expected_line)));
line = nullptr;
_tia->run_for_cycles(228);
uint8_t second_expected_line[] = {
0, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
XCTAssert(line != nullptr, @"228 cycles should have ended the line");
XCTAssert(!memcmp(second_expected_line, line, sizeof(second_expected_line)));
}
@end

View File

@@ -0,0 +1,44 @@
//
// TimeTests.m
// Clock Signal
//
// Created by Thomas Harte on 24/12/2016.
// Copyright © 2016 Thomas Harte. All rights reserved.
//
#import <XCTest/XCTest.h>
#include "Storage.hpp"
@interface TimeTests : XCTestCase
@end
@implementation TimeTests
- (void)testHalf
{
Storage::Time half(0.5f);
XCTAssert(half == Storage::Time(1, 2), @"0.5 should be converted to 1/2");
}
- (void)testTwenty
{
Storage::Time twenty(20.0f);
XCTAssert(twenty == Storage::Time(20, 1), @"20.0 should be converted to 20/1");
}
- (void)testTooSmallFloat
{
float original = 1.0f / powf(2.0f, 25.0f);
Storage::Time time(original);
XCTAssert(time == Storage::Time(0), @"Numbers too small to be represented should be 0");
}
- (void)testTooBigFloat
{
float original = powf(2.0f, 48.0f);
Storage::Time time(original);
XCTAssert(time == Storage::Time::max(), @"Numbers too big to be represented should saturate");
}
@end

View File

@@ -14,8 +14,7 @@
using namespace Outputs::CRT;
void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator)
{
void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, bool should_alternate) {
openGL_output_builder_.set_colour_format(colour_space, colour_cycle_numerator, colour_cycle_denominator);
const unsigned int syncCapacityLineChargeThreshold = 2;
@@ -29,10 +28,16 @@ void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_di
// for horizontal retrace and 500 to 750 µs for vertical retrace in NTSC and PAL TV."
time_multiplier_ = IntermediateBufferWidth / cycles_per_line;
phase_denominator_ = cycles_per_line * colour_cycle_denominator * time_multiplier_;
phase_numerator_ = 0;
colour_cycle_numerator_ = colour_cycle_numerator;
phase_alternates_ = should_alternate;
is_alernate_line_ &= phase_alternates_;
cycles_per_line_ = cycles_per_line;
unsigned int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
// generate timing values implied by the given arbuments
sync_capacitor_charge_threshold_ = (int)(syncCapacityLineChargeThreshold * multiplied_cycles_per_line);
// generate timing values implied by the given arguments
sync_capacitor_charge_threshold_ = ((int)(syncCapacityLineChargeThreshold * cycles_per_line) * 3) / 4;
// create the two flywheels
horizontal_flywheel_.reset(new Flywheel(multiplied_cycles_per_line, (millisecondsHorizontalRetraceTime * multiplied_cycles_per_line) >> 6, multiplied_cycles_per_line >> 6));
@@ -45,20 +50,26 @@ void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_di
openGL_output_builder_.set_timing(cycles_per_line, multiplied_cycles_per_line, height_of_display, horizontal_flywheel_->get_scan_period(), vertical_flywheel_->get_scan_period(), vertical_flywheel_output_divider_);
}
void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType displayType)
{
switch(displayType)
{
void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType displayType) {
switch(displayType) {
case DisplayType::PAL50:
set_new_timing(cycles_per_line, 312, ColourSpace::YUV, 709379, 2500); // i.e. 283.7516
set_new_timing(cycles_per_line, 312, ColourSpace::YUV, 709379, 2500, true); // i.e. 283.7516
break;
case DisplayType::NTSC60:
set_new_timing(cycles_per_line, 262, ColourSpace::YIQ, 545, 2);
set_new_timing(cycles_per_line, 262, ColourSpace::YIQ, 455, 2, false); // i.e. 227.5
break;
}
}
void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_first_sample) {
if(type == DiscreteFourSamplesPerCycle) {
colour_burst_phase_adjustment_ = (uint8_t)(offset_of_first_sample * 256.0f) & 63;
} else {
colour_burst_phase_adjustment_ = 0xff;
}
}
CRT::CRT(unsigned int common_output_divisor, unsigned int buffer_depth) :
sync_capacitor_charge_level_(0),
is_receiving_sync_(false),
@@ -67,29 +78,26 @@ CRT::CRT(unsigned int common_output_divisor, unsigned int buffer_depth) :
is_writing_composite_run_(false),
delegate_(nullptr),
frames_since_last_delegate_call_(0),
openGL_output_builder_(buffer_depth) {}
openGL_output_builder_(buffer_depth),
is_alernate_line_(false) {}
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int buffer_depth) :
CRT(common_output_divisor, buffer_depth)
{
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator);
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, bool should_alternate, unsigned int buffer_depth) :
CRT(common_output_divisor, buffer_depth) {
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator, should_alternate);
}
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, DisplayType displayType, unsigned int buffer_depth) :
CRT(common_output_divisor, buffer_depth)
{
CRT(common_output_divisor, buffer_depth) {
set_new_display_type(cycles_per_line, displayType);
}
#pragma mark - Sync loop
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
return vertical_flywheel_->get_next_event_in_period(vsync_is_requested, cycles_to_run_for, cycles_advanced);
}
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
return horizontal_flywheel_->get_next_event_in_period(hsync_is_requested, cycles_to_run_for, cycles_advanced);
}
@@ -106,10 +114,8 @@ Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested,
#define source_output_position_x2() (*(uint16_t *)&next_run[SourceVertexOffsetOfEnds + 2])
#define source_phase() next_run[SourceVertexOffsetOfPhaseTimeAndAmplitude + 0]
#define source_amplitude() next_run[SourceVertexOffsetOfPhaseTimeAndAmplitude + 2]
#define source_phase_time() next_run[SourceVertexOffsetOfPhaseTimeAndAmplitude + 1]
void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type)
{
void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type) {
std::unique_lock<std::mutex> output_lock = openGL_output_builder_.get_output_lock();
number_of_cycles *= time_multiplier_;
@@ -124,42 +130,34 @@ void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bo
// get the next sync event and its timing; hsync request is instantaneous (being edge triggered) so
// set it to false for the next run through this loop (if any)
unsigned int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
phase_numerator_ += next_run_length * colour_cycle_numerator_;
phase_numerator_ %= phase_denominator_;
hsync_requested = false;
vsync_requested = false;
bool is_output_segment = ((is_output_run && next_run_length) && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
uint8_t *next_run = nullptr;
if(is_output_segment && !openGL_output_builder_.composite_output_buffer_is_full())
{
if(is_output_segment && !openGL_output_builder_.composite_output_buffer_is_full()) {
next_run = openGL_output_builder_.array_builder.get_input_storage(SourceVertexSize);
}
if(next_run)
{
if(next_run) {
// output_y and texture locations will be written later; we won't necessarily know what it is outside of the locked region
source_output_position_x1() = (uint16_t)horizontal_flywheel_->get_current_output_position();
source_phase() = colour_burst_phase_;
source_amplitude() = colour_burst_amplitude_;
source_phase_time() = (uint8_t)colour_burst_time_; // assumption: burst was within the first 1/16 of the line
}
// decrement the number of cycles left to run for and increment the
// horizontal counter appropriately
number_of_cycles -= next_run_length;
// either charge or deplete the vertical retrace capacitor (making sure it stops at 0)
if(vsync_charging)
sync_capacitor_charge_level_ += next_run_length;
else
sync_capacitor_charge_level_ = std::max(sync_capacitor_charge_level_ - (int)next_run_length, 0);
// react to the incoming event...
horizontal_flywheel_->apply_event(next_run_length, (next_run_length == time_until_horizontal_sync_event) ? next_horizontal_sync_event : Flywheel::SyncEvent::None);
vertical_flywheel_->apply_event(next_run_length, (next_run_length == time_until_vertical_sync_event) ? next_vertical_sync_event : Flywheel::SyncEvent::None);
if(next_run)
{
if(next_run) {
source_output_position_x2() = (uint16_t)horizontal_flywheel_->get_current_output_position();
}
@@ -171,68 +169,60 @@ void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bo
(honoured_event == Flywheel::SyncEvent::StartRetrace && is_writing_composite_run_) ||
(honoured_event == Flywheel::SyncEvent::EndRetrace && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
if(needs_endpoint)
{
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) is_alernate_line_ ^= phase_alternates_;
if(needs_endpoint) {
if(
!openGL_output_builder_.array_builder.is_full() &&
!openGL_output_builder_.composite_output_buffer_is_full())
{
if(!is_writing_composite_run_)
{
!openGL_output_builder_.composite_output_buffer_is_full()) {
if(!is_writing_composite_run_) {
output_run_.x1 = (uint16_t)horizontal_flywheel_->get_current_output_position();
output_run_.y = (uint16_t)(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_);
}
else
{
} else {
// Get and write all those previously unwritten output ys
const uint16_t output_y = openGL_output_builder_.get_composite_output_y();
// Construct the output run
uint8_t *next_run = openGL_output_builder_.array_builder.get_output_storage(OutputVertexSize);
if(next_run)
{
if(next_run) {
output_x1() = output_run_.x1;
output_position_y() = output_run_.y;
output_tex_y() = output_y;
output_x2() = (uint16_t)horizontal_flywheel_->get_current_output_position();
}
openGL_output_builder_.array_builder.flush(
[output_y, this] (uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size)
{
[output_y, this] (uint8_t *input_buffer, size_t input_size, uint8_t *output_buffer, size_t output_size) {
openGL_output_builder_.texture_builder.flush(
[output_y, input_buffer] (const std::vector<TextureBuilder::WriteArea> &write_areas, size_t number_of_write_areas)
{
for(size_t run = 0; run < number_of_write_areas; run++)
{
[output_y, input_buffer] (const std::vector<TextureBuilder::WriteArea> &write_areas, size_t number_of_write_areas) {
for(size_t run = 0; run < number_of_write_areas; run++) {
*(uint16_t *)&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfInputStart + 0] = write_areas[run].x;
*(uint16_t *)&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfInputStart + 2] = write_areas[run].y;
*(uint16_t *)&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfEnds + 0] = write_areas[run].x + write_areas[run].length;
}
});
for(size_t position = 0; position < input_size; position += SourceVertexSize)
{
for(size_t position = 0; position < input_size; position += SourceVertexSize) {
(*(uint16_t *)&input_buffer[position + SourceVertexOffsetOfOutputStart + 2]) = output_y;
}
});
colour_burst_amplitude_ = 0;
}
is_writing_composite_run_ ^= true;
}
}
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace)
{
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) {
openGL_output_builder_.increment_composite_output_y();
}
// if this is vertical retrace then adcance a field
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event == Flywheel::SyncEvent::EndRetrace)
{
if(delegate_)
{
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event == Flywheel::SyncEvent::EndRetrace) {
if(delegate_) {
frames_since_last_delegate_call_++;
if(frames_since_last_delegate_call_ == 20)
{
if(frames_since_last_delegate_call_ == 20) {
output_lock.unlock();
delegate_->crt_did_end_batch_of_frames(this, frames_since_last_delegate_call_, vertical_flywheel_->get_and_reset_number_of_surprises());
output_lock.lock();
frames_since_last_delegate_call_ = 0;
}
}
@@ -257,42 +247,54 @@ void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bo
#pragma mark - stream feeding methods
void CRT::output_scan(const Scan *const scan)
{
void CRT::output_scan(const Scan *const scan) {
const bool this_is_sync = (scan->type == Scan::Type::Sync);
const bool is_trailing_edge = (is_receiving_sync_ && !this_is_sync);
const bool is_leading_edge = (!is_receiving_sync_ && this_is_sync);
is_receiving_sync_ = this_is_sync;
// Accumulate: (i) a total of the amount of time in sync; and (ii) the amount of time since sync.
if(this_is_sync) { cycles_of_sync_ += scan->number_of_cycles; cycles_since_sync_ = 0; }
else cycles_since_sync_ += scan->number_of_cycles;
bool vsync_requested = false;
// If it has been at least half a line since sync ended, then it is safe to decide whether what ended
// was vertical sync.
if(cycles_since_sync_ > (cycles_per_line_ >> 1)) {
// If it was vertical sync, set that flag. If it wasn't, clear the summed amount of sync to avoid
// a mistaken vertical sync due to an aggregate of horizontals.
vsync_requested = (cycles_of_sync_ > sync_capacitor_charge_threshold_);
if(vsync_requested || cycles_of_sync_ < (cycles_per_line_ >> 2))
cycles_of_sync_ = 0;
}
// This introduces a blackout period close to the expected vertical sync point in which horizontal syncs are not
// recognised, effectively causing the horizontal flywheel to freewheel during that period. This attempts to seek
// the problem that vertical sync otherwise often starts halfway through a scanline, which confuses the horizontal
// flywheel. I'm currently unclear whether this is an accurate solution to this problem.
const bool hsync_requested = is_leading_edge && !vertical_flywheel_->is_near_expected_sync();
const bool vsync_requested = is_trailing_edge && (sync_capacitor_charge_level_ >= sync_capacitor_charge_threshold_);
// simplified colour burst logic: if it's within the back porch we'll take it
if(scan->type == Scan::Type::ColourBurst)
{
if(horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6)
{
colour_burst_time_ = (uint16_t)horizontal_flywheel_->get_current_time();
colour_burst_phase_ = scan->phase;
if(scan->type == Scan::Type::ColourBurst) {
if(!colour_burst_amplitude_ && horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6) {
unsigned int position_phase = (horizontal_flywheel_->get_current_time() * colour_cycle_numerator_ * 256) / phase_denominator_;
colour_burst_phase_ = (position_phase + scan->phase) & 255;
colour_burst_amplitude_ = scan->amplitude;
if(colour_burst_phase_adjustment_ != 0xff)
colour_burst_phase_ = (colour_burst_phase_ & ~63) + colour_burst_phase_adjustment_;
}
}
// TODO: inspect raw data for potential colour burst if required
sync_period_ = is_receiving_sync_ ? (sync_period_ + scan->number_of_cycles) : 0;
advance_cycles(scan->number_of_cycles, hsync_requested, vsync_requested, this_is_sync, scan->type);
advance_cycles(scan->number_of_cycles, hsync_requested, vsync_requested, scan->type);
}
/*
These all merely channel into advance_cycles, supplying appropriate arguments
*/
void CRT::output_sync(unsigned int number_of_cycles)
{
void CRT::output_sync(unsigned int number_of_cycles) {
Scan scan{
.type = Scan::Type::Sync,
.number_of_cycles = number_of_cycles
@@ -300,8 +302,7 @@ void CRT::output_sync(unsigned int number_of_cycles)
output_scan(&scan);
}
void CRT::output_blank(unsigned int number_of_cycles)
{
void CRT::output_blank(unsigned int number_of_cycles) {
Scan scan {
.type = Scan::Type::Blank,
.number_of_cycles = number_of_cycles
@@ -309,8 +310,7 @@ void CRT::output_blank(unsigned int number_of_cycles)
output_scan(&scan);
}
void CRT::output_level(unsigned int number_of_cycles)
{
void CRT::output_level(unsigned int number_of_cycles) {
Scan scan {
.type = Scan::Type::Level,
.number_of_cycles = number_of_cycles,
@@ -318,8 +318,7 @@ void CRT::output_level(unsigned int number_of_cycles)
output_scan(&scan);
}
void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude)
{
void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude) {
Scan scan {
.type = Scan::Type::ColourBurst,
.number_of_cycles = number_of_cycles,
@@ -329,8 +328,17 @@ void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint
output_scan(&scan);
}
void CRT::output_data(unsigned int number_of_cycles, unsigned int source_divider)
{
void CRT::output_default_colour_burst(unsigned int number_of_cycles) {
Scan scan {
.type = Scan::Type::ColourBurst,
.number_of_cycles = number_of_cycles,
.phase = (uint8_t)((phase_numerator_ * 256) / phase_denominator_ + (is_alernate_line_ ? 128 : 0)),
.amplitude = 32
};
output_scan(&scan);
}
void CRT::output_data(unsigned int number_of_cycles, unsigned int source_divider) {
openGL_output_builder_.texture_builder.reduce_previous_allocation_to(number_of_cycles / source_divider);
Scan scan {
.type = Scan::Type::Data,
@@ -339,8 +347,7 @@ void CRT::output_data(unsigned int number_of_cycles, unsigned int source_divider
output_scan(&scan);
}
Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio)
{
Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio) {
first_cycle_after_sync *= time_multiplier_;
number_of_cycles *= time_multiplier_;
@@ -376,13 +383,10 @@ Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_
// adjust to ensure aspect ratio is correct
float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f);
float ideal_width = height * adjusted_aspect_ratio;
if(ideal_width > width)
{
if(ideal_width > width) {
start_x -= (ideal_width - width) * 0.5f;
width = ideal_width;
}
else
{
} else {
float ideal_height = width / adjusted_aspect_ratio;
start_y -= (ideal_height - height) * 0.5f;
height = ideal_height;

View File

@@ -46,7 +46,6 @@ class CRT {
int sync_capacitor_charge_threshold_; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
unsigned int sync_period_;
// each call to output_* generates a scan. A two-slot queue for scans allows edge extensions.
struct Scan {
enum Type {
Sync, Level, Data, Blank, ColourBurst
@@ -60,12 +59,14 @@ class CRT {
};
void output_scan(const Scan *scan);
uint8_t colour_burst_phase_, colour_burst_amplitude_;
uint16_t colour_burst_time_;
uint8_t colour_burst_phase_, colour_burst_amplitude_, colour_burst_phase_adjustment_;
bool is_writing_composite_run_;
unsigned int phase_denominator_, phase_numerator_, colour_cycle_numerator_;
bool is_alernate_line_, phase_alternates_;
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
void advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type);
void advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type);
// the inner entry point that determines whether and when the next sync event will occur within
// the current output window
@@ -80,10 +81,23 @@ class CRT {
uint16_t x1, y;
} output_run_;
// The delegate
// the delegate
Delegate *delegate_;
unsigned int frames_since_last_delegate_call_;
// queued tasks for the OpenGL queue; performed before the next draw
std::mutex function_mutex_;
std::vector<std::function<void(void)>> enqueued_openGL_functions_;
inline void enqueue_openGL_function(const std::function<void(void)> &function) {
std::lock_guard<std::mutex> function_guard(function_mutex_);
enqueued_openGL_functions_.push_back(function);
}
// sync counter, for determining vertical sync
unsigned int cycles_of_sync_;
unsigned int cycles_since_sync_;
unsigned int cycles_per_line_;
public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
The requested number of buffers, each with the requested number of bytes per pixel,
@@ -91,7 +105,7 @@ class CRT {
@param cycles_per_line The clock rate at which this CRT will be driven, specified as the number
of cycles expected to take up one whole scanline of the display.
@param common_output_divisor The greatest a priori common divisor of all cycle counts that will be
supplied to @c output_sync, @c output_data, etc; supply 1 if no greater divisor is known. For many
machines output will run at a fixed multiple of the clock rate; knowing this divisor can improve
@@ -113,7 +127,7 @@ class CRT {
@see @c set_rgb_sampling_function , @c set_composite_sampling_function
*/
CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int buffer_depth);
CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, bool should_alternate, unsigned int buffer_depth);
/*! Constructs the CRT with the specified clock rate, with the display height and colour
subcarrier frequency dictated by a standard display type and with the requested number of
@@ -126,7 +140,7 @@ class CRT {
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
been provided at construction. */
void set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator);
void set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, bool should_alternate);
/*! Resets the CRT with new timing information derived from a new display type. The CRT then continues
as though the new timing had been provided at construction. */
@@ -175,6 +189,12 @@ class CRT {
*/
void output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude);
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.
@param number_of_cycles The length of the colour burst;
*/
void output_default_colour_burst(unsigned int number_of_cycles);
/*! Attempts to allocate the given number of output samples for writing.
The beginning of the most recently allocated area is used as the start
@@ -186,8 +206,7 @@ class CRT {
@param required_length The number of samples to allocate.
@returns A pointer to the allocated area if room is available; @c nullptr otherwise.
*/
inline uint8_t *allocate_write_area(size_t required_length)
{
inline uint8_t *allocate_write_area(size_t required_length) {
std::unique_lock<std::mutex> output_lock = openGL_output_builder_.get_output_lock();
return openGL_output_builder_.texture_builder.allocate_write_area(required_length);
}
@@ -195,8 +214,15 @@ class CRT {
/*! Causes appropriate OpenGL or OpenGL ES calls to be issued in order to draw the current CRT state.
The caller is responsible for ensuring that a valid OpenGL context exists for the duration of this call.
*/
inline void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
{
inline void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty) {
{
std::lock_guard<std::mutex> function_guard(function_mutex_);
for(std::function<void(void)> function : enqueued_openGL_functions_)
{
function();
}
enqueued_openGL_functions_.clear();
}
openGL_output_builder_.draw_frame(output_width, output_height, only_if_dirty);
}
@@ -207,9 +233,10 @@ class CRT {
currently held by the CRT will be deleted now via calls to glDeleteTexture and equivalent. If
@c false then the references are simply marked as invalid.
*/
inline void set_openGL_context_will_change(bool should_delete_resources)
{
openGL_output_builder_.set_openGL_context_will_change(should_delete_resources);
inline void set_openGL_context_will_change(bool should_delete_resources) {
enqueue_openGL_function([should_delete_resources, this] {
openGL_output_builder_.set_openGL_context_will_change(should_delete_resources);
});
}
/*! Sets a function that will map from whatever data the machine provided to a composite signal.
@@ -219,11 +246,32 @@ class CRT {
that evaluates to the composite signal level as a function of a source buffer, sampling location, colour
carrier phase and amplitude.
*/
inline void set_composite_sampling_function(const char *shader)
{
openGL_output_builder_.set_composite_sampling_function(shader);
inline void set_composite_sampling_function(const std::string &shader) {
enqueue_openGL_function([shader, this] {
openGL_output_builder_.set_composite_sampling_function(shader);
});
}
enum CompositeSourceType {
/// The composite function provides continuous output.
Continuous,
/// The composite function provides discrete output with four unique values per colour cycle.
DiscreteFourSamplesPerCycle
};
/*! Provides information about the type of output the composite sampling function provides — discrete or continuous.
This is necessary because the CRT implementation samples discretely and therefore can use fewer intermediate
samples if it can exactly duplicate the sampling rate and placement of the composite sampling function.
A continuous function is assumed by default.
@param type The type of output provided by the function supplied to `set_composite_sampling_function`.
@param offset_of_first_sample The relative position within a full cycle of the colour subcarrier at which the
first sample falls. E.g. 0.125 means "at 1/8th of the way through the complete cycle".
*/
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
/*! Sets a function that will map from whatever data the machine provided to an RGB signal.
If the output mode is composite then a default mapping from RGB to the display's composite
@@ -237,25 +285,27 @@ class CRT {
* `vec2 coordinate` representing the source buffer location to sample from in the range [0, 1); and
* `vec2 icoordinate` representing the source buffer location to sample from as a pixel count, for easier multiple-pixels-per-byte unpacking.
*/
inline void set_rgb_sampling_function(const char *shader)
{
openGL_output_builder_.set_rgb_sampling_function(shader);
inline void set_rgb_sampling_function(const std::string &shader) {
enqueue_openGL_function([shader, this] {
openGL_output_builder_.set_rgb_sampling_function(shader);
});
}
inline void set_output_device(OutputDevice output_device)
{
openGL_output_builder_.set_output_device(output_device);
inline void set_output_device(OutputDevice output_device) {
enqueue_openGL_function([output_device, this] {
openGL_output_builder_.set_output_device(output_device);
});
}
inline void set_visible_area(Rect visible_area)
{
openGL_output_builder_.set_visible_area(visible_area);
inline void set_visible_area(Rect visible_area) {
enqueue_openGL_function([visible_area, this] {
openGL_output_builder_.set_visible_area(visible_area);
});
}
Rect get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio);
inline void set_delegate(Delegate *delegate)
{
inline void set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
};

View File

@@ -11,37 +11,30 @@
using namespace Outputs::CRT;
ArrayBuilder::ArrayBuilder(size_t input_size, size_t output_size) :
output_(output_size, nullptr),
input_(input_size, nullptr)
{}
output_(output_size, nullptr),
input_(input_size, nullptr) {}
ArrayBuilder::ArrayBuilder(size_t input_size, size_t output_size, std::function<void(bool is_input, uint8_t *, size_t)> submission_function) :
output_(output_size, submission_function),
input_(input_size, submission_function)
{}
output_(output_size, submission_function),
input_(input_size, submission_function) {}
bool ArrayBuilder::is_full()
{
bool ArrayBuilder::is_full() {
bool was_full;
was_full = is_full_;
return was_full;
}
uint8_t *ArrayBuilder::get_input_storage(size_t size)
{
uint8_t *ArrayBuilder::get_input_storage(size_t size) {
return get_storage(size, input_);
}
uint8_t *ArrayBuilder::get_output_storage(size_t size)
{
uint8_t *ArrayBuilder::get_output_storage(size_t size) {
return get_storage(size, output_);
}
void ArrayBuilder::flush(const std::function<void(uint8_t *input, size_t input_size, uint8_t *output, size_t output_size)> &function)
{
if(!is_full_)
{
size_t input_size, output_size;
void ArrayBuilder::flush(const std::function<void(uint8_t *input, size_t input_size, uint8_t *output, size_t output_size)> &function) {
if(!is_full_) {
size_t input_size = 0, output_size = 0;
uint8_t *input = input_.get_unflushed(input_size);
uint8_t *output = output_.get_unflushed(output_size);
function(input, input_size, output, output_size);
@@ -51,24 +44,20 @@ void ArrayBuilder::flush(const std::function<void(uint8_t *input, size_t input_s
}
}
void ArrayBuilder::bind_input()
{
void ArrayBuilder::bind_input() {
input_.bind();
}
void ArrayBuilder::bind_output()
{
void ArrayBuilder::bind_output() {
output_.bind();
}
ArrayBuilder::Submission ArrayBuilder::submit()
{
ArrayBuilder::Submission ArrayBuilder::submit() {
ArrayBuilder::Submission submission;
submission.input_size = input_.submit(true);
submission.output_size = output_.submit(false);
if(is_full_)
{
if(is_full_) {
is_full_ = false;
input_.reset();
output_.reset();
@@ -78,12 +67,10 @@ ArrayBuilder::Submission ArrayBuilder::submit()
}
ArrayBuilder::Buffer::Buffer(size_t size, std::function<void(bool is_input, uint8_t *, size_t)> submission_function) :
is_full(false), was_reset(false),
submission_function_(submission_function),
allocated_data(0), flushed_data(0), submitted_data(0)
{
if(!submission_function_)
{
is_full(false),
submission_function_(submission_function),
allocated_data(0), flushed_data(0), submitted_data(0) {
if(!submission_function_) {
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)size, NULL, GL_STREAM_DRAW);
@@ -91,23 +78,19 @@ ArrayBuilder::Buffer::Buffer(size_t size, std::function<void(bool is_input, uint
data.resize(size);
}
ArrayBuilder::Buffer::~Buffer()
{
ArrayBuilder::Buffer::~Buffer() {
if(!submission_function_)
glDeleteBuffers(1, &buffer);
}
uint8_t *ArrayBuilder::get_storage(size_t size, Buffer &buffer)
{
uint8_t *ArrayBuilder::get_storage(size_t size, Buffer &buffer) {
uint8_t *pointer = buffer.get_storage(size);
if(!pointer) is_full_ = true;
return pointer;
}
uint8_t *ArrayBuilder::Buffer::get_storage(size_t size)
{
if(is_full || allocated_data + size > data.size())
{
uint8_t *ArrayBuilder::Buffer::get_storage(size_t size) {
if(is_full || allocated_data + size > data.size()) {
is_full = true;
return nullptr;
}
@@ -116,44 +99,30 @@ uint8_t *ArrayBuilder::Buffer::get_storage(size_t size)
return pointer;
}
uint8_t *ArrayBuilder::Buffer::get_unflushed(size_t &size)
{
if(is_full)
{
uint8_t *ArrayBuilder::Buffer::get_unflushed(size_t &size) {
if(is_full) {
return nullptr;
}
size = allocated_data - flushed_data;
return &data[flushed_data];
}
void ArrayBuilder::Buffer::flush()
{
if(submitted_data)
{
memcpy(data.data(), &data[submitted_data], allocated_data - submitted_data);
void ArrayBuilder::Buffer::flush() {
if(submitted_data) {
memmove(data.data(), &data[submitted_data], allocated_data - submitted_data);
allocated_data -= submitted_data;
flushed_data -= submitted_data;
submitted_data = 0;
}
flushed_data = allocated_data;
if(was_reset)
{
allocated_data = 0;
flushed_data = 0;
submitted_data = 0;
was_reset = false;
}
}
size_t ArrayBuilder::Buffer::submit(bool is_input)
{
size_t ArrayBuilder::Buffer::submit(bool is_input) {
size_t length = flushed_data;
if(submission_function_)
if(submission_function_) {
submission_function_(is_input, data.data(), length);
else
{
} else {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
uint8_t *destination = (uint8_t *)glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)length, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_FLUSH_EXPLICIT_BIT);
memcpy(destination, data.data(), length);
@@ -164,13 +133,13 @@ size_t ArrayBuilder::Buffer::submit(bool is_input)
return length;
}
void ArrayBuilder::Buffer::bind()
{
void ArrayBuilder::Buffer::bind() {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
}
void ArrayBuilder::Buffer::reset()
{
was_reset = true;
void ArrayBuilder::Buffer::reset() {
is_full = false;
allocated_data = 0;
flushed_data = 0;
submitted_data = 0;
}

View File

@@ -82,7 +82,7 @@ class ArrayBuilder {
void reset();
private:
bool is_full, was_reset;
bool is_full;
GLuint buffer;
std::function<void(bool is_input, uint8_t *, size_t)> submission_function_;
std::vector<uint8_t> data;

View File

@@ -6,8 +6,9 @@
//
#include "CRT.hpp"
#include <stdlib.h>
#include <math.h>
#include <cstdlib>
#include <cmath>
#include "CRTOpenGL.hpp"
#include "../../../SignalProcessing/FIRFilter.hpp"
@@ -16,29 +17,24 @@
using namespace Outputs::CRT;
namespace {
static const GLenum composite_texture_unit = GL_TEXTURE0;
static const GLenum separated_texture_unit = GL_TEXTURE1;
static const GLenum filtered_y_texture_unit = GL_TEXTURE2;
static const GLenum filtered_texture_unit = GL_TEXTURE3;
static const GLenum source_data_texture_unit = GL_TEXTURE4;
static const GLenum pixel_accumulation_texture_unit = GL_TEXTURE5;
static const GLenum source_data_texture_unit = GL_TEXTURE0;
static const GLenum pixel_accumulation_texture_unit = GL_TEXTURE1;
static const GLenum composite_texture_unit = GL_TEXTURE2;
static const GLenum separated_texture_unit = GL_TEXTURE3;
static const GLenum filtered_texture_unit = GL_TEXTURE4;
static const GLenum work_texture_unit = GL_TEXTURE2;
}
OpenGLOutputBuilder::OpenGLOutputBuilder(size_t bytes_per_pixel) :
visible_area_(Rect(0, 0, 1, 1)),
composite_src_output_y_(0),
composite_shader_(nullptr),
rgb_shader_(nullptr),
last_output_width_(0),
last_output_height_(0),
fence_(nullptr),
texture_builder(bytes_per_pixel, source_data_texture_unit),
array_builder(SourceVertexBufferDataSize, OutputVertexBufferDataSize),
composite_texture_(IntermediateBufferWidth, IntermediateBufferHeight, composite_texture_unit),
separated_texture_(IntermediateBufferWidth, IntermediateBufferHeight, separated_texture_unit),
filtered_y_texture_(IntermediateBufferWidth, IntermediateBufferHeight, filtered_y_texture_unit),
filtered_texture_(IntermediateBufferWidth, IntermediateBufferHeight, filtered_texture_unit)
{
visible_area_(Rect(0, 0, 1, 1)),
composite_src_output_y_(0),
last_output_width_(0),
last_output_height_(0),
fence_(nullptr),
texture_builder(bytes_per_pixel, source_data_texture_unit),
array_builder(SourceVertexBufferDataSize, OutputVertexBufferDataSize) {
glBlendFunc(GL_SRC_ALPHA, GL_CONSTANT_COLOR);
glBlendColor(0.6f, 0.6f, 0.6f, 1.0f);
@@ -47,24 +43,43 @@ OpenGLOutputBuilder::OpenGLOutputBuilder(size_t bytes_per_pixel) :
// create the source vertex array
glGenVertexArrays(1, &source_vertex_array_);
bool supports_texture_barrier = false;
#ifdef GL_NV_texture_barrier
GLint number_of_extensions;
glGetIntegerv(GL_NUM_EXTENSIONS, &number_of_extensions);
for(GLuint c = 0; c < (GLuint)number_of_extensions; c++) {
const char *extension_name = (const char *)glGetStringi(GL_EXTENSIONS, c);
if(!strcmp(extension_name, "GL_NV_texture_barrier")) {
supports_texture_barrier = true;
}
}
#endif
// if(supports_texture_barrier) {
// work_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight*2, work_texture_unit));
// } else {
composite_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, composite_texture_unit, GL_NEAREST));
separated_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, separated_texture_unit, GL_NEAREST));
filtered_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, filtered_texture_unit, GL_LINEAR));
// }
}
OpenGLOutputBuilder::~OpenGLOutputBuilder()
{
OpenGLOutputBuilder::~OpenGLOutputBuilder() {
glDeleteVertexArrays(1, &output_vertex_array_);
free(composite_shader_);
free(rgb_shader_);
}
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
{
bool OpenGLOutputBuilder::get_is_television_output() {
return output_device_ == Television || !rgb_input_shader_program_;
}
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty) {
// lock down any other draw_frames
draw_mutex_.lock();
// establish essentials
if(!output_shader_program_)
{
if(!output_shader_program_) {
prepare_composite_input_shaders();
prepare_rgb_input_shaders();
prepare_source_vertex_array();
@@ -76,11 +91,9 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
set_colour_space_uniforms();
}
if(fence_ != nullptr)
{
if(fence_ != nullptr) {
// if the GPU is still busy, don't wait; we'll catch it next time
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, only_if_dirty ? 0 : GL_TIMEOUT_IGNORED) == GL_TIMEOUT_EXPIRED)
{
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, only_if_dirty ? 0 : GL_TIMEOUT_IGNORED) == GL_TIMEOUT_EXPIRED) {
draw_mutex_.unlock();
return;
}
@@ -89,11 +102,9 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
}
// make sure there's a target to draw to
if(!framebuffer_ || framebuffer_->get_height() != output_height || framebuffer_->get_width() != output_width)
{
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(new OpenGL::TextureTarget((GLsizei)output_width, (GLsizei)output_height, pixel_accumulation_texture_unit));
if(framebuffer_)
{
if(!framebuffer_ || framebuffer_->get_height() != output_height || framebuffer_->get_width() != output_width) {
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(new OpenGL::TextureTarget((GLsizei)output_width, (GLsizei)output_height, pixel_accumulation_texture_unit, GL_LINEAR));
if(framebuffer_) {
new_framebuffer->bind_framebuffer();
glClear(GL_COLOR_BUFFER_BIT);
@@ -123,55 +134,62 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
output_mutex_.unlock();
struct RenderStage {
OpenGL::TextureTarget *const target;
OpenGL::Shader *const shader;
OpenGL::TextureTarget *const target;
float clear_colour[3];
};
// for composite video, go through four steps to get to something that can be painted to the output
RenderStage composite_render_stages[] =
{
{&composite_texture_, composite_input_shader_program_.get(), {0.0, 0.0, 0.0}},
{&separated_texture_, composite_separation_filter_program_.get(), {0.0, 0.5, 0.5}},
{&filtered_y_texture_, composite_y_filter_shader_program_.get(), {0.0, 0.5, 0.5}},
{&filtered_texture_, composite_chrominance_filter_shader_program_.get(), {0.0, 0.0, 0.0}},
RenderStage composite_render_stages[] = {
{composite_input_shader_program_.get(), composite_texture_.get(), {0.0, 0.0, 0.0}},
{composite_separation_filter_program_.get(), separated_texture_.get(), {0.0, 0.5, 0.5}},
{composite_chrominance_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
{nullptr}
};
// for RGB video, there's only two steps
RenderStage rgb_render_stages[] =
{
{&composite_texture_, rgb_input_shader_program_.get(), {0.0, 0.0, 0.0}},
{&filtered_texture_, rgb_filter_shader_program_.get(), {0.0, 0.0, 0.0}},
RenderStage rgb_render_stages[] = {
{rgb_input_shader_program_.get(), composite_texture_.get(), {0.0, 0.0, 0.0}},
{rgb_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
{nullptr}
};
RenderStage *active_pipeline = (output_device_ == Television || !rgb_input_shader_program_) ? composite_render_stages : rgb_render_stages;
RenderStage *active_pipeline = get_is_television_output() ? composite_render_stages : rgb_render_stages;
if(array_submission.input_size || array_submission.output_size)
{
if(array_submission.input_size || array_submission.output_size) {
// all drawing will be from the source vertex array and without blending
glBindVertexArray(source_vertex_array_);
glDisable(GL_BLEND);
while(active_pipeline->target)
{
#ifdef GL_NV_texture_barrier
if(work_texture_) {
work_texture_->bind_framebuffer();
glClear(GL_COLOR_BUFFER_BIT);
}
#endif
while(active_pipeline->shader) {
// switch to the framebuffer and shader associated with this stage
active_pipeline->shader->bind();
active_pipeline->target->bind_framebuffer();
// if this is the final stage before painting to the CRT, clear the framebuffer before drawing in order to blank out
// those portions for which no input was provided
if(!active_pipeline[1].target)
{
glClearColor(active_pipeline->clear_colour[0], active_pipeline->clear_colour[1], active_pipeline->clear_colour[2], 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if(!work_texture_) {
active_pipeline->target->bind_framebuffer();
// if this is the final stage before painting to the CRT, clear the framebuffer before drawing in order to blank out
// those portions for which no input was provided
// if(!active_pipeline[1].shader) {
glClearColor(active_pipeline->clear_colour[0], active_pipeline->clear_colour[1], active_pipeline->clear_colour[2], 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// }
}
// draw
glDrawArraysInstanced(GL_LINES, 0, 2, (GLsizei)array_submission.input_size / SourceVertexSize);
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)array_submission.input_size / SourceVertexSize);
active_pipeline++;
#ifdef GL_NV_texture_barrier
glTextureBarrierNV();
#endif
}
// prepare to transfer to framebuffer
@@ -182,8 +200,7 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
glEnable(GL_BLEND);
// update uniforms, then bind the target
if(last_output_width_ != output_width || last_output_height_ != output_height)
{
if(last_output_width_ != output_width || last_output_height_ != output_height) {
output_shader_program_->set_output_size(output_width, output_height, visible_area_);
last_output_width_ = output_width;
last_output_height_ = output_height;
@@ -194,6 +211,10 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)array_submission.output_size / OutputVertexSize);
}
#ifdef GL_NV_texture_barrier
glTextureBarrierNV();
#endif
// copy framebuffer to the intended place
glDisable(GL_BLEND);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
@@ -207,11 +228,9 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
draw_mutex_.unlock();
}
void OpenGLOutputBuilder::reset_all_OpenGL_state()
{
void OpenGLOutputBuilder::reset_all_OpenGL_state() {
composite_input_shader_program_ = nullptr;
composite_separation_filter_program_ = nullptr;
composite_y_filter_shader_program_ = nullptr;
composite_chrominance_filter_shader_program_ = nullptr;
rgb_input_shader_program_ = nullptr;
rgb_filter_shader_program_ = nullptr;
@@ -220,54 +239,52 @@ void OpenGLOutputBuilder::reset_all_OpenGL_state()
last_output_width_ = last_output_height_ = 0;
}
void OpenGLOutputBuilder::set_openGL_context_will_change(bool should_delete_resources)
{
void OpenGLOutputBuilder::set_openGL_context_will_change(bool should_delete_resources) {
output_mutex_.lock();
reset_all_OpenGL_state();
output_mutex_.unlock();
}
void OpenGLOutputBuilder::set_composite_sampling_function(const char *shader)
{
output_mutex_.lock();
composite_shader_ = strdup(shader);
void OpenGLOutputBuilder::set_composite_sampling_function(const std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
composite_shader_ = shader;
reset_all_OpenGL_state();
output_mutex_.unlock();
}
void OpenGLOutputBuilder::set_rgb_sampling_function(const char *shader)
{
output_mutex_.lock();
rgb_shader_ = strdup(shader);
void OpenGLOutputBuilder::set_rgb_sampling_function(const std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
rgb_shader_ = shader;
reset_all_OpenGL_state();
output_mutex_.unlock();
}
#pragma mark - Program compilation
void OpenGLOutputBuilder::prepare_composite_input_shaders()
{
void OpenGLOutputBuilder::prepare_composite_input_shaders() {
composite_input_shader_program_ = OpenGL::IntermediateShader::make_source_conversion_shader(composite_shader_, rgb_shader_);
composite_input_shader_program_->set_source_texture_unit(source_data_texture_unit);
composite_input_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
composite_separation_filter_program_ = OpenGL::IntermediateShader::make_chroma_luma_separation_shader();
composite_separation_filter_program_->set_source_texture_unit(composite_texture_unit);
composite_separation_filter_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : composite_texture_unit);
composite_separation_filter_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
composite_y_filter_shader_program_ = OpenGL::IntermediateShader::make_luma_filter_shader();
composite_y_filter_shader_program_->set_source_texture_unit(separated_texture_unit);
composite_y_filter_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
composite_chrominance_filter_shader_program_ = OpenGL::IntermediateShader::make_chroma_filter_shader();
composite_chrominance_filter_shader_program_->set_source_texture_unit(filtered_y_texture_unit);
composite_chrominance_filter_shader_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : separated_texture_unit);
composite_chrominance_filter_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
if(work_texture_) {
composite_input_shader_program_->set_is_double_height(true, 0.0f, 0.0f);
composite_separation_filter_program_->set_is_double_height(true, 0.0f, 0.5f);
composite_chrominance_filter_shader_program_->set_is_double_height(true, 0.5f, 0.0f);
} else {
composite_input_shader_program_->set_is_double_height(false);
composite_separation_filter_program_->set_is_double_height(false);
composite_chrominance_filter_shader_program_->set_is_double_height(false);
}
}
void OpenGLOutputBuilder::prepare_rgb_input_shaders()
{
if(rgb_shader_)
{
void OpenGLOutputBuilder::prepare_rgb_input_shaders() {
if(rgb_shader_.size()) {
rgb_input_shader_program_ = OpenGL::IntermediateShader::make_rgb_source_shader(rgb_shader_);
rgb_input_shader_program_->set_source_texture_unit(source_data_texture_unit);
rgb_input_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
@@ -278,10 +295,8 @@ void OpenGLOutputBuilder::prepare_rgb_input_shaders()
}
}
void OpenGLOutputBuilder::prepare_source_vertex_array()
{
if(composite_input_shader_program_)
{
void OpenGLOutputBuilder::prepare_source_vertex_array() {
if(composite_input_shader_program_) {
glBindVertexArray(source_vertex_array_);
array_builder.bind_input();
@@ -292,16 +307,15 @@ void OpenGLOutputBuilder::prepare_source_vertex_array()
}
}
void OpenGLOutputBuilder::prepare_output_shader()
{
void OpenGLOutputBuilder::prepare_output_shader() {
output_shader_program_ = OpenGL::OutputShader::make_shader("", "texture(texID, srcCoordinatesVarying).rgb", false);
output_shader_program_->set_source_texture_unit(filtered_texture_unit);
output_shader_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : filtered_texture_unit);
// output_shader_program_->set_source_texture_unit(composite_texture_unit);
output_shader_program_->set_origin_is_double_height(!!work_texture_);
}
void OpenGLOutputBuilder::prepare_output_vertex_array()
{
if(output_shader_program_)
{
void OpenGLOutputBuilder::prepare_output_vertex_array() {
if(output_shader_program_) {
glBindVertexArray(output_vertex_array_);
array_builder.bind_output();
output_shader_program_->enable_vertex_attribute_with_pointer("horizontal", 2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize, (void *)OutputVertexOffsetOfHorizontal, 1);
@@ -311,20 +325,18 @@ void OpenGLOutputBuilder::prepare_output_vertex_array()
#pragma mark - Public Configuration
void OpenGLOutputBuilder::set_output_device(OutputDevice output_device)
{
if(output_device_ != output_device)
{
void OpenGLOutputBuilder::set_output_device(OutputDevice output_device) {
if(output_device_ != output_device) {
output_device_ = output_device;
composite_src_output_y_ = 0;
last_output_width_ = 0;
last_output_height_ = 0;
set_output_shader_width();
}
}
void OpenGLOutputBuilder::set_timing(unsigned int input_frequency, unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider)
{
output_mutex_.lock();
void OpenGLOutputBuilder::set_timing(unsigned int input_frequency, unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
input_frequency_ = input_frequency;
cycles_per_line_ = cycles_per_line;
height_of_display_ = height_of_display;
@@ -333,13 +345,11 @@ void OpenGLOutputBuilder::set_timing(unsigned int input_frequency, unsigned int
vertical_period_divider_ = vertical_period_divider;
set_timing_uniforms();
output_mutex_.unlock();
}
#pragma mark - Internal Configuration
void OpenGLOutputBuilder::set_colour_space_uniforms()
{
void OpenGLOutputBuilder::set_colour_space_uniforms() {
GLfloat rgbToYUV[] = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f};
GLfloat yuvToRGB[] = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f};
@@ -348,8 +358,7 @@ void OpenGLOutputBuilder::set_colour_space_uniforms()
GLfloat *fromRGB, *toRGB;
switch(colour_space_)
{
switch(colour_space_) {
case ColourSpace::YIQ:
fromRGB = rgbToYIQ;
toRGB = yiqToRGB;
@@ -362,30 +371,48 @@ void OpenGLOutputBuilder::set_colour_space_uniforms()
}
if(composite_input_shader_program_) composite_input_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
if(composite_separation_filter_program_) composite_separation_filter_program_->set_colour_conversion_matrices(fromRGB, toRGB);
if(composite_chrominance_filter_shader_program_) composite_chrominance_filter_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
}
void OpenGLOutputBuilder::set_timing_uniforms()
{
OpenGL::IntermediateShader *intermediate_shaders[] = {
composite_input_shader_program_.get(),
composite_separation_filter_program_.get(),
composite_y_filter_shader_program_.get(),
composite_chrominance_filter_shader_program_.get()
};
bool extends = false;
float phaseCyclesPerTick = (float)colour_cycle_numerator_ / (float)(colour_cycle_denominator_ * cycles_per_line_);
for(int c = 0; c < 3; c++)
{
if(intermediate_shaders[c]) intermediate_shaders[c]->set_phase_cycles_per_sample(phaseCyclesPerTick, extends);
extends = true;
}
if(output_shader_program_) output_shader_program_->set_timing(height_of_display_, cycles_per_line_, horizontal_scan_period_, vertical_scan_period_, vertical_period_divider_);
float colour_subcarrier_frequency = (float)colour_cycle_numerator_ / (float)colour_cycle_denominator_;
if(composite_separation_filter_program_) composite_separation_filter_program_->set_separation_frequency(cycles_per_line_, colour_subcarrier_frequency);
if(composite_y_filter_shader_program_) composite_y_filter_shader_program_->set_filter_coefficients(cycles_per_line_, colour_subcarrier_frequency * 0.66f);
if(composite_chrominance_filter_shader_program_) composite_chrominance_filter_shader_program_->set_filter_coefficients(cycles_per_line_, colour_subcarrier_frequency * 0.5f);
if(rgb_filter_shader_program_) rgb_filter_shader_program_->set_filter_coefficients(cycles_per_line_, (float)input_frequency_ * 0.5f);
float OpenGLOutputBuilder::get_composite_output_width() const {
return ((float)colour_cycle_numerator_ * 4.0f) / (float)(colour_cycle_denominator_ * IntermediateBufferWidth);
}
void OpenGLOutputBuilder::set_output_shader_width() {
if(output_shader_program_) {
const float width = get_is_television_output() ? get_composite_output_width() : 1.0f;
output_shader_program_->set_input_width_scaler(width);
}
}
void OpenGLOutputBuilder::set_timing_uniforms() {
const float colour_subcarrier_frequency = (float)colour_cycle_numerator_ / (float)colour_cycle_denominator_;
const float output_width = get_composite_output_width();
const float sample_cycles_per_line = cycles_per_line_ / output_width;
if(composite_separation_filter_program_) {
composite_separation_filter_program_->set_width_scalers(output_width, output_width);
composite_separation_filter_program_->set_separation_frequency(sample_cycles_per_line, colour_subcarrier_frequency);
composite_separation_filter_program_->set_extension(6.0f);
}
if(composite_chrominance_filter_shader_program_) {
composite_chrominance_filter_shader_program_->set_width_scalers(output_width, output_width);
composite_chrominance_filter_shader_program_->set_extension(5.0f);
}
if(rgb_filter_shader_program_) {
rgb_filter_shader_program_->set_width_scalers(1.0f, 1.0f);
rgb_filter_shader_program_->set_filter_coefficients(sample_cycles_per_line, (float)input_frequency_ * 0.5f);
}
if(output_shader_program_) {
set_output_shader_width();
output_shader_program_->set_timing(height_of_display_, cycles_per_line_, horizontal_scan_period_, vertical_scan_period_, vertical_period_divider_);
}
if(composite_input_shader_program_) {
composite_input_shader_program_->set_width_scalers(1.0f, output_width);
composite_input_shader_program_->set_extension(0.0f);
}
if(rgb_input_shader_program_) {
rgb_input_shader_program_->set_width_scalers(1.0f, 1.0f);
}
}

View File

@@ -47,8 +47,8 @@ class OpenGLOutputBuilder {
Rect visible_area_;
// Other things the caller may have provided.
char *composite_shader_;
char *rgb_shader_;
std::string composite_shader_;
std::string rgb_shader_;
// Methods used by the OpenGL code
void prepare_output_shader();
@@ -66,13 +66,19 @@ class OpenGLOutputBuilder {
GLsizei composite_src_output_y_;
std::unique_ptr<OpenGL::OutputShader> output_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_input_shader_program_, composite_separation_filter_program_, composite_y_filter_shader_program_, composite_chrominance_filter_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> rgb_input_shader_program_, rgb_filter_shader_program_;
OpenGL::TextureTarget composite_texture_; // receives raw composite levels
OpenGL::TextureTarget separated_texture_; // receives unfiltered Y in the R channel plus unfiltered but demodulated chrominance in G and B
OpenGL::TextureTarget filtered_y_texture_; // receives filtered Y in the R channel plus unfiltered chrominance in G and B
OpenGL::TextureTarget filtered_texture_; // receives filtered YIQ or YUV
std::unique_ptr<OpenGL::IntermediateShader> composite_input_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_separation_filter_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_chrominance_filter_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> rgb_input_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> rgb_filter_shader_program_;
std::unique_ptr<OpenGL::TextureTarget> composite_texture_; // receives raw composite levels
std::unique_ptr<OpenGL::TextureTarget> separated_texture_; // receives filtered Y in the R channel plus unfiltered but demodulated chrominance in G and B
std::unique_ptr<OpenGL::TextureTarget> filtered_texture_; // receives filtered YIQ or YUV
std::unique_ptr<OpenGL::TextureTarget> work_texture_; // used for all intermediate rendering if texture fences are supported
std::unique_ptr<OpenGL::TextureTarget> framebuffer_; // the current pixel output
@@ -88,6 +94,9 @@ class OpenGLOutputBuilder {
void reset_all_OpenGL_state();
GLsync fence_;
float get_composite_output_width() const;
void set_output_shader_width();
bool get_is_television_output();
public:
// These two are protected by output_mutex_.
@@ -97,8 +106,7 @@ class OpenGLOutputBuilder {
OpenGLOutputBuilder(size_t bytes_per_pixel);
~OpenGLOutputBuilder();
inline void set_colour_format(ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator)
{
inline void set_colour_format(ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator) {
std::lock_guard<std::mutex> output_guard(output_mutex_);
colour_space_ = colour_space;
colour_cycle_numerator_ = colour_cycle_numerator;
@@ -106,41 +114,35 @@ class OpenGLOutputBuilder {
set_colour_space_uniforms();
}
inline void set_visible_area(Rect visible_area)
{
inline void set_visible_area(Rect visible_area) {
visible_area_ = visible_area;
}
inline std::unique_lock<std::mutex> get_output_lock()
{
inline std::unique_lock<std::mutex> get_output_lock() {
return std::unique_lock<std::mutex>(output_mutex_);
}
inline OutputDevice get_output_device()
{
inline OutputDevice get_output_device() {
return output_device_;
}
inline uint16_t get_composite_output_y()
{
inline uint16_t get_composite_output_y() {
return (uint16_t)composite_src_output_y_;
}
inline bool composite_output_buffer_is_full()
{
inline bool composite_output_buffer_is_full() {
return composite_src_output_y_ == IntermediateBufferHeight;
}
inline void increment_composite_output_y()
{
inline void increment_composite_output_y() {
if(!composite_output_buffer_is_full())
composite_src_output_y_++;
}
void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty);
void set_openGL_context_will_change(bool should_delete_resources);
void set_composite_sampling_function(const char *shader);
void set_rgb_sampling_function(const char *shader);
void set_composite_sampling_function(const std::string &shader);
void set_rgb_sampling_function(const std::string &shader);
void set_output_device(OutputDevice output_device);
void set_timing(unsigned int input_frequency, unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider);
};

View File

@@ -21,8 +21,7 @@ namespace CRT {
The @c Flywheel will attempt to converge with timing implied by synchronisation pulses.
*/
struct Flywheel
{
struct Flywheel {
/*!
Constructs an instance of @c Flywheel.
@@ -61,26 +60,18 @@ struct Flywheel
@returns The next synchronisation event.
*/
inline SyncEvent get_next_event_in_period(bool sync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced)
{
inline SyncEvent get_next_event_in_period(bool sync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
// do we recognise this hsync, thereby adjusting future time expectations?
if(sync_is_requested)
{
if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_)
{
if(sync_is_requested) {
if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) {
unsigned int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_;
expected_next_sync_ = (3*expected_next_sync_ + time_now) >> 2;
}
else
{
} else {
number_of_surprises_++;
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1))
{
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) {
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ + sync_error_window_) >> 2;
}
else
{
} else {
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ - sync_error_window_) >> 2;
}
}
@@ -90,15 +81,13 @@ struct Flywheel
unsigned int proposed_sync_time = cycles_to_run_for;
// will we end an ongoing retrace?
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_)
{
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) {
proposed_sync_time = retrace_time_ - counter_;
proposed_event = SyncEvent::EndRetrace;
}
// will we start a retrace?
if(counter_ + proposed_sync_time >= expected_next_sync_)
{
if(counter_ + proposed_sync_time >= expected_next_sync_) {
proposed_sync_time = expected_next_sync_ - counter_;
proposed_event = SyncEvent::StartRetrace;
}
@@ -115,12 +104,10 @@ struct Flywheel
@param event The synchronisation event to apply after that period.
*/
inline void apply_event(unsigned int cycles_advanced, SyncEvent event)
{
inline void apply_event(unsigned int cycles_advanced, SyncEvent event) {
counter_ += cycles_advanced;
switch(event)
{
switch(event) {
default: return;
case StartRetrace:
counter_before_retrace_ = counter_ - retrace_time_;
@@ -135,10 +122,8 @@ struct Flywheel
@returns The current output position.
*/
inline unsigned int get_current_output_position()
{
if(counter_ < retrace_time_)
{
inline unsigned int get_current_output_position() {
if(counter_ < retrace_time_) {
unsigned int retrace_distance = (counter_ * standard_period_) / retrace_time_;
if(retrace_distance > counter_before_retrace_) return 0;
return counter_before_retrace_ - retrace_distance;
@@ -150,32 +135,28 @@ struct Flywheel
/*!
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
*/
inline unsigned int get_current_time()
{
inline unsigned int get_current_time() {
return counter_;
}
/*!
@returns whether the output is currently retracing.
*/
inline bool is_in_retrace()
{
inline bool is_in_retrace() {
return counter_ < retrace_time_;
}
/*!
@returns the expected length of the scan period (excluding retrace).
*/
inline unsigned int get_scan_period()
{
inline unsigned int get_scan_period() {
return standard_period_ - retrace_time_;
}
/*!
@returns the expected length of a complete scan and retrace cycle.
*/
inline unsigned int get_standard_period()
{
inline unsigned int get_standard_period() {
return standard_period_;
}
@@ -183,8 +164,7 @@ struct Flywheel
@returns the number of synchronisation events that have seemed surprising since the last time this method was called;
a low number indicates good synchronisation.
*/
inline unsigned int get_and_reset_number_of_surprises()
{
inline unsigned int get_and_reset_number_of_surprises() {
unsigned int result = number_of_surprises_;
number_of_surprises_ = 0;
return result;
@@ -193,8 +173,7 @@ struct Flywheel
/*!
@returns `true` if a sync is expected soon or the time at which it was expected was recent.
*/
inline bool is_near_expected_sync()
{
inline bool is_near_expected_sync() {
return abs((int)counter_ - (int)expected_next_sync_) < (int)standard_period_ / 50;
}

View File

@@ -15,6 +15,7 @@
#else
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <OpenGL/gl3ext.h>
#endif
#endif

View File

@@ -16,8 +16,7 @@
using namespace OpenGL;
namespace {
const OpenGL::Shader::AttributeBinding bindings[] =
{
const OpenGL::Shader::AttributeBinding bindings[] = {
{"inputPosition", 0},
{"outputPosition", 1},
{"phaseAndAmplitude", 2},
@@ -26,8 +25,7 @@ namespace {
};
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const char *fragment_shader, bool use_usampler, bool input_is_inputPosition)
{
std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const std::string &fragment_shader, bool use_usampler, bool input_is_inputPosition) {
const char *sampler_type = use_usampler ? "usampler2D" : "sampler2D";
const char *input_variable = input_is_inputPosition ? "inputPosition" : "outputPosition";
@@ -40,11 +38,14 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const char *
"in vec2 ends;"
"in vec3 phaseTimeAndAmplitude;"
"uniform float phaseCyclesPerTick;"
"uniform ivec2 outputTextureSize;"
"uniform float extension;"
"uniform %s texID;"
"uniform float offsets[5];"
"uniform vec2 widthScalers;"
"uniform float inputVerticalOffset;"
"uniform float outputVerticalOffset;"
"uniform float textureHeightDivisor;"
"out vec2 phaseAndAmplitudeVarying;"
"out vec2 inputPositionsVarying[11];"
@@ -53,36 +54,52 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const char *
"void main(void)"
"{"
// odd vertices are on the left, even on the right
"float extent = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
"vec2 inputPosition = vec2(mix(inputStart.x, ends.x, extent), inputStart.y);"
"vec2 outputPosition = vec2(mix(outputStart.x, ends.y, extent), outputStart.y);"
// inputPosition.x is either inputStart.x or ends.x, depending on whether it is on the left or the right;
// outputPosition.x is either outputStart.x or ends.y;
// .ys are inputStart.y and outputStart.y respectively
"vec2 inputPosition = vec2(mix(inputStart.x, ends.x, extent)*widthScalers[0], inputStart.y + inputVerticalOffset);"
"vec2 outputPosition = vec2(mix(outputStart.x, ends.y, extent)*widthScalers[1], outputStart.y + outputVerticalOffset);"
"inputPosition.y += longitudinal;"
"outputPosition.y += longitudinal;"
// extension is the amount to extend both the input and output by to add a full colour cycle at each end
"vec2 extensionVector = vec2(extension, 0.0) * 2.0 * (extent - 0.5);"
// extended[Input/Output]Position are [input/output]Position with the necessary applied extension
"vec2 extendedInputPosition = %s + extensionVector;"
"vec2 extendedOutputPosition = outputPosition + extensionVector;"
// keep iInputPositionVarying in whole source pixels, scale mappedInputPosition to the ordinary normalised range
"vec2 textureSize = vec2(textureSize(texID, 0));"
"iInputPositionVarying = extendedInputPosition;"
"vec2 mappedInputPosition = (extendedInputPosition + vec2(0.0, 0.5)) / textureSize;"
"vec2 mappedInputPosition = extendedInputPosition / textureSize;" // + vec2(0.0, 0.5)
"inputPositionsVarying[0] = mappedInputPosition - (vec2(offsets[0], 0.0) / textureSize);"
"inputPositionsVarying[1] = mappedInputPosition - (vec2(offsets[1], 0.0) / textureSize);"
"inputPositionsVarying[2] = mappedInputPosition - (vec2(offsets[2], 0.0) / textureSize);"
"inputPositionsVarying[3] = mappedInputPosition - (vec2(offsets[3], 0.0) / textureSize);"
"inputPositionsVarying[4] = mappedInputPosition - (vec2(offsets[4], 0.0) / textureSize);"
// setup input positions spaced as per the supplied offsets; these are for filtering where required
"inputPositionsVarying[0] = mappedInputPosition - (vec2(5.0, 0.0) / textureSize);"
"inputPositionsVarying[1] = mappedInputPosition - (vec2(4.0, 0.0) / textureSize);"
"inputPositionsVarying[2] = mappedInputPosition - (vec2(3.0, 0.0) / textureSize);"
"inputPositionsVarying[3] = mappedInputPosition - (vec2(2.0, 0.0) / textureSize);"
"inputPositionsVarying[4] = mappedInputPosition - (vec2(1.0, 0.0) / textureSize);"
"inputPositionsVarying[5] = mappedInputPosition;"
"inputPositionsVarying[6] = mappedInputPosition + (vec2(offsets[4], 0.0) / textureSize);"
"inputPositionsVarying[7] = mappedInputPosition + (vec2(offsets[3], 0.0) / textureSize);"
"inputPositionsVarying[8] = mappedInputPosition + (vec2(offsets[2], 0.0) / textureSize);"
"inputPositionsVarying[9] = mappedInputPosition + (vec2(offsets[1], 0.0) / textureSize);"
"inputPositionsVarying[10] = mappedInputPosition + (vec2(offsets[0], 0.0) / textureSize);"
"inputPositionsVarying[6] = mappedInputPosition + (vec2(1.0, 0.0) / textureSize);"
"inputPositionsVarying[7] = mappedInputPosition + (vec2(2.0, 0.0) / textureSize);"
"inputPositionsVarying[8] = mappedInputPosition + (vec2(3.0, 0.0) / textureSize);"
"inputPositionsVarying[9] = mappedInputPosition + (vec2(4.0, 0.0) / textureSize);"
"inputPositionsVarying[10] = mappedInputPosition + (vec2(5.0, 0.0) / textureSize);"
"delayLinePositionVarying = mappedInputPosition - vec2(0.0, 1.0);"
"phaseAndAmplitudeVarying.x = (phaseCyclesPerTick * (extendedOutputPosition.x - phaseTimeAndAmplitude.y) + (phaseTimeAndAmplitude.x / 256.0)) * 2.0 * 3.141592654;"
// setup phaseAndAmplitudeVarying.x as colour burst subcarrier phase, in radians;
// setup phaseAndAmplitudeVarying.x as colour burst amplitude
"phaseAndAmplitudeVarying.x = (extendedOutputPosition.x + (phaseTimeAndAmplitude.x / 64.0)) * 0.5 * 3.141592654;"
"phaseAndAmplitudeVarying.y = 0.33;" // TODO: reinstate connection with (phaseTimeAndAmplitude.y/256.0)
"vec2 eyePosition = 2.0*(extendedOutputPosition / outputTextureSize) - vec2(1.0) + vec2(1.0)/outputTextureSize;"
// determine output position by scaling the output position according to the texture size
"vec2 eyePosition = 2.0*(extendedOutputPosition / outputTextureSize) - vec2(1.0);"
"gl_Position = vec4(eyePosition, 0.0, 1.0);"
"}", sampler_type, input_variable);
@@ -92,12 +109,11 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const char *
return shader;
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_source_conversion_shader(const char *composite_shader, const char *rgb_shader)
{
char *composite_sample = (char *)composite_shader;
if(!composite_sample)
{
asprintf(&composite_sample,
std::unique_ptr<IntermediateShader> IntermediateShader::make_source_conversion_shader(const std::string &composite_shader, const std::string &rgb_shader) {
char *derived_composite_sample = nullptr;
const char *composite_sample = composite_shader.c_str();
if(!composite_shader.size()) {
asprintf(&derived_composite_sample,
"%s\n"
"uniform mat3 rgbToLumaChroma;"
"float composite_sample(usampler2D texID, vec2 coordinate, vec2 iCoordinate, float phase, float amplitude)"
@@ -107,7 +123,8 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_source_conversion_s
"vec2 quadrature = vec2(cos(phase), -sin(phase)) * amplitude;"
"return dot(lumaChromaColour, vec3(1.0 - amplitude, quadrature));"
"}",
rgb_shader);
rgb_shader.c_str());
composite_sample = derived_composite_sample;
}
char *fragment_shader;
@@ -129,7 +146,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_source_conversion_s
"fragColour = vec4(composite_sample(texID, inputPositionsVarying[5], iInputPositionVarying, phaseAndAmplitudeVarying.x, phaseAndAmplitudeVarying.y));"
"}"
, composite_sample);
if(!composite_shader) free(composite_sample);
free(derived_composite_sample);
std::unique_ptr<IntermediateShader> shader = make_shader(fragment_shader, true, true);
free(fragment_shader);
@@ -137,8 +154,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_source_conversion_s
return shader;
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_source_shader(const char *rgb_shader)
{
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_source_shader(const std::string &rgb_shader) {
char *fragment_shader;
asprintf(&fragment_shader,
"#version 150\n"
@@ -157,7 +173,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_source_shader(c
"{"
"fragColour = rgb_sample(texID, inputPositionsVarying[5], iInputPositionVarying);"
"}"
, rgb_shader);
, rgb_shader.c_str());
std::unique_ptr<IntermediateShader> shader = make_shader(fragment_shader, true, true);
free(fragment_shader);
@@ -165,14 +181,12 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_source_shader(c
return shader;
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_luma_separation_shader()
{
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_luma_separation_shader() {
return make_shader(
"#version 150\n"
"in vec2 phaseAndAmplitudeVarying;"
"in vec2 inputPositionsVarying[11];"
"uniform vec4 weights[3];"
"out vec3 fragColour;"
@@ -180,44 +194,26 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_luma_separat
"void main(void)"
"{"
"vec4 samples[3] = vec4[]("
"vec4("
"texture(texID, inputPositionsVarying[0]).r,"
"texture(texID, inputPositionsVarying[1]).r,"
"texture(texID, inputPositionsVarying[2]).r,"
"texture(texID, inputPositionsVarying[3]).r"
"),"
"vec4("
"texture(texID, inputPositionsVarying[4]).r,"
"texture(texID, inputPositionsVarying[5]).r,"
"texture(texID, inputPositionsVarying[6]).r,"
"texture(texID, inputPositionsVarying[7]).r"
"),"
"vec4("
"texture(texID, inputPositionsVarying[8]).r,"
"texture(texID, inputPositionsVarying[9]).r,"
"texture(texID, inputPositionsVarying[10]).r,"
"0.0"
")"
"vec4 samples = vec4("
"texture(texID, inputPositionsVarying[3]).r,"
"texture(texID, inputPositionsVarying[4]).r,"
"texture(texID, inputPositionsVarying[5]).r,"
"texture(texID, inputPositionsVarying[6]).r"
");"
"float luminance = dot(samples, vec4(0.25));"
"float luminance = "
"dot(vec3("
"dot(samples[0], weights[0]),"
"dot(samples[1], weights[1]),"
"dot(samples[2], weights[2])"
"), vec3(1.0));"
"float chrominance = 0.5 * (samples[1].y - luminance) / phaseAndAmplitudeVarying.y;"
// define chroma to be whatever was here, minus luma
"float chrominance = 0.5 * (samples.z - luminance) / phaseAndAmplitudeVarying.y;"
"luminance /= (1.0 - phaseAndAmplitudeVarying.y);"
// split choma colours here, as the most direct place, writing out
// RGB = (luma, chroma.x, chroma.y)
"vec2 quadrature = vec2(cos(phaseAndAmplitudeVarying.x), -sin(phaseAndAmplitudeVarying.x));"
"fragColour = vec3(luminance, vec2(0.5) + (chrominance * quadrature));"
"}",false, false);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shader()
{
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shader() {
return make_shader(
"#version 150\n"
@@ -232,41 +228,18 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shade
"void main(void)"
"{"
"vec3 samples[] = vec3[]("
"texture(texID, inputPositionsVarying[0]).rgb,"
"texture(texID, inputPositionsVarying[1]).rgb,"
"texture(texID, inputPositionsVarying[2]).rgb,"
"texture(texID, inputPositionsVarying[3]).rgb,"
"texture(texID, inputPositionsVarying[4]).rgb,"
"texture(texID, inputPositionsVarying[5]).rgb,"
"texture(texID, inputPositionsVarying[6]).rgb,"
"texture(texID, inputPositionsVarying[7]).rgb,"
"texture(texID, inputPositionsVarying[8]).rgb,"
"texture(texID, inputPositionsVarying[9]).rgb,"
"texture(texID, inputPositionsVarying[10]).rgb"
"texture(texID, inputPositionsVarying[6]).rgb"
");"
"vec4 chromaChannel1[] = vec4[]("
"vec4(samples[0].g, samples[1].g, samples[2].g, samples[3].g),"
"vec4(samples[4].g, samples[5].g, samples[6].g, samples[7].g),"
"vec4(samples[8].g, samples[9].g, samples[10].g, 0.0)"
");"
"vec4 chromaChannel2[] = vec4[]("
"vec4(samples[0].b, samples[1].b, samples[2].b, samples[3].b),"
"vec4(samples[4].b, samples[5].b, samples[6].b, samples[7].b),"
"vec4(samples[8].b, samples[9].b, samples[10].b, 0.0)"
");"
"vec4 chromaChannel1 = vec4(samples[0].g, samples[1].g, samples[2].g, samples[3].g);"
"vec4 chromaChannel2 = vec4(samples[0].b, samples[1].b, samples[2].b, samples[3].b);"
"vec3 lumaChromaColour = vec3(samples[5].r,"
"dot(vec3("
"dot(chromaChannel1[0], weights[0]),"
"dot(chromaChannel1[1], weights[1]),"
"dot(chromaChannel1[2], weights[2])"
"), vec3(1.0)),"
"dot(vec3("
"dot(chromaChannel2[0], weights[0]),"
"dot(chromaChannel2[1], weights[1]),"
"dot(chromaChannel2[2], weights[2])"
"), vec3(1.0))"
"vec3 lumaChromaColour = vec3(samples[2].r,"
"dot(chromaChannel1, vec4(0.25)),"
"dot(chromaChannel2, vec4(0.25))"
");"
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 2.0, 2.0);"
@@ -274,54 +247,7 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shade
"}", false, false);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_luma_filter_shader()
{
return make_shader(
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"uniform vec4 weights[3];"
"out vec3 fragColour;"
"uniform sampler2D texID;"
"uniform mat3 lumaChromaToRGB;"
"void main(void)"
"{"
"vec3 samples[] = vec3[]("
"texture(texID, inputPositionsVarying[0]).rgb,"
"texture(texID, inputPositionsVarying[1]).rgb,"
"texture(texID, inputPositionsVarying[2]).rgb,"
"texture(texID, inputPositionsVarying[3]).rgb,"
"texture(texID, inputPositionsVarying[4]).rgb,"
"texture(texID, inputPositionsVarying[5]).rgb,"
"texture(texID, inputPositionsVarying[6]).rgb,"
"texture(texID, inputPositionsVarying[7]).rgb,"
"texture(texID, inputPositionsVarying[8]).rgb,"
"texture(texID, inputPositionsVarying[9]).rgb,"
"texture(texID, inputPositionsVarying[10]).rgb"
");"
"vec4 luminance[] = vec4[]("
"vec4(samples[0].r, samples[1].r, samples[2].r, samples[3].r),"
"vec4(samples[4].r, samples[5].r, samples[6].r, samples[7].r),"
"vec4(samples[8].r, samples[9].r, samples[10].r, 0.0)"
");"
"fragColour = vec3("
"dot(vec3("
"dot(luminance[0], weights[0]),"
"dot(luminance[1], weights[1]),"
"dot(luminance[2], weights[2])"
"), vec3(1.0)),"
"samples[5].gb"
");"
"}", false, false);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_filter_shader()
{
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_filter_shader() {
return make_shader(
"#version 150\n"
@@ -384,18 +310,15 @@ std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_filter_shader()
"}", false, false);
}
void IntermediateShader::set_output_size(unsigned int output_width, unsigned int output_height)
{
void IntermediateShader::set_output_size(unsigned int output_width, unsigned int output_height) {
set_uniform("outputTextureSize", (GLint)output_width, (GLint)output_height);
}
void IntermediateShader::set_source_texture_unit(GLenum unit)
{
void IntermediateShader::set_source_texture_unit(GLenum unit) {
set_uniform("texID", (GLint)(unit - GL_TEXTURE0));
}
void IntermediateShader::set_filter_coefficients(float sampling_rate, float cutoff_frequency)
{
void IntermediateShader::set_filter_coefficients(float sampling_rate, float cutoff_frequency) {
// The process below: the source texture will have bilinear filtering enabled; so by
// sampling at non-integral offsets from the centre the shader can get a weighted sum
// of two source pixels, then scale that once, to do two taps per sample. However
@@ -404,44 +327,45 @@ void IntermediateShader::set_filter_coefficients(float sampling_rate, float cuto
// Perform a linear search for the highest number of taps we can use with 11 samples.
GLfloat weights[12];
GLfloat offsets[5];
unsigned int taps = 21;
while(1)
{
unsigned int taps = 11;
// unsigned int taps = 21;
while(1) {
float coefficients[21];
SignalProcessing::FIRFilter luminance_filter(taps, sampling_rate, 0.0f, cutoff_frequency, SignalProcessing::FIRFilter::DefaultAttenuation);
luminance_filter.get_coefficients(coefficients);
int sample = 0;
int c = 0;
// int sample = 0;
// int c = 0;
memset(weights, 0, sizeof(float)*12);
memset(offsets, 0, sizeof(float)*5);
int halfSize = (taps >> 1);
while(c < halfSize && sample < 5)
{
offsets[sample] = (float)(halfSize - c);
if((coefficients[c] < 0.0f) == (coefficients[c+1] < 0.0f) && c+1 < (taps >> 1))
{
weights[sample] = coefficients[c] + coefficients[c+1];
offsets[sample] -= (coefficients[c+1] / weights[sample]);
c += 2;
}
else
{
weights[sample] = coefficients[c];
c++;
}
sample ++;
}
if(c == halfSize) // i.e. we finished combining inputs before we ran out of space
{
weights[sample] = coefficients[c];
for(int c = 0; c < sample; c++)
{
weights[sample+c+1] = weights[sample-c-1];
}
break;
for(int c = 0; c < taps; c++) {
if(c < 5) offsets[c] = (halfSize - c);
weights[c] = coefficients[c];
}
break;
// int halfSize = (taps >> 1);
// while(c < halfSize && sample < 5) {
// offsets[sample] = (float)(halfSize - c);
// if((coefficients[c] < 0.0f) == (coefficients[c+1] < 0.0f) && c+1 < (taps >> 1)) {
// weights[sample] = coefficients[c] + coefficients[c+1];
// offsets[sample] -= (coefficients[c+1] / weights[sample]);
// c += 2;
// } else {
// weights[sample] = coefficients[c];
// c++;
// }
// sample ++;
// }
// if(c == halfSize) { // i.e. we finished combining inputs before we ran out of space
// weights[sample] = coefficients[c];
// for(int c = 0; c < sample; c++) {
// weights[sample+c+1] = weights[sample-c-1];
// }
// break;
// }
taps -= 2;
}
@@ -449,19 +373,25 @@ void IntermediateShader::set_filter_coefficients(float sampling_rate, float cuto
set_uniform("offsets", 1, 5, offsets);
}
void IntermediateShader::set_separation_frequency(float sampling_rate, float colour_burst_frequency)
{
void IntermediateShader::set_separation_frequency(float sampling_rate, float colour_burst_frequency) {
set_filter_coefficients(sampling_rate, colour_burst_frequency);
}
void IntermediateShader::set_phase_cycles_per_sample(float phase_cycles_per_sample, bool extend_runs_to_full_cycle)
{
set_uniform("phaseCyclesPerTick", (GLfloat)phase_cycles_per_sample);
set_uniform("extension", extend_runs_to_full_cycle ? ceilf(1.0f / phase_cycles_per_sample) : 0.0f);
void IntermediateShader::set_extension(float extension) {
set_uniform("extension", extension);
}
void IntermediateShader::set_colour_conversion_matrices(float *fromRGB, float *toRGB)
{
void IntermediateShader::set_colour_conversion_matrices(float *fromRGB, float *toRGB) {
set_uniform_matrix("lumaChromaToRGB", 3, false, toRGB);
set_uniform_matrix("rgbToLumaChroma", 3, false, fromRGB);
}
void IntermediateShader::set_width_scalers(float input_scaler, float output_scaler) {
set_uniform("widthScalers", input_scaler, output_scaler);
}
void IntermediateShader::set_is_double_height(bool is_double_height, float input_offset, float output_offset) {
set_uniform("textureHeightDivisor", is_double_height ? 2.0f : 1.0f);
set_uniform("inputVerticalOffset", input_offset);
set_uniform("outputVerticalOffset", output_offset);
}

View File

@@ -25,13 +25,13 @@ public:
converting them to single-channel composite values using @c composite_shader if supplied
or @c rgb_shader and a reference composite conversion if @c composite_shader is @c nullptr.
*/
static std::unique_ptr<IntermediateShader> make_source_conversion_shader(const char *composite_shader, const char *rgb_shader);
static std::unique_ptr<IntermediateShader> make_source_conversion_shader(const std::string &composite_shader, const std::string &rgb_shader);
/*!
Constructs and returns an intermediate shader that will take runs from the inputPositions,
converting them to RGB values using @c rgb_shader.
*/
static std::unique_ptr<IntermediateShader> make_rgb_source_shader(const char *rgb_shader);
static std::unique_ptr<IntermediateShader> make_rgb_source_shader(const std::string &rgb_shader);
/*!
Constructs and returns an intermediate shader that will read composite samples from the R channel,
@@ -44,11 +44,6 @@ public:
*/
static std::unique_ptr<IntermediateShader> make_chroma_filter_shader();
/*!
Constructs and returns an intermediate shader that will filter R while passing through G and B unchanged.
*/
static std::unique_ptr<IntermediateShader> make_luma_filter_shader();
/*!
Constructs and returns an intermediate shader that will filter R, G and B.
*/
@@ -81,15 +76,26 @@ public:
geometry should be extended so that a complete colour cycle is included at both the beginning and end,
to occur upon the next `bind`.
*/
void set_phase_cycles_per_sample(float phase_cycles_per_sample, bool extend_runs_to_full_cycle);
void set_extension(float extension);
/*!
Queues setting the matrices that convert between RGB and chrominance/luminance to occur on the next `bind`.
*/
void set_colour_conversion_matrices(float *fromRGB, float *toRGB);
/*!
Sets the proportions of the input and output areas that should be considered the whole width — 1.0 means use all available
space, 0.5 means use half, etc.
*/
void set_width_scalers(float input_scaler, float output_scaler);
/*!
Sets source and target vertical offsets.
*/
void set_is_double_height(bool is_double_height, float input_offset = 0.0f, float output_offset = 0.0f);
private:
static std::unique_ptr<IntermediateShader> make_shader(const char *fragment_shader, bool use_usampler, bool input_is_inputPosition);
static std::unique_ptr<IntermediateShader> make_shader(const std::string &fragment_shader, bool use_usampler, bool input_is_inputPosition);
};
}

View File

@@ -14,16 +14,14 @@
using namespace OpenGL;
namespace {
const OpenGL::Shader::AttributeBinding bindings[] =
{
const OpenGL::Shader::AttributeBinding bindings[] = {
{"position", 0},
{"srcCoordinates", 1},
{nullptr}
};
}
std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_methods, const char *colour_expression, bool use_usampler)
{
std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_methods, const char *colour_expression, bool use_usampler) {
const char *sampler_type = use_usampler ? "usampler2D" : "sampler2D";
char *vertex_shader;
@@ -38,6 +36,8 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
"uniform vec2 positionConversion;"
"uniform vec2 scanNormal;"
"uniform %s texID;"
"uniform float inputScaler;"
"uniform int textureHeightDivisor;"
"out float lateralVarying;"
"out vec2 srcCoordinatesVarying;"
@@ -52,9 +52,10 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
"lateralVarying = lateral - 0.5;"
"vec2 vSrcCoordinates = vec2(x, vertical.y);"
"ivec2 textureSize = textureSize(texID, 0);"
"ivec2 textureSize = textureSize(texID, 0) * ivec2(1, textureHeightDivisor);"
"iSrcCoordinatesVarying = vSrcCoordinates;"
"srcCoordinatesVarying = vec2(vSrcCoordinates.x / textureSize.x, (vSrcCoordinates.y + 0.5) / textureSize.y);"
"srcCoordinatesVarying = vec2(inputScaler * vSrcCoordinates.x / textureSize.x, (vSrcCoordinates.y + 0.5) / textureSize.y);"
"srcCoordinatesVarying.x = srcCoordinatesVarying.x - mod(srcCoordinatesVarying.x, 1.0 / textureSize.x);"
"vec2 vPosition = vec2(x, vertical.x);"
"vec2 floatingPosition = (vPosition / positionConversion) + lateral * scanNormal;"
@@ -89,8 +90,7 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
return result;
}
void OutputShader::set_output_size(unsigned int output_width, unsigned int output_height, Outputs::CRT::Rect visible_area)
{
void OutputShader::set_output_size(unsigned int output_width, unsigned int output_height, Outputs::CRT::Rect visible_area) {
GLfloat outputAspectRatioMultiplier = ((float)output_width / (float)output_height) / (4.0f / 3.0f);
GLfloat bonusWidth = (outputAspectRatioMultiplier - 1.0f) * visible_area.size.width;
@@ -101,13 +101,11 @@ void OutputShader::set_output_size(unsigned int output_width, unsigned int outpu
set_uniform("boundsSize", (GLfloat)visible_area.size.width, (GLfloat)visible_area.size.height);
}
void OutputShader::set_source_texture_unit(GLenum unit)
{
void OutputShader::set_source_texture_unit(GLenum unit) {
set_uniform("texID", (GLint)(unit - GL_TEXTURE0));
}
void OutputShader::set_timing(unsigned int height_of_display, unsigned int cycles_per_line, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider)
{
void OutputShader::set_timing(unsigned int height_of_display, unsigned int cycles_per_line, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider) {
GLfloat scan_angle = atan2f(1.0f / (float)height_of_display, 1.0f);
GLfloat scan_normal[] = { -sinf(scan_angle), cosf(scan_angle)};
GLfloat multiplier = (float)cycles_per_line / ((float)height_of_display * (float)horizontal_scan_period);
@@ -117,3 +115,11 @@ void OutputShader::set_timing(unsigned int height_of_display, unsigned int cycle
set_uniform("scanNormal", scan_normal[0], scan_normal[1]);
set_uniform("positionConversion", (GLfloat)horizontal_scan_period, (GLfloat)vertical_scan_period / (GLfloat)vertical_period_divider);
}
void OutputShader::set_input_width_scaler(float input_scaler) {
set_uniform("inputScaler", input_scaler);
}
void OutputShader::set_origin_is_double_height(bool is_double_height) {
set_uniform("textureHeightDivisor", is_double_height ? 2 : 1);
}

View File

@@ -58,6 +58,16 @@ public:
to occur upon the next `bind`.
*/
void set_timing(unsigned int height_of_display, unsigned int cycles_per_line, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider);
/*!
*/
void set_origin_is_double_height(bool is_double_height);
/*!
Sets the proportion of the input area that should be considered the whole width — 1.0 means use all available
space, 0.5 means use half, etc.
*/
void set_input_width_scaler(float input_scaler);
};
}

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