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

Compare commits

...

263 Commits

Author SHA1 Message Date
Thomas Harte
2e1b245cd8 Merge pull request #758 from TomHarte/JasminDriveSelection
Switches Jasmin drive selection logic.
2020-02-16 21:21:41 -05:00
Thomas Harte
5400c47f07 Merge pull request #757 from TomHarte/ByteDrive
Updates Byte Drive implementation as per latest information.
2020-02-16 21:16:58 -05:00
Thomas Harte
4153442703 Switches Jasmin drive selection logic. 2020-02-16 21:15:16 -05:00
Thomas Harte
5e4b721e97 Updates Byte Drive implementation as per latest information. 2020-02-16 21:07:03 -05:00
Thomas Harte
aca41ac089 Merge pull request #756 from TomHarte/Stereo
Adds stereo sound processing.
2020-02-16 19:20:21 -05:00
Thomas Harte
01a883e669 Corrects fullscreen switch. 2020-02-16 19:07:13 -05:00
Thomas Harte
1e4356f83a Adds a sensible is_stereo to the MultiSpeaker. 2020-02-16 18:50:34 -05:00
Thomas Harte
545a6177bb Makes CompoundSource mono/stereo-aware. 2020-02-16 18:45:36 -05:00
Thomas Harte
50d356be2f Ensures all audio sources, including compound sources, announce whether they're stereo correctly. 2020-02-16 18:31:45 -05:00
Thomas Harte
9835e800ec Fixed: individual audio generators now either are or are not stereo. The speaker acts accordingly. 2020-02-16 18:28:03 -05:00
Thomas Harte
5242362f31 Slightly shuffles preprocessor use, the better for testing. 2020-02-16 17:53:26 -05:00
Thomas Harte
808e4e8537 Adds comment to explain channel allocations. 2020-02-16 15:04:52 -05:00
Thomas Harte
43740a4b2f Adds support for stereo output. 2020-02-16 14:14:10 -05:00
Thomas Harte
f99d672237 The macOS port now selects stereo output if appropriate. 2020-02-16 14:05:50 -05:00
Thomas Harte
337cb4fb86 Resolves implicit type conversion warnings. 2020-02-16 14:05:23 -05:00
Thomas Harte
90856a0e7a Adds mixdown/up capability to Speaker.
To deal with occasions when the host machine just always is either mono or stereo, and the emulated machine must cope.
2020-02-16 13:50:18 -05:00
Thomas Harte
ea1c8a3b81 Ensures the stereo audio queue is the same length (in time) as the mono. 2020-02-16 12:46:25 -05:00
Thomas Harte
d55d077a95 Fixes the input buffer partial-keep step in stereo. 2020-02-16 00:20:22 -05:00
Thomas Harte
f760a68173 Corrects stereo audio generation. 2020-02-16 00:19:49 -05:00
Thomas Harte
e66a3523b6 Makes some attempt at stereo support, with the Amstrad CPC being the test case. 2020-02-15 18:55:19 -05:00
Thomas Harte
89d6b85b83 Adds optional stereo output for the AY.
The real chip provides the three tone channels as separate outputs, so a variety of different mixings can exist.
2020-02-15 18:09:17 -05:00
Thomas Harte
e02d109864 Nudges the LowpassSpeaker towards supporting stereo generation. 2020-02-15 18:03:12 -05:00
Thomas Harte
743981e9ad Adds stereo output for SDL. 2020-02-15 17:23:40 -05:00
Thomas Harte
49b8e771b5 Adds the messaging that would allow a Speaker to output stereo, semantically. 2020-02-15 13:40:19 -05:00
Thomas Harte
dde672701f Merge pull request #755 from TomHarte/ExpliticLambdas
Tries to be less lazy with lambda captures.
2020-02-15 12:38:12 -05:00
Thomas Harte
9ca2d8f9f2 Tried to be less lazy with lambda captures.
This is primarily defensive.
2020-02-14 23:39:08 -05:00
Thomas Harte
fd786412aa Merge pull request #754 from TomHarte/AYVolume
Introduces more correct AY volume levels.
2020-02-14 23:38:17 -05:00
Thomas Harte
eb88c7cfba Allows up to half a second of hard processing. 2020-02-14 23:24:51 -05:00
Thomas Harte
e1892ff370 Resolves crash upon File -> New..., Cancel; also ensures slow performance can't equal no progression. 2020-02-14 23:16:44 -05:00
Thomas Harte
763159a6f6 More neatly ties volume level 0 to silence. 2020-02-14 23:16:10 -05:00
Thomas Harte
6810a6ee58 Adjusts the AY volume scale.
Hopefully more accurately to model the real thing.
2020-02-14 22:51:20 -05:00
Thomas Harte
65e6c3a9fe Merge pull request #753 from TomHarte/IconForReal
Experimentally introduces an application icon to the README
2020-02-13 23:39:06 -05:00
Thomas Harte
dcbbf988c1 Crops empty space around icon, and increases size slightly. 2020-02-13 23:27:33 -05:00
Thomas Harte
199cafebcf Attempts direct HTML tag for image sizing. 2020-02-13 23:25:26 -05:00
Thomas Harte
555d807d76 Attempt smaller icon. 2020-02-13 23:23:09 -05:00
Thomas Harte
003c6ad11b Fixes image link. 2020-02-13 23:21:48 -05:00
Thomas Harte
dc77d87427 Experiments with a README icon. 2020-02-13 23:20:19 -05:00
Thomas Harte
cfc44cf778 Merge pull request #752 from TomHarte/MacintoshBus
Reduces 8-bit memory access costs for the Macintosh.
2020-02-13 23:03:07 -05:00
Thomas Harte
3df99788ff Removes TODOs, as I think they're probably inappropriate. 2020-02-13 21:19:23 -05:00
Thomas Harte
3600d2d193 Starts switching towards a byte-oriented bus. 2020-02-13 21:14:13 -05:00
Thomas Harte
5f661adb7f Merge pull request #750 from TomHarte/CatchupCap
Avoids trying to paper over huge gaps in running time.
2020-02-12 23:57:01 -05:00
Thomas Harte
109d072cb6 Avoids trying to paper over huge gaps in running time. Also attempts to improve SDL shutdown reliability. 2020-02-12 23:47:04 -05:00
Thomas Harte
0c1c5a0ab8 Merge pull request #749 from TomHarte/DiskIndicator
Moves ownership of drives inside Disk::Controller.
2020-02-12 23:37:32 -05:00
Thomas Harte
e01c66fd65 Implements multidrive support. 2020-02-12 23:32:01 -05:00
Thomas Harte
9f32fa7f5b Resolves potential random RAM writes at startup. 2020-02-12 23:31:48 -05:00
Thomas Harte
91a3d42919 Ensures no DMA clocking whatsoever when asleep. 2020-02-12 23:23:42 -05:00
Thomas Harte
3cb6bbf771 Uses the union of all drive statuses to determine Drive::Controller's preferred clocking. 2020-02-12 22:28:42 -05:00
Thomas Harte
452e281009 Ensures what is currently the only drive is selected. 2020-02-11 22:13:13 -05:00
Thomas Harte
3da948db52 Eliminates local drive. They're not local any more. 2020-02-11 22:12:54 -05:00
Thomas Harte
0c2f77305f Eliminates dangling printf. 2020-02-11 22:12:30 -05:00
Thomas Harte
05bcd73f82 Attempts to pull drive ownership into DiskController.
For the sake of being more intelligent as to drive clocking, hopefully. And, eventually, to support multiple drive selection.
2020-02-11 21:59:13 -05:00
Thomas Harte
654f5b0478 Merge pull request #748 from TomHarte/SDLLatency
Introduces sync matching to the SDL version.
2020-02-10 23:38:24 -05:00
Thomas Harte
886d923e30 Attempts to permit fixed speed multiplication. 2020-02-10 23:30:32 -05:00
Thomas Harte
6624cb7a78 Corrects set_ram macro to act more like a function. 2020-02-10 23:19:47 -05:00
Thomas Harte
6147134423 Introduces frame locking for SDL. 2020-02-10 23:07:09 -05:00
Thomas Harte
bf6bc7c684 Adds speed control into the SDL build.
If I can just figure out how to manipulate OpenGL from the timer thread to SDL's satisfaction, this'll be as good as it probably gets via SDL.
2020-02-09 22:27:02 -05:00
Thomas Harte
0b0a7e241b Factors out the stuff of time warping. 2020-02-09 22:11:06 -05:00
Thomas Harte
705d14259c Experimentally switches to a 'high-resolution' clock for SDL. 2020-02-09 21:44:55 -05:00
Thomas Harte
f1cd35fa16 Merge pull request #746 from TomHarte/LatencyChop
Reduces latency in macOS, improves concurrency
2020-02-09 21:07:40 -05:00
Thomas Harte
6bda4034c6 Ensures no input data is dropped when changing output rates.
I think this 'completely' deals with the problem. At least until someone wants dynamic output buffer sizes or something like that. We'll see.
2020-02-09 19:14:25 -05:00
Thomas Harte
b04daca98e Picks a safer default construction. 2020-02-09 19:13:21 -05:00
Thomas Harte
85dcdbfe9e Adopts a log prefix for the Master System. 2020-02-09 19:12:44 -05:00
Thomas Harte
24340d1d4f Resolves fetch errors. 2020-02-09 17:04:49 -05:00
Thomas Harte
6ae42d07a7 Retains existing output when switching filter coefficients.
This eliminates an issue with dynamic rate matching and throwing away the beginnings of buffers.
2020-02-09 16:42:07 -05:00
Thomas Harte
2ea1e059a8 Softens swings in emulated machine speed. 2020-02-09 16:34:13 -05:00
Thomas Harte
b5d6126a2d Avoids unnecessary filter recalculation. 2020-02-09 16:32:32 -05:00
Thomas Harte
dac217c98c Defers starting the macOS audio queue, and attempts to restart it upon packet loss.
Hopefully forever to vanish permanent audio loss?
2020-02-08 22:08:27 -05:00
Thomas Harte
c26c8992ae Reintroduces joystick support; eliminates CSBestEffortUpdater. 2020-02-08 21:27:04 -05:00
Thomas Harte
b76a5870b3 Moves drawing into the next timer tick after retrace if sync locked.
... which should mean it occurs within 1/600th of a second of announced retrace, which I assume always will be less than the retrace period. So: does the frame buffer update during retrace.

This should completely eliminate tearing for machines that can be synced to the native output rate.
2020-02-08 18:07:13 -05:00
Thomas Harte
7c0f3bb237 Gets to slightly adjusting execution speed and matching up respective vertical syncs.
I probably still need to move the ->draw inline.
2020-02-08 18:01:48 -05:00
Thomas Harte
f615d096ca Switch to obtaining refresh periods ephemerally.
Which simplifies the necessary delegate protocol.
2020-02-08 15:03:18 -05:00
Thomas Harte
09132306e4 Removes two temporary debugging steps. 2020-02-06 23:35:23 -05:00
Thomas Harte
f95b07efea Continues edging towards raster racing and/or time warping. 2020-02-06 23:35:03 -05:00
Thomas Harte
14d976eecb Starts towards an implementation of time warping. 2020-02-04 23:08:54 -05:00
Thomas Harte
e1cbad0b6d Ensures new displayLinkDelegates get a nudge with the initial display link. 2020-02-04 23:08:25 -05:00
Thomas Harte
e7410b8ed8 Uses objective clock for updates. 2020-02-04 22:24:54 -05:00
Thomas Harte
5caf74b930 Corrects typo. 2020-02-04 22:24:37 -05:00
Thomas Harte
b41920990f Moves submit step to end of line, rather than end of scan. 2020-02-04 22:15:20 -05:00
Thomas Harte
709c229cd7 Gets a bit more explicit with ScanTarget documentation. 2020-02-04 20:19:46 -05:00
Thomas Harte
01fd1b1a2e Pulls out ticks as a macro constant.
For playing.
2020-02-03 22:44:39 -05:00
Thomas Harte
96769c52f6 Prevents an endless queue of backlogged updates. 2020-02-03 22:08:07 -05:00
Thomas Harte
cf9729c74f Takes a first shot at running OpenGL work throughout a frame.
Rather than en masse at the end. But it seems I've been lazy with my threading. Work to do!
2020-02-03 21:58:29 -05:00
Thomas Harte
0f2783075f Moves responsibility for timed updates to CSMachine, which gives the CSHighPrecisionTimer a shot. 2020-02-02 21:39:20 -05:00
Thomas Harte
256f4a6679 Fixes -invalidate: cancel the dispatch source, don't just suspend it, and wait until that is done. 2020-02-02 21:29:22 -05:00
Thomas Harte
0310f94f0c Merge pull request #745 from TomHarte/STMonochrome
Adds some amount of 1bpp/72Hz output support for the Atari ST.
2020-02-02 17:46:36 -05:00
Thomas Harte
085529ed72 Makes the shifter behaviour conform to its documentation. 2020-02-02 17:26:39 -05:00
Thomas Harte
8aabf1b374 Allows receivers of nullptr from begin_data to output any quantity of data. 2020-02-01 21:43:48 -05:00
Thomas Harte
ff39f71ca0 Eliminates meaningless constants from the Macintosh video's CRT setup. 2020-01-30 23:29:04 -05:00
Thomas Harte
019474300d Centralises responsibility for picking irrelevant numbers for a computer-style monitor. 2020-01-30 23:26:02 -05:00
Thomas Harte
af976b8b3d Eliminates modulus operation per ROM access. 2020-01-30 23:09:24 -05:00
Thomas Harte
f3db1a0c60 Eliminates ad hoc scheduling for delayed DE -> LOAD. 2020-01-29 22:50:22 -05:00
Thomas Harte
ce28213a5e [Mostly] unifies deferral process. 2020-01-29 22:46:08 -05:00
Thomas Harte
f9ce50d2bb Adds some debugging `asserts. 2020-01-29 22:45:44 -05:00
Thomas Harte
ee16095863 Withdraws advance_to_next; once it has to cope with simultaneous events it stops being faster than advance.
I could possibly try to deal with those at insertion time, but it'd get messy.
2020-01-29 22:45:10 -05:00
Thomas Harte
f0a6e0f3d5 Splits out the queue management stuff from queue+action.
Temporarily breaks ST video in the endeavour.
2020-01-29 22:18:41 -05:00
Thomas Harte
8c4fb0f688 Extends the DeferredQueue to allow out-of-order enqueing. 2020-01-29 21:49:52 -05:00
Thomas Harte
baa51853c4 Introduces RealTimeActor, providing the same interface as JustInTimeActor. 2020-01-29 21:26:15 -05:00
Thomas Harte
0e29c6b0ab On further reflection, I think events should occur after the running period.
I'm testing this now for sanity in 2/4bpp mode.
2020-01-28 23:26:37 -05:00
Thomas Harte
1b27eedf6b Ensure this can definitely never divide by 0. 2020-01-28 23:25:21 -05:00
Thomas Harte
8b1f183198 Reduce test duration much closer to two frames. 2020-01-28 23:25:01 -05:00
Thomas Harte
4766ec55fe Documents units. 2020-01-28 23:23:51 -05:00
Thomas Harte
c5edc879b6 Switches back to testing the monochrome monitor. 2020-01-28 22:12:57 -05:00
Thomas Harte
65309e60c4 Corrects sequence point generation by allowing for hsync_end != end of line. 2020-01-28 20:38:20 -05:00
Thomas Harte
5c4623e9f7 Adds a sequence-point test for 72Hz mode.
Which immediately appears to trigger the hsync issue I'm also seeing in manual testing.
2020-01-28 20:27:24 -05:00
Thomas Harte
2c0cab9e4d Adds line length latching as a line event. 2020-01-28 20:22:37 -05:00
Thomas Harte
d0117556d1 Reintroduces CSHighPrecisionTimer. 2020-01-28 20:09:46 -05:00
Thomas Harte
b1ff031b54 Fixes runtime test. 2020-01-27 23:41:08 -05:00
Thomas Harte
7e8405e68a Makes 72Hz horizontal sync independently relocatable.
... and moves and shortens it, based on my guesswork as to requirements.
2020-01-27 23:40:01 -05:00
Thomas Harte
c8fd00217d Resolves loss of horizontal resolution in 1bpp mode. 2020-01-27 23:08:28 -05:00
Thomas Harte
9d340599a6 Towards ST 1bpp support: puts vsync in an appropriate location, starts experimenting with proper CRT timings. 2020-01-27 23:00:30 -05:00
Thomas Harte
8e094598ca Merge pull request #744 from TomHarte/CRCTemplate
Better templates the CRC generator.
2020-01-27 21:54:32 -05:00
Thomas Harte
189122ab84 Fixes test units. 2020-01-27 20:35:58 -05:00
Thomas Harte
4b53f6a9f0 Renames T to the more-communicative IntType, adds some explicit constexpra. 2020-01-27 08:28:20 -05:00
Thomas Harte
561e149058 Better templates the CRC generator. 2020-01-27 00:03:01 -05:00
Thomas Harte
5975fc8e63 Merge pull request #738 from TomHarte/FinalOverride
Switches to [just] `final` where relevant to mark overrides.
2020-01-26 23:50:05 -05:00
Thomas Harte
7316a3aa88 Merge branch 'master' into FinalOverride 2020-01-26 23:42:25 -05:00
Thomas Harte
50be991415 Merge pull request #743 from TomHarte/macOSScreens
Ensures macOS window can properly be dragged between screens, Retina or not.
2020-01-26 23:33:50 -05:00
Thomas Harte
52e49439a6 Recreates display link upon a screen change.
Different screens may have different refresh rates, and I can find no guarantees about how Apple handles that.
2020-01-26 23:23:33 -05:00
Thomas Harte
6bcdd3177d Ensures that a change of screen issues a reshape. Just in case.
Thereby resolves display mis-sizing when dragging from a Retina display to a regular one, or vice versa.
2020-01-26 18:04:25 -05:00
Thomas Harte
83dbd257e1 Merge pull request #742 from TomHarte/SpeedMultiplier
Adds the option to run machines at a multiple of their real speeds.
2020-01-26 13:30:43 -05:00
Thomas Harte
b514756272 Adds the option to run machines at a multiple of their real speeds.
Exposed to SDL users only, for now.
2020-01-26 13:25:23 -05:00
Thomas Harte
7e4c13c43e Merge pull request #741 from TomHarte/ZX80RateReporting
Corrects ZX80, ZX81 and Amstrad CPC scan status scales.
2020-01-26 11:42:54 -05:00
Thomas Harte
79bb0f8222 Updates comment. 2020-01-26 11:36:06 -05:00
Thomas Harte
43bf6aca67 Corrects reported scan status for the Amstrad CPC. 2020-01-25 23:46:18 -05:00
Thomas Harte
03d23aad41 Corrects reported ZX80/81 scan status. 2020-01-25 23:27:09 -05:00
Thomas Harte
c398aa60c1 Merge pull request #739 from TomHarte/SDLThreadSafety
Resolves thread safety issues in SDK kiosk mode.
2020-01-25 14:56:29 -05:00
Thomas Harte
9666193c67 Pulls the call to .update out of the critical section. 2020-01-25 14:50:28 -05:00
Thomas Harte
3f57020b00 Resolves thread safety oversights in SDK kiosk mode. 2020-01-25 14:48:00 -05:00
Thomas Harte
294e09f275 All these 'override's can be 'final's.
At least for the purpose of being communicative. I doubt there's much to gain in terms of compiler output — the DiskImageHolder can avoid some virtual lookups but nothing else leaps out.
2020-01-23 22:57:51 -05:00
Thomas Harte
ba516387ba In all these instances, final => override. So no need to repeat myself. 2020-01-23 22:35:39 -05:00
Thomas Harte
2103e1b470 Merge pull request #737 from TomHarte/Multisync
Adds multisync monitor support to the Oric.
2020-01-23 22:20:34 -05:00
Thomas Harte
7bac439e95 Adds, and comments out, a useful temporary piece of debugging logging. 2020-01-23 22:14:22 -05:00
Thomas Harte
9136917f00 Enables the Oric for 50/60Hz mode switching, inventing PAL60 for the purpose. 2020-01-23 22:14:02 -05:00
Thomas Harte
6802318784 Removes audio_queue_.flush() calls; I don't think I really need to block. At least, not usually. 2020-01-23 20:13:16 -05:00
Thomas Harte
428d141bc9 Factors out the logic behind the Atari 2600's frequency switching. 2020-01-23 20:12:44 -05:00
Thomas Harte
a86fb33789 Ensures that the ColecoVision, MSX and Master System fully flush. 2020-01-22 22:57:16 -05:00
Thomas Harte
beefb70f75 Adds vertical sync as something that can be run_until. 2020-01-22 22:20:56 -05:00
Thomas Harte
3c6a00dc3c Breaks a potential deadlock. 2020-01-22 22:10:20 -05:00
Thomas Harte
8404409c0d Causes the Atari 2600 to obey normal flush semantics.
This stuff is going to become more important with run_until.
2020-01-22 22:05:51 -05:00
Thomas Harte
a5f285b4ce Enhances reported data. 2020-01-22 22:01:17 -05:00
Thomas Harte
9d97a294a7 Corrects the TMS' get_scaled_scan_status.
I think all platforms are now returning credible numbers.
2020-01-22 19:34:10 -05:00
Thomas Harte
56448373ae Splits one line into two, for the benefit of step debugging. 2020-01-22 19:32:23 -05:00
Thomas Harte
a71c5946f0 Ensures proper manipulation of scan_statuses, leading to the correct result out of a CRTMachine.
Possibly with the exception of the TMS, as I appear to have uncovered an unrelated issue there.
2020-01-21 22:28:25 -05:00
Thomas Harte
e7fff6e123 Minor step towards correct answers: divide by time_multiplier_. 2020-01-20 22:33:51 -05:00
Thomas Harte
82e5def7c4 Implements get_scan_status, but for scale being incorrect. 2020-01-20 21:58:34 -05:00
Thomas Harte
d97a073d1b Adds the necessary routine for all machines to be able to respond to get_scan_status.
They all just as the CRT, as all are currently based on the CRT. Which doesn't currently know the total clock rate it would need to in order properly to scale the answer to the question. Further thought coming.
2020-01-20 21:45:10 -05:00
Thomas Harte
e74f37d6ed Merge pull request #736 from TomHarte/RunUntil
Implements a nascent `run_until`
2020-01-20 17:48:13 -05:00
Thomas Harte
3aa2c297a2 Adds feedback to the best-effort updater; enables the Cocoa port for audio event requests. 2020-01-20 17:38:25 -05:00
Thomas Harte
290db67f09 Adds a forward route for event flags. Doesn't yet account for extra time expended. 2020-01-20 17:09:01 -05:00
Thomas Harte
4de121142b Adds a flags parameter to the BestEffortUpdater delegate.
On the Cocoa side, cuts Swift out of the update loop, as that seems merely to add code.
2020-01-20 16:21:53 -05:00
Thomas Harte
3c760e585a Switches to accepting a bit mask of events to run_until. 2020-01-20 16:08:21 -05:00
Thomas Harte
8adb2283b5 Rewrites the best-effort updater to try to get better thread affinity. 2020-01-20 13:38:46 -05:00
Thomas Harte
cb61e84868 Starts building out higher-level run_until functionality.
Specifically: you can now run until the next set of speaker samples has been delivered.
2020-01-20 12:12:23 -05:00
Thomas Harte
8349005c4b Adds CRTMachine::run_until, which will run until a condition is true.
I want to get to being able to say "run until the beam is 60% of the way down", "run until a new packet of audio has been delivered", etc.
2020-01-19 23:52:47 -05:00
Thomas Harte
a2847f4f8e Merge pull request #735 from TomHarte/Numeric
Eliminates homegrown factoring code
2020-01-19 23:32:50 -05:00
Thomas Harte
add3ebcb44 Updates Xcode project. 2020-01-19 23:23:44 -05:00
Thomas Harte
98daad45c7 Removers Factors.hpp; now this is a C++17 project. 2020-01-19 23:18:59 -05:00
Thomas Harte
1b4b6b0aee Renames: NumberTheory -> Numeric. 2020-01-19 23:14:35 -05:00
Thomas Harte
8f94da9daf Merge pull request #734 from TomHarte/FuzzyBits
Adds PCMSegementEventSource support for 'fuzzy' bits
2020-01-19 21:48:07 -05:00
Thomas Harte
357137918d Adds fuzzy but marking through the GetTrackWithSectors interface. 2020-01-19 21:41:10 -05:00
Thomas Harte
b0f7b762af Adds a possible const. 2020-01-19 21:40:30 -05:00
Thomas Harte
da3ee381f4 Attempts a full wiring up of fuzzy bits. 2020-01-19 21:20:21 -05:00
Thomas Harte
d27d14d2b0 Supplies fuzzy masks where specified. 2020-01-19 21:08:49 -05:00
Thomas Harte
b0326530d6 Allows fuzzy masks to be fed into the FM and MFM encoders. 2020-01-19 21:08:15 -05:00
Thomas Harte
c2bd5be51a This seems to be the proper interpretation of speeds? 2020-01-19 20:42:51 -05:00
Thomas Harte
84f5feab70 Properly flags up overloads. 2020-01-19 20:37:54 -05:00
Thomas Harte
4b2c68c3d3 Documents next. 2020-01-19 20:32:58 -05:00
Thomas Harte
5391a699a4 Adds the ability for a PCMSegment to maintain 'fuzzy' (i.e. random) bits. Implements an LFSR for bit generation.
I'm not necessarily happy with the idea of just shoving in a [pseudo-]random number generator in rather than emulating the proper process underneath, but for now I throw my arms up.
2020-01-19 20:09:11 -05:00
Thomas Harte
f3f8345e5e Corrects spelling mistake. 2020-01-19 20:05:52 -05:00
Thomas Harte
c755411636 Slightly improves comments. 2020-01-19 20:05:22 -05:00
Thomas Harte
f02759b76b Merge pull request #733 from TomHarte/STXTiming
Adds support for STX speed zones.
2020-01-19 12:41:31 -05:00
Thomas Harte
f34ddce28f Adds support for STX speed zones. 2020-01-19 12:38:33 -05:00
Thomas Harte
50348c9fe7 Adds the ability to substitute a target during encoding. 2020-01-19 12:11:56 -05:00
Thomas Harte
3bfeebf2a1 Merge pull request #732 from TomHarte/TraceFlag
Improves 68000 trace support
2020-01-18 23:17:21 -05:00
Thomas Harte
dca79ea10e Requires trace flag currently set. 2020-01-18 22:52:53 -05:00
Thomas Harte
b7fd4de32f Ensures a one-instruction latency on the trace flag. 2020-01-18 22:06:00 -05:00
Thomas Harte
78d08278ed Merge pull request #731 from TomHarte/ShifterSync
Improves STX track locating, plus minor WD emulation improvements
2020-01-18 14:59:07 -05:00
Thomas Harte
d4be052e76 Switch to matching fragments. 2020-01-18 14:18:59 -05:00
Thomas Harte
d674fd0e67 The WD uses only the low two bits for sector size. 2020-01-18 13:40:50 -05:00
Thomas Harte
229b7b36ed Merge branch 'master' into ShifterSync 2020-01-18 13:38:56 -05:00
Thomas Harte
8a8b8db5d1 Merge pull request #729 from TomHarte/JasminLED
Corrects Jasmin activity light.
2020-01-16 23:16:55 -05:00
Thomas Harte
d30f83871d Corrects Jasmin activity light. 2020-01-16 22:59:43 -05:00
Thomas Harte
1422f8a93a Merge pull request #728 from TomHarte/HardenedRuntime
Opts in for the hardened macOS runtime.
2020-01-16 22:27:58 -05:00
Thomas Harte
f0da75f8e9 Opts in for the hardened macOS runtime.
Seemingly with no ill effects.
2020-01-16 22:18:18 -05:00
Thomas Harte
cb8a7a4137 Merge pull request #727 from TomHarte/RDYs
Adds emulation of non-default types of floppy drive RDY output
2020-01-16 22:07:41 -05:00
Thomas Harte
efd684dc56 Opts the BD-500 in for modified Shugart RDY.
Hopefully this is correct. I'm presently mystified as to other options.
2020-01-16 21:34:57 -05:00
Thomas Harte
aeac6b5888 Allows the type of RDY signal to be specified. 2020-01-16 21:34:48 -05:00
Thomas Harte
9bb294a023 Merge pull request #726 from TomHarte/BD-DOS
Implements highly-provisional Byte Drive 500 support for the Oric.
2020-01-16 00:09:34 -05:00
Thomas Harte
1972ca00a4 Fixes quick-NTSC-avoidance fix.
I suspect this is very temporary, but here it is.
2020-01-16 00:01:16 -05:00
Thomas Harte
6a185a574a Adds the BD-500 to the Mac GUI. 2020-01-15 23:56:56 -05:00
Thomas Harte
c606931c93 Ensures a safe default-selected drive. 2020-01-15 23:56:44 -05:00
Thomas Harte
93cecf0882 Ensures no possible initial NTSC, removes printfs. 2020-01-15 23:47:45 -05:00
Thomas Harte
aac3d27c10 Adds activity indicators for the BD-500 and Jasmin.
Also slightly cleans up DiskController a little further.
2020-01-15 23:39:15 -05:00
Thomas Harte
99122efbbc Adds a slight cool-down period on end-of-rotation.
Along with the corresponding inactive transition of the ready signal.
2020-01-15 23:29:52 -05:00
Thomas Harte
30e856b9e4 Renames motor_is_on_ to motor_input_is_on_ to start to disambiguate the two things. 2020-01-15 23:16:25 -05:00
Thomas Harte
91fae86e73 Factors out paging, implements a bit more of the BD500.
That is, enough seemingly fully to work, if I force the drive to report ready.
2020-01-15 23:15:39 -05:00
Thomas Harte
f5c194386c Ties head load to ready.
BD-DOS no longer perpetually retries.
2020-01-14 23:45:36 -05:00
Thomas Harte
98f7662185 Force BASIC 1.0 for the BD-500. 2020-01-14 23:33:52 -05:00
Thomas Harte
62c3720c97 Adds status register and shout-outs on other address access. 2020-01-14 23:24:11 -05:00
Thomas Harte
6b08239199 Adapts slightly; it would seem that BD-DOS disks really fill up space. 2020-01-14 23:16:06 -05:00
Thomas Harte
f258fc2971 Adds enough of a BD500 for the boot sector seemingly to load. 2020-01-14 23:15:27 -05:00
Thomas Harte
6b84ae3095 Makes the Microdisc also a DiskController, and simplifies delegate interface. 2020-01-14 22:53:27 -05:00
Thomas Harte
5dd8c677f1 Factors out from the Jasmin the stuff that I'm going to need to repeat for the BD-500. 2020-01-14 22:23:00 -05:00
Thomas Harte
1cbcd5355f Adds a detector and enumerated Byte Drive 500 disk interface type. 2020-01-14 21:55:04 -05:00
Thomas Harte
9799250f2c Updates to mention the Jasmin's ROM and list the BD-DOS. 2020-01-14 21:54:37 -05:00
Thomas Harte
ecb5807ec0 Enssures STX interprets sector sizes correctly. 2020-01-14 21:35:37 -05:00
Thomas Harte
942986aadc Insures against badly-placed locations. 2020-01-13 22:49:12 -05:00
Thomas Harte
1f539822ee Adds better support for WD-esque false sync, improves STX track patching. 2020-01-13 22:19:48 -05:00
Thomas Harte
fab35b360a Ensure an encoder is created even if no sectors are placed. 2020-01-12 22:37:00 -05:00
Thomas Harte
80fcf5b5c0 Merge pull request #724 from TomHarte/STX2
Adds some support for the STX file format.
2020-01-12 22:28:50 -05:00
Thomas Harte
b3b2e18c4b Ensures head and track counts are reported accurately. 2020-01-12 22:23:34 -05:00
Thomas Harte
2d233b6358 Makes a more concrete attempt at track/sector combination. 2020-01-12 22:18:31 -05:00
Thomas Harte
83ed36eb08 Add missing #include. 2020-01-12 17:56:04 -05:00
Thomas Harte
89f4032ffc Merge branch 'master' into STX2 2020-01-12 17:55:19 -05:00
Thomas Harte
8c90ec4636 Merge pull request #725 from TomHarte/FasterDPLL
Improve DPLL implementation.
2020-01-12 17:54:55 -05:00
Thomas Harte
514141f8c5 Eliminates the optionality of a DPLL receiver. 2020-01-12 17:45:02 -05:00
Thomas Harte
8e3a618619 Corrects Mac build, shrinks default history [back] to 3 slots. 2020-01-12 17:33:34 -05:00
Thomas Harte
6df6af09de Remove dead .cpp. 2020-01-12 17:25:59 -05:00
Thomas Harte
f42655a0fc Promote DigitalPhaseLockedLoop to a template, simplify to O(1) add_pulse. 2020-01-12 17:25:21 -05:00
Thomas Harte
f81a7f0faf Ensures prefixes are MFM encoded and decoded. 2020-01-11 22:10:41 -05:00
Thomas Harte
2b4c924399 Makes an effort to locate address and data bodies within track.
"Not completely successful" would be the polite term.
2020-01-09 23:28:07 -05:00
Thomas Harte
64517a02b7 Adds code to deal with sector-free tracks. 2020-01-09 21:50:32 -05:00
Thomas Harte
b4befd57a9 Advances to being able to cope with STXs with no special features whatsoever.
Well, other than perhaps a broken data CRC. Fuzzy bits, timing differences and the stuff between sectors are all currently absent.
2020-01-09 21:03:01 -05:00
Thomas Harte
2c742a051e Merge pull request #723 from TomHarte/LSLTiming
Introduces a timing test for LSL. Which already passes.
2020-01-08 22:43:50 -05:00
Thomas Harte
6595f8f527 Introduces a timing test for LSL. Which already passes. 2020-01-08 22:35:28 -05:00
Thomas Harte
985b36da73 Starts towards STX support. 2020-01-07 23:21:32 -05:00
Thomas Harte
cdb31b1c2b Merge pull request #721 from TomHarte/AYZero
Ensures programmatic AY volume level 0 is completely off.
2020-01-05 22:50:42 -05:00
Thomas Harte
6a44936a7c Ensures programmatic volume level 0 is completely off. 2020-01-05 22:44:52 -05:00
Thomas Harte
45afb13a54 Merge pull request #720 from TomHarte/Jasmin
Adds emulation of the Oric's Jasmin disk interface.
2020-01-05 22:13:11 -05:00
Thomas Harte
3ced31043a Makes Jasmin autoboot optional, adds a Jasmin reset key, adds the Jasmin to File -> New... .
Also finally implements KeyNMI.
2020-01-05 21:57:33 -05:00
Thomas Harte
7361e7ec34 Fixed: the issue was failing to propagate motor control.
Also it seems to be incorrect to have the Jasmin paged at initial boot.
2020-01-05 21:35:20 -05:00
Thomas Harte
533729638c It seems like Jasmin paged in at boot, and button = page back in and reset works?
At least, that gets me a 'boot failed' error. Which is something.
2020-01-05 20:34:15 -05:00
Thomas Harte
9f30be1c13 Attempts to implement most of a Jasmin disk interface.
With one obvious omission: there's no way to start it? The real interface had a dedicated button, but I don't yet know what that button did. Research needed.
2020-01-05 20:05:55 -05:00
Thomas Harte
09289f383d Makes an effort to detect Jasmin disks, and flags the target if found.
It'll try the Jasmin only if the Microdisc check fails; this is because the latter is preferable — it automatically boots, and is much better tested in Clock Signal terms.
2020-01-05 18:44:58 -05:00
Thomas Harte
20b25ce866 Merge pull request #719 from TomHarte/CleanUps
Standardises on `read` and `write` for bus accesses.
2020-01-05 13:59:02 -05:00
Thomas Harte
c1bae49a92 Standardises on read and write for bus accesses.
Logic being: name these things for the bus action they model, not the effect they have.
2020-01-05 13:40:02 -05:00
Thomas Harte
b3f806201b Merge pull request #718 from TomHarte/BusErrorStack
Adds a test and fixes for the bus error stack frame.
2020-01-05 00:08:53 -05:00
Thomas Harte
9f2f547932 Adds and satisfies test on the function code word.
Thanks to ijor's "68000 Address and Bus Error Stack Frame" re: contents.
2020-01-04 23:58:07 -05:00
Thomas Harte
f0d5bbecf2 Introduces a test of stack contents after an address error.
Fixes: stacked PC, address of fault.
2020-01-04 23:22:07 -05:00
Thomas Harte
3d7ef43293 Merge pull request #717 from TomHarte/JSRA7
Fixes A7-relative JSRs.
2020-01-04 22:29:04 -05:00
Thomas Harte
4578b65487 Merge pull request #716 from TomHarte/PartialDecoding
Clarifies IO decoding, adds YM mirrors.
2020-01-04 22:23:22 -05:00
Thomas Harte
a28c52c250 Fixes A7-relative JSRs.
I completely withdraw my earlier statement re: the test cases.
2020-01-04 22:22:33 -05:00
Thomas Harte
e4349f5e05 Slightly clarifies logic. 2020-01-04 21:32:34 -05:00
Thomas Harte
7b2777ac08 Sorts cases into order; adds copious audio mirrors. 2020-01-04 21:06:21 -05:00
Thomas Harte
0fbcbfc61b Switches to more idiomatic address listing. 2020-01-04 20:35:47 -05:00
Thomas Harte
3ab4fb8c79 Enables an assumption of partial address decoding at the ACIA and PSG. 2020-01-04 17:27:55 -05:00
Thomas Harte
42a9585321 Merge pull request #715 from TomHarte/TestsRedux
After rerunning all tests, adds some notes on questionable results.
2020-01-04 16:41:01 -05:00
Thomas Harte
937cba8978 After rerunning all tests, adds some notes on questionable results.
Also renames a file. But no code changes are currently suggested, at least until I can learn more about DIVU/DIVS.
2020-01-04 16:31:45 -05:00
Thomas Harte
627d3c28ea Merge pull request #714 from TomHarte/STJoystick
Adds Joystick key code mode, ensures events aren't posted in interrogation mode.
2020-01-04 10:01:57 -05:00
Thomas Harte
19ddfae6d6 Adds Joystick key code mode, ensures events aren't posted in interrogation mode.
This should fix Turrican due to the latter change; I'm not aware of software that uses the former.
2020-01-04 09:45:59 -05:00
Thomas Harte
56ebd08af0 Merge pull request #713 from TomHarte/MULUS
Adds DIV and MUL tests, correcting some DIV flags.
2020-01-04 09:22:03 -05:00
Thomas Harte
7de1181213 Make a new guess at post-overflow DIV flags, based on tests.
Specifically: for DIVU, stick with the current guess of a fixed set. For DIVS, leave N and Z alone.
2020-01-03 23:44:49 -05:00
Thomas Harte
c7a5b054db There's no TODO here; overflow is always 0 for a 16x16 multiply.
... and the original 68000 doesn't support 32x32 multiplies.
2020-01-03 22:44:19 -05:00
Thomas Harte
ca12ba297b Renames all files that test multiple opcodes; introduces DIV and MUL tests. 2020-01-03 22:43:24 -05:00
Thomas Harte
7abf527084 Merge pull request #712 from TomHarte/MercsTweaks
Corrects vsync placement and BPP-change pipeline flushing.
2020-01-02 23:50:30 -05:00
Thomas Harte
c0b5bfe726 Ensure no possible return without value. 2020-01-02 23:43:53 -05:00
Thomas Harte
414b0cc234 Reintroduces sync write delay. 2020-01-02 23:36:11 -05:00
Thomas Harte
134e828336 Updates note to self. 2020-01-02 23:33:35 -05:00
Thomas Harte
455e831b87 Corrects bug whereby changing pixel mode mid-line will produce an improper amount of data. 2020-01-02 23:18:21 -05:00
Thomas Harte
617e0bada9 Adds some minor extra testing. Highly duplicative, to be honest. 2020-01-02 23:14:05 -05:00
Thomas Harte
7dea99b1cc Update comment, for sense. 2020-01-02 23:13:12 -05:00
Thomas Harte
42ccf48966 Judging by Pompey Pirates Menu 88, vsync should occur a line earlier, ending during line 0. 2020-01-02 20:16:28 -05:00
Thomas Harte
2f8078db22 Switches to should_log as a global when I'm hacking about. 2020-01-02 20:15:48 -05:00
223 changed files with 6929 additions and 2049 deletions

View File

@@ -22,7 +22,7 @@ namespace Dynamic {
class ConfidenceCounter: public ConfidenceSource {
public:
/*! @returns The computed probability, based on the history of events. */
float get_confidence() override;
float get_confidence() final;
/*! Records an event that implies this is the appropriate class: pushes probability up towards 1.0. */
void add_hit();

View File

@@ -32,7 +32,7 @@ class ConfidenceSummary: public ConfidenceSource {
const std::vector<float> &weights);
/*! @returns The weighted sum of all sources. */
float get_confidence() override;
float get_confidence() final;
private:
std::vector<ConfidenceSource *> sources_;

View File

@@ -60,12 +60,19 @@ void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target)
if(crt_machine) crt_machine->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus MultiCRTMachine::get_scan_status() const {
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
if(crt_machine) crt_machine->get_scan_status();
return Outputs::Display::ScanStatus();
}
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
return speaker_;
}
void MultiCRTMachine::run_for(Time::Seconds duration) {
perform_parallel([=](::CRTMachine::Machine *machine) {
perform_parallel([duration](::CRTMachine::Machine *machine) {
if(machine->get_confidence() >= 0.01f) machine->run_for(duration);
});
@@ -75,7 +82,7 @@ void MultiCRTMachine::run_for(Time::Seconds duration) {
void MultiCRTMachine::did_change_machine_order() {
if(scan_target_) scan_target_->will_change_owner();
perform_serial([=](::CRTMachine::Machine *machine) {
perform_serial([](::CRTMachine::Machine *machine) {
machine->set_scan_target(nullptr);
});
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();

View File

@@ -53,12 +53,13 @@ class MultiCRTMachine: public CRTMachine::Machine {
}
// Below is the standard CRTMachine::Machine interface; see there for documentation.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
Outputs::Speaker::Speaker *get_speaker() override;
void run_for(Time::Seconds duration) override;
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final;
Outputs::Display::ScanStatus get_scan_status() const final;
Outputs::Speaker::Speaker *get_speaker() final;
void run_for(Time::Seconds duration) final;
private:
void run_for(const Cycles cycles) override {}
void run_for(const Cycles cycles) final {}
const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines_;
std::recursive_mutex &machines_mutex_;
std::vector<Concurrency::AsyncTaskQueue> queues_;

View File

@@ -28,10 +28,10 @@ class MultiConfigurable: public Configurable::Device {
MultiConfigurable(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
// Below is the standard Configurable::Device interface; see there for documentation.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override;
void set_selections(const Configurable::SelectionSet &selection_by_option) override;
Configurable::SelectionSet get_accurate_selections() override;
Configurable::SelectionSet get_user_friendly_selections() override;
std::vector<std::unique_ptr<Configurable::Option>> get_options() final;
void set_selections(const Configurable::SelectionSet &selection_by_option) final;
Configurable::SelectionSet get_accurate_selections() final;
Configurable::SelectionSet get_user_friendly_selections() final;
private:
std::vector<Configurable::Device *> devices_;

View File

@@ -25,7 +25,7 @@ class MultiJoystick: public Inputs::Joystick {
}
}
std::vector<Input> &get_inputs() override {
std::vector<Input> &get_inputs() final {
if(inputs.empty()) {
for(const auto &joystick: joysticks_) {
std::vector<Input> joystick_inputs = joystick->get_inputs();
@@ -40,19 +40,19 @@ class MultiJoystick: public Inputs::Joystick {
return inputs;
}
void set_input(const Input &digital_input, bool is_active) override {
void set_input(const Input &digital_input, bool is_active) final {
for(const auto &joystick: joysticks_) {
joystick->set_input(digital_input, is_active);
}
}
void set_input(const Input &digital_input, float value) override {
void set_input(const Input &digital_input, float value) final {
for(const auto &joystick: joysticks_) {
joystick->set_input(digital_input, value);
}
}
void reset_all_inputs() override {
void reset_all_inputs() final {
for(const auto &joystick: joysticks_) {
joystick->reset_all_inputs();
}

View File

@@ -28,7 +28,7 @@ class MultiJoystickMachine: public JoystickMachine::Machine {
MultiJoystickMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
// Below is the standard JoystickMachine::Machine interface; see there for documentation.
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override;
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final;
private:
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;

View File

@@ -32,10 +32,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
public:
MultiKeyboard(const std::vector<::KeyboardMachine::Machine *> &machines);
void set_key_pressed(Key key, char value, bool is_pressed) override;
void reset_all_keys() override;
const std::set<Key> &observed_keys() override;
bool is_exclusive() override;
void set_key_pressed(Key key, char value, bool is_pressed) final;
void reset_all_keys() final;
const std::set<Key> &observed_keys() final;
bool is_exclusive() final;
private:
const std::vector<::KeyboardMachine::Machine *> &machines_;
@@ -48,10 +48,10 @@ class MultiKeyboardMachine: public KeyboardMachine::Machine {
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
// Below is the standard KeyboardMachine::Machine interface; see there for documentation.
void clear_all_keys() override;
void set_key_state(uint16_t key, bool is_pressed) override;
void type_string(const std::string &) override;
Inputs::Keyboard &get_keyboard() override;
void clear_all_keys() final;
void set_key_state(uint16_t key, bool is_pressed) final;
void type_string(const std::string &) final;
Inputs::Keyboard &get_keyboard() final;
};
}

View File

@@ -29,7 +29,7 @@ struct MultiMediaTarget: public MediaTarget::Machine {
MultiMediaTarget(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);
// Below is the standard MediaTarget::Machine interface; see there for documentation.
bool insert_media(const Analyser::Static::Media &media) override;
bool insert_media(const Analyser::Static::Media &media) final;
private:
std::vector<MediaTarget::Machine *> targets_;

View File

@@ -37,12 +37,23 @@ float MultiSpeaker::get_ideal_clock_rate_in_range(float minimum, float maximum)
return ideal / static_cast<float>(speakers_.size());
}
void MultiSpeaker::set_output_rate(float cycles_per_second, int buffer_size) {
void MultiSpeaker::set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) {
stereo_output_ = stereo;
for(const auto &speaker: speakers_) {
speaker->set_output_rate(cycles_per_second, buffer_size);
speaker->set_computed_output_rate(cycles_per_second, buffer_size, stereo);
}
}
bool MultiSpeaker::get_is_stereo() {
// Return as stereo if any subspeaker is stereo.
for(const auto &speaker: speakers_) {
if(speaker->get_is_stereo()) {
return true;
}
}
return false;
}
void MultiSpeaker::set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) {
delegate_ = delegate;
}
@@ -53,7 +64,7 @@ void MultiSpeaker::speaker_did_complete_samples(Speaker *speaker, const std::vec
std::lock_guard<std::mutex> lock_guard(front_speaker_mutex_);
if(speaker != front_speaker_) return;
}
delegate_->speaker_did_complete_samples(this, buffer);
did_complete_samples(this, buffer, stereo_output_);
}
void MultiSpeaker::speaker_did_change_input_clock(Speaker *speaker) {

View File

@@ -39,18 +39,21 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
// Below is the standard Outputs::Speaker::Speaker interface; see there for documentation.
float get_ideal_clock_rate_in_range(float minimum, float maximum) override;
void set_output_rate(float cycles_per_second, int buffer_size) override;
void set_computed_output_rate(float cycles_per_second, int buffer_size, bool stereo) override;
void set_delegate(Outputs::Speaker::Speaker::Delegate *delegate) override;
bool get_is_stereo() override;
private:
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) override;
void speaker_did_change_input_clock(Speaker *speaker) override;
void speaker_did_complete_samples(Speaker *speaker, const std::vector<int16_t> &buffer) final;
void speaker_did_change_input_clock(Speaker *speaker) final;
MultiSpeaker(const std::vector<Outputs::Speaker::Speaker *> &speakers);
std::vector<Outputs::Speaker::Speaker *> speakers_;
Outputs::Speaker::Speaker *front_speaker_ = nullptr;
Outputs::Speaker::Speaker::Delegate *delegate_ = nullptr;
std::mutex front_speaker_mutex_;
bool stereo_output_ = false;
};
}

View File

@@ -50,17 +50,17 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines);
MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
Activity::Source *activity_source() override;
Configurable::Device *configurable_device() override;
CRTMachine::Machine *crt_machine() override;
JoystickMachine::Machine *joystick_machine() override;
MouseMachine::Machine *mouse_machine() override;
KeyboardMachine::Machine *keyboard_machine() override;
MediaTarget::Machine *media_target() override;
void *raw_pointer() override;
Activity::Source *activity_source() final;
Configurable::Device *configurable_device() final;
CRTMachine::Machine *crt_machine() final;
JoystickMachine::Machine *joystick_machine() final;
MouseMachine::Machine *mouse_machine() final;
KeyboardMachine::Machine *keyboard_machine() final;
MediaTarget::Machine *media_target() final;
void *raw_pointer() final;
private:
void multi_crt_did_run_machines() override;
void multi_crt_did_run_machines() final;
std::vector<std::unique_ptr<DynamicMachine>> machines_;
std::recursive_mutex machines_mutex_;

View File

@@ -10,7 +10,7 @@
#include "../../../Storage/Disk/Controller/DiskController.hpp"
#include "../../../Storage/Disk/Encodings/MFM/Parser.hpp"
#include "../../../NumberTheory/CRC.hpp"
#include "../../../Numeric/CRC.hpp"
#include <algorithm>

View File

@@ -10,7 +10,7 @@
#include <deque>
#include "../../../NumberTheory/CRC.hpp"
#include "../../../Numeric/CRC.hpp"
#include "../../../Storage/Tape/Parsers/Acorn.hpp"
using namespace Analyser::Static::Acorn;

View File

@@ -19,12 +19,10 @@ using namespace Analyser::Static::Commodore;
class CommodoreGCRParser: public Storage::Disk::Controller {
public:
std::shared_ptr<Storage::Disk::Drive> drive;
CommodoreGCRParser() : Storage::Disk::Controller(4000000), shift_register_(0), track_(1) {
drive = std::make_shared<Storage::Disk::Drive>(4000000, 300, 2);
set_drive(drive);
drive->set_motor_on(true);
emplace_drive(4000000, 300, 2);
set_drive(1);
get_drive().set_motor_on(true);
}
struct Sector {
@@ -61,6 +59,10 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
return get_sector(sector);
}
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk) {
get_drive().set_disk(disk);
}
private:
unsigned int shift_register_;
int index_count_;
@@ -170,7 +172,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller {
std::vector<File> Analyser::Static::Commodore::GetFiles(const std::shared_ptr<Storage::Disk::Disk> &disk) {
std::vector<File> files;
CommodoreGCRParser parser;
parser.drive->set_disk(disk);
parser.set_disk(disk);
// find any sector whatsoever to establish the current track
std::shared_ptr<CommodoreGCRParser::Sector> sector;

View File

@@ -20,7 +20,9 @@
using namespace Analyser::Static::Oric;
static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
namespace {
int score(const Analyser::Static::MOS6502::Disassembly &disassembly, const std::set<uint16_t> &rom_functions, const std::set<uint16_t> &variable_locations) {
int score = 0;
for(const auto address : disassembly.outward_calls) score += (rom_functions.find(address) != rom_functions.end()) ? 1 : -1;
@@ -30,7 +32,7 @@ static int Score(const Analyser::Static::MOS6502::Disassembly &disassembly, cons
return score;
}
static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
int basic10_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
const std::set<uint16_t> rom_functions = {
0x0228, 0x022b,
0xc3ca, 0xc3f8, 0xc448, 0xc47c, 0xc4b5, 0xc4e3, 0xc4e0, 0xc524, 0xc56f, 0xc5a2, 0xc5f8, 0xc60a, 0xc6a5, 0xc6de, 0xc719, 0xc738,
@@ -51,10 +53,10 @@ static int Basic10Score(const Analyser::Static::MOS6502::Disassembly &disassembl
0x0228, 0x0229, 0x022a, 0x022b, 0x022c, 0x022d, 0x0230
};
return Score(disassembly, rom_functions, variable_locations);
return score(disassembly, rom_functions, variable_locations);
}
static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
int basic11_score(const Analyser::Static::MOS6502::Disassembly &disassembly) {
const std::set<uint16_t> rom_functions = {
0x0238, 0x023b, 0x023e, 0x0241, 0x0244, 0x0247,
0xc3c6, 0xc3f4, 0xc444, 0xc47c, 0xc4a8, 0xc4d3, 0xc4e0, 0xc524, 0xc55f, 0xc592, 0xc5e8, 0xc5fa, 0xc692, 0xc6b3, 0xc6ee, 0xc70d,
@@ -76,10 +78,10 @@ static int Basic11Score(const Analyser::Static::MOS6502::Disassembly &disassembl
0x0244, 0x0245, 0x0246, 0x0247, 0x0248, 0x0249, 0x024a, 0x024b, 0x024c
};
return Score(disassembly, rom_functions, variable_locations);
return score(disassembly, rom_functions, variable_locations);
}
static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
bool is_microdisc(Storage::Encodings::MFM::Parser &parser) {
/*
The Microdisc boot sector is sector 2 of track 0 and contains a 23-byte signature.
*/
@@ -100,6 +102,49 @@ static bool IsMicrodisc(Storage::Encodings::MFM::Parser &parser) {
return !std::memcmp(signature, first_sample.data(), sizeof(signature));
}
bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start, uint16_t range_end) {
/*
Both the Jasmin and BD-DOS boot sectors are sector 1 of track 0 and are loaded at $400;
use disassembly to test for likely matches.
*/
Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1);
if(!sector) return false;
if(sector->samples.empty()) return false;
// Take a copy of the first sampling, and keep only the final 256 bytes (assuming at least that many were found).
std::vector<uint8_t> first_sample = sector->samples[0];
if(first_sample.size() < 256) return false;
if(first_sample.size() > 256) {
first_sample.erase(first_sample.end() - 256, first_sample.end());
}
// Grab a disassembly.
const auto disassembly =
Analyser::Static::MOS6502::Disassemble(first_sample, Analyser::Static::Disassembler::OffsetMapper(0x400), {0x400});
// Check for references to the Jasmin registers.
int register_hits = 0;
for(auto list : {disassembly.external_stores, disassembly.external_loads, disassembly.external_modifies}) {
for(auto address : list) {
register_hits += (address >= range_start && address <= range_end);
}
}
// Arbitrary, sure, but as long as at least two accesses to the requested register range are found, accept this.
return register_hits >= 2;
}
bool is_jasmin(Storage::Encodings::MFM::Parser &parser) {
return is_400_loader(parser, 0x3f4, 0x3ff);
}
bool is_bd500(Storage::Encodings::MFM::Parser &parser) {
return is_400_loader(parser, 0x310, 0x323);
}
}
Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms) {
auto target = std::make_unique<Target>();
target->machine = Machine::Oric;
@@ -115,12 +160,10 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
for(const auto &file : tape_files) {
if(file.data_type == File::MachineCode) {
std::vector<uint16_t> entry_points = {file.starting_address};
Analyser::Static::MOS6502::Disassembly disassembly =
const Analyser::Static::MOS6502::Disassembly disassembly =
Analyser::Static::MOS6502::Disassemble(file.data, Analyser::Static::Disassembler::OffsetMapper(file.starting_address), entry_points);
int basic10_score = Basic10Score(disassembly);
int basic11_score = Basic11Score(disassembly);
if(basic10_score > basic11_score) basic10_votes++; else basic11_votes++;
if(basic10_score(disassembly) > basic11_score(disassembly)) ++basic10_votes; else ++basic11_votes;
}
}
@@ -130,12 +173,22 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med
}
if(!media.disks.empty()) {
// Only the Microdisc is emulated right now, so accept only disks that it can boot from.
// 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc,
// Jasmin and BD-DOS formats here.
for(auto &disk: media.disks) {
Storage::Encodings::MFM::Parser parser(true, disk);
if(IsMicrodisc(parser)) {
if(is_microdisc(parser)) {
target->disk_interface = Target::DiskInterface::Microdisc;
target->media.disks.push_back(disk);
} else if(is_jasmin(parser)) {
target->disk_interface = Target::DiskInterface::Jasmin;
target->should_start_jasmin = true;
target->media.disks.push_back(disk);
} else if(is_bd500(parser)) {
target->disk_interface = Target::DiskInterface::BD500;
target->media.disks.push_back(disk);
target->rom = Target::ROM::BASIC10;
}
}
}

View File

@@ -26,12 +26,15 @@ struct Target: public ::Analyser::Static::Target {
enum class DiskInterface {
Microdisc,
Pravetz,
Jasmin,
BD500,
None
};
ROM rom = ROM::BASIC11;
DiskInterface disk_interface = DiskInterface::None;
std::string loading_command;
bool should_start_jasmin = false;
};
}

View File

@@ -47,6 +47,7 @@
#include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp"
#include "../../Storage/Disk/DiskImage/Formats/SSD.hpp"
#include "../../Storage/Disk/DiskImage/Formats/ST.hpp"
#include "../../Storage/Disk/DiskImage/Formats/STX.hpp"
#include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp"
// Mass Storage Devices (i.e. usually, hard disks)
@@ -147,6 +148,7 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::
Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS
Format("ssd", result.disks, Disk::DiskImageHolder<Storage::Disk::SSD>, TargetPlatform::Acorn) // SSD
Format("st", result.disks, Disk::DiskImageHolder<Storage::Disk::ST>, TargetPlatform::AtariST) // ST
Format("stx", result.disks, Disk::DiskImageHolder<Storage::Disk::STX>, TargetPlatform::AtariST) // STX
Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore)
Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric)
Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX

View File

@@ -13,62 +13,68 @@
#include <vector>
/*!
A DeferredQueue maintains a list of ordered actions and the times at which
they should happen, and divides a total execution period up into the portions
that occur between those actions, triggering each action when it is reached.
Provides the logic to insert into and traverse a list of future scheduled items.
*/
template <typename TimeUnit> class DeferredQueue {
public:
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
DeferredQueue(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
/*!
Schedules @c action to occur in @c delay units of time.
Actions must be scheduled in the order they will occur. It is undefined behaviour
to schedule them out of order.
*/
void defer(TimeUnit delay, const std::function<void(void)> &action) {
pending_actions_.emplace_back(delay, action);
}
/*!
Runs for @c length units of time.
The constructor-supplied target will be called with one or more periods that add up to @c length;
any scheduled actions will be called between periods.
*/
void run_for(TimeUnit length) {
// If there are no pending actions, just run for the entire length.
// This should be the normal branch.
if(pending_actions_.empty()) {
target_(length);
// Apply immediately if there's no delay (or a negative delay).
if(delay <= TimeUnit(0)) {
action();
return;
}
// Divide the time to run according to the pending actions.
while(length > TimeUnit(0)) {
TimeUnit next_period = pending_actions_.empty() ? length : std::min(length, pending_actions_[0].delay);
target_(next_period);
length -= next_period;
if(!pending_actions_.empty()) {
// Otherwise enqueue, having subtracted the delay for any preceding events,
// and subtracting from the subsequent, if any.
auto insertion_point = pending_actions_.begin();
while(insertion_point != pending_actions_.end() && insertion_point->delay < delay) {
delay -= insertion_point->delay;
++insertion_point;
}
if(insertion_point != pending_actions_.end()) {
insertion_point->delay -= delay;
}
off_t performances = 0;
for(auto &action: pending_actions_) {
action.delay -= next_period;
if(!action.delay) {
action.action();
++performances;
}
}
if(performances) {
pending_actions_.erase(pending_actions_.begin(), pending_actions_.begin() + performances);
pending_actions_.emplace(insertion_point, delay, action);
} else {
pending_actions_.emplace_back(delay, action);
}
}
/*!
@returns The amount of time until the next enqueued action will occur,
or TimeUnit(-1) if the queue is empty.
*/
TimeUnit time_until_next_action() {
if(pending_actions_.empty()) return TimeUnit(-1);
return pending_actions_.front().delay;
}
/*!
Advances the queue the specified amount of time, performing any actions it reaches.
*/
void advance(TimeUnit time) {
auto erase_iterator = pending_actions_.begin();
while(erase_iterator != pending_actions_.end()) {
erase_iterator->delay -= time;
if(erase_iterator->delay <= TimeUnit(0)) {
time = -erase_iterator->delay;
erase_iterator->action();
++erase_iterator;
} else {
break;
}
}
if(erase_iterator != pending_actions_.begin()) {
pending_actions_.erase(pending_actions_.begin(), erase_iterator);
}
}
private:
std::function<void(TimeUnit)> target_;
// The list of deferred actions.
struct DeferredAction {
TimeUnit delay;
@@ -79,4 +85,40 @@ template <typename TimeUnit> class DeferredQueue {
std::vector<DeferredAction> pending_actions_;
};
/*!
A DeferredQueue maintains a list of ordered actions and the times at which
they should happen, and divides a total execution period up into the portions
that occur between those actions, triggering each action when it is reached.
This list is efficient only for short queues.
*/
template <typename TimeUnit> class DeferredQueuePerformer: public DeferredQueue<TimeUnit> {
public:
/// Constructs a DeferredQueue that will call target(period) in between deferred actions.
DeferredQueuePerformer(std::function<void(TimeUnit)> &&target) : target_(std::move(target)) {}
/*!
Runs for @c length units of time.
The constructor-supplied target will be called with one or more periods that add up to @c length;
any scheduled actions will be called between periods.
*/
void run_for(TimeUnit length) {
auto time_to_next = DeferredQueue<TimeUnit>::time_until_next_action();
while(time_to_next != TimeUnit(-1) && time_to_next <= length) {
target_(time_to_next);
length -= time_to_next;
DeferredQueue<TimeUnit>::advance(time_to_next);
}
DeferredQueue<TimeUnit>::advance(length);
target_(length);
// TODO: optimise this to avoid the multiple std::vector deletes. Find a neat way to expose that solution, maybe?
}
private:
std::function<void(TimeUnit)> target_;
};
#endif /* DeferredQueue_h */

View File

@@ -43,6 +43,13 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
return &object_;
}
/// Acts exactly as per the standard ->, but preserves constness.
forceinline const T *operator->() const {
auto non_const_this = const_cast<JustInTimeActor<T, multiplier, divider, LocalTimeScale, TargetTimeScale> *>(this);
non_const_this->flush();
return &object_;
}
/// Returns a pointer to the included object without flushing time.
forceinline T *last_valid() {
return &object_;
@@ -53,7 +60,8 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
if(!is_flushed_) {
is_flushed_ = true;
if constexpr (divider == 1) {
object_.run_for(time_since_update_.template flush<TargetTimeScale>());
const auto duration = time_since_update_.template flush<TargetTimeScale>();
object_.run_for(duration);
} else {
const auto duration = time_since_update_.template divide<TargetTimeScale>(LocalTimeScale(divider));
if(duration > TargetTimeScale(0))
@@ -68,6 +76,48 @@ template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = H
bool is_flushed_ = true;
};
/*!
A RealTimeActor presents the same interface as a JustInTimeActor but doesn't defer work.
Time added will be performed immediately.
Its primary purpose is to allow consumers to remain flexible in their scheduling.
*/
template <class T, int multiplier = 1, int divider = 1, class LocalTimeScale = HalfCycles, class TargetTimeScale = LocalTimeScale> class RealTimeActor {
public:
template<typename... Args> RealTimeActor(Args&&... args) : object_(std::forward<Args>(args)...) {}
forceinline void operator += (const LocalTimeScale &rhs) {
if constexpr (multiplier == 1 && divider == 1) {
object_.run_for(TargetTimeScale(rhs));
return;
}
if constexpr (multiplier == 1) {
accumulated_time_ += rhs;
} else {
accumulated_time_ += rhs * multiplier;
}
if constexpr (divider == 1) {
const auto duration = accumulated_time_.template flush<TargetTimeScale>();
object_.run_for(duration);
} else {
const auto duration = accumulated_time_.template divide<TargetTimeScale>(LocalTimeScale(divider));
if(duration > TargetTimeScale(0))
object_.run_for(duration);
}
}
forceinline T *operator->() { return &object_; }
forceinline const T *operator->() const { return &object_; }
forceinline T *last_valid() { return &object_; }
forceinline void flush() {}
private:
T object_;
LocalTimeScale accumulated_time_;
};
/*!
A AsyncJustInTimeActor acts like a JustInTimeActor but additionally contains an AsyncTaskQueue.
Any time the amount of accumulated time crosses a threshold provided at construction time,

View File

@@ -0,0 +1,88 @@
//
// ScanSynchroniser.hpp
// Clock Signal
//
// Created by Thomas Harte on 09/02/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef ScanSynchroniser_h
#define ScanSynchroniser_h
#include "../Outputs/ScanTarget.hpp"
#include <cmath>
namespace Time {
/*!
Where an emulated machine is sufficiently close to a host machine's frame rate that a small nudge in
its speed multiplier will bring it into frame synchronisation, the ScanSynchroniser provides a sequence of
speed multipliers designed both to adjust the machine to the proper speed and, in a reasonable amount
of time, to bring it into phase.
*/
class ScanSynchroniser {
public:
/*!
@returns @c true if the emulated machine can be synchronised with the host frame output based on its
current @c [scan]status and the host machine's @c frame_duration; @c false otherwise.
*/
bool can_synchronise(const Outputs::Display::ScanStatus &scan_status, double frame_duration) {
ratio_ = 1.0;
if(scan_status.field_duration_gradient < 0.00001) {
// Check out the machine's current frame time.
// If it's within 3% of a non-zero integer multiple of the
// display rate, mark this time window to be split over the sync.
ratio_ = (frame_duration * base_multiplier_) / scan_status.field_duration;
const double integer_ratio = round(ratio_);
if(integer_ratio > 0.0) {
ratio_ /= integer_ratio;
return ratio_ <= maximum_rate_adjustment && ratio_ >= 1.0 / maximum_rate_adjustment;
}
}
return false;
}
/*!
@returns The appropriate speed multiplier for the next frame based on the inputs previously supplied to @c can_synchronise.
Results are undefined if @c can_synchroise returned @c false.
*/
double next_speed_multiplier(const Outputs::Display::ScanStatus &scan_status) {
// The host versus emulated ratio is calculated based on the current perceived frame duration of the machine.
// Either that number is exactly correct or it's already the result of some sort of low-pass filter. So there's
// no benefit to second guessing it here — just take it to be correct.
//
// ... with one slight caveat, which is that it is desireable to adjust phase here, to align vertical sync points.
// So the set speed multiplier may be adjusted slightly to aim for that.
double speed_multiplier = 1.0 / (ratio_ / base_multiplier_);
if(scan_status.current_position > 0.0) {
if(scan_status.current_position < 0.5) speed_multiplier /= phase_adjustment_ratio;
else speed_multiplier *= phase_adjustment_ratio;
}
speed_multiplier_ = (speed_multiplier_ * 0.95) + (speed_multiplier * 0.05);
return speed_multiplier_ * base_multiplier_;
}
void set_base_speed_multiplier(double multiplier) {
base_multiplier_ = multiplier;
}
double get_base_speed_multiplier() {
return base_multiplier_;
}
private:
static constexpr double maximum_rate_adjustment = 1.03;
static constexpr double phase_adjustment_ratio = 1.005;
// Managed local state.
double speed_multiplier_ = 1.0;
double base_multiplier_ = 1.0;
// Temporary storage to bridge the can_synchronise -> next_speed_multiplier gap.
double ratio_ = 1.0;
};
}
#endif /* ScanSynchroniser_h */

View File

@@ -9,9 +9,16 @@
#ifndef TimeTypes_h
#define TimeTypes_h
#include <chrono>
namespace Time {
typedef double Seconds;
typedef int64_t Nanos;
inline Nanos nanos_now() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::high_resolution_clock::now().time_since_epoch()).count();
}
}

View File

@@ -23,7 +23,7 @@ WD1770::WD1770(Personality p) :
posit_event(int(Event1770::Command));
}
void WD1770::set_register(int address, uint8_t value) {
void WD1770::write(int address, uint8_t value) {
switch(address&3) {
case 0: {
if((value&0xf0) == 0xd0) {
@@ -54,7 +54,7 @@ void WD1770::set_register(int address, uint8_t value) {
}
}
uint8_t WD1770::get_register(int address) {
uint8_t WD1770::read(int address) {
switch(address&3) {
default: {
update_status([] (Status &status) {
@@ -336,6 +336,7 @@ void WD1770::posit_event(int new_event_type) {
READ_ID();
if(index_hole_count_ == 6) {
LOG("Nothing found to verify");
update_status([] (Status &status) {
status.seek_error = true;
});
@@ -480,7 +481,7 @@ void WD1770::posit_event(int new_event_type) {
status.data_request = true;
});
distance_into_section_++;
if(distance_into_section_ == 128 << header_[3]) {
if(distance_into_section_ == 128 << (header_[3]&3)) {
distance_into_section_ = 0;
goto type2_check_crc;
}
@@ -563,7 +564,7 @@ void WD1770::posit_event(int new_event_type) {
*/
write_byte(data_);
distance_into_section_++;
if(distance_into_section_ == 128 << header_[3]) {
if(distance_into_section_ == 128 << (header_[3]&3)) {
goto type2_write_crc;
}
@@ -823,6 +824,10 @@ void WD1770::set_head_loaded(bool head_loaded) {
if(head_loaded) posit_event(int(Event1770::HeadLoad));
}
bool WD1770::get_head_loaded() {
return head_is_loaded_;
}
ClockingHint::Preference WD1770::preferred_clocking() {
if(status_.busy) return ClockingHint::Preference::RealTime;
return Storage::Disk::MFMController::preferred_clocking();

View File

@@ -36,29 +36,29 @@ class WD1770: public Storage::Disk::MFMController {
using Storage::Disk::MFMController::set_is_double_density;
/// Writes @c value to the register at @c address. Only the low two bits of the address are decoded.
void set_register(int address, uint8_t value);
void write(int address, uint8_t value);
/// Fetches the value of the register @c address. Only the low two bits of the address are decoded.
uint8_t get_register(int address);
uint8_t read(int address);
/// Runs the controller for @c number_of_cycles cycles.
void run_for(const Cycles cycles);
enum Flag: uint8_t {
NotReady = 0x80,
NotReady = 0x80, // 0x80
MotorOn = 0x80,
WriteProtect = 0x40,
RecordType = 0x20,
WriteProtect = 0x40, // 0x40
RecordType = 0x20, // 0x20
SpinUp = 0x20,
HeadLoaded = 0x20,
RecordNotFound = 0x10,
RecordNotFound = 0x10, // 0x10
SeekError = 0x10,
CRCError = 0x08,
LostData = 0x04,
CRCError = 0x08, // 0x08
LostData = 0x04, // 0x04
TrackZero = 0x04,
DataRequest = 0x02,
DataRequest = 0x02, // 0x02
Index = 0x02,
Busy = 0x01
Busy = 0x01 // 0x01
};
/// @returns The current value of the IRQ line output.
@@ -80,6 +80,9 @@ class WD1770: public Storage::Disk::MFMController {
virtual void set_motor_on(bool motor_on);
void set_head_loaded(bool head_loaded);
/// @returns The last value posted to @c set_head_loaded.
bool get_head_loaded();
private:
Personality personality_;
inline bool has_motor_on_line() { return (personality_ != P1793 ) && (personality_ != P1773); }

View File

@@ -94,10 +94,10 @@ template <class T> class MOS6522: public MOS6522Storage {
MOS6522(const MOS6522 &) = delete;
/*! Sets a register value. */
void set_register(int address, uint8_t value);
void write(int address, uint8_t value);
/*! Gets a register value. */
uint8_t get_register(int address);
uint8_t read(int address);
/*! @returns the bus handler. */
T &bus_handler();

View File

@@ -30,7 +30,7 @@ template <typename T> void MOS6522<T>::access(int address) {
}
}
template <typename T> void MOS6522<T>::set_register(int address, uint8_t value) {
template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
address &= 0xf;
access(address);
switch(address) {
@@ -155,7 +155,7 @@ template <typename T> void MOS6522<T>::set_register(int address, uint8_t value)
}
}
template <typename T> uint8_t MOS6522<T>::get_register(int address) {
template <typename T> uint8_t MOS6522<T>::read(int address) {
address &= 0xf;
access(address);
switch(address) {

View File

@@ -32,7 +32,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 write(int address, uint8_t value) {
const uint8_t decodedAddress = address & 0x07;
switch(decodedAddress) {
// Port output
@@ -63,7 +63,7 @@ template <class T> class MOS6532 {
}
}
inline uint8_t get_register(int address) {
inline uint8_t read(int address) {
const uint8_t decodedAddress = address & 0x7;
switch(decodedAddress) {
// Port input

View File

@@ -17,13 +17,13 @@ AudioGenerator::AudioGenerator(Concurrency::DeferringAsyncTaskQueue &audio_queue
void AudioGenerator::set_volume(uint8_t volume) {
audio_queue_.defer([=]() {
volume_ = static_cast<int16_t>(volume) * range_multiplier_;
audio_queue_.defer([this, volume]() {
volume_ = int16_t(volume) * range_multiplier_;
});
}
void AudioGenerator::set_control(int channel, uint8_t value) {
audio_queue_.defer([=]() {
audio_queue_.defer([this, channel, value]() {
control_registers_[channel] = value;
});
}

View File

@@ -30,6 +30,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
void get_samples(std::size_t number_of_samples, int16_t *target);
void skip_samples(std::size_t number_of_samples);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
@@ -58,7 +59,7 @@ enum class OutputMode {
To run the VIC for a cycle, the caller should call @c get_address, make the requested bus access
and call @c set_graphics_value with the result.
@c set_register and @c get_register provide register access.
@c write and @c read provide register access.
*/
template <class BusHandler> class MOS6560 {
public:
@@ -83,8 +84,9 @@ template <class BusHandler> class MOS6560 {
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status() / 4.0f; }
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
void set_high_frequency_cutoff(float cutoff) {
@@ -353,7 +355,7 @@ template <class BusHandler> class MOS6560 {
/*!
Writes to a 6560 register.
*/
void set_register(int address, uint8_t value) {
void write(int address, uint8_t value) {
address &= 0xf;
registers_.direct_values[address] = value;
switch(address) {
@@ -417,7 +419,7 @@ template <class BusHandler> class MOS6560 {
/*
Reads from a 6560 register.
*/
uint8_t get_register(int address) {
uint8_t read(int address) {
address &= 0xf;
switch(address) {
default: return registers_.direct_values[address];

View File

@@ -27,7 +27,7 @@ template <class T> class i8255 {
Stores the value @c value to the register at @c address. If this causes a change in 8255 output
then the PortHandler will be informed.
*/
void set_register(int address, uint8_t value) {
void write(int address, uint8_t value) {
switch(address & 3) {
case 0:
if(!(control_ & 0x10)) {
@@ -60,7 +60,7 @@ template <class T> class i8255 {
Obtains the current value for the register at @c address. If this provides a reading
of input then the PortHandler will be queried.
*/
uint8_t get_register(int address) {
uint8_t read(int address) {
switch(address & 3) {
case 0: return (control_ & 0x10) ? port_handler_.get_value(0) : outputs_[0];
case 1: return (control_ & 0x02) ? port_handler_.get_value(1) : outputs_[1];

View File

@@ -163,7 +163,7 @@ void i8272::run_for(Cycles cycles) {
if(is_sleeping_) update_clocking_observer();
}
void i8272::set_register(int address, uint8_t value) {
void i8272::write(int address, uint8_t value) {
// don't consider attempted sets to the status register
if(!address) return;
@@ -181,7 +181,7 @@ void i8272::set_register(int address, uint8_t value) {
}
}
uint8_t i8272::get_register(int address) {
uint8_t i8272::read(int address) {
if(address) {
if(result_stack_.empty()) return 0xff;
uint8_t result = result_stack_.back();
@@ -865,7 +865,7 @@ void i8272::posit_event(int event_type) {
SetDataRequest();
SetDataDirectionToProcessor();
// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait
// The actual stuff of unwinding result_stack_ is handled by ::read; wait
// until the processor has read all result bytes.
WAIT_FOR_EVENT(Event8272::ResultEmpty);

View File

@@ -33,8 +33,8 @@ class i8272 : public Storage::Disk::MFMController {
void set_data_input(uint8_t value);
uint8_t get_data_output();
void set_register(int address, uint8_t value);
uint8_t get_register(int address);
void write(int address, uint8_t value);
uint8_t read(int address);
void set_dma_acknowledge(bool dack);
void set_terminal_count(bool tc);
@@ -67,7 +67,7 @@ class i8272 : public Storage::Disk::MFMController {
ResultEmpty = (1 << 5),
NoLongerReady = (1 << 6)
};
void posit_event(int type) override;
void posit_event(int type) final;
int interesting_event_mask_ = static_cast<int>(Event8272::CommandByte);
int resume_point_ = 0;
bool is_access_command_ = false;

View File

@@ -117,6 +117,14 @@ void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const {
// The input was scaled by 3/4 to convert half cycles to internal ticks,
// so undo that and also allow for: (i) the multiply by 4 that it takes
// to reach the CRT; and (ii) the fact that the half-cycles value was scaled,
// and this should really reply in whole cycles.
return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f));
}
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
@@ -492,7 +500,7 @@ void Base::output_border(int cycles, uint32_t cram_dot) {
}
}
void TMS9918::set_register(int address, uint8_t value) {
void TMS9918::write(int address, uint8_t value) {
// Writes to address 0 are writes to the video RAM. Store
// the value and return.
if(!(address & 1)) {
@@ -670,7 +678,7 @@ void TMS9918::latch_horizontal_counter() {
latched_column_ = write_pointer_.column;
}
uint8_t TMS9918::get_register(int address) {
uint8_t TMS9918::read(int address) {
write_phase_ = false;
// Reads from address 0 read video RAM, via the read-ahead buffer.

View File

@@ -44,6 +44,9 @@ class TMS9918: public Base {
/*! Sets the scan target this TMS will post content to. */
void set_scan_target(Outputs::Display::ScanTarget *);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*! Sets the type of display the CRT will request. */
void set_display_type(Outputs::Display::DisplayType);
@@ -54,10 +57,10 @@ class TMS9918: public Base {
void run_for(const HalfCycles cycles);
/*! Sets a register value. */
void set_register(int address, uint8_t value);
void write(int address, uint8_t value);
/*! Gets a register value. */
uint8_t get_register(int address);
uint8_t read(int address);
/*! Gets the current scan line; provided by the Master System only. */
uint8_t get_current_line();
@@ -69,8 +72,8 @@ class TMS9918: public Base {
void latch_horizontal_counter();
/*!
Returns the amount of time until get_interrupt_line would next return true if
there are no interceding calls to set_register or get_register.
Returns the amount of time until @c get_interrupt_line would next return true if
there are no interceding calls to @c write or to @c read.
If get_interrupt_line is true now, returns zero. If get_interrupt_line would
never return true, returns -1.

View File

@@ -6,13 +6,17 @@
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include <cmath>
#include "AY38910.hpp"
#include <cmath>
//namespace GI {
//namespace AY38910 {
using namespace GI::AY38910;
AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
template <bool is_stereo>
AY38910<is_stereo>::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(task_queue) {
// Don't use the low bit of the envelope position if this is an AY.
envelope_position_mask_ |= personality == Personality::AY38910;
@@ -70,17 +74,34 @@ AY38910::AY38910(Personality personality, Concurrency::DeferringAsyncTaskQueue &
set_sample_volume_range(0);
}
void AY38910::set_sample_volume_range(std::int16_t range) {
// set up volume lookup table
template <bool is_stereo> void AY38910<is_stereo>::set_sample_volume_range(std::int16_t range) {
// Set up volume lookup table; the function below is based on a combination of the graph
// from the YM's datasheet, showing a clear power curve, and fitting that to observed
// values reported elsewhere.
const float max_volume = float(range) / 3.0f; // As there are three channels.
constexpr float root_two = 1.414213562373095f;
for(int v = 0; v < 32; v++) {
volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 2.0f));
volumes_[v] = int(max_volume / powf(root_two, float(v ^ 0x1f) / 3.18f));
}
// Tie level 0 to silence.
for(int v = 31; v >= 0; --v) {
volumes_[v] -= volumes_[0];
}
evaluate_output_volume();
}
void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
template <bool is_stereo> void AY38910<is_stereo>::set_output_mixing(float a_left, float b_left, float c_left, float a_right, float b_right, float c_right) {
a_left_ = uint8_t(a_left * 255.0f);
b_left_ = uint8_t(b_left * 255.0f);
c_left_ = uint8_t(c_left * 255.0f);
a_right_ = uint8_t(a_right * 255.0f);
b_right_ = uint8_t(b_right * 255.0f);
c_right_ = uint8_t(c_right * 255.0f);
}
template <bool is_stereo> void AY38910<is_stereo>::get_samples(std::size_t number_of_samples, int16_t *target) {
// Note on structure below: the real AY has a built-in divider of 8
// prior to applying its tone and noise dividers. But the YM fills the
// same total periods for noise and tone with double-precision envelopes.
@@ -92,7 +113,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
std::size_t c = 0;
while((master_divider_&3) && c < number_of_samples) {
target[c] = output_volume_;
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
master_divider_++;
c++;
}
@@ -134,7 +159,11 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
evaluate_output_volume();
for(int ic = 0; ic < 4 && c < number_of_samples; ic++) {
target[c] = output_volume_;
if constexpr (is_stereo) {
reinterpret_cast<uint32_t *>(target)[c] = output_volume_;
} else {
target[c] = int16_t(output_volume_);
}
c++;
master_divider_++;
}
@@ -143,7 +172,7 @@ void AY38910::get_samples(std::size_t number_of_samples, int16_t *target) {
master_divider_ &= 3;
}
void AY38910::evaluate_output_volume() {
template <bool is_stereo> void AY38910<is_stereo>::evaluate_output_volume() {
int envelope_volume = envelope_shapes_[output_registers_[13]][envelope_position_ | envelope_position_mask_];
// The output level for a channel is:
@@ -161,10 +190,20 @@ void AY38910::evaluate_output_volume() {
};
#undef level
// This remapping table seeks to map 'channel volumes', i.e. the levels produced from the
// 16-step progammatic volumes set per channel to 'envelope volumes', i.e. the 32-step
// volumes that are produced by the envelope generators (on a YM at least). My reading of
// the data sheet is that '0' is still off, but 15 should be as loud as peak envelope. So
// I've thrown in the discontinuity at the low end, where it'll be very quiet.
const int channel_volumes[] = {
0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31
};
static_assert(sizeof(channel_volumes) == 16*sizeof(int));
// Channel volume is a simple selection: if the bit at 0x10 is set, use the envelope volume; otherwise use the lower four bits,
// mapped to the range 131 in case this is a YM.
#define channel_volume(c) \
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * (((output_registers_[c]&0xf) << 1) + 1)
((output_registers_[c] >> 4)&1) * envelope_volume + (((output_registers_[c] >> 4)&1)^1) * channel_volumes[output_registers_[c]&0xf]
const int volumes[3] = {
channel_volume(8),
@@ -173,34 +212,47 @@ void AY38910::evaluate_output_volume() {
};
#undef channel_volume
// Mix additively.
output_volume_ = static_cast<int16_t>(
volumes_[volumes[0]] * channel_levels[0] +
volumes_[volumes[1]] * channel_levels[1] +
volumes_[volumes[2]] * channel_levels[2]
);
// Mix additively, weighting if in stereo.
if constexpr (is_stereo) {
int16_t *const output_volumes = reinterpret_cast<int16_t *>(&output_volume_);
output_volumes[0] = int16_t((
volumes_[volumes[0]] * channel_levels[0] * a_left_ +
volumes_[volumes[1]] * channel_levels[1] * b_left_ +
volumes_[volumes[2]] * channel_levels[2] * c_left_
) >> 8);
output_volumes[1] = int16_t((
volumes_[volumes[0]] * channel_levels[0] * a_right_ +
volumes_[volumes[1]] * channel_levels[1] * b_right_ +
volumes_[volumes[2]] * channel_levels[2] * c_right_
) >> 8);
} else {
output_volume_ = uint32_t(
volumes_[volumes[0]] * channel_levels[0] +
volumes_[volumes[1]] * channel_levels[1] +
volumes_[volumes[2]] * channel_levels[2]
);
}
}
bool AY38910::is_zero_level() {
template <bool is_stereo> bool AY38910<is_stereo>::is_zero_level() {
// Confirm that the AY is trivially at the zero level if all three volume controls are set to fixed zero.
return output_registers_[0x8] == 0 && output_registers_[0x9] == 0 && output_registers_[0xa] == 0;
}
// MARK: - Register manipulation
void AY38910::select_register(uint8_t r) {
template <bool is_stereo> void AY38910<is_stereo>::select_register(uint8_t r) {
selected_register_ = r;
}
void AY38910::set_register_value(uint8_t value) {
template <bool is_stereo> void AY38910<is_stereo>::set_register_value(uint8_t value) {
// There are only 16 registers.
if(selected_register_ > 15) return;
// If this is a register that affects audio output, enqueue a mutation onto the
// audio generation thread.
if(selected_register_ < 14) {
const int selected_register = selected_register_;
task_queue_.defer([=] () {
task_queue_.defer([this, selected_register = selected_register_, value] () {
// Perform any register-specific mutation to output generation.
uint8_t masked_value = value;
switch(selected_register) {
@@ -209,7 +261,7 @@ void AY38910::set_register_value(uint8_t value) {
int channel = selected_register >> 1;
if(selected_register & 1)
tone_periods_[channel] = (tone_periods_[channel] & 0xff) | static_cast<uint16_t>((value&0xf) << 8);
tone_periods_[channel] = (tone_periods_[channel] & 0xff) | uint16_t((value&0xf) << 8);
else
tone_periods_[channel] = (tone_periods_[channel] & ~0xff) | value;
}
@@ -224,7 +276,7 @@ void AY38910::set_register_value(uint8_t value) {
break;
case 12:
envelope_period_ = (envelope_period_ & 0xff) | static_cast<int>(value << 8);
envelope_period_ = (envelope_period_ & 0xff) | int(value << 8);
break;
case 13:
@@ -262,7 +314,7 @@ void AY38910::set_register_value(uint8_t value) {
if(update_port_a) set_port_output(false);
}
uint8_t AY38910::get_register_value() {
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_register_value() {
// This table ensures that bits that aren't defined within the AY are returned as 0s
// when read, conforming to CPC-sourced unit tests.
const uint8_t register_masks[16] = {
@@ -276,24 +328,24 @@ uint8_t AY38910::get_register_value() {
// MARK: - Port querying
uint8_t AY38910::get_port_output(bool port_b) {
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_port_output(bool port_b) {
return registers_[port_b ? 15 : 14];
}
// MARK: - Bus handling
void AY38910::set_port_handler(PortHandler *handler) {
template <bool is_stereo> void AY38910<is_stereo>::set_port_handler(PortHandler *handler) {
port_handler_ = handler;
set_port_output(true);
set_port_output(false);
}
void AY38910::set_data_input(uint8_t r) {
template <bool is_stereo> void AY38910<is_stereo>::set_data_input(uint8_t r) {
data_input_ = r;
update_bus();
}
void AY38910::set_port_output(bool port_b) {
template <bool is_stereo> void AY38910<is_stereo>::set_port_output(bool port_b) {
// Per the data sheet: "each [IO] pin is provided with an on-chip pull-up resistor,
// so that when in the "input" mode, all pins will read normally high". Therefore,
// report programmer selection of input mode as creating an output of 0xff.
@@ -303,7 +355,7 @@ void AY38910::set_port_output(bool port_b) {
}
}
uint8_t AY38910::get_data_output() {
template <bool is_stereo> uint8_t AY38910<is_stereo>::get_data_output() {
if(control_state_ == Read && selected_register_ >= 14 && selected_register_ < 16) {
// Per http://cpctech.cpc-live.com/docs/psgnotes.htm if a port is defined as output then the
// value returned to the CPU when reading it is the and of the output value and any input.
@@ -319,22 +371,22 @@ uint8_t AY38910::get_data_output() {
return data_output_;
}
void AY38910::set_control_lines(ControlLines control_lines) {
switch(static_cast<int>(control_lines)) {
template <bool is_stereo> void AY38910<is_stereo>::set_control_lines(ControlLines control_lines) {
switch(int(control_lines)) {
default: control_state_ = Inactive; break;
case static_cast<int>(BDIR | BC2 | BC1):
case int(BDIR | BC2 | BC1):
case BDIR:
case BC1: control_state_ = LatchAddress; break;
case static_cast<int>(BC2 | BC1): control_state_ = Read; break;
case static_cast<int>(BDIR | BC2): control_state_ = Write; break;
case int(BC2 | BC1): control_state_ = Read; break;
case int(BDIR | BC2): control_state_ = Write; break;
}
update_bus();
}
void AY38910::update_bus() {
template <bool is_stereo> void AY38910<is_stereo>::update_bus() {
// Assume no output, unless this turns out to be a read.
data_output_ = 0xff;
switch(control_state_) {
@@ -344,3 +396,7 @@ void AY38910::update_bus() {
case Read: data_output_ = get_register_value(); break;
}
}
// Ensure both mono and stereo versions of the AY are built.
template class GI::AY38910::AY38910<true>;
template class GI::AY38910::AY38910<false>;

View File

@@ -63,8 +63,10 @@ enum class Personality {
Provides emulation of an AY-3-8910 / YM2149, which is a three-channel sound chip with a
noise generator and a volume envelope generator, which also provides two bidirectional
interface ports.
This AY has an attached mono or stereo mixer.
*/
class AY38910: public ::Outputs::Speaker::SampleSource {
template <bool is_stereo> class AY38910: public ::Outputs::Speaker::SampleSource {
public:
/// Creates a new AY38910.
AY38910(Personality, Concurrency::DeferringAsyncTaskQueue &);
@@ -91,10 +93,23 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
*/
void set_port_handler(PortHandler *);
/*!
Enables or disables stereo output; if stereo output is enabled then also sets the weight of each of the AY's
channels in each of the output channels.
If a_left_ = b_left = c_left = a_right = b_right = c_right = 1.0 then you'll get output that's effectively mono.
a_left = 0.0, a_right = 1.0 will make A full volume on the right output, and silent on the left.
a_left = 0.5, a_right = 0.5 will make A half volume on both outputs.
*/
void set_output_mixing(float a_left, float b_left, float c_left, float a_right = 1.0, float b_right = 1.0, float c_right = 1.0);
// to satisfy ::Outputs::Speaker (included via ::Outputs::Filter.
void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level();
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return is_stereo; }
private:
Concurrency::DeferringAsyncTaskQueue &task_queue_;
@@ -135,14 +150,21 @@ class AY38910: public ::Outputs::Speaker::SampleSource {
uint8_t data_input_, data_output_;
int16_t output_volume_;
void evaluate_output_volume();
uint32_t output_volume_;
void update_bus();
PortHandler *port_handler_ = nullptr;
void set_port_output(bool port_b);
void evaluate_output_volume();
// Output mixing control.
uint8_t a_left_ = 255, a_right_ = 255;
uint8_t b_left_ = 255, b_right_ = 255;
uint8_t c_left_ = 255, c_right_ = 255;
};
}
}

View File

@@ -28,7 +28,7 @@ void Toggle::skip_samples(const std::size_t number_of_samples) {}
void Toggle::set_output(bool enabled) {
if(is_enabled_ == enabled) return;
is_enabled_ = enabled;
audio_queue_.defer([=] {
audio_queue_.defer([this, enabled] {
level_ = enabled ? volume_ : 0;
});
}

View File

@@ -24,6 +24,7 @@ class Toggle: public Outputs::Speaker::SampleSource {
void get_samples(std::size_t number_of_samples, std::int16_t *target);
void set_sample_volume_range(std::int16_t range);
void skip_samples(const std::size_t number_of_samples);
static constexpr bool get_is_stereo() { return false; }
void set_output(bool enabled);
bool get_output();

View File

@@ -26,7 +26,7 @@ namespace Apple {
/*!
Provides an emulation of the Apple Disk II.
*/
class DiskII final:
class DiskII :
public Storage::Disk::Drive::EventDelegate,
public ClockingHint::Source,
public ClockingHint::Observer {
@@ -98,8 +98,8 @@ class DiskII final:
void select_drive(int drive);
uint8_t trigger_address(int address, uint8_t value);
void process_event(const Storage::Disk::Drive::Event &event) override;
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) override;
void process_event(const Storage::Disk::Drive::Event &event) final;
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference preference) final;
const Cycles::IntType clock_rate_ = 0;

View File

@@ -76,7 +76,7 @@ class IWM:
private:
// Storage::Disk::Drive::EventDelegate.
void process_event(const Storage::Disk::Drive::Event &event) override;
void process_event(const Storage::Disk::Drive::Event &event) final;
const int clock_rate_;
@@ -91,7 +91,7 @@ class IWM:
IWMDrive *drives_[2] = {nullptr, nullptr};
bool drive_is_rotating_[2] = {false, false};
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
Cycles cycles_until_disable_;
uint8_t write_handshake_ = 0x80;

View File

@@ -32,14 +32,14 @@ class DoubleDensityDrive: public IWMDrive {
*/
void set_rotation_speed(float revolutions_per_minute);
void set_enabled(bool) override;
void set_control_lines(int) override;
bool read() override;
private:
void set_enabled(bool) final;
void set_control_lines(int) final;
bool read() final;
// To receive the proper notifications from Storage::Disk::Drive.
void did_step(Storage::Disk::HeadPosition to_position) override;
void did_set_disk() override;
void did_step(Storage::Disk::HeadPosition to_position) final;
void did_set_disk() final;
const bool is_800k_;
bool has_new_disk_ = false;

View File

@@ -55,7 +55,7 @@ void SCC::write(uint16_t address, uint8_t value) {
address &= 0xff;
if(address < 0x80) ram_[address] = value;
task_queue_.defer([=] {
task_queue_.defer([this, address, value] {
// Check for a write into waveform memory.
if(address < 0x80) {
waves_[address >> 5].samples[address & 0x1f] = value;

View File

@@ -32,6 +32,7 @@ class SCC: public ::Outputs::Speaker::SampleSource {
/// As per ::SampleSource; provides audio output.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
/// Writes to the SCC.
void write(uint16_t address, uint8_t value);

View File

@@ -48,7 +48,7 @@ void SN76489::set_sample_volume_range(std::int16_t range) {
evaluate_output_volume();
}
void SN76489::set_register(uint8_t value) {
void SN76489::write(uint8_t value) {
task_queue_.defer([value, this] () {
if(value & 0x80) {
active_register_ = value;

View File

@@ -26,12 +26,13 @@ class SN76489: public Outputs::Speaker::SampleSource {
SN76489(Personality personality, Concurrency::DeferringAsyncTaskQueue &task_queue, int additional_divider = 1);
/// Writes a new value to the SN76489.
void set_register(uint8_t value);
void write(uint8_t value);
// As per SampleSource.
void get_samples(std::size_t number_of_samples, std::int16_t *target);
bool is_zero_level();
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
int master_divider_ = 0;

View File

@@ -12,61 +12,89 @@
using namespace Concurrency;
BestEffortUpdater::BestEffortUpdater() {
// ATOMIC_FLAG_INIT isn't necessarily safe to use, so establish default state by other means.
update_is_ongoing_.clear();
}
BestEffortUpdater::BestEffortUpdater() :
update_thread_([this]() {
this->update_loop();
}) {}
BestEffortUpdater::~BestEffortUpdater() {
// Don't allow further deconstruction until the task queue is stopped.
// Sever the delegate now, as soon as possible, then wait for any
// pending tasks to finish.
set_delegate(nullptr);
flush();
// Wind up the update thread.
should_quit_ = true;
update();
update_thread_.join();
}
void BestEffortUpdater::update() {
// Perform an update only if one is not currently ongoing.
if(!update_is_ongoing_.test_and_set()) {
async_task_queue_.enqueue([this]() {
// Get time now using the highest-resolution clock provided by the implementation, and determine
// the duration since the last time this section was entered.
const std::chrono::time_point<std::chrono::high_resolution_clock> now = std::chrono::high_resolution_clock::now();
const auto elapsed = now - previous_time_point_;
previous_time_point_ = now;
void BestEffortUpdater::update(int flags) {
// Bump the requested target time and set the update requested flag.
{
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
has_skipped_ = update_requested_;
update_requested_ = true;
flags_ |= flags;
target_time_ = std::chrono::high_resolution_clock::now().time_since_epoch().count();
}
update_condition_.notify_one();
}
if(has_previous_time_point_) {
// If the duration is valid, convert it to integer cycles, maintaining a rolling error and call the delegate
// if there is one. Proceed only if the number of cycles is positive, and cap it to the per-second maximum as
// it's possible this is an adjustable clock so be ready to swallow unexpected adjustments.
const int64_t integer_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
if(integer_duration > 0) {
if(delegate_) {
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
// brief system interruption.
const double duration = std::min(static_cast<double>(integer_duration) / 1e9, 0.2);
delegate_->update(this, duration, has_skipped_);
}
has_skipped_ = false;
}
} else {
has_previous_time_point_ = true;
}
void BestEffortUpdater::update_loop() {
while(true) {
std::unique_lock<decltype(update_mutex_)> lock(update_mutex_);
is_updating_ = false;
// Allow furthers updates to occur.
update_is_ongoing_.clear();
});
} else {
async_task_queue_.enqueue([this]() {
has_skipped_ = true;
// Wait to be signalled.
update_condition_.wait(lock, [this]() -> bool {
return update_requested_;
});
// Possibly this signalling really means 'quit'.
if(should_quit_) return;
// Note update started, crib the target time.
auto target_time = target_time_;
update_requested_ = false;
// If this was actually the first update request, silently swallow it.
if(!has_previous_time_point_) {
has_previous_time_point_ = true;
previous_time_point_ = target_time;
continue;
}
// Release the lock on requesting new updates.
is_updating_ = true;
const int flags = flags_;
flags_ = 0;
lock.unlock();
// Invoke the delegate, if supplied, in order to run.
const int64_t integer_duration = std::max(target_time - previous_time_point_, int64_t(0));
const auto delegate = delegate_.load();
if(delegate) {
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
// brief system interruption.
const double duration = std::min(double(integer_duration) / 1e9, 0.2);
const double elapsed_duration = delegate->update(this, duration, has_skipped_, flags);
previous_time_point_ += int64_t(elapsed_duration * 1e9);
has_skipped_ = false;
}
}
}
void BestEffortUpdater::flush() {
async_task_queue_.flush();
// Spin lock; this is allowed to be slow.
while(true) {
std::lock_guard<decltype(update_mutex_)> lock(update_mutex_);
if(!is_updating_) return;
}
}
void BestEffortUpdater::set_delegate(Delegate *const delegate) {
async_task_queue_.enqueue([this, delegate]() {
delegate_ = delegate;
});
delegate_.store(delegate);
}

View File

@@ -11,8 +11,10 @@
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <thread>
#include "AsyncTaskQueue.hpp"
#include "../ClockReceiver/TimeTypes.hpp"
namespace Concurrency {
@@ -31,7 +33,13 @@ class BestEffortUpdater {
/// A delegate receives timing cues.
struct Delegate {
virtual void update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update) = 0;
/*!
Instructs the delegate to run for at least @c duration, providing hints as to whether multiple updates were requested before the previous had completed
(as @c did_skip_previous_update) and providing the union of any flags supplied to @c update.
@returns The amount of time actually run for.
*/
virtual Time::Seconds update(BestEffortUpdater *updater, Time::Seconds duration, bool did_skip_previous_update, int flags) = 0;
};
/// Sets the current delegate.
@@ -41,20 +49,32 @@ class BestEffortUpdater {
If the delegate is not currently in the process of an `update` call, calls it now to catch up to the current time.
The call is asynchronous; this method will return immediately.
*/
void update();
void update(int flags = 0);
/// Blocks until any ongoing update is complete.
/// Blocks until any ongoing update is complete; may spin.
void flush();
private:
std::atomic_flag update_is_ongoing_;
AsyncTaskQueue async_task_queue_;
std::atomic<bool> should_quit_;
std::atomic<bool> is_updating_;
std::chrono::time_point<std::chrono::high_resolution_clock> previous_time_point_;
int64_t target_time_;
int flags_ = 0;
bool update_requested_;
std::mutex update_mutex_;
std::condition_variable update_condition_;
decltype(target_time_) previous_time_point_;
bool has_previous_time_point_ = false;
bool has_skipped_ = false;
std::atomic<bool> has_skipped_ = false;
Delegate *delegate_ = nullptr;
std::atomic<Delegate *>delegate_ = nullptr;
void update_loop();
// This is deliberately at the bottom, to ensure it constructs after the various
// mutexs, conditions, etc, that it'll depend upon.
std::thread update_thread_;
};
}

View File

@@ -170,11 +170,11 @@ class ConcreteJoystick: public Joystick {
}
}
std::vector<Input> &get_inputs() override final {
std::vector<Input> &get_inputs() final {
return inputs_;
}
void set_input(const Input &input, bool is_active) override final {
void set_input(const Input &input, bool is_active) final {
// If this is a digital setting to a digital property, just pass it along.
if(input.is_button() || stick_types_[input.info.control.index] == StickType::Digital) {
did_set_input(input, is_active);
@@ -193,7 +193,7 @@ class ConcreteJoystick: public Joystick {
}
}
void set_input(const Input &input, float value) override final {
void set_input(const Input &input, float value) final {
// If this is an analogue setting to an analogue property, just pass it along.
if(!input.is_button() && stick_types_[input.info.control.index] == StickType::Analogue) {
did_set_input(input, value);

View File

@@ -34,24 +34,24 @@ class QuadratureMouse: public Mouse {
/*
Inputs, to satisfy the Mouse interface.
*/
void move(int x, int y) override {
void move(int x, int y) final {
// Accumulate all provided motion.
axes_[0] += x;
axes_[1] += y;
}
int get_number_of_buttons() override {
int get_number_of_buttons() final {
return number_of_buttons_;
}
void set_button_pressed(int index, bool is_pressed) override {
void set_button_pressed(int index, bool is_pressed) final {
if(is_pressed)
button_flags_ |= (1 << index);
else
button_flags_ &= ~(1 << index);
}
void reset_all_buttons() override {
void reset_all_buttons() final {
button_flags_ = 0;
}

View File

@@ -126,6 +126,9 @@ class AYDeferrer {
/// Constructs a new AY instance and sets its clock rate.
AYDeferrer() : ay_(GI::AY38910::Personality::AY38910, audio_queue_), speaker_(ay_) {
speaker_.set_input_rate(1000000);
// Per the CPC Wiki:
// "A is output to the right, channel C is output left, and channel B is output to both left and right".
ay_.set_output_mixing(0.0, 0.5, 1.0, 1.0, 0.5, 0.0);
}
~AYDeferrer() {
@@ -153,14 +156,14 @@ class AYDeferrer {
}
/// @returns the AY itself.
GI::AY38910::AY38910 &ay() {
GI::AY38910::AY38910<true> &ay() {
return ay_;
}
private:
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay_;
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
GI::AY38910::AY38910<true> ay_;
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<true>> speaker_;
HalfCycles cycles_since_update_;
};
@@ -335,6 +338,11 @@ class CRTCBusHandler {
crt_.set_scan_target(scan_target);
}
/// @returns The current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 4.0f;
}
/// Sets the type of display.
void set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
@@ -627,7 +635,7 @@ class KeyboardState: public GI::AY38910::PortHandler {
}),
state_(state) {}
void did_set_input(const Input &input, bool is_active) override {
void did_set_input(const Input &input, bool is_active) final {
uint8_t mask = 0;
switch(input.type) {
default: return;
@@ -656,17 +664,15 @@ class KeyboardState: public GI::AY38910::PortHandler {
class FDC: public Intel::i8272::i8272 {
private:
Intel::i8272::BusHandler bus_handler_;
std::shared_ptr<Storage::Disk::Drive> drive_;
public:
FDC() :
i8272(bus_handler_, Cycles(8000000)),
drive_(new Storage::Disk::Drive(8000000, 300, 1)) {
set_drive(drive_);
FDC() : i8272(bus_handler_, Cycles(8000000)) {
emplace_drive(8000000, 300, 1);
set_drive(1);
}
void set_motor_on(bool on) {
drive_->set_motor_on(on);
get_drive().set_motor_on(on);
}
void select_drive(int c) {
@@ -674,11 +680,11 @@ class FDC: public Intel::i8272::i8272 {
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
drive_->set_disk(disk);
get_drive().set_disk(disk);
}
void set_activity_observer(Activity::Observer *observer) {
drive_->set_activity_observer(observer, "Drive 1", true);
get_drive().set_activity_observer(observer, "Drive 1", true);
}
};
@@ -924,14 +930,14 @@ template <bool has_fdc> class ConcreteMachine:
// Check for an 8255 PIO access
if(!(address & 0x800)) {
i8255_.set_register((address >> 8) & 3, *cycle.value);
i8255_.write((address >> 8) & 3, *cycle.value);
}
if constexpr (has_fdc) {
// Check for an FDC access
if((address & 0x580) == 0x100) {
flush_fdc();
fdc_.set_register(address & 1, *cycle.value);
fdc_.write(address & 1, *cycle.value);
}
// Check for a disk motor access
@@ -947,14 +953,14 @@ template <bool has_fdc> class ConcreteMachine:
// Check for a PIO access
if(!(address & 0x800)) {
*cycle.value &= i8255_.get_register((address >> 8) & 3);
*cycle.value &= i8255_.read((address >> 8) & 3);
}
// Check for an FDC access
if constexpr (has_fdc) {
if((address & 0x580) == 0x100) {
flush_fdc();
*cycle.value &= fdc_.get_register(address & 1);
*cycle.value &= fdc_.read(address & 1);
}
}
@@ -1002,26 +1008,31 @@ template <bool has_fdc> class ConcreteMachine:
}
/// A CRTMachine function; sets the destination for video.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
crtc_bus_handler_.set_scan_target(scan_target);
}
/// A CRTMachine function; returns the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return crtc_bus_handler_.get_scaled_scan_status();
}
/// A CRTMachine function; sets the output display type.
void set_display_type(Outputs::Display::DisplayType display_type) override final {
void set_display_type(Outputs::Display::DisplayType display_type) final {
crtc_bus_handler_.set_display_type(display_type);
}
/// @returns the speaker in use.
Outputs::Speaker::Speaker *get_speaker() override final {
Outputs::Speaker::Speaker *get_speaker() final {
return ay_.get_speaker();
}
/// Wires virtual-dispatched CRTMachine run_for requests to the static Z80 method.
void run_for(const Cycles cycles) override final {
void run_for(const Cycles cycles) final {
z80_.run_for(cycles);
}
bool insert_media(const Analyser::Static::Media &media) override final {
bool insert_media(const Analyser::Static::Media &media) final {
// If there are any tapes supplied, use the first of them.
if(!media.tapes.empty()) {
tape_player_.set_tape(media.tapes.front());
@@ -1038,70 +1049,70 @@ template <bool has_fdc> class ConcreteMachine:
return !media.tapes.empty() || (!media.disks.empty() && has_fdc);
}
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override final {
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
fdc_is_sleeping_ = fdc_.preferred_clocking() == ClockingHint::Preference::None;
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
}
// MARK: - Keyboard
void type_string(const std::string &string) override final {
void type_string(const std::string &string) final {
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
Utility::TypeRecipient::add_typer(string, std::move(mapper));
}
HalfCycles get_typer_delay() override final {
HalfCycles get_typer_delay() final {
return Cycles(4000000); // Wait 1 second before typing.
}
HalfCycles get_typer_frequency() override final {
HalfCycles get_typer_frequency() final {
return Cycles(160000); // Type one character per frame.
}
// See header; sets a key as either pressed or released.
void set_key_state(uint16_t key, bool isPressed) override final {
void set_key_state(uint16_t key, bool isPressed) final {
key_state_.set_is_pressed(isPressed, key >> 4, key & 7);
}
// See header; sets all keys to released.
void clear_all_keys() override final {
void clear_all_keys() final {
key_state_.clear_all_keys();
}
KeyboardMapper *get_keyboard_mapper() override {
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
if constexpr (has_fdc) fdc_.set_activity_observer(observer);
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return AmstradCPC::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
Configurable::Display display;
if(Configurable::get_display(selections_by_option, display)) {
set_video_signal_configurable(display);
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
return selection_set;
}
// MARK: - Joysticks
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return key_state_.get_joysticks();
}

View File

@@ -147,7 +147,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
card_became_just_in_time_ |= !is_every_cycle;
}
void card_did_change_select_constraints(Apple::II::Card *card) override {
void card_did_change_select_constraints(Apple::II::Card *card) final {
pick_card_messaging_group(card);
}
@@ -283,12 +283,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
Input(Input::Fire, 2),
}) {}
void did_set_input(const Input &input, float value) override {
void did_set_input(const Input &input, float value) final {
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical))
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
}
void did_set_input(const Input &input, bool value) override {
void did_set_input(const Input &input, bool value) final {
if(input.type == Input::Type::Fire && input.info.control.index < 3) {
buttons[input.info.control.index] = value;
}
@@ -417,16 +417,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
audio_queue_.flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
video_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_.get_scaled_scan_status();
}
/// Sets the type of display.
void set_display_type(Outputs::Display::DisplayType display_type) override {
void set_display_type(Outputs::Display::DisplayType display_type) final {
video_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
@@ -799,15 +803,15 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
audio_queue_.perform();
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
m6502_.run_for(cycles);
}
void reset_all_keys() override {
void reset_all_keys() final {
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
}
void set_key_pressed(Key key, char value, bool is_pressed) override {
void set_key_pressed(Key key, char value, bool is_pressed) final {
switch(key) {
default: break;
case Key::F12:
@@ -846,38 +850,38 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
}
Inputs::Keyboard &get_keyboard() override {
Inputs::Keyboard &get_keyboard() final {
return *this;
}
void type_string(const std::string &string) override {
void type_string(const std::string &string) final {
string_serialiser_ = std::make_unique<Utility::StringSerialiser>(string, true);
}
// MARK:: Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Apple::II::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
Configurable::Display display;
if(Configurable::get_display(selections_by_option, display)) {
set_video_signal_configurable(display);
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
return get_accurate_selections();
}
// MARK: MediaTarget
bool insert_media(const Analyser::Static::Media &media) override {
bool insert_media(const Analyser::Static::Media &media) final {
if(!media.disks.empty()) {
auto diskii = diskii_card();
if(diskii) diskii->set_disk(media.disks[0], 0);
@@ -886,14 +890,14 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
}
// MARK: Activity::Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
for(const auto &card: cards_) {
if(card) card->set_activity_observer(observer);
}
}
// MARK: JoystickMachine
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
};

View File

@@ -27,16 +27,16 @@ class DiskIICard: public Card, public ClockingHint::Observer {
public:
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) override;
void run_for(Cycles cycles, int stretches) override;
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) final;
void run_for(Cycles cycles, int stretches) final;
void set_activity_observer(Activity::Observer *observer) override;
void set_activity_observer(Activity::Observer *observer) final;
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
Storage::Disk::Drive &get_drive(int drive);
private:
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override;
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final;
std::vector<uint8_t> boot_;
Apple::DiskII diskii_;
ClockingHint::Preference diskii_clocking_preference_ = ClockingHint::Preference::RealTime;

View File

@@ -47,6 +47,10 @@ void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus VideoBase::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 14.0f;
}
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
@@ -56,7 +60,7 @@ void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
*/
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
set_alternative_character_set_ = alternative_character_set;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, alternative_character_set] {
alternative_character_set_ = alternative_character_set;
if(alternative_character_set) {
character_zones[1].address_mask = 0xff;
@@ -74,7 +78,7 @@ bool VideoBase::get_alternative_character_set() {
void VideoBase::set_80_columns(bool columns_80) {
set_columns_80_ = columns_80;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, columns_80] {
columns_80_ = columns_80;
});
}
@@ -101,7 +105,7 @@ bool VideoBase::get_page2() {
void VideoBase::set_text(bool text) {
set_text_ = text;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, text] {
text_ = text;
});
}
@@ -112,7 +116,7 @@ bool VideoBase::get_text() {
void VideoBase::set_mixed(bool mixed) {
set_mixed_ = mixed;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, mixed] {
mixed_ = mixed;
});
}
@@ -123,7 +127,7 @@ bool VideoBase::get_mixed() {
void VideoBase::set_high_resolution(bool high_resolution) {
set_high_resolution_ = high_resolution;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, high_resolution] {
high_resolution_ = high_resolution;
});
}
@@ -134,7 +138,7 @@ bool VideoBase::get_high_resolution() {
void VideoBase::set_annunciator_3(bool annunciator_3) {
set_annunciator_3_ = annunciator_3;
deferrer_.defer(Cycles(2), [=] {
deferrer_.defer(Cycles(2), [this, annunciator_3] {
annunciator_3_ = annunciator_3;
high_resolution_mask_ = annunciator_3_ ? 0x7f : 0xff;
});

View File

@@ -40,6 +40,9 @@ class VideoBase {
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
@@ -252,14 +255,14 @@ class VideoBase {
void output_fat_low_resolution(uint8_t *target, const uint8_t *source, size_t length, int column, int row) const;
// Maintain a DeferredQueue for delayed mode switches.
DeferredQueue<Cycles> deferrer_;
DeferredQueuePerformer<Cycles> deferrer_;
};
template <class BusHandler, bool is_iie> class Video: public VideoBase {
public:
/// Constructs an instance of the video feed; a CRT is also created.
Video(BusHandler &bus_handler) :
VideoBase(is_iie, [=] (Cycles cycles) { advance(cycles); }),
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }),
bus_handler_(bus_handler) {}
/*!

View File

@@ -35,7 +35,7 @@ void Audio::set_volume(int volume) {
posted_volume_ = volume;
// Post the volume change as a deferred event.
task_queue_.defer([=] () {
task_queue_.defer([this, volume] () {
volume_ = volume;
set_volume_multiplier();
});
@@ -47,7 +47,7 @@ void Audio::set_enabled(bool on) {
posted_enable_mask_ = int(on);
// Post the enabled mask change as a deferred event.
task_queue_.defer([=] () {
task_queue_.defer([this, on] () {
enabled_mask_ = int(on);
set_volume_multiplier();
});

View File

@@ -55,6 +55,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
void get_samples(std::size_t number_of_samples, int16_t *target);
bool is_zero_level();
void set_sample_volume_range(std::int16_t range);
constexpr static bool get_is_stereo() { return false; }
private:
Concurrency::DeferringAsyncTaskQueue &task_queue_;

View File

@@ -123,10 +123,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s);
} break;
}
ram_mask_ = (ram_size >> 1) - 1;
rom_mask_ = (rom_size >> 1) - 1;
ram_.resize(ram_size >> 1);
video_.set_ram(ram_.data(), ram_mask_);
ram_mask_ = ram_size - 1;
rom_mask_ = rom_size - 1;
ram_.resize(ram_size);
video_.set_ram(reinterpret_cast<uint16_t *>(ram_.data()), ram_mask_ >> 1);
// Grab a copy of the ROM and convert it into big-endian data.
const auto roms = rom_fetcher(rom_descriptions);
@@ -171,15 +171,19 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
audio_.queue.flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
video_.set_scan_target(scan_target);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_.get_scaled_scan_status();
}
Outputs::Speaker::Speaker *get_speaker() final {
return &audio_.speaker;
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
mc68000_.run_for(cycles);
}
@@ -192,11 +196,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
// A null cycle leaves nothing else to do.
if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0);
// Grab the value on the address bus, at word precision.
uint32_t word_address = cycle.active_operation_word_address();
// Grab the address.
auto address = cycle.host_endian_byte_address();
// Everything above E0 0000 is signalled as being on the peripheral bus.
mc68000_.set_is_peripheral_address(word_address >= 0x700000);
mc68000_.set_is_peripheral_address(address >= 0xe0'0000);
// All code below deals only with reads and writes — cycles in which a
// data select is active. So quit now if this is not the active part of
@@ -209,9 +213,9 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0);
// Grab the word-precision address being accessed.
uint16_t *memory_base = nullptr;
uint8_t *memory_base = nullptr;
HalfCycles delay;
switch(memory_map_[word_address >> 16]) {
switch(memory_map_[address >> 17]) {
default: assert(false);
case BusDevice::Unassigned:
@@ -222,14 +226,14 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
if(*cycle.address & 1) {
fill_unmapped(cycle);
} else {
const int register_address = word_address >> 8;
const int register_address = address >> 9;
// VIA accesses are via address 0xefe1fe + register*512,
// which at word precision is 0x77f0ff + register*256.
if(cycle.operation & Microcycle::Read) {
cycle.value->halves.low = via_.get_register(register_address);
cycle.value->halves.low = via_.read(register_address);
} else {
via_.set_register(register_address, cycle.value->halves.low);
via_.write(register_address, cycle.value->halves.low);
}
if(cycle.operation & Microcycle::SelectWord) cycle.value->halves.high = 0xff;
@@ -246,7 +250,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
case BusDevice::IWM: {
if(*cycle.address & 1) {
const int register_address = word_address >> 8;
const int register_address = address >> 9;
// The IWM; this is a purely polled device, so can be run on demand.
if(cycle.operation & Microcycle::Read) {
@@ -262,8 +266,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
} return delay;
case BusDevice::SCSI: {
const int register_address = word_address >> 3;
const bool dma_acknowledge = word_address & 0x100;
const int register_address = address >> 4;
const bool dma_acknowledge = address & 0x200;
// Even accesses = read; odd = write.
if(*cycle.address & 1) {
@@ -304,7 +308,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
cycle.value->halves.low = 0xff;
}
} else {
const auto read = scc_.read(int(word_address));
const auto read = scc_.read(int(address >> 1));
if(cycle.operation & Microcycle::Read) {
cycle.value->halves.low = read;
}
@@ -319,10 +323,10 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
} else {
if(*cycle.address & 1) {
if(cycle.operation & Microcycle::Read) {
scc_.write(int(word_address), 0xff);
scc_.write(int(address >> 1), 0xff);
cycle.value->halves.low = 0xff;
} else {
scc_.write(int(word_address), cycle.value->halves.low);
scc_.write(int(address >> 1), cycle.value->halves.low);
}
} else {
fill_unmapped(cycle);
@@ -334,13 +338,13 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
// This is coupled with the Macintosh implementation of video; the magic
// constant should probably be factored into the Video class.
// It embodies knowledge of the fact that video (and audio) will always
// be fetched from the final $d900 bytes (i.e. $6c80 words) of memory.
// be fetched from the final $d900 bytes of memory.
// (And that ram_mask_ = ram size - 1).
if(word_address > ram_mask_ - 0x6c80)
if(address > ram_mask_ - 0xd900)
update_video();
memory_base = ram_.data();
word_address &= ram_mask_;
address &= ram_mask_;
// Apply a delay due to video contention if applicable; scheme applied:
// only every other access slot is available during the period of video
@@ -355,7 +359,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
case BusDevice::ROM: {
if(!(cycle.operation & Microcycle::Read)) return delay;
memory_base = rom_;
word_address &= rom_mask_;
address &= rom_mask_;
} break;
}
@@ -365,19 +369,16 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
break;
case Microcycle::SelectWord | Microcycle::Read:
cycle.value->full = memory_base[word_address];
cycle.value->full = *reinterpret_cast<uint16_t *>(&memory_base[address]);
break;
case Microcycle::SelectByte | Microcycle::Read:
cycle.value->halves.low = uint8_t(memory_base[word_address] >> cycle.byte_shift());
cycle.value->halves.low = memory_base[address];
break;
case Microcycle::SelectWord:
memory_base[word_address] = cycle.value->full;
*reinterpret_cast<uint16_t *>(&memory_base[address]) = cycle.value->full;
break;
case Microcycle::SelectByte:
memory_base[word_address] = uint16_t(
(cycle.value->halves.low << cycle.byte_shift()) |
(memory_base[word_address] & cycle.untouched_byte_mask())
);
memory_base[address] = cycle.value->halves.low;
break;
}
@@ -395,7 +396,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
via_.flush();
audio_.queue.perform();
// Experimental?
// This avoids deferring IWM costs indefinitely, until
// they become artbitrarily large.
iwm_.flush();
}
@@ -454,7 +456,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
video_.set_use_alternate_buffers(use_alternate_screen_buffer, use_alternate_audio_buffer);
}
bool insert_media(const Analyser::Static::Media &media) override {
bool insert_media(const Analyser::Static::Media &media) final {
if(media.disks.empty() && media.mass_storage_devices.empty())
return false;
@@ -468,7 +470,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
drives_[0].set_disk(media.disks[0]);
}
// TODO: allow this only at machine startup.
// TODO: allow this only at machine startup?
if(!media.mass_storage_devices.empty()) {
const auto volume = dynamic_cast<Storage::MassStorage::Encodings::Macintosh::Volume *>(media.mass_storage_devices.front().get());
if(volume) {
@@ -482,11 +484,11 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
// MARK: Keyboard input.
KeyboardMapper *get_keyboard_mapper() override {
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
void set_key_state(uint16_t key, bool is_pressed) override {
void set_key_state(uint16_t key, bool is_pressed) final {
keyboard_.enqueue_key_state(key, is_pressed);
}
@@ -494,7 +496,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
// MARK: Interrupt updates.
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) override {
void did_change_interrupt_status(Zilog::SCC::z8530 *sender, bool new_status) final {
update_interrupt_input();
}
@@ -511,7 +513,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
}
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
iwm_->set_activity_observer(observer);
if constexpr (model == Analyser::Static::Macintosh::Target::Model::MacPlus) {
@@ -520,40 +522,42 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Apple::Macintosh::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
bool quick_boot;
if(Configurable::get_quick_boot(selections_by_option, quick_boot)) {
if(quick_boot) {
// Cf. Big Mess o' Wires' disassembly of the Mac Plus ROM, and the
// test at $E00. TODO: adapt as(/if?) necessary for other Macs.
ram_[0x02ae >> 1] = 0x40;
ram_[0x02b0 >> 1] = 0x00;
ram_[0x02ae] = 0x40;
ram_[0x02af] = 0x00;
ram_[0x02b0] = 0x00;
ram_[0x02b1] = 0x00;
}
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_boot_selection(selection_set, false);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_boot_selection(selection_set, true);
return selection_set;
}
private:
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
scsi_bus_is_clocked_ = scsi_bus_.preferred_clocking() != ClockingHint::Preference::None;
}
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) override {
void drive_speed_accumulator_set_drive_speed(DriveSpeedAccumulator *, float speed) final {
iwm_.flush();
drives_[0].set_rotation_speed(speed);
drives_[1].set_rotation_speed(speed);
@@ -565,11 +569,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
forceinline void fill_unmapped(const Microcycle &cycle) {
if(!(cycle.operation & Microcycle::Read)) return;
if(cycle.operation & Microcycle::SelectWord) {
cycle.value->full = 0xffff;
} else {
cycle.value->halves.low = 0xff;
}
cycle.set_value16(0xffff);
}
/// Advances all non-CPU components by @c duration half cycles.
@@ -652,7 +652,7 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
time_until_video_event_ = video_.get_next_sequence_point();
}
Inputs::Mouse &get_mouse() override {
Inputs::Mouse &get_mouse() final {
return mouse_;
}
@@ -852,8 +852,8 @@ template <Analyser::Static::Macintosh::Target::Model model> class ConcreteMachin
uint32_t ram_mask_ = 0;
uint32_t rom_mask_ = 0;
uint16_t rom_[64*1024]; // i.e. up to 128kb in size.
std::vector<uint16_t> ram_;
uint8_t rom_[128*1024];
std::vector<uint8_t> ram_;
};
}

View File

@@ -26,7 +26,7 @@ using namespace Apple::Macintosh;
Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) :
audio_(audio),
drive_speed_accumulator_(drive_speed_accumulator),
crt_(704, 1, 370, Outputs::Display::ColourSpace::YIQ, 1, 1, 6, false, Outputs::Display::InputDataType::Luminance1) {
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
@@ -37,6 +37,10 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 2.0f;
}
void Video::run_for(HalfCycles duration) {
// Determine the current video and audio bases. These values don't appear to be latched, they apply immediately.
const size_t video_base = (use_alternate_screen_buffer_ ? (0xffff2700 >> 1) : (0xffffa700 >> 1)) & ram_mask_;

View File

@@ -42,6 +42,9 @@ class Video {
*/
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*!
Produces the next @c duration period of pixels.
*/

View File

@@ -48,7 +48,7 @@ class Joystick: public Inputs::ConcreteJoystick {
}),
bus_(bus), shift_(shift), fire_tia_input_(fire_tia_input) {}
void did_set_input(const Input &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) final {
switch(digital_input.type) {
case Input::Up: bus_->mos6532_.update_port_input(0, 0x10 >> shift_, is_active); break;
case Input::Down: bus_->mos6532_.update_port_input(0, 0x20 >> shift_, is_active); break;
@@ -77,12 +77,9 @@ using Target = Analyser::Static::Atari2600::Target;
class ConcreteMachine:
public Machine,
public CRTMachine::Machine,
public JoystickMachine::Machine,
public Outputs::CRT::Delegate {
public JoystickMachine::Machine {
public:
ConcreteMachine(const Target &target) {
set_clock_rate(NTSC_clock_rate);
ConcreteMachine(const Target &target) : frequency_mismatch_warner_(*this) {
const std::vector<uint8_t> &rom = target.media.cartridges.front()->get_segments().front().data;
using PagingModel = Target::PagingModel;
@@ -122,13 +119,15 @@ class ConcreteMachine:
joysticks_.emplace_back(new Joystick(bus_.get(), 0, 0));
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
set_is_ntsc(is_ntsc_);
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
void set_switch_is_enabled(Atari2600Switch input, bool state) override {
void set_switch_is_enabled(Atari2600Switch input, bool state) final {
switch(input) {
case Atari2600SwitchReset: bus_->mos6532_.update_port_input(1, 0x01, state); break;
case Atari2600SwitchSelect: bus_->mos6532_.update_port_input(1, 0x02, state); break;
@@ -138,7 +137,7 @@ class ConcreteMachine:
}
}
bool get_switch_is_enabled(Atari2600Switch input) override {
bool get_switch_is_enabled(Atari2600Switch input) final {
uint8_t port_input = bus_->mos6532_.get_port_input(1);
switch(input) {
case Atari2600SwitchReset: return !!(port_input & 0x01);
@@ -150,83 +149,62 @@ class ConcreteMachine:
}
}
void set_reset_switch(bool state) override {
void set_reset_switch(bool state) final {
bus_->set_reset_line(state);
}
// to satisfy CRTMachine::Machine
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
bus_->speaker_.set_input_rate(static_cast<float>(get_clock_rate() / static_cast<double>(CPUTicksPerAudioTick)));
bus_->tia_.set_crt_delegate(this);
bus_->tia_.set_crt_delegate(&frequency_mismatch_warner_);
bus_->tia_.set_scan_target(scan_target);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return bus_->tia_.get_scaled_scan_status() / 3.0f;
}
Outputs::Speaker::Speaker *get_speaker() final {
return &bus_->speaker_;
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
bus_->run_for(cycles);
bus_->apply_confidence(confidence_counter_);
}
// to satisfy Outputs::CRT::Delegate
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) override {
const std::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_ ++;
if(frame_record_pointer_ >= 6) {
int total_number_of_frames = 0;
int total_number_of_unexpected_vertical_syncs = 0;
for(std::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(std::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(static_cast<float>(clock_rate / static_cast<double>(CPUTicksPerAudioTick)));
bus_->speaker_.set_high_frequency_cutoff(static_cast<float>(clock_rate / (static_cast<double>(CPUTicksPerAudioTick) * 2.0)));
set_clock_rate(clock_rate);
}
}
void flush() {
bus_->flush();
}
float get_confidence() override {
void register_crt_frequency_mismatch() {
is_ntsc_ ^= true;
set_is_ntsc(is_ntsc_);
}
float get_confidence() final {
return confidence_counter_.get_confidence();
}
private:
// the bus
// The bus.
std::unique_ptr<Bus> bus_;
// output frame rate tracker
struct FrameRecord {
int number_of_frames = 0;
int number_of_unexpected_vertical_syncs = 0;
} frame_records_[4];
unsigned int frame_record_pointer_ = 0;
// Output frame rate tracker.
Outputs::CRT::CRTFrequencyMismatchWarner<ConcreteMachine> frequency_mismatch_warner_;
bool is_ntsc_ = true;
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
// a confidence counter
Analyser::Dynamic::ConfidenceCounter confidence_counter_;
void set_is_ntsc(bool is_ntsc) {
bus_->tia_.set_output_mode(is_ntsc ? TIA::OutputMode::NTSC : TIA::OutputMode::PAL);
const double clock_rate = is_ntsc ? NTSC_clock_rate : PAL_clock_rate;
bus_->speaker_.set_input_rate(float(clock_rate) / float(CPUTicksPerAudioTick));
bus_->speaker_.set_high_frequency_cutoff(float(clock_rate) / float(CPUTicksPerAudioTick * 2));
set_clock_rate(clock_rate);
}
};
}

View File

@@ -33,6 +33,7 @@ class Bus {
virtual void run_for(const Cycles cycles) = 0;
virtual void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) = 0;
virtual void set_reset_line(bool state) = 0;
virtual void flush() = 0;
// the RIOT, TIA and speaker
PIA mos6532_;

View File

@@ -39,7 +39,7 @@ template<class T> class Cartridge:
// consider doing something less fragile.
}
void run_for(const Cycles cycles) {
void run_for(const Cycles cycles) override {
// Horizontal counter resets are used as a proxy for whether this really is an Atari 2600
// title. Random memory accesses are likely to trigger random counter resets.
horizontal_counter_resets_ = 0;
@@ -50,13 +50,13 @@ template<class T> class Cartridge:
/*!
Adjusts @c confidence_counter according to the results of the most recent run_for.
*/
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) {
void apply_confidence(Analyser::Dynamic::ConfidenceCounter &confidence_counter) override {
if(cycle_count_.as_integral() < 200) return;
if(horizontal_counter_resets_ > 10)
confidence_counter.add_miss();
}
void set_reset_line(bool state) { m6502_.set_reset_line(state); }
void set_reset_line(bool state) override { m6502_.set_reset_line(state); }
// to satisfy CPU::MOS6502::Processor
Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
@@ -181,9 +181,9 @@ template<class T> class Cartridge:
if((address&0x1280) == 0x280) {
update_6532();
if(isReadOperation(operation)) {
returnValue &= mos6532_.get_register(address);
returnValue &= mos6532_.read(address);
} else {
mos6532_.set_register(address, *value);
mos6532_.write(address, *value);
}
}
@@ -197,7 +197,7 @@ template<class T> class Cartridge:
return Cycles(cycles_run_for / 3);
}
void flush() {
void flush() override {
update_audio();
update_video();
audio_queue_.perform();

View File

@@ -142,6 +142,10 @@ void TIA::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus TIA::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 2.0f;
}
void TIA::run_for(const Cycles cycles) {
int number_of_cycles = int(cycles.as_integral());

View File

@@ -75,6 +75,7 @@ class TIA {
void set_crt_delegate(Outputs::CRT::Delegate *);
void set_scan_target(Outputs::Display::ScanTarget *);
Outputs::Display::ScanStatus get_scaled_scan_status() const;
private:
Outputs::CRT::CRT crt_;

View File

@@ -18,21 +18,21 @@ Atari2600::TIASound::TIASound(Concurrency::DeferringAsyncTaskQueue &audio_queue)
{}
void Atari2600::TIASound::set_volume(int channel, uint8_t volume) {
audio_queue_.defer([=]() {
volume_[channel] = volume & 0xf;
audio_queue_.defer([target = &volume_[channel], volume]() {
*target = volume & 0xf;
});
}
void Atari2600::TIASound::set_divider(int channel, uint8_t divider) {
audio_queue_.defer([=]() {
audio_queue_.defer([this, channel, divider]() {
divider_[channel] = divider & 0x1f;
divider_counter_[channel] = 0;
});
}
void Atari2600::TIASound::set_control(int channel, uint8_t control) {
audio_queue_.defer([=]() {
control_[channel] = control & 0xf;
audio_queue_.defer([target = &control_[channel], control]() {
*target = control & 0xf;
});
}

View File

@@ -29,6 +29,7 @@ class TIASound: public Outputs::Speaker::SampleSource {
// To satisfy ::SampleSource.
void get_samples(std::size_t number_of_samples, int16_t *target);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;

View File

@@ -16,6 +16,7 @@
#include "../../../Activity/Source.hpp"
//#define LOG_TRACE
//bool should_log = false;
#include "../../../Processors/68000/68000.hpp"
#include "../../../Components/AY38910/AY38910.hpp"
@@ -100,8 +101,10 @@ class ConcreteMachine:
const bool is_early_tos = true;
if(is_early_tos) {
rom_start_ = 0xfc0000;
for(c = 0xfc; c < 0xff; ++c) memory_map_[c] = BusDevice::ROM;
} else {
rom_start_ = 0xe00000;
for(c = 0xe0; c < 0xe4; ++c) memory_map_[c] = BusDevice::ROM;
}
@@ -138,6 +141,10 @@ class ConcreteMachine:
video_->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
video_->set_display_type(display_type);
}
@@ -240,7 +247,7 @@ class ConcreteMachine:
case BusDevice::ROM:
memory = rom_.data();
address %= rom_.size();
address -= rom_start_;
break;
case BusDevice::Floating:
@@ -263,11 +270,11 @@ class ConcreteMachine:
return delay;
case BusDevice::IO:
switch(address >> 1) {
switch(address & 0xfffe) { // TODO: surely it's going to be even less precise than this?
default:
// assert(false);
case 0x7fc000:
case 0x8000:
/* Memory controller configuration:
b0, b1: bank 1
b2, b3: bank 0
@@ -279,8 +286,64 @@ class ConcreteMachine:
*/
break;
case 0x7fc400: /* PSG: write to select register, read to read register. */
case 0x7fc401: /* PSG: write to write register. */
// Video controls.
case 0x8200: case 0x8202: case 0x8204: case 0x8206:
case 0x8208: case 0x820a: case 0x820c: case 0x820e:
case 0x8210: case 0x8212: case 0x8214: case 0x8216:
case 0x8218: case 0x821a: case 0x821c: case 0x821e:
case 0x8220: case 0x8222: case 0x8224: case 0x8226:
case 0x8228: case 0x822a: case 0x822c: case 0x822e:
case 0x8230: case 0x8232: case 0x8234: case 0x8236:
case 0x8238: case 0x823a: case 0x823c: case 0x823e:
case 0x8240: case 0x8242: case 0x8244: case 0x8246:
case 0x8248: case 0x824a: case 0x824c: case 0x824e:
case 0x8250: case 0x8252: case 0x8254: case 0x8256:
case 0x8258: case 0x825a: case 0x825c: case 0x825e:
case 0x8260: case 0x8262:
if(!cycle.data_select_active()) return delay;
if(cycle.operation & Microcycle::Read) {
cycle.set_value16(video_->read(int(address >> 1)));
} else {
video_->write(int(address >> 1), cycle.value16());
}
break;
// DMA.
case 0x8604: case 0x8606: case 0x8608: case 0x860a: case 0x860c:
if(!cycle.data_select_active()) return delay;
if(cycle.operation & Microcycle::Read) {
cycle.set_value16(dma_->read(int(address >> 1)));
} else {
dma_->write(int(address >> 1), cycle.value16());
}
break;
// Audio.
//
// Re: mirrors, Dan Hollis' hardware register list asserts:
//
// "Note: PSG Registers are now fixed at these addresses. All other addresses are masked out on the Falcon. Any
// writes to the shadow registers $8804-$88FF will cause bus errors.", which I am taking to imply that those shadow
// registers exist on the Atari ST.
case 0x8800: case 0x8802: case 0x8804: case 0x8806: case 0x8808: case 0x880a: case 0x880c: case 0x880e:
case 0x8810: case 0x8812: case 0x8814: case 0x8816: case 0x8818: case 0x881a: case 0x881c: case 0x881e:
case 0x8820: case 0x8822: case 0x8824: case 0x8826: case 0x8828: case 0x882a: case 0x882c: case 0x882e:
case 0x8830: case 0x8832: case 0x8834: case 0x8836: case 0x8838: case 0x883a: case 0x883c: case 0x883e:
case 0x8840: case 0x8842: case 0x8844: case 0x8846: case 0x8848: case 0x884a: case 0x884c: case 0x884e:
case 0x8850: case 0x8852: case 0x8854: case 0x8856: case 0x8858: case 0x885a: case 0x885c: case 0x885e:
case 0x8860: case 0x8862: case 0x8864: case 0x8866: case 0x8868: case 0x886a: case 0x886c: case 0x886e:
case 0x8870: case 0x8872: case 0x8874: case 0x8876: case 0x8878: case 0x887a: case 0x887c: case 0x887e:
case 0x8880: case 0x8882: case 0x8884: case 0x8886: case 0x8888: case 0x888a: case 0x888c: case 0x888e:
case 0x8890: case 0x8892: case 0x8894: case 0x8896: case 0x8898: case 0x889a: case 0x889c: case 0x889e:
case 0x88a0: case 0x88a2: case 0x88a4: case 0x88a6: case 0x88a8: case 0x88aa: case 0x88ac: case 0x88ae:
case 0x88b0: case 0x88b2: case 0x88b4: case 0x88b6: case 0x88b8: case 0x88ba: case 0x88bc: case 0x88be:
case 0x88c0: case 0x88c2: case 0x88c4: case 0x88c6: case 0x88c8: case 0x88ca: case 0x88cc: case 0x88ce:
case 0x88d0: case 0x88d2: case 0x88d4: case 0x88d6: case 0x88d8: case 0x88da: case 0x88dc: case 0x88de:
case 0x88e0: case 0x88e2: case 0x88e4: case 0x88e6: case 0x88e8: case 0x88ea: case 0x88ec: case 0x88ee:
case 0x88f0: case 0x88f2: case 0x88f4: case 0x88f6: case 0x88f8: case 0x88fa: case 0x88fc: case 0x88fe:
if(!cycle.data_select_active()) return delay;
advance_time(HalfCycles(2));
@@ -291,25 +354,26 @@ class ConcreteMachine:
cycle.set_value8_high(ay_.get_data_output());
ay_.set_control_lines(GI::AY38910::ControlLines(0));
} else {
if((address >> 1) == 0x7fc400) {
ay_.set_control_lines(GI::AY38910::BC1);
} else {
ay_.set_control_lines(GI::AY38910::ControlLines(GI::AY38910::BC2 | GI::AY38910::BDIR));
}
// Net effect here: addresses with bit 1 set write to a register,
// addresses with bit 1 clear select a register.
ay_.set_control_lines(GI::AY38910::ControlLines(
GI::AY38910::BC2 | GI::AY38910::BDIR
| ((address&2) ? 0 : GI::AY38910::BC1)
));
ay_.set_data_input(cycle.value8_high());
ay_.set_control_lines(GI::AY38910::ControlLines(0));
}
return delay + HalfCycles(2);
// The MFP block:
case 0x7ffd00: case 0x7ffd01: case 0x7ffd02: case 0x7ffd03:
case 0x7ffd04: case 0x7ffd05: case 0x7ffd06: case 0x7ffd07:
case 0x7ffd08: case 0x7ffd09: case 0x7ffd0a: case 0x7ffd0b:
case 0x7ffd0c: case 0x7ffd0d: case 0x7ffd0e: case 0x7ffd0f:
case 0x7ffd10: case 0x7ffd11: case 0x7ffd12: case 0x7ffd13:
case 0x7ffd14: case 0x7ffd15: case 0x7ffd16: case 0x7ffd17:
case 0x7ffd18: case 0x7ffd19: case 0x7ffd1a: case 0x7ffd1b:
case 0x7ffd1c: case 0x7ffd1d: case 0x7ffd1e: case 0x7ffd1f:
case 0xfa00: case 0xfa02: case 0xfa04: case 0xfa06:
case 0xfa08: case 0xfa0a: case 0xfa0c: case 0xfa0e:
case 0xfa10: case 0xfa12: case 0xfa14: case 0xfa16:
case 0xfa18: case 0xfa1a: case 0xfa1c: case 0xfa1e:
case 0xfa20: case 0xfa22: case 0xfa24: case 0xfa26:
case 0xfa28: case 0xfa2a: case 0xfa2c: case 0xfa2e:
case 0xfa30: case 0xfa32: case 0xfa34: case 0xfa36:
case 0xfa38: case 0xfa3a: case 0xfa3c: case 0xfa3e:
if(!cycle.data_select_active()) return delay;
if(cycle.operation & Microcycle::Read) {
@@ -319,53 +383,19 @@ class ConcreteMachine:
}
break;
// Video controls.
case 0x7fc100: case 0x7fc101: case 0x7fc102: case 0x7fc103:
case 0x7fc104: case 0x7fc105: case 0x7fc106: case 0x7fc107:
case 0x7fc108: case 0x7fc109: case 0x7fc10a: case 0x7fc10b:
case 0x7fc10c: case 0x7fc10d: case 0x7fc10e: case 0x7fc10f:
case 0x7fc110: case 0x7fc111: case 0x7fc112: case 0x7fc113:
case 0x7fc114: case 0x7fc115: case 0x7fc116: case 0x7fc117:
case 0x7fc118: case 0x7fc119: case 0x7fc11a: case 0x7fc11b:
case 0x7fc11c: case 0x7fc11d: case 0x7fc11e: case 0x7fc11f:
case 0x7fc120: case 0x7fc121: case 0x7fc122: case 0x7fc123:
case 0x7fc124: case 0x7fc125: case 0x7fc126: case 0x7fc127:
case 0x7fc128: case 0x7fc129: case 0x7fc12a: case 0x7fc12b:
case 0x7fc12c: case 0x7fc12d: case 0x7fc12e: case 0x7fc12f:
case 0x7fc130: case 0x7fc131:
if(!cycle.data_select_active()) return delay;
if(cycle.operation & Microcycle::Read) {
cycle.set_value16(video_->read(int(address >> 1)));
} else {
video_->write(int(address >> 1), cycle.value16());
}
break;
// ACIAs.
case 0x7ffe00: case 0x7ffe01: case 0x7ffe02: case 0x7ffe03: {
case 0xfc00: case 0xfc02: case 0xfc04: case 0xfc06: {
// Set VPA.
mc68000_.set_is_peripheral_address(!cycle.data_select_active());
if(!cycle.data_select_active()) return delay;
const auto acia_ = ((address >> 1) < 0x7ffe02) ? &keyboard_acia_ : &midi_acia_;
const auto acia_ = (address & 4) ? &midi_acia_ : &keyboard_acia_;
if(cycle.operation & Microcycle::Read) {
cycle.set_value8_high((*acia_)->read(int(address >> 1)));
} else {
(*acia_)->write(int(address >> 1), cycle.value8_high());
}
} break;
// DMA.
case 0x7fc302: case 0x7fc303: case 0x7fc304: case 0x7fc305: case 0x7fc306:
if(!cycle.data_select_active()) return delay;
if(cycle.operation & Microcycle::Read) {
cycle.set_value16(dma_->read(int(address >> 1)));
} else {
dma_->write(int(address >> 1), cycle.value16());
}
break;
}
return HalfCycles(0);
}
@@ -411,7 +441,8 @@ class ConcreteMachine:
// Advance the relevant counters.
cycles_since_audio_update_ += length;
mfp_ += length;
dma_ += length;
if(dma_clocking_preference_ != ClockingHint::Preference::None)
dma_ += length;
keyboard_acia_ += length;
midi_acia_ += length;
bus_phase_ += length;
@@ -432,7 +463,7 @@ class ConcreteMachine:
mfp_.flush();
}
if(dma_is_realtime_) {
if(dma_clocking_preference_ == ClockingHint::Preference::RealTime) {
dma_.flush();
}
@@ -441,6 +472,7 @@ class ConcreteMachine:
length -= cycles_until_video_event_;
video_ += cycles_until_video_event_;
cycles_until_video_event_ = video_->get_next_sequence_point();
assert(cycles_until_video_event_ > HalfCycles(0));
mfp_->set_timer_event_input(1, video_->display_enabled());
update_interrupt_input();
@@ -465,8 +497,8 @@ class ConcreteMachine:
JustInTimeActor<Motorola::ACIA::ACIA, 16> midi_acia_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay_;
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910> speaker_;
GI::AY38910::AY38910<false> ay_;
Outputs::Speaker::LowpassSpeaker<GI::AY38910::AY38910<false>> speaker_;
HalfCycles cycles_since_audio_update_;
JustInTimeActor<DMAController> dma_;
@@ -476,6 +508,7 @@ class ConcreteMachine:
std::vector<uint8_t> ram_;
std::vector<uint8_t> rom_;
uint32_t rom_start_ = 0;
enum class BusDevice {
/// A mostly RAM page is one that returns ROM for the first 8 bytes, RAM elsewhere.
@@ -499,7 +532,7 @@ class ConcreteMachine:
bool may_defer_acias_ = true;
bool keyboard_needs_clock_ = false;
bool mfp_is_realtime_ = false;
bool dma_is_realtime_ = false;
ClockingHint::Preference dma_clocking_preference_ = ClockingHint::Preference::None;
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
// This is being called by one of the components; avoid any time flushing here as that's
// already dealt with (and, just to be absolutely sure, to avoid recursive mania).
@@ -508,7 +541,7 @@ class ConcreteMachine:
(midi_acia_.last_valid()->preferred_clocking() != ClockingHint::Preference::RealTime);
keyboard_needs_clock_ = ikbd_.preferred_clocking() != ClockingHint::Preference::None;
mfp_is_realtime_ = mfp_.last_valid()->preferred_clocking() == ClockingHint::Preference::RealTime;
dma_is_realtime_ = dma_.last_valid()->preferred_clocking() == ClockingHint::Preference::RealTime;
dma_clocking_preference_ = dma_.last_valid()->preferred_clocking();
}
// MARK: - GPIP input.
@@ -539,7 +572,7 @@ class ConcreteMachine:
GPIP 0: centronics busy
*/
mfp_->set_port_input(
0x80 | // b7: Monochrome monitor detect (1 = is monochrome).
0x80 | // b7: Monochrome monitor detect (0 = is monochrome).
0x40 | // b6: RS-232 ring indicator.
(dma_->get_interrupt_line() ? 0x00 : 0x20) | // b5: FD/HS interrupt (0 = interrupt requested).
((keyboard_acia_->get_interrupt_line() || midi_acia_->get_interrupt_line()) ? 0x00 : 0x10) | // b4: Keyboard/MIDI interrupt (0 = interrupt requested).
@@ -634,7 +667,7 @@ class ConcreteMachine:
}
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
dma_->set_activity_observer(observer);
}

View File

@@ -45,7 +45,7 @@ uint16_t DMAController::read(int address) {
if(control_ & Control::CPUTarget) {
return 0xffff;
} else {
return 0xff00 | fdc_.get_register(control_ >> 1);
return 0xff00 | fdc_.read(control_ >> 1);
}
}
break;
@@ -78,7 +78,7 @@ void DMAController::write(int address, uint16_t value) {
if(control_ & Control::CPUTarget) {
// TODO: HDC.
} else {
fdc_.set_register(control_ >> 1, uint8_t(value));
fdc_.write(control_ >> 1, uint8_t(value));
}
}
break;
@@ -126,7 +126,7 @@ void DMAController::set_floppy_drive_selection(bool drive1, bool drive2, bool si
}
void DMAController::set_floppy_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
fdc_.drives_[drive]->set_disk(disk);
fdc_.set_disk(disk, drive);
}
void DMAController::run_for(HalfCycles duration) {
@@ -153,7 +153,7 @@ void DMAController::wd1770_did_change_output(WD::WD1770 *) {
// Read from the data register into the active buffer.
if(bytes_received_ < 16) {
buffer_[active_buffer_].contents[bytes_received_] = fdc_.get_register(3);
buffer_[active_buffer_].contents[bytes_received_] = fdc_.read(3);
++bytes_received_;
}
if(bytes_received_ == 16) {
@@ -256,6 +256,5 @@ ClockingHint::Preference DMAController::preferred_clocking() {
}
void DMAController::set_activity_observer(Activity::Observer *observer) {
fdc_.drives_[0]->set_activity_observer(observer, "Internal", true);
fdc_.drives_[1]->set_activity_observer(observer, "External", true);
fdc_.set_activity_observer(observer);
}

View File

@@ -56,30 +56,36 @@ class DMAController: public WD::WD1770::Delegate, public ClockingHint::Source, p
HalfCycles running_time_;
struct WD1772: public WD::WD1770 {
WD1772(): WD::WD1770(WD::WD1770::P1772) {
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
set_drive(drives_[0]);
emplace_drives(2, 8000000, 300, 2);
set_is_double_density(true); // TODO: is this selectable on the ST?
}
void set_motor_on(bool motor_on) final {
drives_[0]->set_motor_on(motor_on);
drives_[1]->set_motor_on(motor_on);
for_all_drives([motor_on] (Storage::Disk::Drive &drive, size_t) {
drive.set_motor_on(motor_on);
});
}
void set_floppy_drive_selection(bool drive1, bool drive2, bool side2) {
// TODO: handle no drives and/or both drives selected.
if(drive1) {
set_drive(drives_[0]);
} else {
set_drive(drives_[1]);
}
set_drive(
(drive1 ? 1 : 0) |
(drive2 ? 2 : 0)
);
drives_[0]->set_head(side2);
drives_[1]->set_head(side2);
for_all_drives([side2] (Storage::Disk::Drive &drive, size_t) {
drive.set_head(side2);
});
}
void set_activity_observer(Activity::Observer *observer) {
get_drive(0).set_activity_observer(observer, "Internal", true);
get_drive(1).set_activity_observer(observer, "External", true);
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
get_drive(drive).set_disk(disk);
}
std::vector<std::shared_ptr<Storage::Disk::Drive>> drives_;
} fdc_;
void wd1770_did_change_output(WD::WD1770 *) final;

View File

@@ -100,13 +100,46 @@ void IntelligentKeyboard::run_for(HalfCycles duration) {
// machine advertises as joystick 1 is mapped to the Atari ST's joystick 2, so as to
// maintain both the normal emulation expections that the first joystick is the primary
// one and the Atari ST's convention that the main joystick is in port 2.
for(size_t c = 0; c < 2; ++c) {
const auto joystick = static_cast<Joystick *>(joysticks_[c ^ 1].get());
if(joystick->has_event()) {
output_bytes({
uint8_t(0xfe | c),
joystick->get_state()
});
if(joystick_mode_ == JoystickMode::Event || joystick_mode_ == JoystickMode::KeyCode) {
for(size_t c = 0; c < 2; ++c) {
const auto joystick = static_cast<Joystick *>(joysticks_[c ^ 1].get());
if(joystick->has_event()) {
if(joystick_mode_ == JoystickMode::Event) {
// Event mode: forward a joystick event message.
output_bytes({
uint8_t(0xfe | c),
joystick->get_state()
});
} else {
// Key code mode: decompose the joystick event into
// instantaneous key events.
const auto event_mask = joystick->event_mask();
const auto new_state = joystick->get_state();
const auto new_presses = (event_mask ^ new_state) & new_state;
// Send cursor keys for the movement.
const Key keys[] = {Key::Up, Key::Down, Key::Left, Key::Right};
for(int key = 0; key < 4; ++key) {
if(new_presses & (1 << key)) {
output_bytes({
uint8_t(keys[key]),
uint8_t(0x80 | uint8_t(keys[key]))
});
}
}
// Check also for fire, but the key to send depends
// on the joystick.
if(new_presses & 0x80) {
const Key fire_buttons[] = {Key::Joystick1Button, Key::Joystick2Button};
output_bytes({
uint8_t(fire_buttons[c]),
uint8_t(0x80 | uint8_t(fire_buttons[c]))
});
}
}
}
}
}
@@ -425,21 +458,45 @@ void IntelligentKeyboard::disable_joysticks() {
void IntelligentKeyboard::set_joystick_event_mode() {
joystick_mode_ = JoystickMode::Event;
clear_joystick_events();
}
void IntelligentKeyboard::set_joystick_interrogation_mode() {
joystick_mode_ = JoystickMode::Interrogation;
}
void IntelligentKeyboard::interrogate_joysticks() {
void IntelligentKeyboard::set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical) {
joystick_mode_ = JoystickMode::KeyCode;
clear_joystick_events();
}
void IntelligentKeyboard::clear_joystick_events() {
const auto joystick1 = static_cast<Joystick *>(joysticks_[0].get());
const auto joystick2 = static_cast<Joystick *>(joysticks_[1].get());
joystick1->get_state();
joystick2->get_state();
}
output_bytes({
0xfd,
joystick2->get_state(),
joystick1->get_state()
});
void IntelligentKeyboard::interrogate_joysticks() {
if(joystick_mode_ != JoystickMode::Interrogation) {
// Joystick::get_state() implicitly clears Joystick::has_event,
// so don't permit interrogation if the user isn't in interrogation
// mode because it might cause dropped events.
output_bytes({
0xfd,
0x00,
0x00
});
} else {
const auto joystick1 = static_cast<Joystick *>(joysticks_[0].get());
const auto joystick2 = static_cast<Joystick *>(joysticks_[1].get());
output_bytes({
0xfd,
joystick2->get_state(),
joystick1->get_state()
});
}
}
void IntelligentKeyboard::set_joystick_monitoring_mode(uint8_t rate) {
@@ -449,7 +506,3 @@ void IntelligentKeyboard::set_joystick_monitoring_mode(uint8_t rate) {
void IntelligentKeyboard::set_joystick_fire_button_monitoring_mode() {
LOG("Unimplemented: joystick fire button monitoring mode");
}
void IntelligentKeyboard::set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical) {
LOG("Unimplemented: joystick keycode mode");
}

View File

@@ -40,7 +40,9 @@ enum class Key: uint16_t {
Insert = 0x52, Delete,
ISO = 0x60, Undo, Help, KeypadOpenBracket, KeypadCloseBracket, KeypadDivide, KeypadMultiply,
Keypad7, Keypad8, Keypad9, Keypad4, Keypad5, Keypad6, Keypad1, Keypad2, Keypad3, Keypad0, KeypadDecimalPoint,
KeypadEnter
KeypadEnter,
Joystick1Button = 0x74, // These keycodes are used only in joystick keycode mode.
Joystick2Button = 0x75,
};
static_assert(uint16_t(Key::RightShift) == 0x36, "RightShift should have key code 0x36; check intermediate entries");
static_assert(uint16_t(Key::F10) == 0x44, "F10 should have key code 0x44; check intermediate entries");
@@ -143,8 +145,10 @@ class IntelligentKeyboard:
void set_joystick_keycode_mode(VelocityThreshold horizontal, VelocityThreshold vertical);
void interrogate_joysticks();
void clear_joystick_events();
enum class JoystickMode {
Disabled, Event, Interrogation
Disabled, Event, Interrogation, KeyCode
} joystick_mode_ = JoystickMode::Event;
class Joystick: public Inputs::ConcreteJoystick {
@@ -158,7 +162,7 @@ class IntelligentKeyboard:
Input(Input::Fire, 0),
}) {}
void did_set_input(const Input &input, bool is_active) override {
void did_set_input(const Input &input, bool is_active) final {
uint8_t mask = 0;
switch(input.type) {
default: return;
@@ -181,6 +185,10 @@ class IntelligentKeyboard:
return returned_state_ != state_;
}
uint8_t event_mask() {
return returned_state_ ^ state_;
}
private:
uint8_t state_ = 0x00;
uint8_t returned_state_ = 0x00;

View File

@@ -13,6 +13,8 @@
#include <algorithm>
#include <cstring>
#define CYCLE(x) ((x) * 2)
using namespace Atari::ST;
namespace {
@@ -29,7 +31,7 @@ const struct VerticalParams {
} vertical_params[3] = {
{63, 263, 313}, // 47 rather than 63 on early machines.
{34, 234, 263},
{1, 401, 500} // 72 Hz mode: who knows?
{34, 434, 500} // Guesswork: (i) nobody ever recommends 72Hz mode for opening the top border, so it's likely to be the same as another mode; (ii) being the same as PAL feels too late.
};
/// @returns The correct @c VerticalParams for output at @c frequency.
@@ -37,8 +39,6 @@ const VerticalParams &vertical_parameters(Video::FieldFrequency frequency) {
return vertical_params[int(frequency)];
}
#define CYCLE(x) ((x) * 2)
/*!
Defines the horizontal counts at which mode-specific events will occur:
horizontal enable being set and being reset, blank being set and reset, and the
@@ -57,13 +57,23 @@ const struct HorizontalParams {
const int set_blank;
const int reset_blank;
const int length;
const int vertical_decision;
LineLength length;
} horizontal_params[3] = {
{CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(512)},
{CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(508)},
{CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(224)} // 72Hz mode doesn't set or reset blank.
{CYCLE(56), CYCLE(376), CYCLE(450), CYCLE(28), CYCLE(502), { CYCLE(512), CYCLE(464), CYCLE(504) }},
{CYCLE(52), CYCLE(372), CYCLE(450), CYCLE(24), CYCLE(502), { CYCLE(508), CYCLE(460), CYCLE(500) }},
{CYCLE(4), CYCLE(164), CYCLE(999), CYCLE(999), CYCLE(214), { CYCLE(224), CYCLE(194), CYCLE(212) }}
// 72Hz mode doesn't set or reset blank.
};
// Re: 'vertical_decision':
// This is cycle 502 if in 50 or 60 Hz mode; in 70 Hz mode I've put it on cycle 214
// in order to be analogous to 50 and 60 Hz mode. I have no idea where it should
// actually go.
//
// Ditto the horizontal sync timings for 72Hz are plucked out of thin air.
const HorizontalParams &horizontal_parameters(Video::FieldFrequency frequency) {
return horizontal_params[int(frequency)];
}
@@ -79,10 +89,10 @@ struct Checker {
assert(horizontal.reset_blank < horizontal.set_enable);
assert(horizontal.set_enable < horizontal.reset_enable);
assert(horizontal.reset_enable < horizontal.set_blank);
assert(horizontal.set_blank+50 < horizontal.length);
assert(horizontal.set_blank+50 < horizontal.length.length);
} else {
assert(horizontal.set_enable < horizontal.reset_enable);
assert(horizontal.set_enable+50 <horizontal.length);
assert(horizontal.set_enable+50 <horizontal.length.length);
}
// Expected vertical order of events: reset blank, enable display, disable display, enable blank (at least 50 before end of line), end of line
@@ -94,15 +104,16 @@ struct Checker {
} checker;
#endif
const int de_delay_period = CYCLE(28); // Number of half cycles after DE that observed DE changes.
const int de_delay_period = CYCLE(28); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
const int vsync_x_position = CYCLE(56); // Horizontal cycle on which vertical sync changes happen.
const int hsync_start = CYCLE(48); // Cycles before end of line when hsync starts.
const int hsync_end = CYCLE(8); // Cycles before end of line when hsync ends.
const int line_length_latch_position = CYCLE(54);
const int hsync_delay_period = hsync_end; // Signal hsync at the end of the line.
const int hsync_delay_period = CYCLE(8); // Signal hsync at the end of the line.
const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same delay as hsync.
const int load_delay_period = CYCLE(4); // Amount of time after DE that observed DE changes. NB: HACK HERE. This currently incorporates the MFP recognition delay. MUST FIX.
// "VSYNC starts 104 cycles after the start of the previous line's HSYNC, so that's 4 cycles before DE would be activated. ";
// that's an inconsistent statement since it would imply VSYNC at +54, which is 2 cycles before DE in 60Hz mode and 6 before
// in 50Hz mode. I've gone with 56, to be four cycles ahead of DE in 50Hz mode.
@@ -110,13 +121,13 @@ const int vsync_delay_period = hsync_delay_period; // Signal vsync with the same
}
Video::Video() :
deferrer_([=] (HalfCycles duration) { advance(duration); }),
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
crt_(2048, 2, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
// crt_(896, 1, 500, 5, Outputs::Display::InputDataType::Red4Green4Blue4),
video_stream_(crt_, palette_) {
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
// usual output height of 200 lines.
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 216, 850, 4.0f / 3.0f));
crt_.set_visible_area(crt_.get_rect_for_area(33, 260, 440, 1700, 4.0f / 3.0f));
}
void Video::set_ram(uint16_t *ram, size_t size) {
@@ -127,52 +138,41 @@ void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus Video::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / 4.0f;
}
void Video::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
void Video::run_for(HalfCycles duration) {
deferrer_.run_for(duration);
}
void Video::advance(HalfCycles duration) {
const auto horizontal_timings = horizontal_parameters(field_frequency_);
const auto vertical_timings = vertical_parameters(field_frequency_);
int integer_duration = int(duration.as_integral());
// Effect any changes in visible state out here; they're not relevant in the inner loop.
if(!pending_events_.empty()) {
auto erase_iterator = pending_events_.begin();
int duration_remaining = integer_duration;
while(erase_iterator != pending_events_.end()) {
erase_iterator->delay -= duration_remaining;
if(erase_iterator->delay <= 0) {
duration_remaining = -erase_iterator->delay;
erase_iterator->apply(public_state_);
++erase_iterator;
} else {
break;
}
}
if(erase_iterator != pending_events_.begin()) {
pending_events_.erase(pending_events_.begin(), erase_iterator);
}
}
assert(integer_duration >= 0);
while(integer_duration) {
const auto horizontal_timings = horizontal_parameters(field_frequency_);
const auto vertical_timings = vertical_parameters(field_frequency_);
// Determine time to next event; this'll either be one of the ones informally scheduled in here,
// or something from the deferral queue.
// Seed next event to end of line.
int next_event = line_length_;
int next_event = line_length_.length;
const int next_deferred_event = deferrer_.time_until_next_action().as<int>();
if(next_deferred_event >= 0)
next_event = std::min(next_event, next_deferred_event + x_);
// Check the explicitly-placed events.
if(horizontal_timings.reset_blank > x_) next_event = std::min(next_event, horizontal_timings.reset_blank);
if(horizontal_timings.set_blank > x_) next_event = std::min(next_event, horizontal_timings.set_blank);
if(horizontal_timings.reset_enable > x_) next_event = std::min(next_event, horizontal_timings.reset_enable);
if(horizontal_timings.set_enable > x_) next_event = std::min(next_event, horizontal_timings.set_enable);
if(next_load_toggle_ > x_) next_event = std::min(next_event, next_load_toggle_);
// Check for events that are relative to existing latched state.
if(line_length_ - hsync_start > x_) next_event = std::min(next_event, line_length_ - hsync_start);
if(line_length_ - hsync_end > x_) next_event = std::min(next_event, line_length_ - hsync_end);
if(line_length_.hsync_start > x_) next_event = std::min(next_event, line_length_.hsync_start);
if(line_length_.hsync_end > x_) next_event = std::min(next_event, line_length_.hsync_end);
// Also, a vertical sync event might intercede.
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ < vsync_x_position && next_event >= vsync_x_position) {
@@ -185,6 +185,8 @@ void Video::advance(HalfCycles duration) {
const bool hsync = horizontal_.sync;
const bool vsync = vertical_.sync;
assert(run_length > 0);
// Ensure proper fetching irrespective of the output.
if(load_) {
const int since_load = x_ - load_base_;
@@ -192,8 +194,8 @@ void Video::advance(HalfCycles duration) {
// There will be pixels this line, subject to the shifter pipeline.
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out.
int start_column = since_load >> 3;
const int end_column = (since_load + run_length) >> 3;
int start_column = (since_load - 1) >> 3;
const int end_column = (since_load + run_length - 1) >> 3;
while(start_column != end_column) {
data_latch_[data_latch_position_] = ram_[current_address_ & 262143];
@@ -210,13 +212,16 @@ void Video::advance(HalfCycles duration) {
} else if(!load_) {
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
} else {
const int since_load = x_ - load_base_;
const int start = x_ - load_base_;
const int end = start + run_length;
// There will be pixels this line, subject to the shifter pipeline.
// Divide into 8-[half-]cycle windows; at the start of each window fetch a word,
// and during the rest of the window, shift out.
int start_column = since_load >> 3;
const int end_column = (since_load + run_length) >> 3;
int start_column = start >> 3;
const int end_column = end >> 3;
const int start_offset = start & 7;
const int end_offset = end & 7;
// Rules obeyed below:
//
@@ -225,35 +230,40 @@ void Video::advance(HalfCycles duration) {
// was reloaded by the fetch depends on the FIFO.
if(start_column == end_column) {
if(!start_offset) {
push_latched_data();
}
video_stream_.output(run_length, VideoStream::OutputMode::Pixels);
} else {
// Continue the current column if partway across.
if(since_load&7) {
if(start_offset) {
// If at least one column boundary is crossed, complete this column.
video_stream_.output(8 - (since_load & 7), VideoStream::OutputMode::Pixels);
video_stream_.output(8 - start_offset, VideoStream::OutputMode::Pixels);
++start_column; // This starts a new column, so latch a new word.
push_latched_data();
}
// Run for all columns that have their starts in this time period.
int complete_columns = end_column - start_column;
while(complete_columns--) {
video_stream_.output(8, VideoStream::OutputMode::Pixels);
push_latched_data();
video_stream_.output(8, VideoStream::OutputMode::Pixels);
}
// Output the start of the next column, if necessary.
if((since_load + run_length) & 7) {
video_stream_.output((since_load + run_length) & 7, VideoStream::OutputMode::Pixels);
if(end_offset) {
push_latched_data();
video_stream_.output(end_offset, VideoStream::OutputMode::Pixels);
}
}
}
// Check for whether line length should have been latched during this run.
if(x_ <= CYCLE(54) && (x_ + run_length) > CYCLE(54)) line_length_ = horizontal_timings.length;
if(x_ < line_length_latch_position && (x_ + run_length) >= line_length_latch_position) {
line_length_ = horizontal_timings.length;
}
// Make a decision about vertical state on cycle 502.
if(x_ <= CYCLE(502) && (x_ + run_length) > CYCLE(502)) {
// Make a decision about vertical state on the appropriate cycle.
if(x_ < horizontal_timings.vertical_decision && (x_ + run_length) >= horizontal_timings.vertical_decision) {
next_y_ = y_ + 1;
next_vertical_ = vertical_;
next_vertical_.sync_schedule = VerticalState::SyncSchedule::None;
@@ -267,33 +277,28 @@ void Video::advance(HalfCycles duration) {
next_vertical_.enable = true;
} else if(y_ == vertical_timings.reset_enable) {
next_vertical_.enable = false;
} else if(next_y_ == vertical_timings.height) {
} else if(next_y_ == vertical_timings.height - 2) {
next_vertical_.sync_schedule = VerticalState::SyncSchedule::Begin;
} else if(next_y_ == vertical_timings.height) {
next_y_ = 0;
} else if(next_y_ == 2) {
} else if(y_ == 0) {
next_vertical_.sync_schedule = VerticalState::SyncSchedule::End;
}
}
// Apply the next event.
x_ += run_length;
assert(integer_duration >= run_length);
integer_duration -= run_length;
deferrer_.advance(HalfCycles(run_length));
// Check horizontal events; the first six are guaranteed to occur separately.
if(horizontal_timings.reset_blank == x_) horizontal_.blank = false;
else if(horizontal_timings.set_blank == x_) horizontal_.blank = true;
else if(horizontal_timings.reset_enable == x_) horizontal_.enable = false;
else if(horizontal_timings.set_enable == x_) horizontal_.enable = true;
else if(line_length_ - hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; }
else if(line_length_ - hsync_end == x_) horizontal_.sync = false;
// next_load_toggle_ is less predictable; test separately because it may coincide
// with one of the above tests.
if(next_load_toggle_ == x_) {
next_load_toggle_ = -1;
load_ ^= true;
load_base_ = x_;
}
else if(line_length_.hsync_start == x_) { horizontal_.sync = true; horizontal_.enable = false; }
else if(line_length_.hsync_end == x_) horizontal_.sync = false;
// Check vertical events.
if(vertical_.sync_schedule != VerticalState::SyncSchedule::None && x_ == vsync_x_position) {
@@ -305,7 +310,7 @@ void Video::advance(HalfCycles duration) {
// Check whether the terminating event was end-of-line; if so then advance
// the vertical bits of state.
if(x_ == line_length_) {
if(x_ == line_length_.length) {
x_ = 0;
vertical_ = next_vertical_;
y_ = next_y_;
@@ -328,21 +333,30 @@ void Video::advance(HalfCycles duration) {
// Chuck any deferred output changes into the queue.
const bool next_display_enable = vertical_.enable && horizontal_.enable;
if(display_enable != next_display_enable) {
// Schedule change in outwardly-visible DE line.
add_event(de_delay_period - integer_duration, next_display_enable ? Event::Type::SetDisplayEnable : Event::Type::ResetDisplayEnable);
// Schedule change in load line.
deferrer_.defer(load_delay_period, [this, next_display_enable] {
this->load_ = next_display_enable;
this->load_base_ = this->x_;
});
// Schedule change in inwardly-visible effect.
next_load_toggle_ = x_ + 8; // 4 cycles = 8 half-cycles
// Schedule change in outwardly-visible DE line.
deferrer_.defer(de_delay_period, [this, next_display_enable] {
this->public_state_.display_enable = next_display_enable;
});
}
if(horizontal_.sync != hsync) {
// Schedule change in outwardly-visible hsync line.
add_event(hsync_delay_period - integer_duration, horizontal_.sync ? Event::Type::SetHsync : Event::Type::ResetHsync);
deferrer_.defer(hsync_delay_period, [this, next_horizontal_sync = horizontal_.sync] {
this->public_state_.hsync = next_horizontal_sync;
});
}
if(vertical_.sync != vsync) {
// Schedule change in outwardly-visible hsync line.
add_event(vsync_delay_period - integer_duration, vertical_.sync ? Event::Type::SetVsync : Event::Type::ResetVsync);
deferrer_.defer(vsync_delay_period, [this, next_vertical_sync = vertical_.sync] {
this->public_state_.vsync = next_vertical_sync;
});
}
}
}
@@ -395,11 +409,12 @@ HalfCycles Video::get_next_sequence_point() {
const auto horizontal_timings = horizontal_parameters(field_frequency_);
int event_time = line_length_; // Worst case: report end of line.
int event_time = line_length_.length; // Worst case: report end of line.
// If any events are pending, give the first of those the chance to be next.
if(!pending_events_.empty()) {
event_time = std::min(event_time, x_ + pending_events_.front().delay);
const auto next_deferred_item = deferrer_.time_until_next_action();
if(next_deferred_item != HalfCycles(-1)) {
event_time = std::min(event_time, x_ + next_deferred_item.as<int>());
}
// If this is a vertically-enabled line, check for the display enable boundaries, + the standard delay.
@@ -418,11 +433,17 @@ HalfCycles Video::get_next_sequence_point() {
}
// Test for beginning and end of horizontal sync.
if(x_ < line_length_ - hsync_start + hsync_delay_period) {
event_time = std::min(line_length_ - hsync_start + hsync_delay_period, event_time);
if(x_ < line_length_.hsync_start + hsync_delay_period) {
event_time = std::min(line_length_.hsync_start + hsync_delay_period, event_time);
}
if(x_ < line_length_.hsync_end + hsync_delay_period) {
event_time = std::min(line_length_.hsync_end + hsync_delay_period, event_time);
}
// Also factor in the line length latching time.
if(x_ < line_length_latch_position) {
event_time = std::min(line_length_latch_position, event_time);
}
/* Hereby assumed: hsync end will be communicated at end of line: */
static_assert(hsync_end == hsync_delay_period);
// It wasn't any of those, just supply end of line. That's when the static_assert above assumes a visible hsync transition.
return HalfCycles(event_time - x_);
@@ -464,7 +485,7 @@ void Video::write(int address, uint16_t value) {
// Sync mode and pixel mode.
case 0x05:
// Writes to sync mode have a one-cycle delay in effect.
deferrer_.defer(HalfCycles(2), [=] {
deferrer_.defer(HalfCycles(2), [this, value] {
sync_mode_ = value;
update_output_mode();
});
@@ -554,12 +575,12 @@ void Video::VideoStream::generate(int duration, OutputMode mode, bool is_termina
if(mode != OutputMode::Pixels) {
switch(mode) {
default:
case OutputMode::Sync: crt_.output_sync(duration_); break;
case OutputMode::Blank: crt_.output_blank(duration_); break;
case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_); break;
case OutputMode::Sync: crt_.output_sync(duration_*2); break;
case OutputMode::Blank: crt_.output_blank(duration_*2); break;
case OutputMode::ColourBurst: crt_.output_default_colour_burst(duration_*2); break;
}
// Reseed duration
// Reseed duration.
duration_ = duration;
// The shifter should keep running, so throw away the proper amount of content.
@@ -609,7 +630,7 @@ void Video::VideoStream::flush_border() {
// Output colour 0 for the entirety of duration_ (or black, if this is 1bpp mode).
uint16_t *const colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = (bpp_ != OutputBpp::One) ? palette_[0] : 0;
crt_.output_level(duration_);
crt_.output_level(duration_*2);
duration_ = 0;
}
@@ -710,7 +731,7 @@ void Video::VideoStream::output_pixels(int duration) {
}
// Check whether the limit has been reached.
if(pixel_pointer_ == allocation_size) {
if(pixel_pointer_ >= allocation_size - 32) {
flush_pixels();
}
}
@@ -720,20 +741,23 @@ void Video::VideoStream::output_pixels(int duration) {
if(pixels) {
int leftover_duration = pixels;
switch(bpp_) {
case OutputBpp::One: leftover_duration >>= 1; break;
default: break;
case OutputBpp::Four: leftover_duration <<= 1; break;
default: leftover_duration >>= 1; break;
case OutputBpp::Two: break;
case OutputBpp::Four: leftover_duration <<= 1; break;
}
shift(leftover_duration);
crt_.output_data(leftover_duration);
crt_.output_data(leftover_duration*2);
}
}
void Video::VideoStream::flush_pixels() {
switch(bpp_) {
case OutputBpp::One: crt_.output_data(pixel_pointer_ >> 1, size_t(pixel_pointer_)); break;
default: crt_.output_data(pixel_pointer_); break;
case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break;
// Flush only if there's something to flush.
if(pixel_pointer_) {
switch(bpp_) {
case OutputBpp::One: crt_.output_data(pixel_pointer_); break;
default: crt_.output_data(pixel_pointer_ << 1, size_t(pixel_pointer_)); break;
case OutputBpp::Four: crt_.output_data(pixel_pointer_ << 2, size_t(pixel_pointer_)); break;
}
}
pixel_pointer_ = 0;
@@ -741,13 +765,25 @@ void Video::VideoStream::flush_pixels() {
}
void Video::VideoStream::set_bpp(OutputBpp bpp) {
// Terminate the allocated block of memory (if any).
flush_pixels();
// Reset the shifter.
// TODO: is flushing like this correct?
output_shifter_ = 0;
// Store the new BPP.
bpp_ = bpp;
}
void Video::VideoStream::load(uint64_t value) {
output_shifter_ = value;
// In 1bpp mode, a 0 bit is white and a 1 bit is black.
// Invert the input so that the 'just output the border colour
// when the shifter is empty' optimisation works.
if(bpp_ == OutputBpp::One)
output_shifter_ = ~value;
else
output_shifter_ = value;
}
// MARK: - Range observer.

View File

@@ -21,6 +21,12 @@ class VideoTester;
namespace Atari {
namespace ST {
struct LineLength {
int length = 1024;
int hsync_start = 1024;
int hsync_end = 1024;
};
/*!
Models a combination of the parts of the GLUE, MMU and Shifter that in net
form the video subsystem of the Atari ST. So not accurate to a real chip, but
@@ -40,6 +46,9 @@ class Video {
*/
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/*!
Sets the type of output.
*/
@@ -112,7 +121,6 @@ class Video {
Range get_memory_access_range();
private:
void advance(HalfCycles duration);
DeferredQueue<HalfCycles> deferrer_;
Outputs::CRT::CRT crt_;
@@ -125,10 +133,8 @@ class Video {
int current_address_ = 0;
uint16_t *ram_ = nullptr;
uint16_t line_buffer_[256];
int x_ = 0, y_ = 0, next_y_ = 0;
int next_load_toggle_ = -1;
bool load_ = false;
int load_base_ = 0;
@@ -160,7 +166,7 @@ class Video {
} sync_schedule = SyncSchedule::None;
bool sync = false;
} vertical_, next_vertical_;
int line_length_ = 1024;
LineLength line_length_;
int data_latch_position_ = 0;
int data_latch_read_position_ = 0;
@@ -234,59 +240,6 @@ class Video {
bool vsync = false;
} public_state_;
struct Event {
int delay;
enum class Type {
SetDisplayEnable, ResetDisplayEnable,
SetHsync, ResetHsync,
SetVsync, ResetVsync,
} type;
Event(Type type, int delay) : delay(delay), type(type) {}
void apply(PublicState &state) {
apply(type, state);
}
static void apply(Type type, PublicState &state) {
switch(type) {
default:
case Type::SetDisplayEnable: state.display_enable = true; break;
case Type::ResetDisplayEnable: state.display_enable = false; break;
case Type::SetHsync: state.hsync = true; break;
case Type::ResetHsync: state.hsync = false; break;
case Type::SetVsync: state.vsync = true; break;
case Type::ResetVsync: state.vsync = false; break;
}
}
};
std::vector<Event> pending_events_;
void add_event(int delay, Event::Type type) {
// Apply immediately if there's no delay (or a negative delay).
if(delay <= 0) {
Event::apply(type, public_state_);
return;
}
if(!pending_events_.empty()) {
// Otherwise enqueue, having subtracted the delay for any preceding events,
// and subtracting from the subsequent, if any.
auto insertion_point = pending_events_.begin();
while(insertion_point != pending_events_.end() && insertion_point->delay < delay) {
delay -= insertion_point->delay;
++insertion_point;
}
if(insertion_point != pending_events_.end()) {
insertion_point->delay -= delay;
}
pending_events_.emplace(insertion_point, type, delay);
} else {
pending_events_.emplace_back(type, delay);
}
}
friend class ::VideoTester;
};

View File

@@ -17,6 +17,7 @@
#include "../Configurable/StandardOptions.hpp"
#include <array>
#include <cmath>
// TODO: rename.
@@ -37,6 +38,13 @@ class Machine {
*/
virtual void set_scan_target(Outputs::Display::ScanTarget *scan_target) = 0;
/*!
@returns The current scan status.
*/
virtual Outputs::Display::ScanStatus get_scan_status() const {
return get_scaled_scan_status() / float(clock_rate_);
}
/// @returns The speaker that receives this machine's output, or @c nullptr if this machine is mute.
virtual Outputs::Speaker::Speaker *get_speaker() = 0;
@@ -46,11 +54,89 @@ class Machine {
/// Runs the machine for @c duration seconds.
virtual void run_for(Time::Seconds duration) {
const double cycles = (duration * clock_rate_) + clock_conversion_error_;
const double cycles = (duration * clock_rate_ * speed_multiplier_) + clock_conversion_error_;
clock_conversion_error_ = std::fmod(cycles, 1.0);
run_for(Cycles(static_cast<int>(cycles)));
}
/*!
Sets a speed multiplier to apply to this machine; e.g. a multiplier of 1.5 will cause the
emulated machine to run 50% faster than a real machine. This speed-up is an emulation
fiction: it will apply across the system, including to the CRT.
*/
virtual void set_speed_multiplier(double multiplier) {
speed_multiplier_ = multiplier;
auto speaker = get_speaker();
if(speaker) {
speaker->set_input_rate_multiplier(float(multiplier));
}
}
/*!
@returns The current speed multiplier.
*/
virtual double get_speed_multiplier() {
return speed_multiplier_;
}
/*!
Runs for the machine for at least @c duration seconds, and then until @c condition is true.
@returns The amount of time run for.
*/
Time::Seconds run_until(Time::Seconds minimum_duration, std::function<bool()> condition) {
Time::Seconds total_runtime = minimum_duration;
run_for(minimum_duration);
while(!condition()) {
// Advance in increments of one 500th of a second until the condition
// is true; that's 1/10th of a 50Hz frame, but more like 1/8.33 of a
// 60Hz frame. Though most machines aren't exactly 50Hz or 60Hz, and some
// are arbitrary other refresh rates. So those observations are merely
// for scale.
run_for(0.002);
total_runtime += 0.002;
}
return total_runtime;
}
enum MachineEvent: int {
/// At least one new packet of audio has been delivered to the spaker's delegate.
NewSpeakerSamplesGenerated = 1 << 0,
/// The next vertical retrace has begun.
VerticalSync = 1 << 1,
};
/*!
Runs for at least @c duration seconds, and then every one of the @c events has occurred at least once since this
call to @c run_until_event.
@param events A bitmask comprised of @c MachineEvent flags.
@returns The amount of time run for.
*/
Time::Seconds run_until(Time::Seconds minimum_duration, int events) {
// Tie up a wait-for-samples, if requested.
const Outputs::Speaker::Speaker *speaker = nullptr;
int sample_sets = 0;
if(events & MachineEvent::NewSpeakerSamplesGenerated) {
speaker = get_speaker();
if(!speaker) events &= ~MachineEvent::NewSpeakerSamplesGenerated;
sample_sets = speaker->completed_sample_sets();
}
int retraces = 0;
if(events & MachineEvent::VerticalSync) {
retraces = get_scan_status().hsync_count;
}
// Run until all requested events are satisfied.
return run_until(minimum_duration, [=]() {
return
(!(events & MachineEvent::NewSpeakerSamplesGenerated) || (sample_sets != speaker->completed_sample_sets())) &&
(!(events & MachineEvent::VerticalSync) || (retraces != get_scan_status().hsync_count));
});
}
protected:
/// Runs the machine for @c cycles.
virtual void run_for(const Cycles cycles) = 0;
@@ -61,6 +147,15 @@ class Machine {
return clock_rate_;
}
virtual Outputs::Display::ScanStatus get_scaled_scan_status() const {
// This deliberately sets up an infinite loop if the user hasn't
// overridden at least one of this or get_scan_status.
//
// Most likely you want to override this, and let the base class
// throw in a divide-by-clock-rate at the end for you.
return get_scan_status();
}
/*!
Maps from Configurable::Display to Outputs::Display::VideoSignal and calls
@c set_display_type with the result.
@@ -90,9 +185,11 @@ class Machine {
*/
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
private:
double clock_rate_ = 1.0;
double clock_conversion_error_ = 0.0;
double speed_multiplier_ = 1.0;
};
}

View File

@@ -57,7 +57,7 @@ class Joystick: public Inputs::ConcreteJoystick {
Input('9'), Input('*'), Input('#'),
}) {}
void did_set_input(const Input &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) final {
switch(digital_input.type) {
default: return;
@@ -177,23 +177,27 @@ class ConcreteMachine:
audio_queue_.flush();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
vdp_->set_scan_target(scan_target);
}
void set_display_type(Outputs::Display::DisplayType display_type) override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return vdp_->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
z80_.run_for(cycles);
}
@@ -256,7 +260,7 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Input:
switch((address >> 5) & 7) {
case 5:
*cycle.value = vdp_->get_register(address);
*cycle.value = vdp_->read(address);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
@@ -299,14 +303,14 @@ class ConcreteMachine:
break;
case 5:
vdp_->set_register(address, *cycle.value);
vdp_->write(address, *cycle.value);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
case 7:
update_audio();
sn76489_.set_register(*cycle.value);
sn76489_.write(*cycle.value);
break;
default:
@@ -358,30 +362,30 @@ class ConcreteMachine:
audio_queue_.perform();
}
float get_confidence() override {
float get_confidence() final {
if(pc_zero_accesses_ > 1) return 0.0f;
return confidence_counter_.get_confidence();
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Coleco::Vision::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
Configurable::Display display;
if(Configurable::get_display(selections_by_option, display)) {
set_video_signal_configurable(display);
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::SVideo);
return selection_set;
@@ -401,9 +405,9 @@ class ConcreteMachine:
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TI::SN76489 sn76489_;
GI::AY38910::AY38910 ay_;
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910>> speaker_;
GI::AY38910::AY38910<false> ay_;
Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<TI::SN76489, GI::AY38910::AY38910<false>>> speaker_;
std::vector<uint8_t> bios_;
std::vector<uint8_t> cartridge_;

View File

@@ -19,7 +19,6 @@ using namespace Commodore::C1540;
MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &rom_fetcher) :
Storage::Disk::Controller(1000000),
m6502_(*this),
drive_(new Storage::Disk::Drive(1000000, 300, 2)),
serial_port_VIA_port_handler_(new SerialPortVIA(serial_port_VIA_)),
serial_port_(new SerialPort),
drive_VIA_(drive_VIA_port_handler_),
@@ -37,7 +36,8 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher &
set_expected_bit_length(Storage::Encodings::CommodoreGCR::length_of_a_bit_in_time_zone(3));
// attach the only drive there is
set_drive(drive_);
emplace_drive(1000000, 300, 2);
set_drive(1);
std::string device_name;
uint32_t crc = 0;
@@ -86,14 +86,14 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
}
} else if(address >= 0x1800 && address <= 0x180f) {
if(isReadOperation(operation))
*value = serial_port_VIA_.get_register(address);
*value = serial_port_VIA_.read(address);
else
serial_port_VIA_.set_register(address, *value);
serial_port_VIA_.write(address, *value);
} else if(address >= 0x1c00 && address <= 0x1c0f) {
if(isReadOperation(operation))
*value = drive_VIA_.get_register(address);
*value = drive_VIA_.read(address);
else
drive_VIA_.set_register(address, *value);
drive_VIA_.write(address, *value);
}
serial_port_VIA_.run_for(Cycles(1));
@@ -103,21 +103,21 @@ Cycles MachineBase::perform_bus_operation(CPU::MOS6502::BusOperation operation,
}
void Machine::set_disk(std::shared_ptr<Storage::Disk::Disk> disk) {
drive_->set_disk(disk);
get_drive().set_disk(disk);
}
void Machine::run_for(const Cycles cycles) {
m6502_.run_for(cycles);
bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
drive_->set_motor_on(drive_motor);
const bool drive_motor = drive_VIA_port_handler_.get_motor_enabled();
get_drive().set_motor_on(drive_motor);
if(drive_motor)
Storage::Disk::Controller::run_for(cycles);
}
void MachineBase::set_activity_observer(Activity::Observer *observer) {
drive_VIA_.bus_handler().set_activity_observer(observer);
drive_->set_activity_observer(observer, "Drive", false);
get_drive().set_activity_observer(observer, "Drive", false);
}
// MARK: - 6522 delegate
@@ -154,7 +154,7 @@ void MachineBase::process_index_hole() {}
// MARK: - Drive VIA delegate
void MachineBase::drive_via_did_step_head(void *driveVIA, int direction) {
drive_->step(Storage::Disk::HeadPosition(direction, 2));
get_drive().step(Storage::Disk::HeadPosition(direction, 2));
}
void MachineBase::drive_via_did_set_data_density(void *driveVIA, int density) {

View File

@@ -144,7 +144,6 @@ class MachineBase:
protected:
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, MachineBase, false> m6502_;
std::shared_ptr<Storage::Disk::Drive> drive_;
uint8_t ram_[0x800];
uint8_t rom_[0x4000];

View File

@@ -259,7 +259,7 @@ class Joystick: public Inputs::ConcreteJoystick {
user_port_via_port_handler_(user_port_via_port_handler),
keyboard_via_port_handler_(keyboard_via_port_handler) {}
void did_set_input(const Input &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) final {
JoystickInput mapped_input;
switch(digital_input.type) {
default: return;
@@ -397,9 +397,10 @@ class ConcreteMachine:
memset(processor_write_memory_map_, 0, sizeof(processor_write_memory_map_));
memset(mos6560_bus_handler_.video_memory_map, 0, sizeof(mos6560_bus_handler_.video_memory_map));
#define set_ram(baseaddr, length) \
write_to_map(processor_read_memory_map_, &ram_[baseaddr], baseaddr, length); \
write_to_map(processor_write_memory_map_, &ram_[baseaddr], baseaddr, length);
#define set_ram(baseaddr, length) { \
write_to_map(processor_read_memory_map_, &ram_[baseaddr], baseaddr, length); \
write_to_map(processor_write_memory_map_, &ram_[baseaddr], baseaddr, length); \
}
// Add 6502-visible RAM as requested.
set_ram(0x0000, 0x0400);
@@ -453,7 +454,7 @@ class ConcreteMachine:
}
}
bool insert_media(const Analyser::Static::Media &media) override final {
bool insert_media(const Analyser::Static::Media &media) final {
if(!media.tapes.empty()) {
tape_->set_tape(media.tapes.front());
}
@@ -477,18 +478,18 @@ class ConcreteMachine:
return !media.tapes.empty() || (!media.disks.empty() && c1540_ != nullptr) || !media.cartridges.empty();
}
void set_key_state(uint16_t key, bool is_pressed) override final {
void set_key_state(uint16_t key, bool is_pressed) final {
if(key != KeyRestore)
keyboard_via_port_handler_->set_key_state(key, is_pressed);
else
user_port_via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !is_pressed);
}
void clear_all_keys() override final {
void clear_all_keys() final {
keyboard_via_port_handler_->clear_all_keys();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
@@ -503,10 +504,10 @@ class ConcreteMachine:
if((address&0xfc00) == 0x9000) {
if(!(address&0x100)) {
update_video();
result &= mos6560_.get_register(address);
result &= mos6560_.read(address);
}
if(address & 0x10) result &= user_port_via_.get_register(address);
if(address & 0x20) result &= keyboard_via_.get_register(address);
if(address & 0x10) result &= user_port_via_.read(address);
if(address & 0x20) result &= keyboard_via_.read(address);
}
*value = result;
@@ -590,12 +591,12 @@ class ConcreteMachine:
// The VIC is selected by bit 8 = 0
if(!(address&0x100)) {
update_video();
mos6560_.set_register(address, *value);
mos6560_.write(address, *value);
}
// The first VIA is selected by bit 4 = 1.
if(address & 0x10) user_port_via_.set_register(address, *value);
if(address & 0x10) user_port_via_.write(address, *value);
// The second VIA is selected by bit 5 = 1.
if(address & 0x20) keyboard_via_.set_register(address, *value);
if(address & 0x20) keyboard_via_.write(address, *value);
}
}
@@ -618,45 +619,49 @@ class ConcreteMachine:
mos6560_.flush();
}
void run_for(const Cycles cycles) override final {
void run_for(const Cycles cycles) final {
m6502_.run_for(cycles);
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
mos6560_.set_scan_target(scan_target);
}
void set_display_type(Outputs::Display::DisplayType display_type) override final {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return mos6560_.get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
mos6560_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override final {
Outputs::Speaker::Speaker *get_speaker() final {
return mos6560_.get_speaker();
}
void mos6522_did_change_interrupt_status(void *mos6522) override final {
void mos6522_did_change_interrupt_status(void *mos6522) final {
m6502_.set_nmi_line(user_port_via_.get_interrupt_line());
m6502_.set_irq_line(keyboard_via_.get_interrupt_line());
}
void type_string(const std::string &string) override final {
void type_string(const std::string &string) final {
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
}
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) override final {
void tape_did_change_input(Storage::Tape::BinaryTapePlayer *tape) final {
keyboard_via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !tape->get_input());
}
KeyboardMapper *get_keyboard_mapper() override {
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Commodore::Vic20::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
bool quickload;
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
allow_fast_tape_hack_ = quickload;
@@ -669,27 +674,27 @@ class ConcreteMachine:
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, true);
Configurable::append_display_selection(selection_set, Configurable::Display::SVideo);
return selection_set;
}
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
tape_is_sleeping_ = clocking == ClockingHint::Preference::None;
set_use_fast_tape();
}
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
if(c1540_) c1540_->set_activity_observer(observer);
}

View File

@@ -114,7 +114,7 @@ class ConcreteMachine:
audio_queue_.flush();
}
void set_key_state(uint16_t key, bool isPressed) override final {
void set_key_state(uint16_t key, bool isPressed) final {
if(key == KeyBreak) {
m6502_.set_reset_line(isPressed);
} else {
@@ -125,12 +125,12 @@ class ConcreteMachine:
}
}
void clear_all_keys() override final {
void clear_all_keys() final {
memset(key_states_, 0, sizeof(key_states_));
if(is_holding_shift_) set_key_state(KeyShift, true);
}
bool insert_media(const Analyser::Static::Media &media) override final {
bool insert_media(const Analyser::Static::Media &media) final {
if(!media.tapes.empty()) {
tape_.set_tape(media.tapes.front());
}
@@ -204,7 +204,7 @@ class ConcreteMachine:
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
if(!isReadOperation(operation)) {
update_display();
video_output_.set_register(address, *value);
video_output_.write(address, *value);
video_access_range_ = video_output_.get_memory_access_range();
queue_next_display_interrupt();
}
@@ -260,9 +260,9 @@ class ConcreteMachine:
set_key_state(KeyShift, false);
}
if(isReadOperation(operation))
*value = plus3_->get_register(address);
*value = plus3_->read(address);
else
plus3_->set_register(address, *value);
plus3_->write(address, *value);
}
break;
case 0xfc00:
@@ -379,49 +379,53 @@ class ConcreteMachine:
audio_queue_.perform();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
video_output_.set_scan_target(scan_target);
}
void set_display_type(Outputs::Display::DisplayType display_type) override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return video_output_.get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
video_output_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override final {
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
void run_for(const Cycles cycles) override final {
void run_for(const Cycles cycles) final {
m6502_.run_for(cycles);
}
void tape_did_change_interrupt_status(Tape *tape) override final {
void tape_did_change_interrupt_status(Tape *tape) final {
interrupt_status_ = (interrupt_status_ & ~(Interrupt::TransmitDataEmpty | Interrupt::ReceiveDataFull | Interrupt::HighToneDetect)) | tape_.get_interrupt_status();
evaluate_interrupts();
}
HalfCycles get_typer_delay() override final {
HalfCycles get_typer_delay() final {
return m6502_.get_is_resetting() ? Cycles(625*25*128) : Cycles(0); // wait one second if resetting
}
HalfCycles get_typer_frequency() override final {
HalfCycles get_typer_frequency() final {
return Cycles(625*128*2); // accept a new character every two frames
}
void type_string(const std::string &string) override final {
void type_string(const std::string &string) final {
Utility::TypeRecipient::add_typer(string, std::make_unique<CharacterMapper>());
}
KeyboardMapper *get_keyboard_mapper() override {
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Electron::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
bool quickload;
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
allow_fast_tape_hack_ = quickload;
@@ -434,14 +438,14 @@ class ConcreteMachine:
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, true);
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
@@ -449,7 +453,7 @@ class ConcreteMachine:
}
// MARK: - Activity Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
activity_observer_ = observer;
if(activity_observer_) {
activity_observer_->register_led(caps_led);

View File

@@ -34,7 +34,7 @@ enum Key: uint16_t {
};
struct KeyboardMapper: public KeyboardMachine::MappedMachine::KeyboardMapper {
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) override;
uint16_t mapped_key_for_key(Inputs::Keyboard::Key key) final;
};
struct CharacterMapper: public ::Utility::CharacterMapper {

View File

@@ -11,13 +11,12 @@
using namespace Electron;
Plus3::Plus3() : WD1770(P1770) {
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
drives_.emplace_back(new Storage::Disk::Drive(8000000, 300, 2));
emplace_drives(2, 8000000, 300, 2);
set_control_register(last_control_, 0xff);
}
void Plus3::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
drives_[drive]->set_disk(disk);
get_drive(drive).set_disk(disk);
}
void Plus3::set_control_register(uint8_t control) {
@@ -33,16 +32,15 @@ void Plus3::set_control_register(uint8_t control) {
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;
}
set_drive(control&3);
}
// Select the side on both drives at once.
if(changes & 0x04) {
drives_[0]->set_head((control & 0x04) ? 1 : 0);
drives_[1]->set_head((control & 0x04) ? 1 : 0);
get_drive(0).set_head((control & 0x04) ? 1 : 0);
get_drive(1).set_head((control & 0x04) ? 1 : 0);
}
if(changes & 0x08) set_is_double_density(!(control & 0x08));
}
@@ -53,9 +51,7 @@ void Plus3::set_motor_on(bool on) {
}
void Plus3::set_activity_observer(Activity::Observer *observer) {
size_t index = 0;
for(const auto &drive: drives_) {
drive->set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
++index;
}
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
});
}

View File

@@ -24,8 +24,6 @@ class Plus3 : public WD::WD1770 {
private:
void set_control_register(uint8_t control, uint8_t changes);
std::vector<std::shared_ptr<Storage::Disk::Drive>> drives_;
int selected_drive_ = 0;
uint8_t last_control_ = 0;
void set_motor_on(bool on);

View File

@@ -36,13 +36,13 @@ void SoundGenerator::skip_samples(std::size_t number_of_samples) {
}
void SoundGenerator::set_divider(uint8_t divider) {
audio_queue_.defer([=]() {
audio_queue_.defer([this, divider]() {
divider_ = divider * 32 / clock_rate_divider;
});
}
void SoundGenerator::set_is_enabled(bool is_enabled) {
audio_queue_.defer([=]() {
audio_queue_.defer([this, is_enabled]() {
is_enabled_ = is_enabled;
counter_ = 0;
});

View File

@@ -28,6 +28,7 @@ class SoundGenerator: public ::Outputs::Speaker::SampleSource {
void get_samples(std::size_t number_of_samples, int16_t *target);
void skip_samples(std::size_t number_of_samples);
void set_sample_volume_range(std::int16_t range);
static constexpr bool get_is_stereo() { return false; }
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;

View File

@@ -56,6 +56,10 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status() / float(crt_cycles_multiplier);
}
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
@@ -250,7 +254,7 @@ void VideoOutput::run_for(const Cycles cycles) {
// MARK: - Register hub
void VideoOutput::set_register(int address, uint8_t value) {
void VideoOutput::write(int address, uint8_t value) {
switch(address & 0xf) {
case 0x02:
start_screen_address_ = (start_screen_address_ & 0xfe00) | static_cast<uint16_t>((value & 0xe0) << 1);

View File

@@ -39,6 +39,9 @@ class VideoOutput {
/// Sets the destination for output.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const;
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
@@ -46,7 +49,7 @@ class VideoOutput {
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);
void write(int address, uint8_t value);
/*!
Describes an interrupt the video hardware will generate by its identity and scheduling time.
@@ -62,7 +65,7 @@ class VideoOutput {
The time until signalling returned is the number of cycles after the final one triggered
by the most recent call to @c run_for.
This result may be mutated by calls to @c set_register.
This result may be mutated by calls to @c write.
*/
Interrupt get_next_interrupt();

View File

@@ -19,7 +19,7 @@ class ASCII16kbROMSlotHandler: public ROMSlotHandler {
ASCII16kbROMSlotHandler(MSX::MemoryMap &map, int slot) :
map_(map), slot_(slot) {}
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
switch(address >> 11) {
default:
if(pc_is_outside_bios) confidence_counter_.add_miss();
@@ -39,7 +39,7 @@ class ASCII16kbROMSlotHandler: public ROMSlotHandler {
}
}
virtual std::string debug_type() override {
virtual std::string debug_type() final {
return "A16";
}

View File

@@ -19,7 +19,7 @@ class ASCII8kbROMSlotHandler: public ROMSlotHandler {
ASCII8kbROMSlotHandler(MSX::MemoryMap &map, int slot) :
map_(map), slot_(slot) {}
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
switch(address >> 11) {
default:
if(pc_is_outside_bios) confidence_counter_.add_miss();
@@ -51,7 +51,7 @@ class ASCII8kbROMSlotHandler: public ROMSlotHandler {
}
}
virtual std::string debug_type() override {
virtual std::string debug_type() final {
return "A8";
}

View File

@@ -19,7 +19,7 @@ class KonamiROMSlotHandler: public ROMSlotHandler {
KonamiROMSlotHandler(MSX::MemoryMap &map, int slot) :
map_(map), slot_(slot) {}
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
switch(address >> 13) {
default:
if(pc_is_outside_bios) confidence_counter_.add_miss();
@@ -45,7 +45,7 @@ class KonamiROMSlotHandler: public ROMSlotHandler {
}
}
virtual std::string debug_type() override {
virtual std::string debug_type() final {
return "K";
}
private:

View File

@@ -20,7 +20,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
KonamiWithSCCROMSlotHandler(MSX::MemoryMap &map, int slot, Konami::SCC &scc) :
map_(map), slot_(slot), scc_(scc) {}
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override {
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final {
switch(address >> 11) {
default:
if(pc_is_outside_bios) confidence_counter_.add_miss();
@@ -66,7 +66,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
}
}
uint8_t read(uint16_t address) override {
uint8_t read(uint16_t address) final {
if(scc_is_visible_ && address >= 0x9800 && address < 0xa000) {
confidence_counter_.add_hit();
return scc_.read(address);
@@ -75,7 +75,7 @@ class KonamiWithSCCROMSlotHandler: public ROMSlotHandler {
return 0xff;
}
virtual std::string debug_type() override {
virtual std::string debug_type() final {
return "KSCC";
}

View File

@@ -13,35 +13,35 @@ using namespace MSX;
DiskROM::DiskROM(const std::vector<uint8_t> &rom) :
WD1770(P1793),
rom_(rom) {
drives_[0] = std::make_shared<Storage::Disk::Drive>(8000000, 300, 2);
drives_[1] = std::make_shared<Storage::Disk::Drive>(8000000, 300, 2);
emplace_drives(2, 8000000, 300, 2);
set_is_double_density(true);
}
void DiskROM::write(uint16_t address, uint8_t value, bool pc_is_outside_bios) {
switch(address) {
case 0x7ff8: case 0x7ff9: case 0x7ffa: case 0x7ffb:
set_register(address, value);
break;
case 0x7ffc:
selected_head_ = value & 1;
drives_[0]->set_head(selected_head_);
drives_[1]->set_head(selected_head_);
WD::WD1770::write(address, value);
break;
case 0x7ffc: {
const int selected_head = value & 1;
for_all_drives([selected_head] (Storage::Disk::Drive &drive, size_t index) {
drive.set_head(selected_head);
});
} break;
case 0x7ffd: {
selected_drive_ = value & 1;
set_drive(drives_[selected_drive_]);
set_drive(1 << (value & 1));
bool drive_motor = !!(value & 0x80);
drives_[0]->set_motor_on(drive_motor);
drives_[1]->set_motor_on(drive_motor);
const bool drive_motor = value & 0x80;
for_all_drives([drive_motor] (Storage::Disk::Drive &drive, size_t index) {
drive.set_motor_on(drive_motor);
});
} break;
}
}
uint8_t DiskROM::read(uint16_t address) {
if(address >= 0x7ff8 && address < 0x7ffc) {
return get_register(address);
return WD::WD1770::read(address);
}
if(address == 0x7fff) {
return (get_data_request_line() ? 0x00 : 0x80) | (get_interrupt_request_line() ? 0x00 : 0x40);
@@ -59,7 +59,7 @@ void DiskROM::run_for(HalfCycles half_cycles) {
}
void DiskROM::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
drives_[drive]->set_disk(disk);
get_drive(drive).set_disk(disk);
}
void DiskROM::set_head_load_request(bool head_load) {
@@ -68,9 +68,7 @@ void DiskROM::set_head_load_request(bool head_load) {
}
void DiskROM::set_activity_observer(Activity::Observer *observer) {
size_t c = 1;
for(auto &drive: drives_) {
drive->set_activity_observer(observer, "Drive " + std::to_string(c), true);
++c;
}
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
drive.set_activity_observer(observer, "Drive " + std::to_string(index), true);
});
}

View File

@@ -25,9 +25,9 @@ class DiskROM: public ROMSlotHandler, public WD::WD1770 {
public:
DiskROM(const std::vector<uint8_t> &rom);
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) override;
uint8_t read(uint16_t address) override;
void run_for(HalfCycles half_cycles) override;
void write(uint16_t address, uint8_t value, bool pc_is_outside_bios) final;
uint8_t read(uint16_t address) final;
void run_for(HalfCycles half_cycles) final;
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
void set_activity_observer(Activity::Observer *observer);
@@ -36,11 +36,8 @@ class DiskROM: public ROMSlotHandler, public WD::WD1770 {
const std::vector<uint8_t> &rom_;
long int controller_cycles_ = 0;
size_t selected_drive_ = 0;
int selected_head_ = 0;
std::array<std::shared_ptr<Storage::Disk::Drive>, 2> drives_;
void set_head_load_request(bool head_load) override;
void set_head_load_request(bool head_load) final;
};
}

View File

@@ -110,7 +110,7 @@ class AYPortHandler: public GI::AY38910::PortHandler {
Input(Input::Fire, 1),
}) {}
void did_set_input(const Input &input, bool is_active) override {
void did_set_input(const Input &input, bool is_active) final {
uint8_t mask = 0;
switch(input.type) {
default: return;
@@ -278,23 +278,27 @@ class ConcreteMachine:
audio_queue_.flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
vdp_->set_scan_target(scan_target);
}
void set_display_type(Outputs::Display::DisplayType display_type) override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return vdp_->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
z80_.run_for(cycles);
}
float get_confidence() override {
float get_confidence() final {
if(performed_unmapped_access_ || pc_zero_accesses_ > 1) return 0.0f;
if(memory_slots_[1].handler) {
return memory_slots_[1].handler->get_confidence();
@@ -302,14 +306,14 @@ class ConcreteMachine:
return 0.5f;
}
std::string debug_type() override {
std::string debug_type() final {
if(memory_slots_[1].handler) {
return "MSX:" + memory_slots_[1].handler->debug_type();
}
return "MSX";
}
bool insert_media(const Analyser::Static::Media &media) override {
bool insert_media(const Analyser::Static::Media &media) final {
if(!media.cartridges.empty()) {
const auto &segment = media.cartridges.front()->get_segments().front();
memory_slots_[1].source = segment.data;
@@ -356,7 +360,7 @@ class ConcreteMachine:
return true;
}
void type_string(const std::string &string) override final {
void type_string(const std::string &string) final {
std::transform(
string.begin(),
string.end(),
@@ -366,7 +370,7 @@ class ConcreteMachine:
}
// MARK: MSX::MemoryMap
void map(int slot, std::size_t source_address, uint16_t destination_address, std::size_t length) override {
void map(int slot, std::size_t source_address, uint16_t destination_address, std::size_t length) final {
assert(!(destination_address & 8191));
assert(!(length & 8191));
assert(static_cast<std::size_t>(destination_address) + length <= 65536);
@@ -381,7 +385,7 @@ class ConcreteMachine:
page_memory(paged_memory_);
}
void unmap(int slot, uint16_t destination_address, std::size_t length) override {
void unmap(int slot, uint16_t destination_address, std::size_t length) final {
assert(!(destination_address & 8191));
assert(!(length & 8191));
assert(static_cast<std::size_t>(destination_address) + length <= 65536);
@@ -428,7 +432,7 @@ class ConcreteMachine:
// TAPION
// Enable the tape motor.
i8255_.set_register(0xab, 0x8);
i8255_.write(0xab, 0x8);
// Disable interrupts.
z80_.set_value_of_register(CPU::Z80::Register::IFF1, 0);
@@ -509,7 +513,7 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Input:
switch(address & 0xff) {
case 0x98: case 0x99:
*cycle.value = vdp_->get_register(address);
*cycle.value = vdp_->read(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
@@ -523,7 +527,7 @@ class ConcreteMachine:
case 0xa8: case 0xa9:
case 0xaa: case 0xab:
*cycle.value = i8255_.get_register(address);
*cycle.value = i8255_.read(address);
break;
default:
@@ -536,7 +540,7 @@ class ConcreteMachine:
const int port = address & 0xff;
switch(port) {
case 0x98: case 0x99:
vdp_->set_register(address, *cycle.value);
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
@@ -550,7 +554,7 @@ class ConcreteMachine:
case 0xa8: case 0xa9:
case 0xaa: case 0xab:
i8255_.set_register(address, *cycle.value);
i8255_.write(address, *cycle.value);
break;
case 0xfc: case 0xfd: case 0xfe: case 0xff:
@@ -624,26 +628,26 @@ class ConcreteMachine:
return key_states_[selected_key_line_];
}
void clear_all_keys() override {
void clear_all_keys() final {
std::memset(key_states_, 0xff, sizeof(key_states_));
}
void set_key_state(uint16_t key, bool is_pressed) override {
void set_key_state(uint16_t key, bool is_pressed) final {
int mask = 1 << (key & 7);
int line = key >> 4;
if(is_pressed) key_states_[line] &= ~mask; else key_states_[line] |= mask;
}
KeyboardMapper *get_keyboard_mapper() override {
KeyboardMapper *get_keyboard_mapper() final {
return &keyboard_mapper_;
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return MSX::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
bool quickload;
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
allow_fast_tape_ = quickload;
@@ -656,14 +660,14 @@ class ConcreteMachine:
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, true);
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
@@ -671,13 +675,13 @@ class ConcreteMachine:
}
// MARK: - Sleeper
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) override {
void set_component_prefers_clocking(ClockingHint::Source *component, ClockingHint::Preference clocking) final {
tape_player_is_sleeping_ = tape_player_.preferred_clocking() == ClockingHint::Preference::None;
set_use_fast_tape();
}
// MARK: - Activity::Source
void set_activity_observer(Activity::Observer *observer) override {
void set_activity_observer(Activity::Observer *observer) final {
DiskROM *disk_rom = get_disk_rom();
if(disk_rom) {
disk_rom->set_activity_observer(observer);
@@ -686,7 +690,7 @@ class ConcreteMachine:
}
// MARK: - Joysticks
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return ay_port_handler_.get_joysticks();
}
@@ -756,11 +760,11 @@ class ConcreteMachine:
Intel::i8255::i8255<i8255PortHandler> i8255_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay_;
GI::AY38910::AY38910<false> ay_;
Audio::Toggle audio_toggle_;
Konami::SCC scc_;
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910, Audio::Toggle, Konami::SCC>> speaker_;
Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC> mixer_;
Outputs::Speaker::LowpassSpeaker<Outputs::Speaker::CompoundSource<GI::AY38910::AY38910<false>, Audio::Toggle, Konami::SCC>> speaker_;
Storage::Tape::BinaryTapePlayer tape_player_;
bool tape_player_is_sleeping_ = false;

View File

@@ -21,6 +21,8 @@
#include "../../ClockReceiver/JustInTime.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#define LOG_PREFIX "[SMS] "
#include "../../Outputs/Log.hpp"
#include "../../Analyser/Static/Sega/Target.hpp"
@@ -54,7 +56,7 @@ class Joystick: public Inputs::ConcreteJoystick {
Input(Input::Fire, 1)
}) {}
void did_set_input(const Input &digital_input, bool is_active) override {
void did_set_input(const Input &digital_input, bool is_active) final {
switch(digital_input.type) {
default: return;
@@ -168,7 +170,7 @@ class ConcreteMachine:
audio_queue_.flush();
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
vdp_->set_tv_standard(
(region_ == Target::Region::Europe) ?
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
@@ -177,15 +179,19 @@ class ConcreteMachine:
vdp_->set_scan_target(scan_target);
}
void set_display_type(Outputs::Display::DisplayType display_type) override {
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return vdp_->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
Outputs::Speaker::Speaker *get_speaker() final {
return &speaker_;
}
void run_for(const Cycles cycles) override {
void run_for(const Cycles cycles) final {
z80_.run_for(cycles);
}
@@ -240,7 +246,7 @@ class ConcreteMachine:
*cycle.value = vdp_.last_valid()->get_latched_horizontal_counter();
break;
case 0x80: case 0x81:
*cycle.value = vdp_->get_register(address);
*cycle.value = vdp_->read(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
@@ -288,10 +294,10 @@ class ConcreteMachine:
} break;
case 0x40: case 0x41:
update_audio();
sn76489_.set_register(*cycle.value);
sn76489_.write(*cycle.value);
break;
case 0x80: case 0x81:
vdp_->set_register(address, *cycle.value);
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
break;
@@ -340,16 +346,16 @@ class ConcreteMachine:
audio_queue_.perform();
}
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
const std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() final {
return joysticks_;
}
// MARK: - Keyboard (i.e. the pause and reset buttons).
Inputs::Keyboard &get_keyboard() override {
Inputs::Keyboard &get_keyboard() final {
return keyboard_;
}
void keyboard_did_change_key(Inputs::Keyboard *, Inputs::Keyboard::Key key, bool is_pressed) override {
void keyboard_did_change_key(Inputs::Keyboard *, Inputs::Keyboard::Key key, bool is_pressed) final {
if(key == Inputs::Keyboard::Key::Enter) {
pause_is_pressed_ = is_pressed;
} else if(key == Inputs::Keyboard::Key::Escape) {
@@ -357,28 +363,28 @@ class ConcreteMachine:
}
}
void reset_all_keys(Inputs::Keyboard *) override {
void reset_all_keys(Inputs::Keyboard *) final {
}
// MARK: - Configuration options.
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
std::vector<std::unique_ptr<Configurable::Option>> get_options() final {
return Sega::MasterSystem::get_options();
}
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
void set_selections(const Configurable::SelectionSet &selections_by_option) final {
Configurable::Display display;
if(Configurable::get_display(selections_by_option, display)) {
set_video_signal_configurable(display);
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet get_accurate_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
Configurable::SelectionSet get_user_friendly_selections() final {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::RGB);
return selection_set;

113
Machines/Oric/BD500.cpp Normal file
View File

@@ -0,0 +1,113 @@
//
// BD500.cpp
// Clock Signal
//
// Created by Thomas Harte on 14/01/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "BD500.hpp"
using namespace Oric;
/*
Notes on the code below: the Byte Drive 500 isn't well documented; this implementation is based on
experimentation without access to real hardware as documented in http://forum.defence-force.org/viewtopic.php?f=25&t=2055
and with additional guidance from iss of Oricutron (amongst other things) elsewhere,
e.g. http://forum.defence-force.org/viewtopic.php?f=22&t=1698&start=105#p21468
*/
BD500::BD500() : DiskController(P1793, 9000000, Storage::Disk::Drive::ReadyType::ShugartModifiedRDY) {
disable_basic_rom_ = true;
select_paged_item();
set_is_double_density(true);
set_drive(1);
}
void BD500::write(int address, uint8_t value) {
access(address);
if(address >= 0x0320 && address <= 0x0323) {
// if(address == 0x320) printf("Command %02x\n", value);
WD::WD1770::write(address, value);
}
if(address == 0x031a) {
// Drive select; kudos to iss of Oricutron for figuring this one out;
// cf. http://forum.defence-force.org/viewtopic.php?f=25&p=21409#p21393
switch(value & 0xe0) {
default: set_drive(0); break;
case 0x20: set_drive(1); break;
case 0x40: set_drive(2); break;
case 0x80: set_drive(4); break;
case 0xc0: set_drive(8); break;
}
}
}
uint8_t BD500::read(int address) {
access(address);
switch(address) {
default: return 0xff;
case 0x0320: case 0x0321: case 0x0322: case 0x0323:
return WD::WD1770::read(address);
case 0x312: return (get_data_request_line() ? 0x80 : 0x00) | (get_interrupt_request_line() ? 0x40 : 0x00);
}
}
void BD500::access(int address) {
// Determine whether to perform a command.
switch(address) {
case 0x0320: case 0x0321: case 0x0322: case 0x0323: case 0x0312:
return;
case 0x311: disable_basic_rom_ = true; break;
case 0x313: enable_overlay_ram_ = false; break;
case 0x314: enable_overlay_ram_ = true; break;
case 0x317: disable_basic_rom_ = false; break;
default:
// printf("Switch %04x???\n", address);
break;
}
select_paged_item();
}
void BD500::set_head_load_request(bool head_load) {
// Turn all motors on or off; if off then unload the head instantly.
is_loading_head_ |= head_load;
for_all_drives([head_load] (Storage::Disk::Drive &drive, size_t) {
drive.set_motor_on(head_load);
});
if(!head_load) set_head_loaded(false);
}
void BD500::run_for(const Cycles cycles) {
// If a head load is in progress and the selected drive is now ready,
// declare head loaded.
if(is_loading_head_ && get_drive().get_is_ready()) {
set_head_loaded(true);
is_loading_head_ = false;
}
WD::WD1770::run_for(cycles);
}
void BD500::set_activity_observer(Activity::Observer *observer) {
observer_ = observer;
if(observer) {
observer->register_led("BD-500");
observer_->set_led_status("BD-500", get_head_loaded());
}
}
void BD500::set_head_loaded(bool loaded) {
WD::WD1770::set_head_loaded(loaded);
if(observer_) {
observer_->set_led_status("BD-500", loaded);
}
}

43
Machines/Oric/BD500.hpp Normal file
View File

@@ -0,0 +1,43 @@
//
// BD500.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/01/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef BD500_hpp
#define BD500_hpp
#include "../../Components/1770/1770.hpp"
#include "../../Activity/Observer.hpp"
#include "DiskController.hpp"
#include <array>
#include <memory>
namespace Oric {
class BD500: public DiskController {
public:
BD500();
void write(int address, uint8_t value);
uint8_t read(int address);
void run_for(const Cycles cycles);
void set_activity_observer(Activity::Observer *observer);
private:
void set_head_load_request(bool head_load) final;
bool is_loading_head_ = false;
Activity::Observer *observer_ = nullptr;
void access(int address);
void set_head_loaded(bool loaded);
};
};
#endif /* BD500_hpp */

View File

@@ -0,0 +1,75 @@
//
// DiskController.hpp
// Clock Signal
//
// Created by Thomas Harte on 14/01/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef DiskController_h
#define DiskController_h
namespace Oric {
class DiskController: public WD::WD1770 {
public:
DiskController(WD::WD1770::Personality personality, int clock_rate, Storage::Disk::Drive::ReadyType ready_type) :
WD::WD1770(personality), clock_rate_(clock_rate), ready_type_(ready_type) {
emplace_drives(4, clock_rate_, 300, 2, ready_type_);
// TODO: don't assume four drives?
}
void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int d) {
get_drive(size_t(d)).set_disk(disk);
}
enum class PagedItem {
DiskROM,
BASIC,
RAM
};
struct Delegate: public WD1770::Delegate {
virtual void disk_controller_did_change_paged_item(DiskController *controller) = 0;
};
inline void set_delegate(Delegate *delegate) {
delegate_ = delegate;
WD1770::set_delegate(delegate);
if(delegate) delegate->disk_controller_did_change_paged_item(this);
}
inline PagedItem get_paged_item() {
return paged_item_;
}
protected:
Delegate *delegate_ = nullptr;
bool enable_overlay_ram_ = false;
bool disable_basic_rom_ = false;
void select_paged_item() {
PagedItem item = PagedItem::RAM;
if(!enable_overlay_ram_) {
item = disable_basic_rom_ ? PagedItem::DiskROM : PagedItem::BASIC;
}
set_paged_item(item);
}
private:
PagedItem paged_item_ = PagedItem::DiskROM;
int clock_rate_;
Storage::Disk::Drive::ReadyType ready_type_;
inline void set_paged_item(PagedItem item) {
if(paged_item_ == item) return;
paged_item_ = item;
if(delegate_) {
delegate_->disk_controller_did_change_paged_item(this);
}
}
};
};
#endif /* DiskController_h */

81
Machines/Oric/Jasmin.cpp Normal file
View File

@@ -0,0 +1,81 @@
//
// Jasmin.cpp
// Clock Signal
//
// Created by Thomas Harte on 05/01/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "Jasmin.hpp"
using namespace Oric;
// NB: there's some controversy here on WD1770 versus WD1772, but between those two I think
// the only difference is stepping rates, and it says 1770 on the schematic I'm looking at.
Jasmin::Jasmin() : DiskController(P1770, 8000000, Storage::Disk::Drive::ReadyType::ShugartRDY) {
set_is_double_density(true);
select_paged_item();
}
void Jasmin::write(int address, uint8_t value) {
switch(address) {
// Set side.
case 0x3f8: {
const int head = value & 1;
for_all_drives([head] (Storage::Disk::Drive &drive, size_t) {
drive.set_head(head);
});
} break;
case 0x3f9:
/* TODO: reset. */
break;
case 0x3fa: {
// If b0, enable overlay RAM.
enable_overlay_ram_ = value & 1;
select_paged_item();
} break;
case 0x3fb:
// If b0, disable BASIC ROM.
disable_basic_rom_ = value & 1;
select_paged_item();
break;
case 0x3fc: case 0x3fd: case 0x3fe: case 0x3ff: {
// Updated selected drives mask.
const uint8_t mask = uint8_t(1 << (address - 0x3fc));
selected_drives_ = (selected_drives_ & ~mask) | (mask * (value & 1));
// Select drive.
set_drive(selected_drives_);
// Update motor inputs: apply the motor only to selected drives.
// Which may or may not be correct.
for_all_drives([mask, this] (Storage::Disk::Drive &drive, size_t index) {
const uint8_t shift_mask = uint8_t(1 << index);
drive.set_motor_on( (mask & shift_mask) ? motor_on_ : false );
});
} break;
default:
return WD::WD1770::write(address, value);
}
}
void Jasmin::set_motor_on(bool on) {
motor_on_ = on;
get_drive().set_motor_on(motor_on_);
if(observer_) {
observer_->set_led_status("Jasmin", on);
}
}
void Jasmin::set_activity_observer(Activity::Observer *observer) {
observer_ = observer;
if(observer) {
observer->register_led("Jasmin");
observer_->set_led_status("Jasmin", motor_on_);
}
}

36
Machines/Oric/Jasmin.hpp Normal file
View File

@@ -0,0 +1,36 @@
//
// Jasmin.hpp
// Clock Signal
//
// Created by Thomas Harte on 05/01/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef Jasmin_hpp
#define Jasmin_hpp
#include "../../Components/1770/1770.hpp"
#include "../../Activity/Observer.hpp"
#include "DiskController.hpp"
namespace Oric {
class Jasmin: public DiskController {
public:
Jasmin();
void write(int address, uint8_t value);
void set_activity_observer(Activity::Observer *observer);
private:
void set_motor_on(bool on) final;
bool motor_on_ = false;
uint8_t selected_drives_ = 0;
Activity::Observer *observer_ = nullptr;
};
};
#endif /* Jasmin_hpp */

View File

@@ -45,6 +45,9 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
BIND(Space, KeySpace);
BIND(Enter, KeyReturn);
BIND(F12, KeyNMI);
BIND(F1, KeyJasminReset);
}
#undef BIND

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