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

Compare commits

...

201 Commits

Author SHA1 Message Date
Thomas Harte
8fc3496cc9 Merge pull request #836 from TomHarte/Vic20Tests
Corrects a couple of minor VIA timer issues
2020-09-17 21:56:43 -04:00
Thomas Harte
e807a462a1 My new reading is that only a write to the counter should affect the interrupt flag. 2020-09-17 21:31:29 -04:00
Thomas Harte
18790a90ae Ensures timer 2 doesn't use timed behaviour when in pulse mode. 2020-09-17 21:09:32 -04:00
Thomas Harte
21afc70261 Adds formal data-sheet names. 2020-09-17 19:00:46 -04:00
Thomas Harte
7bb74af478 Merge pull request #835 from TomHarte/ErrantScan
Allows for permitted 1/32nd timing error in `time_multiplier_`.
2020-09-17 18:17:37 -04:00
Thomas Harte
894269aa06 Allows for permitted 1/32nd timing error in time_multiplier_. 2020-09-17 18:12:21 -04:00
Thomas Harte
8b16da9695 Merge pull request #834 from TomHarte/FloatingSpeaker
Resolves lowpass-speaker position aliasing
2020-09-16 19:05:54 -04:00
Thomas Harte
f783ec6269 Since input and output are floating point, using an integer Stepper is not appropriate. 2020-09-16 18:53:44 -04:00
Thomas Harte
22c9734874 Merge pull request #832 from TomHarte/MetalScanTarget
Adds a Metal ScanTarget, for macOS.
2020-09-16 18:19:58 -04:00
Thomas Harte
a17d0e428f Protects against some further uninitialised values. 2020-09-16 18:15:57 -04:00
Thomas Harte
bb57f0bcc7 Ensures all 6560 properties have a valid default value. 2020-09-16 17:24:18 -04:00
Thomas Harte
b1aefbfe85 Separates asserts. 2020-09-15 23:24:06 -04:00
Thomas Harte
061288f5a7 Add the Macintosh to the Mac kiosk mode informal test set. 2020-09-15 22:49:00 -04:00
Thomas Harte
5a53474536 Ensure MultiKeyboard deconstructs properly. 2020-09-15 22:48:44 -04:00
Thomas Harte
18d0fff8da Graduates the Atari ST. 2020-09-15 22:46:38 -04:00
Thomas Harte
0ac2145740 Add Metal/OpenGL distinction. 2020-09-15 22:43:39 -04:00
Thomas Harte
bc8787ded6 Improves macro safety. 2020-09-15 22:26:33 -04:00
Thomas Harte
69d21daaa3 Improves commentary. 2020-09-15 22:21:05 -04:00
Thomas Harte
5651ef606d Resolves failure to advance video address when output is blocked. 2020-09-15 22:20:06 -04:00
Thomas Harte
b831b31382 Adds a further sanity check. 2020-09-15 17:04:04 -04:00
Thomas Harte
2fd5cc056c Adds std::atomic_thread_fences, but these seem not to be a magic bullet. 2020-09-15 16:34:34 -04:00
Thomas Harte
82dbdf7dfc Switches to using regular linear interpolation for supersampling. 2020-09-14 22:36:00 -04:00
Thomas Harte
eb9903cd10 Defensively disables allocation of anything outside of visible lines. 2020-09-14 22:29:05 -04:00
Thomas Harte
227e98d6d7 Slightly simplifies control flow. 2020-09-14 22:27:25 -04:00
Thomas Harte
35476063b7 Resolves potential data races. 2020-09-14 21:07:50 -04:00
Thomas Harte
8557bb2136 Adds minor exposition. 2020-09-14 20:39:52 -04:00
Thomas Harte
c0c7818d5d Reintroduces screenshots. 2020-09-14 20:33:05 -04:00
Thomas Harte
ceeadd6a33 Edges up towards reimplementing screenshots. 2020-09-13 22:30:17 -04:00
Thomas Harte
1a2545fdea Excises dangling references to OpenGLView, reinstates display link. 2020-09-13 22:11:51 -04:00
Thomas Harte
c5e9a74c88 Uses DisplayMetrics to disable supersampling when too slow. 2020-09-13 21:07:59 -04:00
Thomas Harte
d7972a7b86 Enforces across-the-board supersampling.
I'm damned if I can figure out how to talk an MTKView, or Metal in general, into supersampling so as a first effort this does it in software.
2020-09-13 19:30:26 -04:00
Thomas Harte
7dd4c67304 Corrects access to data_type_size, adds sanity check on output area return. 2020-09-13 18:59:27 -04:00
Thomas Harte
e113780fd1 Minor: ensures no possibility of a dangling(-ish) pointer within the Mac video. 2020-09-10 22:13:19 -04:00
Thomas Harte
e32ae6c191 Adds UGLY HACKs to workaround uncovered issues in the OpenGL scan target. 2020-09-10 22:10:24 -04:00
Thomas Harte
bcaceff378 Simplifies in-Metal transform logic, loading responsibility for setup onto the CPU.
I think I've also finally excised whatever order-of-operations issue I was having with regard to non-4:3 displays.
2020-09-10 20:32:58 -04:00
Thomas Harte
d7b405c6f8 Ensures direct luminance -> 'RGB' video doesn't go down the composition pipeline. 2020-09-10 13:20:40 -04:00
Thomas Harte
edf8cf4dc6 Completes the set of with/without gamma, and ensures correct alpha selection.
Also culls some other repetitive TODOs.
2020-09-09 19:28:38 -04:00
Thomas Harte
dfcc8e9822 Switches some of the interpolated fields to half precision. 2020-09-09 18:17:05 -04:00
Thomas Harte
016e96e6f8 Extends usage of half. Possibly towards its conclusion. 2020-09-09 15:10:19 -04:00
Thomas Harte
e7ce03c418 Attempts to ensure initial finalised line texture state.
This was an attempt to remove the vertical line on the left of a composite display. Obviously the cause is not as suspected.
2020-09-09 13:15:21 -04:00
Thomas Harte
3d392dd81d Completes conversion of composite & S-Video per-pixel processing to 16-bit floats. 2020-09-09 13:02:04 -04:00
Thomas Harte
42d810db7f Switches per-pixel uniforms to halfs. 2020-09-09 10:53:09 -04:00
Thomas Harte
18571e8351 Also calculates a chroma kernel size, though it isn't used for anything yet. 2020-09-08 20:08:56 -04:00
Thomas Harte
dda1649ab7 Introduces smaller luma kernel functions where useable. 2020-09-08 19:55:37 -04:00
Thomas Harte
c82e0df071 Starts a transition towards half-precision arithmetic. 2020-09-08 19:37:36 -04:00
Thomas Harte
06b7ea5a6e Strips the luma kernel back to 1d. 2020-09-08 19:15:19 -04:00
Thomas Harte
c49fcb9ec9 Based on further play: one box filter to separate luma/chroma, another to filter chroma, plus a FIR sharpen on luma. 2020-09-08 16:35:05 -04:00
Thomas Harte
0e44d6d214 Experiments with an all-box filter. 2020-09-08 16:19:08 -04:00
Thomas Harte
6adad7fbf5 Starts experimenting again with box filters. 2020-09-07 22:47:49 -04:00
Thomas Harte
de6ed7b615 Corrects phase-linked luminance support. 2020-09-07 20:53:28 -04:00
Thomas Harte
07dcb4dbb1 Starts reintroducing brightness, gamma and transparency for composite and S-Video pipelines. 2020-09-07 18:19:13 -04:00
Thomas Harte
e99896eadc At least nominates alpha, gamma and brightness to metal. 2020-09-04 16:07:58 -04:00
Thomas Harte
489701afcb Fixes window resizing. 2020-09-03 21:28:39 -04:00
Thomas Harte
55e576cc57 Ensures unpainted areas in composite displays have a non-asymptotic effect on luminance calculations. 2020-09-03 21:10:30 -04:00
Thomas Harte
6bd8ec9545 Alas, 1.0 seems to be the limit for proper artefact colour. 2020-09-03 20:53:45 -04:00
Thomas Harte
5cd8d86eef Switches to dynamic generation of the 'sharpness' filter, correcting issues with the Apple II (amongst others). 2020-09-03 20:48:44 -04:00
Thomas Harte
74d0acdaec Fixes non-RGB colour composite generation.
The hard-coded sharpen filter proves to be a really bad fit for the Apple II though.
2020-09-03 19:04:17 -04:00
Thomas Harte
0288a1974b Tries: separate filters for chroma and luma, plus a post-separation sharpen filter on the latter. 2020-09-03 13:18:21 -04:00
Thomas Harte
6efd8782fe Tweaks coefficients some more; makes sure that data is never larger than the intermediate buffers. 2020-09-02 20:14:41 -04:00
Thomas Harte
8bab9d5d60 Corrects S-Video and composite generation for RGB[1/2/4] sources.
Also toys with a double luminance filter in order to try to sharpen chrominance. But maybe I should be looking at other convolutions entirely?
2020-09-02 19:13:54 -04:00
Thomas Harte
6ef1dfd8be Sets a more realistic colour subcarrier amplitude. 2020-09-02 15:52:05 -04:00
Thomas Harte
7e58648743 Corrects front-running bug, plays further with colour amplitude. 2020-09-02 15:51:48 -04:00
Thomas Harte
0f0c3e616d Tweaks some numbers.
I'm largely treading water here. Probably time to think about the race.
2020-09-02 08:17:01 -04:00
Thomas Harte
c7ce65ea4c Attempts fully to restore composite video.
Subject to some sort of nasty race condition for the time being.
2020-09-02 08:03:10 -04:00
Thomas Harte
c36247b609 Ensures reuse of offset buffers.
There seems to be some sort of epic race condition as the drawing pipeline lags though. Will need to investigate.
2020-09-01 22:11:48 -04:00
Thomas Harte
15296e43a4 Attempts correctly to set up the CPU side of a composite video pipeline, at least.
So: I think this is really close, but I'm out of time for the day.
2020-09-01 21:58:33 -04:00
Thomas Harte
f2929230a2 [Experimentally] introduces blending between computed S-Video fragments. 2020-09-01 21:37:36 -04:00
Thomas Harte
bf252b8061 Fixes sizing of buffers to the current output. 2020-09-01 21:33:54 -04:00
Thomas Harte
9e2bf2af7e Restricts S-Video processing to updated lines. 2020-09-01 21:27:40 -04:00
Thomas Harte
245f2654f0 Shifts S-Video processing into the compute shader. 2020-09-01 20:37:11 -04:00
Thomas Harte
67ca298a72 Forces a no-op compute shader into the S-Video pipeline.
The intention is to restrict the area acted over, and to do the S-Video filtering in there. Then I'll just need two such stages for composite.
2020-09-01 18:39:52 -04:00
Thomas Harte
67d4dbf91a Starts girding for a third pipeline. 2020-08-31 20:01:59 -04:00
Thomas Harte
b344269140 I think I accept the need for intermediate steps now.
This allocates storage.
2020-08-30 20:21:01 -04:00
Thomas Harte
bb547610f2 Adds commentary, shrinks some intermediate texture sizes. 2020-08-30 12:06:29 -04:00
Thomas Harte
1e1f007bb7 Possibly convinces myself that no-separation chroma/luma isn't practical.
... as appealing as it may be, were filters perfect.
2020-08-29 21:25:49 -04:00
Thomas Harte
c40d858f02 Switches back to angular stuff at input resolution; ensures all S-Video modes work.
Now to roll back onto composite. Fingers crossed!
2020-08-29 20:54:46 -04:00
Thomas Harte
3d564d85fd Proves that per-pixel sine/cos evaluation avoids phase issues.
Even in PAL mode. But I'd rather not _require_ this as it kind of negates directly-sampled input.
2020-08-29 18:53:37 -04:00
Thomas Harte
02cea40ffa Attempts to avoid introducing phase error in scanToComposition.
Also brightens S-Video up to RGB levels.
2020-08-25 22:41:37 -04:00
Thomas Harte
e502d336db Having decided that these things probably need to be separate, starts drilling down on S-Video. 2020-08-25 22:05:19 -04:00
Thomas Harte
807cb99f6d Composite angles are signed. 2020-08-23 21:39:04 -04:00
Thomas Harte
8b6879a782 Brief detour: introduces myself to C++11 multiline string literals.
A full cleaning coming soon, I imagine.
2020-08-23 21:18:38 -04:00
Thomas Harte
7ca0362f23 Treads water.
Difficult current question: why does the Atari 2600's display change colours as the display tries to achieve horizontal lock? Phase should be unchanged. Ergo something is amiss.
2020-08-23 21:03:26 -04:00
Thomas Harte
56c7bd242a Marginally tidies. 2020-08-22 16:38:36 -04:00
Thomas Harte
5c6112415a Sets appropriate clear colour for composition render pass. 2020-08-21 22:41:54 -04:00
Thomas Harte
bf6a0c9fc4 Achieves a return of composite colour for RGB-producing machines. 2020-08-21 22:06:36 -04:00
Thomas Harte
d54b937ab6 Starts trying to do actual composite processing. 2020-08-21 21:11:25 -04:00
Thomas Harte
7c23c32e44 Corrects composition colour phase. 2020-08-20 20:45:45 -04:00
Thomas Harte
4e21d24b5f Corrects composition colour amplitude. 2020-08-20 20:34:37 -04:00
Thomas Harte
ad6fb85fda Corrects use of composition buffer.
Something is still very obviously amiss in colour processing somewhere down the line, but the correct forms are once again visibly in evidence.
2020-08-20 20:21:28 -04:00
Thomas Harte
5dc39a5d24 Adds the composition render pass.
Albeit that something here doesn't work at present.
2020-08-19 21:56:53 -04:00
Thomas Harte
3597f687de Continues sidling towards composite & S-Video handling. 2020-08-19 21:20:06 -04:00
Thomas Harte
8811506adf Starts towards building a compound[/composition?] buffer.
I now need to discover whether I can use natural integer texture coordinates.
2020-08-17 22:10:02 -04:00
Thomas Harte
11dec6fc0f Avoids a redundant clear. 2020-08-17 22:09:15 -04:00
Thomas Harte
59c4c8233f Generalises existing scanToDisplay to add lineToDisplay. 2020-08-17 21:15:19 -04:00
Thomas Harte
9da79d2d81 Clarifies scaling logic. 2020-08-17 20:29:46 -04:00
Thomas Harte
246b474a25 Removes ONE_BIG_LOCK, having effectively neutered it anyway.
Starts work on more explicit API usage validation. Maybe the issue isn't a race condition?
2020-08-16 22:09:25 -04:00
Thomas Harte
27e8a3a1b5 Obeys modals' zoom.
Subject to an attempt at factoring aspect ratio differences.
2020-08-16 21:11:43 -04:00
Thomas Harte
745797b596 Introduces a stencil buffer plus the inter-frame clearing it allows. 2020-08-16 16:42:32 -04:00
Thomas Harte
940e9e037e Adds first_scan to LineMetadata.
Also reorders `Line` fields to match `Scan` fields, just for visual consistency.
2020-08-16 08:59:37 -04:00
Thomas Harte
512c0079a9 Makes thread safe. 2020-08-15 21:52:55 -04:00
Thomas Harte
645c29f853 Adds an intermediate buffer to correct inter-frame smoothing.
Also goes someway back to the old scan output scheduling, albeit presently with limited thread safety.
2020-08-15 21:24:10 -04:00
Thomas Harte
e55945674d Reduces main thread blocking. 2020-08-14 22:16:49 -04:00
Thomas Harte
7ac88536dd Respects machine aspect ratio.
To an extent. Doesn't currently deal with cropping of machines when the window aspect ratio is smaller.
2020-08-14 21:24:25 -04:00
Thomas Harte
230b9fc9e6 Permits multiple simultaneous scan reading ranges.
Also updates the OpenGL scan target as per the latest movements of things.
2020-08-12 22:08:41 -04:00
Thomas Harte
27ca782cac Enables blending; attempts to enable frame preservation.
The latter seems to be evidencing a double buffer at play. More investigation required.

On the plus side, the direct route is still well within GPU budget at 4k on my Core M. So a huge improvement there.
2020-08-12 19:34:07 -04:00
Thomas Harte
a136a00a2f Takes a shot at adding RGB -> S-Video and composite conversion, for all RGB types. 2020-08-11 22:11:50 -04:00
Thomas Harte
637ec35d6a Adds getters for standard colour-space conversion matrices.
These are just more details on the meaning of the colour spaces, so I think they belong here.
2020-08-11 19:58:57 -04:00
Thomas Harte
4b55df1cb4 Starts upon a macro-oriented means of RGB input function generation. 2020-08-10 22:03:39 -04:00
Thomas Harte
b9309268ba Possibly finally succeeds at moving Accelerate.framework to where it should be. 2020-08-10 21:46:11 -04:00
Thomas Harte
8fa89baf54 Slightly cleans up Xcode project; reenables kiosk-for-Mac builds. 2020-08-10 21:43:32 -04:00
Thomas Harte
8374a5e579 Adds superficially correct compositeSampleLuminance8Phase8 function.
Thereby uncovering a minor error in my decoding of colour phase.
2020-08-10 21:33:59 -04:00
Thomas Harte
525233e10b Ensures all input data types are parseable in Metal.
Though now I need to think a bit more about the best way to compose signal-type conversions, and whether output-type calculations (i.e. gamma, brightness) are applied.
2020-08-10 19:47:47 -04:00
Thomas Harte
eadda6a967 Further strips OpenGL from the macOS target. 2020-08-09 22:17:27 -04:00
Thomas Harte
3d6590af89 Throws out a little more OpenGL. 2020-08-09 22:11:31 -04:00
Thomas Harte
28d933d5d6 Does just enough to get 8-bit RGB and 1-bit luminance machines to display.
Assuming an 'RGB' output.
2020-08-09 21:19:07 -04:00
Thomas Harte
c1dc42a094 Add comment on latent design aim.
In the hope that I don't forget why I did this.
2020-08-09 21:18:23 -04:00
Thomas Harte
6384ff3ee7 Add fix for data_type_size_ for owners that don't change texture pointer upon new modals. 2020-08-09 21:17:51 -04:00
Thomas Harte
a118594c8b Hacks to make RGB1 visible (in a fashion). 2020-08-09 20:45:51 -04:00
Thomas Harte
93c6105442 Corrects calculation of dirty texture area. 2020-08-09 20:45:14 -04:00
Thomas Harte
ced4a75a1a Adds note on the buffering scan target's minor adaptation of data_offset. 2020-08-09 20:44:46 -04:00
Thomas Harte
57fecdc09e Ties everything together in an attempt to display RGB scans.
I'm actually just getting a mess of pixels, but it's something!
2020-08-09 18:41:15 -04:00
Thomas Harte
cd491bb6e0 Cleans up project file; macOS 10.13 is definitely the deployment target. 2020-08-09 18:27:57 -04:00
Thomas Harte
f16ad8f71d Takes a shot at submitting texture changes. 2020-08-09 17:59:52 -04:00
Thomas Harte
e340685a99 Seemingly proves that proper geometry is reaching Metal by drawing scans.
No in-buffer accumulation yet, but this is progress. If I can add accumulation and stencil clearing, I'm not doing badly.
2020-08-08 23:11:44 -04:00
Thomas Harte
df89a8771c Makes an attempt to have the emulator fill the actual GPU buffers.
Not that they're drawn from correctly yet. I might first take a run at a new quick-path output route for emulated RGB displays, that just seeks to use the scans directly. No intermediate buffers. Besides probably being a good feature, it'll be a good way to ramp further up with Metal.
2020-08-08 22:49:02 -04:00
Thomas Harte
bdcf266e45 Having learnt a bit more: eliminates Metal attribute tags, switches to more natural expression of structs.
Also thereby eliminates the need for a forced alignas(4) on various structs.
2020-08-08 17:27:32 -04:00
Thomas Harte
edf41b06fd Eliminates the quad buffer.
Vertices can be adduced from vertex ID.
2020-08-08 17:12:49 -04:00
Thomas Harte
38960a08d6 Adds adjustment for display aspect ratio.
While also realising that I appear to be getting away without an MTLVertexDescriptor for Scans. Maybe OpenGL has prejudiced me, and they're actually optional for interleaved data?
2020-08-07 22:29:24 -04:00
Thomas Harte
fbda7aab23 Does just enough to get the correct (aspect ratio aside) output of scan outlines.
So, up next, can I start streaming these things?
2020-08-07 22:20:01 -04:00
Thomas Harte
c575aa0640 Adds a buffer for scans, and posts two test instances. 2020-08-07 22:03:54 -04:00
Thomas Harte
583f6b1ba2 Modifies BufferingScanTarget to allow has-a relationship.
I might switch fully to has-a. Further consideration required.
2020-08-07 22:03:27 -04:00
Thomas Harte
bb55ecc101 Disables --volume for kiosk mode testing. 2020-08-07 21:19:53 -04:00
Thomas Harte
4421acef34 Gets some uniforms in on the action.
With some effort towards scans, but incompletely so.
2020-08-07 21:19:17 -04:00
Thomas Harte
4c9418f59a Guarantees alignof(4) on all GPU-bound structures.
Taken as given: Metal's requirement here is reasonable enough that it'll either be the same as other frameworks, or at least possibly help them down a fast path.
2020-08-07 21:18:08 -04:00
Thomas Harte
219923bd63 Reduces vertex size, draws a quad. 2020-08-05 21:33:25 -04:00
Thomas Harte
7551782a25 Switches to interleaved vertex data.
This more closely relates to what I actually want to do.
2020-08-05 17:27:43 -04:00
Thomas Harte
5c836604c0 Reenable MaserSystem code.
Accidental/poor branch management is evidenced here.
2020-08-04 21:50:54 -04:00
Thomas Harte
eff24a8726 My first baby steps in Metal continue; here's a triangle. 2020-08-04 21:49:01 -04:00
Thomas Harte
72df6e52cd This is possibly at least dispatching an empty command buffer correctly. 2020-08-04 19:44:56 -04:00
Thomas Harte
e235a45abb Breaks all output.
... by switching out NSOpenGLView for MKLView with no drawing infrastructure yet in place.
2020-08-04 18:22:14 -04:00
Thomas Harte
d20c11e401 Merge pull request #831 from TomHarte/MultiKeyboard
Ensures that the MultiKeyboard functions.
2020-07-31 22:08:08 -04:00
Thomas Harte
693b889fdd Ensures that the MultiKeyboard functions. 2020-07-31 21:48:20 -04:00
Thomas Harte
671f48dc10 Merge pull request #830 from TomHarte/MSXCrash
Restores audio to multimachines
2020-07-31 18:31:23 -04:00
Thomas Harte
7b1708f0bc Gets explicit that the delegate_ doesn't need a memory barrier. 2020-07-31 18:21:47 -04:00
Thomas Harte
f34a9b4346 Corrects audio output from the multi-speaker.
Specifically: local duplication of the delegate is unnecessary, and leads to confusion.
2020-07-31 18:18:19 -04:00
Thomas Harte
1e6d03246b Merge pull request #829 from TomHarte/MSXCrash
Ensures proper handover of speaker state when picking in a multimachine.
2020-07-30 23:04:55 -04:00
Thomas Harte
cdde57fcf2 Remove unused code. 2020-07-30 23:02:01 -04:00
Thomas Harte
c0a61ac1ee Ensures proper handover of speaker state when picking in a multimachine. 2020-07-30 22:50:32 -04:00
Thomas Harte
9c97c0a906 Merge pull request #828 from TomHarte/LockFreeQueue
Completes LockFreeQueue branch.
2020-07-30 21:46:56 -04:00
Thomas Harte
8cacab196d Merge branch 'master' into LockFreeQueue 2020-07-30 21:43:25 -04:00
Thomas Harte
b14bedbe29 Merge pull request #817 from TomHarte/LockFreeQueue
Fully splits buffering from drawing for the existing OpenGL scan target.
2020-07-30 21:42:20 -04:00
Thomas Harte
6bc66d8b96 Tidies, ensures ::will_change_owner acquires the producer mutex. 2020-07-29 23:18:03 -04:00
Thomas Harte
23f381f381 Fixes frame_is_complete_, gets rid of active_line_, explains ONE_BIG_LOCK in set_write_area. 2020-07-29 23:03:38 -04:00
Thomas Harte
51ad423eca Resolves off-by-one error in line writing, adds diagnostic one-big-lock option. 2020-07-29 22:45:13 -04:00
Thomas Harte
72a8fef989 Switches to much more straightforward Line/LineMetadata storage.
Spoiler: covering this whole segment behind producer_mutex_ seems to resolve all output issues, so clearly the existing logic isn't functioning correctly. Making it simpler seems like a pretty obvious way to get to the bottom of that.
2020-07-29 21:49:17 -04:00
Thomas Harte
02f41ee513 This has become the general producer mutex, might as well name it as such. 2020-07-29 21:34:07 -04:00
Thomas Harte
9410594486 Merge branch 'master' into LockFreeQueue 2020-07-29 21:22:19 -04:00
Thomas Harte
1c6223cc11 Merge pull request #825 from TomHarte/Microdisc
Gives Qt disk controllers independent ROM/RAM selection logic.
2020-07-29 21:21:29 -04:00
Thomas Harte
82d6a5387f Gives Qt disk controllers independent ROM/RAM selection logic.
In particular, this fixes the Microdisc.
2020-07-29 21:06:41 -04:00
Thomas Harte
5165e65021 Reduces scan_buffer_ to a saner size.
Albeit still probably overspecified.
2020-07-28 22:36:57 -04:00
Thomas Harte
1942742d73 Resolves thread data race on Macintosh audio output. 2020-07-28 22:21:52 -04:00
Thomas Harte
b7760bb052 Reorders code, gets explicit about memory ordering. 2020-07-28 22:02:22 -04:00
Thomas Harte
2470055d90 Hides the modals. 2020-07-27 23:33:39 -04:00
Thomas Harte
62be2a2eec Merge branch 'master' into LockFreeQueue 2020-07-27 23:18:45 -04:00
Thomas Harte
b1e062945e Merge pull request #821 from TomHarte/QtThreading
Qt issue mega ticket
2020-07-27 21:19:12 -04:00
Thomas Harte
3db4a8c312 Attempts to improve vsync deadline estimation.
Also increases probability of bad estimation being discarded.
2020-07-27 21:08:00 -04:00
Thomas Harte
db8e1b0edf Adds feedback on unidentified ROMs. 2020-07-27 20:45:47 -04:00
Thomas Harte
71c3f58c99 Provides user feedback upon improper command-line usage. 2020-07-27 20:40:38 -04:00
Thomas Harte
7c05b1788e Ensures proper thread confinement for updateStatusBarText. 2020-07-27 20:25:52 -04:00
Thomas Harte
77c5b86acc Moves ownership of the scan and line buffers out of the BufferingScanTarget. 2020-07-26 22:46:03 -04:00
Thomas Harte
bc6426313e Localises three of the four macros. 2020-07-26 17:54:33 -04:00
Thomas Harte
8bef7ff4c5 Makes all three PointerSets and is_updating_ private. 2020-07-26 17:27:19 -04:00
Thomas Harte
a2db6ddea5 Add link to Snap releases. 2020-07-25 23:00:29 -04:00
Thomas Harte
f9f500c194 Merge branch 'master' into LockFreeQueue 2020-07-24 22:29:45 -04:00
Thomas Harte
6ad1e3e17e Merge pull request #819 from TomHarte/Warnings
Resolves GCC 7 warnings.
2020-07-24 22:17:50 -04:00
Thomas Harte
e097a841d2 Adds a c++1z fallback for SDL builds, too. 2020-07-24 22:01:22 -04:00
Thomas Harte
fa95a17af5 Resolves receive_bit_count-unused warnings. 2020-07-24 21:59:27 -04:00
Thomas Harte
b961665985 Ensures WOZ2 behaviour even if type_ has an invalid value.
This pleases GCC 7.
2020-07-24 21:56:20 -04:00
Thomas Harte
8af35bc6bb Resolves signed comparison mismatches. 2020-07-24 21:55:33 -04:00
Thomas Harte
9b75287a52 Merge pull request #818 from TomHarte/QtC++1z
Add c++1z as a config option, for older versions of Qt.
2020-07-24 16:34:04 -04:00
Thomas Harte
84d5316aa7 Add c++1z as a config option, for older versions of Qt. 2020-07-24 16:32:59 -04:00
Thomas Harte
89acb70091 Slightly reorganise. 2020-07-24 16:20:20 -04:00
Thomas Harte
66165a6dea Add missing include files. 2020-07-23 23:24:24 -04:00
Thomas Harte
84dcf9925b Updates Scons and Qt projects to include new files. 2020-07-23 23:14:10 -04:00
Thomas Harte
ee1d7eb61f Makes more buffer-specific stuff private. 2020-07-23 23:06:14 -04:00
Thomas Harte
e260f92988 Privatises write_pointers_mutex_ and write_pointers_.
Also gives subclasses control over write-area texture space allocation.
2020-07-23 22:54:40 -04:00
Thomas Harte
74788ccf8e Pulls the BufferingScanTarget into a separate file. 2020-07-22 22:16:47 -04:00
Thomas Harte
0da5c07942 Starts splitting ring-buffer stuff from OpenGL stuff.
Initially via two very codependent classes.
2020-07-21 22:49:46 -04:00
Thomas Harte
e8cd5a0511 Merge pull request #816 from TomHarte/RelaxedTracks
Corrects a regression in disk image handling; liberalises Disk II analyser
2020-07-20 19:53:34 -04:00
Thomas Harte
5ebbab6f35 Further relax Apple GCR static analysis requirements. 2020-07-20 19:50:33 -04:00
Thomas Harte
84dd194afd Corrects test for ::tracks_differ. 2020-07-20 19:48:20 -04:00
Thomas Harte
e1ad1a4cb6 Update Qt status. 2020-07-20 09:17:43 -04:00
Thomas Harte
47f121ee4c Mark WOZs as read-only, with exposition as to why. 2020-07-19 00:08:49 -04:00
Thomas Harte
d8b699c869 Corrects index pulse signalling. 2020-07-19 00:06:27 -04:00
Thomas Harte
a7855e8c98 Ensure float literals are floats. 2020-07-17 23:18:41 -04:00
Thomas Harte
8dcb48254a Simplifies calculations very slightly. 2020-07-17 23:18:08 -04:00
Thomas Harte
f6b7467d75 Implement custom tracks_differ; support WOZ 2 3.5" drive geometry properly. 2020-07-17 22:09:55 -04:00
Thomas Harte
9d1d162cc8 Add an ability to avoid track flushing when file formats have sub-track precision. 2020-07-17 22:09:21 -04:00
Thomas Harte
4ee29b3266 Switches disk seeking logic fully to floating point. 2020-07-17 22:08:58 -04:00
Thomas Harte
cbb0594e6b Use 16-sector state machine even with the 13-sector boot ROM.
I think I've proven that the Disk II doesn't decode the 13-sector state machine correctly. Work to do there.
2020-07-16 23:27:27 -04:00
Thomas Harte
8aeebdbc99 Remove redundant comment. 2020-07-16 23:26:45 -04:00
76 changed files with 3611 additions and 1322 deletions

View File

@@ -10,12 +10,12 @@
using namespace Analyser::Dynamic;
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) :
keyboard_(machines_) {
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) {
for(const auto &machine: machines) {
auto keyboard_machine = machine->keyboard_machine();
if(keyboard_machine) machines_.push_back(keyboard_machine);
}
keyboard_ = std::make_unique<MultiKeyboard>(machines_);
}
void MultiKeyboardMachine::clear_all_keys() {
@@ -45,7 +45,7 @@ bool MultiKeyboardMachine::can_type(char c) const {
}
Inputs::Keyboard &MultiKeyboardMachine::get_keyboard() {
return keyboard_;
return *keyboard_;
}
MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::MachineTypes::KeyboardMachine *> &machines)

View File

@@ -42,7 +42,7 @@ class MultiKeyboardMachine: public MachineTypes::KeyboardMachine {
std::set<Key> observed_keys_;
bool is_exclusive_ = false;
};
MultiKeyboard keyboard_;
std::unique_ptr<MultiKeyboard> keyboard_;
public:
MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines);

View File

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

View File

@@ -40,7 +40,6 @@ 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_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;
void set_output_volume(float) override;
@@ -51,7 +50,6 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker:
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

@@ -89,9 +89,28 @@ void MultiMachine::did_run_machines(MultiTimedMachine *) {
void MultiMachine::pick_first() {
has_picked_ = true;
// Ensure output rate specifics are properly copied; these may be set only once by the owner,
// but rather than being propagated directly by the MultiSpeaker only the derived computed
// output rate is propagated. So this ensures that if a new derivation is made, it's made correctly.
if(machines_[0]->audio_producer()) {
auto multi_speaker = audio_producer_.get_speaker();
auto specific_speaker = machines_[0]->audio_producer()->get_speaker();
if(specific_speaker && multi_speaker) {
specific_speaker->copy_output_rate(*multi_speaker);
}
}
// TODO: because it is not invalid for a caller to keep a reference to anything previously returned,
// this erase can be added only once the Multi machines that take static copies of the machines list
// are updated.
//
// Example failing use case otherwise: a caller still has reference to the MultiJoystickMachine, and
// it has dangling references to the various JoystickMachines.
//
// This gets into particularly long grass with the MultiConfigurable and its MultiStruct.
// machines_.erase(machines_.begin() + 1, machines_.end());
// TODO: this isn't quite correct, because it may leak OpenGL/etc resources through failure to
// request a close_output while the context is active.
}
void *MultiMachine::raw_pointer() {

View File

@@ -63,6 +63,13 @@ class VSyncPredictor {
frame_duration_ = Nanos(1'000'000'000.0f / rate);
}
/*!
@returns The time this class currently believes a whole frame occupies.
*/
Time::Nanos frame_duration() {
return frame_duration_;
}
/*!
Adds a record of how much jitter was experienced in scheduling; these values will be
factored into the @c suggested_draw_time if supplied.
@@ -87,15 +94,13 @@ class VSyncPredictor {
(if those figures are being supplied).
*/
Nanos suggested_draw_time() {
const auto mean = redraw_period_.mean() - timer_jitter_.mean() - vsync_jitter_.mean();
const auto mean = redraw_period_.mean() + timer_jitter_.mean() + vsync_jitter_.mean();
const auto variance = redraw_period_.variance() + timer_jitter_.variance() + vsync_jitter_.variance();
// Permit three standard deviations from the mean, to cover 99.9% of cases.
const auto period = mean - Nanos(3.0f * sqrt(float(variance)));
const auto period = mean + Nanos(3.0f * sqrt(float(variance)));
assert(abs(period) < 10'000'000'000);
return last_vsync_ + period;
return last_vsync_ + frame_duration_ - period;
}
private:
@@ -109,7 +114,6 @@ class VSyncPredictor {
}
void post(Time::Nanos value) {
assert(abs(value) < 10'000'000'000); // 10 seconds is a very liberal maximum.
sum_ -= history_[write_pointer_];
sum_ += value;
history_[write_pointer_] = value;

View File

@@ -8,6 +8,10 @@
#include "../../../Outputs/Log.hpp"
// As-yet unimplemented (incomplete list):
//
// PB6 count-down mode for timer 2.
namespace MOS {
namespace MOS6522 {
@@ -34,7 +38,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
address &= 0xf;
access(address);
switch(address) {
case 0x0: // Write Port B.
case 0x0: // Write Port B. ('ORB')
// Store locally and communicate outwards.
registers_.output[1] = value;
@@ -45,7 +49,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
reevaluate_interrupts();
break;
case 0xf:
case 0x1: // Write Port A.
case 0x1: // Write Port A. ('ORA')
registers_.output[0] = value;
bus_handler_.run_for(time_since_bus_handler_call_.flush<HalfCycles>());
@@ -59,28 +63,38 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
reevaluate_interrupts();
break;
case 0x2: // Port B direction.
case 0x2: // Port B direction ('DDRB').
registers_.data_direction[1] = value;
break;
case 0x3: // Port A direction.
case 0x3: // Port A direction ('DDRA').
registers_.data_direction[0] = value;
break;
// Timer 1
case 0x6: case 0x4: registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value; break;
case 0x5: case 0x7:
case 0x6: case 0x4: // ('T1L-L' and 'T1C-L')
registers_.timer_latch[0] = (registers_.timer_latch[0]&0xff00) | value;
break;
case 0x7: // Timer 1 latch, high ('T1L-H').
registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8);
break;
case 0x5: // Timer 1 counter, high ('T1C-H').
// Fill latch.
registers_.timer_latch[0] = (registers_.timer_latch[0]&0x00ff) | uint16_t(value << 8);
// Restart timer.
registers_.next_timer[0] = registers_.timer_latch[0];
timer_is_running_[0] = true;
// Clear existing interrupt flag.
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
if(address == 0x05) {
registers_.next_timer[0] = registers_.timer_latch[0];
timer_is_running_[0] = true;
}
reevaluate_interrupts();
break;
// Timer 2
case 0x8: registers_.timer_latch[1] = value; break;
case 0x9:
case 0x8: // ('T2C-L')
registers_.timer_latch[1] = value;
break;
case 0x9: // ('T2C-H')
registers_.interrupt_flags &= ~InterruptFlag::Timer2;
registers_.next_timer[1] = registers_.timer_latch[1] | uint16_t(value << 8);
timer_is_running_[1] = true;
@@ -88,7 +102,7 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
break;
// Shift
case 0xa:
case 0xa: // ('SR')
registers_.shift = value;
shift_bits_remaining_ = 8;
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
@@ -96,11 +110,11 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
break;
// Control
case 0xb:
case 0xb: // Auxiliary control ('ACR').
registers_.auxiliary_control = value;
evaluate_cb2_output();
break;
case 0xc: {
case 0xc: { // Peripheral control ('PCR').
// const auto old_peripheral_control = registers_.peripheral_control;
registers_.peripheral_control = value;
@@ -141,11 +155,11 @@ template <typename T> void MOS6522<T>::write(int address, uint8_t value) {
} break;
// Interrupt control
case 0xd:
case 0xd: // Interrupt flag regiser ('IFR').
registers_.interrupt_flags &= ~value;
reevaluate_interrupts();
break;
case 0xe:
case 0xe: // Interrupt enable register ('IER').
if(value&0x80)
registers_.interrupt_enable |= value;
else
@@ -159,46 +173,46 @@ template <typename T> uint8_t MOS6522<T>::read(int address) {
address &= 0xf;
access(address);
switch(address) {
case 0x0:
case 0x0: // Read Port B ('IRB').
registers_.interrupt_flags &= ~(InterruptFlag::CB1ActiveEdge | InterruptFlag::CB2ActiveEdge);
reevaluate_interrupts();
return get_port_input(Port::B, registers_.data_direction[1], registers_.output[1]);
case 0xf:
case 0x1:
case 0x1: // Read Port A ('IRA').
registers_.interrupt_flags &= ~(InterruptFlag::CA1ActiveEdge | InterruptFlag::CA2ActiveEdge);
reevaluate_interrupts();
return get_port_input(Port::A, registers_.data_direction[0], registers_.output[0]);
case 0x2: return registers_.data_direction[1];
case 0x3: return registers_.data_direction[0];
case 0x2: return registers_.data_direction[1]; // Port B direction ('DDRB').
case 0x3: return registers_.data_direction[0]; // Port A direction ('DDRA').
// Timer 1
case 0x4:
case 0x4: // Timer 1 low-order latches ('T1L-L').
registers_.interrupt_flags &= ~InterruptFlag::Timer1;
reevaluate_interrupts();
return registers_.timer[0] & 0x00ff;
case 0x5: return registers_.timer[0] >> 8;
case 0x6: return registers_.timer_latch[0] & 0x00ff;
case 0x7: return registers_.timer_latch[0] >> 8;
case 0x5: return registers_.timer[0] >> 8; // Timer 1 high-order counter ('T1C-H')
case 0x6: return registers_.timer_latch[0] & 0x00ff; // Timer 1 low-order latches ('T1L-L').
case 0x7: return registers_.timer_latch[0] >> 8; // Timer 1 high-order latches ('T1L-H').
// Timer 2
case 0x8:
case 0x8: // Timer 2 low-order counter ('T2C-L').
registers_.interrupt_flags &= ~InterruptFlag::Timer2;
reevaluate_interrupts();
return registers_.timer[1] & 0x00ff;
case 0x9: return registers_.timer[1] >> 8;
case 0x9: return registers_.timer[1] >> 8; // Timer 2 high-order counter ('T2C-H').
case 0xa:
case 0xa: // Shift register ('SR').
shift_bits_remaining_ = 8;
registers_.interrupt_flags &= ~InterruptFlag::ShiftRegister;
reevaluate_interrupts();
return registers_.shift;
case 0xb: return registers_.auxiliary_control;
case 0xc: return registers_.peripheral_control;
case 0xb: return registers_.auxiliary_control; // Auxiliary control ('ACR').
case 0xc: return registers_.peripheral_control; // Peripheral control ('PCR').
case 0xd: return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00);
case 0xe: return registers_.interrupt_enable | 0x80;
case 0xd: return registers_.interrupt_flags | (get_interrupt_line() ? 0x80 : 0x00); // Interrupt flag register ('IFR').
case 0xe: return registers_.interrupt_enable | 0x80; // Interrupt enable register ('IER').
}
return 0xff;
@@ -276,10 +290,13 @@ template <typename T> void MOS6522<T>::do_phase2() {
registers_.timer_needs_reload = false;
registers_.timer[0] = registers_.timer_latch[0];
} else {
registers_.timer[0] --;
-- registers_.timer[0];
}
registers_.timer[1] --;
// Count down timer 2 if it is in timed interrupt mode (i.e. auxiliary control bit 5 is clear).
// TODO: implement count down on PB6 if this bit isn't set.
registers_.timer[1] -= 1 ^ ((registers_.auxiliary_control >> 5)&1);
if(registers_.next_timer[0] >= 0) {
registers_.timer[0] = uint16_t(registers_.next_timer[0]);
registers_.next_timer[0] = -1;

View File

@@ -445,20 +445,20 @@ template <class BusHandler> class MOS6560 {
// register state
struct {
bool interlaced = false, tall_characters = false;
uint8_t first_column_location, first_row_location;
uint8_t number_of_columns, number_of_rows;
uint16_t character_cell_start_address, video_matrix_start_address;
uint16_t backgroundColour, borderColour, auxiliary_colour;
uint8_t first_column_location = 0, first_row_location = 0;
uint8_t number_of_columns = 0, number_of_rows = 0;
uint16_t character_cell_start_address = 0, video_matrix_start_address = 0;
uint16_t backgroundColour = 0, borderColour = 0, auxiliary_colour = 0;
bool invertedCells = false;
uint8_t direct_values[16];
uint8_t direct_values[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
} registers_;
// output state
enum State {
Sync, ColourBurst, Border, Pixels
} this_state_, output_state_;
int cycles_in_state_;
} this_state_ = State::Sync, output_state_ = State::Sync;
int cycles_in_state_ = 0;
// counters that cover an entire field
int horizontal_counter_ = 0, vertical_counter_ = 0;
@@ -487,23 +487,23 @@ template <class BusHandler> class MOS6560 {
// latches dictating start and length of drawing
bool vertical_drawing_latch_ = false, horizontal_drawing_latch_ = false;
int rows_this_field_, columns_this_line_;
int rows_this_field_ = 0, columns_this_line_ = 0;
// current drawing position counter
int pixel_line_cycle_, column_counter_;
int current_row_;
uint16_t current_character_row_;
uint16_t video_matrix_address_counter_, base_video_matrix_address_counter_;
int pixel_line_cycle_ = 0, column_counter_ = 0;
int current_row_ = 0;
uint16_t current_character_row_ = 0;
uint16_t video_matrix_address_counter_ = 0, base_video_matrix_address_counter_ = 0;
// data latched from the bus
uint8_t character_code_, character_colour_, character_value_;
uint8_t character_code_ = 0, character_colour_ = 0, character_value_ = 0;
bool is_odd_frame_ = false, is_odd_line_ = false;
// lookup table from 6560 colour index to appropriate PAL/NTSC value
uint16_t colours_[16];
uint16_t colours_[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
uint16_t *pixel_pointer;
uint16_t *pixel_pointer = nullptr;
void output_border(int number_of_cycles) {
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = registers_.borderColour;
@@ -511,13 +511,13 @@ template <class BusHandler> class MOS6560 {
}
struct {
int cycles_per_line;
int line_counter_increment_offset;
int final_line_increment_position;
int lines_per_progressive_field;
bool supports_interlacing;
int cycles_per_line = 0;
int line_counter_increment_offset = 0;
int final_line_increment_position = 0;
int lines_per_progressive_field = 0;
bool supports_interlacing = 0;
} timing_;
OutputMode output_mode_;
OutputMode output_mode_ = OutputMode::NTSC;
};
}

View File

@@ -316,6 +316,8 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) {
}
LOG("Receive bit count: " << receive_bit_count);
(void)receive_bit_count;
/*
b7,b6:
00 = 5 receive bits per character

View File

@@ -190,7 +190,9 @@ void TMS9918::run_for(const HalfCycles cycles) {
int read_cycles_pool = int_cycles;
while(write_cycles_pool || read_cycles_pool) {
#ifndef NDEBUG
LineBufferPointer backup = read_pointer_;
#endif
if(write_cycles_pool) {
// Determine how much writing to do.
@@ -329,8 +331,10 @@ void TMS9918::run_for(const HalfCycles cycles) {
}
#ifndef NDEBUG
assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column);
backup = write_pointer_;
#endif
if(read_cycles_pool) {

View File

@@ -191,7 +191,6 @@ void DiskII::set_state_machine(const std::vector<uint8_t> &state_machine) {
((source_address&0x02) ? 0x02 : 0x00);
uint8_t source_value = state_machine[source_address];
// Remap into Beneath Apple Pro-DOS value form.
source_value =
((source_value & 0x80) ? 0x10 : 0x0) |
((source_value & 0x40) ? 0x20 : 0x0) |

View File

@@ -46,6 +46,8 @@ class Keyboard {
/// Constructs a Keyboard that declares itself to observe only members of @c observed_keys.
Keyboard(const std::set<Key> &observed_keys, const std::set<Key> &essential_modifiers);
virtual ~Keyboard() {}
// Host interface.
/// @returns @c true if the key press affects the machine; @c false otherwise.

View File

@@ -20,7 +20,9 @@ DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sec
} else {
roms = rom_fetcher({
{"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff},
{"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
{"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } }
// {"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 }
/* TODO: once the DiskII knows how to decode common images of the 13-sector state machine, use that instead of the 16-sector. */
});
}
if(!roms[0] || !roms[1]) {

View File

@@ -541,7 +541,17 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
if(colour_burst_end > colour_burst_start) {
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0);
// UGLY HACK AHOY!
// The OpenGL scan target introduces a phase error of 1/8th of a wave. The Metal one does not.
// Supply the real phase value if this is an Apple build.
// TODO: eliminate UGLY HACK.
#ifdef __APPLE__
constexpr int phase = 224;
#else
constexpr int phase = 0;
#endif
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, phase);
}
second_blank_start = std::max(first_sync_column + sync_length + 3, column_);

View File

@@ -25,7 +25,7 @@ Audio::Audio(Concurrency::DeferringAsyncTaskQueue &task_queue) : task_queue_(tas
void Audio::post_sample(uint8_t sample) {
// Store sample directly indexed by current write pointer; this ensures that collected samples
// directly map to volume and enabled/disabled states.
sample_queue_.buffer[sample_queue_.write_pointer] = sample;
sample_queue_.buffer[sample_queue_.write_pointer].store(sample, std::memory_order::memory_order_relaxed);
sample_queue_.write_pointer = (sample_queue_.write_pointer + 1) % sample_queue_.buffer.size();
}
@@ -80,7 +80,7 @@ void Audio::get_samples(std::size_t number_of_samples, int16_t *target) {
// Determine the output level, and output that many samples.
// (Hoping that the copiler substitutes an effective memset16-type operation here).
const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer]) - 128);
const int16_t output_level = volume_multiplier_ * (int16_t(sample_queue_.buffer[sample_queue_.read_pointer].load(std::memory_order::memory_order_relaxed)) - 128);
for(size_t c = 0; c < cycles_left_in_sample; ++c) {
target[c] = output_level;
}

View File

@@ -63,7 +63,7 @@ class Audio: public ::Outputs::Speaker::SampleSource {
// A queue of fetched samples; read from by one thread,
// written to by another.
struct {
std::array<uint8_t, 740> buffer;
std::array<std::atomic<uint8_t>, 740> buffer;
size_t read_pointer = 0, write_pointer = 0;
} sample_queue_;

View File

@@ -29,7 +29,17 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
// UGLY HACK. UGLY, UGLY HACK. UGLY!
// The OpenGL scan target fails properly to place visible areas which are not 4:3.
// The [newer] Metal scan target has no such issue. So assume that Apple => Metal,
// and set a visible area to work around the OpenGL issue if required.
// TODO: eliminate UGLY HACK.
#ifdef __APPLE__
crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f));
#else
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
#endif
crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area.
}
@@ -105,10 +115,13 @@ void Video::run_for(HalfCycles duration) {
pixel_buffer_ += 16;
}
} else {
video_address_ += size_t(final_pixel_word - first_word);
}
if(final_pixel_word == 32) {
crt_.output_data(512);
pixel_buffer_ = nullptr;
}
}

View File

@@ -18,6 +18,11 @@
namespace Oric {
/*!
Emulates a Byte Drive 500, at least to some extent. Very little is known about this interface,
and I'm in possession of only a single disk image. So much of the below is community guesswork;
see the thread at https://forum.defence-force.org/viewtopic.php?f=25&t=2055
*/
class BD500: public DiskController {
public:
BD500();
@@ -36,6 +41,16 @@ class BD500: public DiskController {
void access(int address);
void set_head_loaded(bool loaded);
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);
}
};
};

View File

@@ -44,28 +44,18 @@ class DiskController: public WD::WD1770 {
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) {
void set_paged_item(PagedItem item) {
if(paged_item_ == item) return;
paged_item_ = item;
if(delegate_) {
delegate_->disk_controller_did_change_paged_item(this);
}
}
private:
PagedItem paged_item_ = PagedItem::DiskROM;
int clock_rate_;
Storage::Disk::Drive::ReadyType ready_type_;
};

View File

@@ -29,6 +29,16 @@ class Jasmin: public DiskController {
uint8_t selected_drives_ = 0;
Activity::Observer *observer_ = 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);
}
};
};

View File

@@ -38,7 +38,7 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) {
// b4: side select
if(changes & 0x10) {
const int head = (control & 0x10) ? 1 : 0;
const int head = (control & 0x10) >> 4;
for_all_drives([head] (Storage::Disk::Drive &drive, size_t) {
drive.set_head(head);
});
@@ -52,7 +52,7 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) {
// b0: IRQ enable
if(changes & 0x01) {
const bool had_irq = get_interrupt_request_line();
irq_enable_ = !!(control & 0x01);
irq_enable_ = bool(control & 0x01);
const bool has_irq = get_interrupt_request_line();
if(has_irq != had_irq && delegate_) {
delegate_->wd1770_did_change_output(this);
@@ -62,9 +62,14 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) {
// b7: EPROM select (0 = select)
// b1: ROM disable (0 = disable)
if(changes & 0x82) {
enable_overlay_ram_ = control & 0x80;
disable_basic_rom_ = !(control & 0x02);
select_paged_item();
PagedItem item;
if(control & 0x02) item = PagedItem::BASIC;
else if(control & 0x80) {
item = PagedItem::RAM;
} else {
item = PagedItem::DiskROM;
}
set_paged_item(item);
}
}

View File

@@ -98,7 +98,6 @@
4B055AED1FAE9BA20060FFFF /* Z80Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334831F5DA0360097E338 /* Z80Storage.cpp */; };
4B055AEE1FAE9BBF0060FFFF /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B86E2591F8C628F006FAA45 /* Keyboard.cpp */; };
4B055AEF1FAE9BF00060FFFF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; };
4B055AF11FAE9C160060FFFF /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055AF01FAE9C080060FFFF /* OpenGL.framework */; };
4B08A2751EE35D56008B7065 /* Z80InterruptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B08A2741EE35D56008B7065 /* Z80InterruptTests.swift */; };
4B08A2781EE39306008B7065 /* TestMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B08A2771EE39306008B7065 /* TestMachine.mm */; };
@@ -155,6 +154,9 @@
4B1D08061E0F7A1100763741 /* TimeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B1D08051E0F7A1100763741 /* TimeTests.mm */; };
4B1E85811D176468001EF87D /* 6532Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E85801D176468001EF87D /* 6532Tests.swift */; };
4B1EDB451E39A0AC009D6819 /* chip.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B1EDB431E39A0AC009D6819 /* chip.png */; };
4B228CD524D773B40077EF25 /* CSScanTarget.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CD424D773B30077EF25 /* CSScanTarget.mm */; };
4B228CD924DA12C60077EF25 /* CSScanTargetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CD824DA12C60077EF25 /* CSScanTargetView.m */; };
4B228CDB24DA41890077EF25 /* ScanTarget.metal in Sources */ = {isa = PBXBuildFile; fileRef = 4B228CDA24DA41880077EF25 /* ScanTarget.metal */; };
4B2530F4244E6774007980BF /* fm.json in Resources */ = {isa = PBXBuildFile; fileRef = 4B2530F3244E6773007980BF /* fm.json */; };
4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; };
4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; };
@@ -215,7 +217,6 @@
4B54C0C51F8D91D90050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C41F8D91D90050900F /* Keyboard.cpp */; };
4B54C0C81F8D91E50050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0C61F8D91E50050900F /* Keyboard.cpp */; };
4B54C0CB1F8D92590050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0CA1F8D92580050900F /* Keyboard.cpp */; };
4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */; };
4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */; };
4B55DD8320DF06680043F2E5 /* MachinePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B55DD8020DF06680043F2E5 /* MachinePicker.swift */; };
4B55DD8420DF06680043F2E5 /* MachinePicker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B55DD8120DF06680043F2E5 /* MachinePicker.xib */; };
@@ -370,7 +371,6 @@
4B778F6123A5F3560000D260 /* Disk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944FC201967B4007DE474 /* Disk.cpp */; };
4B778F6223A5F35F0000D260 /* File.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894500201967B4007DE474 /* File.cpp */; };
4B778F6323A5F3630000D260 /* Tape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B894501201967B4007DE474 /* Tape.cpp */; };
4B778F6423A5F3730000D260 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7913CA1DFCD80E00175A82 /* Video.cpp */; };
4B79A5011FC913C900EEDAD5 /* MSX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B79A4FF1FC913C900EEDAD5 /* MSX.cpp */; };
4B79E4441E3AF38600141F11 /* cassette.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B79E4411E3AF38600141F11 /* cassette.png */; };
@@ -768,6 +768,10 @@
4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */; };
4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EB61B587A5100552FC2 /* AllSuiteATests.swift */; };
4BB73EC21B587A5100552FC2 /* Clock_SignalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */; };
4BB8616E24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */; };
4BB8616F24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */; };
4BB8617124E22F5700A00E03 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB8617024E22F4900A00E03 /* Accelerate.framework */; };
4BB8617224E22F5A00A00E03 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB8617024E22F4900A00E03 /* Accelerate.framework */; };
4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; };
4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; };
4BBB70A8202014E2002FE009 /* MultiProducer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiProducer.cpp */; };
@@ -792,7 +796,6 @@
4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */; };
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC751B11D157E61006C31D9 /* 6522Tests.swift */; };
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; };
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */; };
4BC890D4230F86020025A55A /* DirectAccessDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */; };
4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */; };
@@ -809,19 +812,14 @@
4BCE0060227D39AB000CA200 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCE005E227D39AB000CA200 /* Video.cpp */; };
4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; };
4BD0FBC3233706A200148981 /* CSApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BD0FBC2233706A200148981 /* CSApplication.m */; };
4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD388882239E198002D14B5 /* 68000Tests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD388872239E198002D14B5 /* 68000Tests.mm */; };
4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3A3091EE755C800B5B501 /* Video.cpp */; };
4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; };
4BD424E02193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; };
4BD424E52193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; };
4BD424E62193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; };
4BD424E72193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; };
4BD424E82193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; };
4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD468F51D8DF41D0084958B /* 1770.cpp */; };
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; };
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; };
4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; };
@@ -1002,6 +1000,11 @@
4B1E857B1D174DEC001EF87D /* 6532.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = 6532.hpp; sourceTree = "<group>"; };
4B1E85801D176468001EF87D /* 6532Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6532Tests.swift; sourceTree = "<group>"; };
4B1EDB431E39A0AC009D6819 /* chip.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = chip.png; sourceTree = "<group>"; };
4B228CD424D773B30077EF25 /* CSScanTarget.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CSScanTarget.mm; sourceTree = "<group>"; };
4B228CD624D773CA0077EF25 /* CSScanTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSScanTarget.h; sourceTree = "<group>"; };
4B228CD724DA12C50077EF25 /* CSScanTargetView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSScanTargetView.h; sourceTree = "<group>"; };
4B228CD824DA12C60077EF25 /* CSScanTargetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSScanTargetView.m; sourceTree = "<group>"; };
4B228CDA24DA41880077EF25 /* ScanTarget.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = ScanTarget.metal; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.metal; };
4B24095A1C45DF85004DA684 /* Stepper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Stepper.hpp; sourceTree = "<group>"; };
4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = "<group>"; };
4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; };
@@ -1113,6 +1116,7 @@
4B4DC8271D2C2470003C5BF8 /* C1540.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = C1540.hpp; sourceTree = "<group>"; };
4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = "<group>"; };
4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; };
4B4F2B7024DF99D4000DA6B0 /* CSScanTarget+CppScanTarget.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CSScanTarget+CppScanTarget.h"; sourceTree = "<group>"; };
4B50AF7F242817F40099BBD7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; };
4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; };
@@ -1127,8 +1131,6 @@
4B54C0C71F8D91E50050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = Electron/Keyboard.hpp; sourceTree = "<group>"; };
4B54C0C91F8D92580050900F /* Keyboard.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = ZX8081/Keyboard.hpp; sourceTree = "<group>"; };
4B54C0CA1F8D92580050900F /* Keyboard.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = ZX8081/Keyboard.cpp; sourceTree = "<group>"; };
4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSOpenGLView.h; sourceTree = "<group>"; };
4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSOpenGLView.m; sourceTree = "<group>"; };
4B55CE5E1C3B7D960093A61B /* MachineDocument.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachineDocument.swift; sourceTree = "<group>"; };
4B55DD8020DF06680043F2E5 /* MachinePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MachinePicker.swift; sourceTree = "<group>"; };
4B55DD8220DF06680043F2E5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MachinePicker.xib; sourceTree = "<group>"; };
@@ -1636,6 +1638,9 @@
4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clock_SignalUITests.swift; sourceTree = "<group>"; };
4BB73EC31B587A5100552FC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Clock Signal.entitlements"; sourceTree = "<group>"; };
4BB8616C24E22DC500A00E03 /* BufferingScanTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = BufferingScanTarget.hpp; sourceTree = "<group>"; };
4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BufferingScanTarget.cpp; sourceTree = "<group>"; };
4BB8617024E22F4900A00E03 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
4BBB709C2020109C002FE009 /* DynamicMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DynamicMachine.hpp; sourceTree = "<group>"; };
4BBB70A2202011C2002FE009 /* MultiMediaTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MultiMediaTarget.hpp; sourceTree = "<group>"; };
4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MultiMediaTarget.cpp; sourceTree = "<group>"; };
@@ -1678,7 +1683,6 @@
4BC751B11D157E61006C31D9 /* 6522Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6522Tests.swift; sourceTree = "<group>"; };
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FIRFilter.cpp; sourceTree = "<group>"; };
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FIRFilter.hpp; sourceTree = "<group>"; };
4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
4BC890D1230F86020025A55A /* DirectAccessDevice.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DirectAccessDevice.cpp; sourceTree = "<group>"; };
4BC890D2230F86020025A55A /* DirectAccessDevice.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DirectAccessDevice.hpp; sourceTree = "<group>"; };
4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CommodoreTAP.cpp; sourceTree = "<group>"; };
@@ -1805,7 +1809,7 @@
buildActionMask = 2147483647;
files = (
4B055AF21FAE9C1C0060FFFF /* OpenGL.framework in Frameworks */,
4B055AF11FAE9C160060FFFF /* Accelerate.framework in Frameworks */,
4BB8617224E22F5A00A00E03 /* Accelerate.framework in Frameworks */,
4B055ABD1FAE86530060FFFF /* libz.tbd in Frameworks */,
4B055A7A1FAE78A00060FFFF /* SDL2.framework in Frameworks */,
);
@@ -1815,8 +1819,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
4BB8617124E22F5700A00E03 /* Accelerate.framework in Frameworks */,
4B50AF80242817F40099BBD7 /* QuartzCore.framework in Frameworks */,
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */,
4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1826,7 +1830,6 @@
buildActionMask = 2147483647;
files = (
4B9F11CA2272433900701480 /* libz.tbd in Frameworks */,
4B778F6423A5F3730000D260 /* Accelerate.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1843,6 +1846,7 @@
4B055A761FAE78210060FFFF /* Frameworks */ = {
isa = PBXGroup;
children = (
4BB8617024E22F4900A00E03 /* Accelerate.framework */,
4B50AF7F242817F40099BBD7 /* QuartzCore.framework */,
4B055AF01FAE9C080060FFFF /* OpenGL.framework */,
4B055A771FAE78210060FFFF /* SDL2.framework */,
@@ -2056,6 +2060,17 @@
path = Icons;
sourceTree = "<group>";
};
4B228CD324D773B30077EF25 /* ScanTarget */ = {
isa = PBXGroup;
children = (
4B228CDA24DA41880077EF25 /* ScanTarget.metal */,
4B228CD424D773B30077EF25 /* CSScanTarget.mm */,
4B228CD624D773CA0077EF25 /* CSScanTarget.h */,
4B4F2B7024DF99D4000DA6B0 /* CSScanTarget+CppScanTarget.h */,
);
path = ScanTarget;
sourceTree = "<group>";
};
4B2409591C45DF85004DA684 /* SignalProcessing */ = {
isa = PBXGroup;
children = (
@@ -2219,6 +2234,7 @@
4BF52672218E752E00313227 /* ScanTarget.hpp */,
4B0CCC411C62D0B3001CAC5F /* CRT */,
4BD191D5219113B80042E144 /* OpenGL */,
4BB8616B24E22DC500A00E03 /* ScanTargets */,
4BD060A41FE49D3C006E14BE /* Speaker */,
);
name = Outputs;
@@ -2467,8 +2483,8 @@
4B55CE5A1C3B7D6F0093A61B /* Views */ = {
isa = PBXGroup;
children = (
4B55CE5B1C3B7D6F0093A61B /* CSOpenGLView.h */,
4B55CE5C1C3B7D6F0093A61B /* CSOpenGLView.m */,
4B228CD724DA12C50077EF25 /* CSScanTargetView.h */,
4B228CD824DA12C60077EF25 /* CSScanTargetView.m */,
);
path = Views;
sourceTree = "<group>";
@@ -3307,7 +3323,6 @@
4BB73E951B587A5100552FC2 = {
isa = PBXGroup;
children = (
4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */,
4B51F70820A521D700AFA2C1 /* Activity */,
4B8944E2201967B4007DE474 /* Analyser */,
4BB73EA01B587A5100552FC2 /* Clock Signal */,
@@ -3365,6 +3380,7 @@
4BB73EAA1B587A5100552FC2 /* MainMenu.xib */,
4BE5F85A1C3E1C2500C43F01 /* Resources */,
4BDA00DB22E60EE900AC3CD0 /* ROMRequester */,
4B228CD324D773B30077EF25 /* ScanTarget */,
4B55CE5A1C3B7D6F0093A61B /* Views */,
);
path = "Clock Signal";
@@ -3474,6 +3490,16 @@
path = ../../Processors;
sourceTree = "<group>";
};
4BB8616B24E22DC500A00E03 /* ScanTargets */ = {
isa = PBXGroup;
children = (
4BB8616C24E22DC500A00E03 /* BufferingScanTarget.hpp */,
4BB8616D24E22DC500A00E03 /* BufferingScanTarget.cpp */,
);
name = ScanTargets;
path = ../../Outputs/ScanTargets;
sourceTree = "<group>";
};
4BBB70A1202011C2002FE009 /* Implementation */ = {
isa = PBXGroup;
children = (
@@ -4485,6 +4511,7 @@
4BEBFB522002DB30000708CC /* DiskROM.cpp in Sources */,
4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */,
4B055AA11FAE85DA0060FFFF /* OricMFMDSK.cpp in Sources */,
4BB8616F24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */,
4B0ACC2923775819008902D0 /* DMAController.cpp in Sources */,
4B055A951FAE85BB0060FFFF /* BitReverse.cpp in Sources */,
4B055ACE1FAE9B030060FFFF /* Plus3.cpp in Sources */,
@@ -4567,11 +4594,11 @@
4B0E04EA1FC9E5DA00F43484 /* CAS.cpp in Sources */,
4B7A90ED20410A85008514A2 /* StaticAnalyser.cpp in Sources */,
4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */,
4B228CD924DA12C60077EF25 /* CSScanTargetView.m in Sources */,
4B6AAEAD230E40250078E864 /* Target.cpp in Sources */,
4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */,
4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */,
4B4518A01F75FD1C00926311 /* CPCDSK.cpp in Sources */,
4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */,
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */,
4BC23A2C2467600F001A6030 /* OPLL.cpp in Sources */,
4B322E041F5A2E3C004EB04C /* Z80Base.cpp in Sources */,
@@ -4597,6 +4624,7 @@
4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */,
4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */,
4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */,
4BB8616E24E22DC500A00E03 /* BufferingScanTarget.cpp in Sources */,
4BB4BFB022A42F290069048D /* MacintoshIMG.cpp in Sources */,
4B05401E219D1618001BF69C /* ScanTarget.cpp in Sources */,
4B4518861F75E91A00926311 /* MFMDiskController.cpp in Sources */,
@@ -4609,13 +4637,11 @@
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */,
4BD424E52193B5830097291A /* Shader.cpp in Sources */,
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */,
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
4BCE005A227CFFCA000CA200 /* Macintosh.cpp in Sources */,
4B6AAEA4230E3E1D0078E864 /* MassStorageDevice.cpp in Sources */,
4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */,
4BC890D3230F86020025A55A /* DirectAccessDevice.cpp in Sources */,
4B7BA03723CEB86000B98D9E /* BD500.cpp in Sources */,
4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */,
@@ -4664,7 +4690,8 @@
4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */,
4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */,
4BDA00E022E644AF00AC3CD0 /* CSROMReceiverView.m in Sources */,
4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */,
4B228CDB24DA41890077EF25 /* ScanTarget.metal in Sources */,
4B228CD524D773B40077EF25 /* CSScanTarget.mm in Sources */,
4BCD634922D6756400F567F1 /* MacintoshDoubleDensityDrive.cpp in Sources */,
4B0F94FE208C1A1600FE41D9 /* NIB.cpp in Sources */,
4B89452A201967B4007DE474 /* File.cpp in Sources */,
@@ -4691,7 +4718,6 @@
4B0E04FA1FC9FA3100F43484 /* 9918.cpp in Sources */,
4B69FB3D1C4D908A00B5F0AA /* Tape.cpp in Sources */,
4B4518841F75E91A00926311 /* UnformattedTrack.cpp in Sources */,
4B55CE5D1C3B7D6F0093A61B /* CSOpenGLView.m in Sources */,
4B65086022F4CF8D009C1100 /* Keyboard.cpp in Sources */,
4B894528201967B4007DE474 /* Disk.cpp in Sources */,
4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */,
@@ -4748,7 +4774,6 @@
4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */,
4BCE0053227CE8CA000CA200 /* AppleII.cpp in Sources */,
4B8334821F5D9FF70097E338 /* PartialMachineCycle.cpp in Sources */,
4BD424E72193B5830097291A /* Rectangle.cpp in Sources */,
4B1B88C0202E3DB200B67DFF /* MultiConfigurable.cpp in Sources */,
4BFF1D3922337B0300838EA1 /* 68000Storage.cpp in Sources */,
4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */,
@@ -5088,6 +5113,7 @@
);
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_OPTIMIZATION_LEVEL = 2;
GCC_PREPROCESSOR_DEFINITIONS = NDEBUG;
MACOSX_DEPLOYMENT_TARGET = 10.10;
PRODUCT_NAME = "$(TARGET_NAME)";
};
@@ -5141,8 +5167,9 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_PARAMETER = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MTL_ENABLE_DEBUG_INFO = YES;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -5192,8 +5219,9 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_PARAMETER = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.10;
MACOSX_DEPLOYMENT_TARGET = 10.13;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
@@ -5232,7 +5260,7 @@
GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12.2;
MTL_TREAT_WARNINGS_AS_ERRORS = YES;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-Wreorder",
@@ -5280,7 +5308,7 @@
GCC_WARN_UNUSED_LABEL = YES;
INFOPLIST_FILE = "Clock Signal/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 10.12.2;
MTL_TREAT_WARNINGS_AS_ERRORS = YES;
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-Wreorder",

View File

@@ -31,7 +31,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
disableMainThreadChecker = "YES"
@@ -57,9 +57,13 @@
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--volume=0.001"
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Macintosh/MusicWorks 0.42.image&quot;"
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--volume=0.001"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--new=amstradcpc"
isEnabled = "NO">
@@ -86,7 +90,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "&quot;/Users/thomasharte/Library/Mobile Documents/com~apple~CloudDocs/Desktop/Soft/Amstrad CPC/Robocop.dsk&quot;"
isEnabled = "YES">
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--speed=5"
@@ -94,7 +98,7 @@
</CommandLineArgument>
<CommandLineArgument
argument = "--rompath=/Users/thomasharte/Projects/CLK/ROMImages"
isEnabled = "NO">
isEnabled = "YES">
</CommandLineArgument>
<CommandLineArgument
argument = "--help"

View File

@@ -67,7 +67,7 @@
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
enableASanStackUseAfterReturn = "YES"

View File

@@ -1,14 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="15705" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15705"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16097.2"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="MachineDocument" customModule="Clock_Signal" customModuleProvider="target">
<connections>
<outlet property="openGLView" destination="DEG-fq-cjd" id="Gxs-2u-n7B"/>
<outlet property="scanTargetView" destination="DEG-fq-cjd" id="5aX-3R-eXQ"/>
<outlet property="volumeSlider" destination="zaz-lB-Iyt" id="flY-Th-oG4"/>
<outlet property="volumeView" destination="4ap-Gi-2AO" id="v4e-k6-Fqf"/>
<outlet property="window" destination="xOd-HO-29H" id="JIz-fz-R2o"/>
@@ -27,7 +27,7 @@
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSOpenGLView">
<openGLView hidden="YES" wantsLayer="YES" useAuxiliaryDepthBufferStencil="NO" allowOffline="YES" wantsBestResolutionOpenGLSurface="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DEG-fq-cjd" customClass="CSScanTargetView">
<rect key="frame" x="0.0" y="0.0" width="600" height="450"/>
</openGLView>
<box hidden="YES" boxType="custom" cornerRadius="4" title="Box" titlePosition="noTitle" translatesAutoresizingMaskIntoConstraints="NO" id="4ap-Gi-2AO">

View File

@@ -10,7 +10,7 @@
#import "CSStaticAnalyser.h"
#import "CSAudioQueue.h"
#import "CSOpenGLView.h"
#import "CSScanTargetView.h"
#import "CSROMReceiverView.h"
#import "CSJoystickManager.h"

View File

@@ -14,8 +14,7 @@ class MachineDocument:
NSDocument,
NSWindowDelegate,
CSMachineDelegate,
CSOpenGLViewDelegate,
CSOpenGLViewResponderDelegate,
CSScanTargetViewResponderDelegate,
CSAudioQueueDelegate,
CSROMReciverViewDelegate
{
@@ -45,7 +44,7 @@ class MachineDocument:
// MARK: - Main NIB connections.
/// The OpenGL view to receive this machine's display.
@IBOutlet weak var openGLView: CSOpenGLView!
@IBOutlet weak var scanTargetView: CSScanTargetView!
/// The options panel, if any.
@IBOutlet var optionsPanel: MachinePanel!
@@ -100,8 +99,7 @@ class MachineDocument:
actionLock.lock()
drawLock.lock()
machine = nil
openGLView.delegate = nil
openGLView.invalidate()
scanTargetView.invalidate()
actionLock.unlock()
drawLock.unlock()
@@ -181,10 +179,10 @@ class MachineDocument:
// MARK: - Connections Between Machine and the Outside World
private func setupMachineOutput() {
if let machine = self.machine, let openGLView = self.openGLView, machine.view != openGLView {
if let machine = self.machine, let scanTargetView = self.scanTargetView, machine.view != scanTargetView {
// Establish the output aspect ratio and audio.
let aspectRatio = self.aspectRatio()
machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
machine.setView(scanTargetView, aspectRatio: Float(aspectRatio.width / aspectRatio.height))
// Attach an options panel if one is available.
if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName {
@@ -198,20 +196,20 @@ class MachineDocument:
// Callbacks from the OpenGL may come on a different thread, immediately following the .delegate set;
// hence the full setup of the best-effort updater prior to setting self as a delegate.
openGLView.delegate = self
openGLView.responderDelegate = self
// scanTargetView.delegate = self
scanTargetView.responderDelegate = self
// If this machine has a mouse, enable mouse capture; also indicate whether usurption
// of the command key is desired.
openGLView.shouldCaptureMouse = machine.hasMouse
openGLView.shouldUsurpCommand = machine.shouldUsurpCommand
scanTargetView.shouldCaptureMouse = machine.hasMouse
scanTargetView.shouldUsurpCommand = machine.shouldUsurpCommand
setupAudioQueueClockRate()
// Bring OpenGL view-holding window on top of the options panel and show the content.
openGLView.isHidden = false
openGLView.window!.makeKeyAndOrderFront(self)
openGLView.window!.makeFirstResponder(openGLView)
scanTargetView.isHidden = false
scanTargetView.window!.makeKeyAndOrderFront(self)
scanTargetView.window!.makeFirstResponder(scanTargetView)
// Start forwarding best-effort updates.
machine.start()
@@ -252,18 +250,6 @@ class MachineDocument:
final func audioQueueIsRunningDry(_ audioQueue: CSAudioQueue) {
}
/// Responds to the CSOpenGLViewDelegate redraw message by requesting a machine update if this is a timed
/// request, and ordering a redraw regardless of the motivation.
final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
if drawLock.try() {
if redrawEvent == .timer {
machine.updateView(forPixelSize: view.backingSize)
}
machine.drawView(forPixelSize: view.backingSize)
drawLock.unlock()
}
}
// MARK: - Pasteboard Forwarding.
/// Forwards any text currently on the pasteboard into the active machine.
@@ -277,7 +263,7 @@ class MachineDocument:
// MARK: - Runtime Media Insertion.
/// Delegate message to receive drag and drop files.
final func openGLView(_ view: CSOpenGLView, didReceiveFileAt URL: URL) {
final func scanTargetView(_ view: CSScanTargetView, didReceiveFileAt URL: URL) {
let mediaSet = CSMediaSet(fileAt: URL)
if let mediaSet = mediaSet {
mediaSet.apply(to: self.machine)
@@ -310,7 +296,7 @@ class MachineDocument:
machine.clearAllKeys()
machine.joystickManager = nil
}
self.openGLView.releaseMouse()
self.scanTargetView.releaseMouse()
}
/// Upon becoming key, attaches joystick input to the machine.
@@ -608,23 +594,20 @@ class MachineDocument:
let url = pictursURL.appendingPathComponent(filename)
// Obtain the machine's current display.
var imageRepresentation: NSBitmapImageRep? = nil
self.openGLView.perform {
imageRepresentation = self.machine.imageRepresentation
}
let imageRepresentation = self.machine.imageRepresentation
// Encode as a PNG and save.
let pngData = imageRepresentation!.representation(using: .png, properties: [:])
let pngData = imageRepresentation.representation(using: .png, properties: [:])
try! pngData?.write(to: url)
}
// MARK: - Window Title Updates.
private var unadornedWindowTitle = ""
func openGLViewDidCaptureMouse(_ view: CSOpenGLView) {
internal func scanTargetViewDidCaptureMouse(_ view: CSScanTargetView) {
self.windowControllers[0].window?.title = self.unadornedWindowTitle + " (press ⌘+control to release mouse)"
}
func openGLViewDidReleaseMouse(_ view: CSOpenGLView) {
internal func scanTargetViewDidReleaseMouse(_ view: CSScanTargetView) {
self.windowControllers[0].window?.title = self.unadornedWindowTitle
}
@@ -750,7 +733,7 @@ class MachineDocument:
}
fileprivate var animationFader: ViewFader? = nil
func openGLViewDidShowOSMouseCursor(_ view: CSOpenGLView) {
internal func scanTargetViewDidShowOSMouseCursor(_ view: CSScanTargetView) {
// The OS mouse cursor became visible, so show the volume controls.
animationFader = nil
volumeView.layer?.removeAllAnimations()
@@ -758,7 +741,7 @@ class MachineDocument:
volumeView.layer?.opacity = 1.0
}
func openGLViewWillHideOSMouseCursor(_ view: CSOpenGLView) {
internal func scanTargetViewWillHideOSMouseCursor(_ view: CSScanTargetView) {
// The OS mouse cursor will be hidden, so hide the volume controls.
if !volumeView.isHidden && volumeView.layer?.animation(forKey: "opacity") == nil {
let fadeAnimation = CABasicAnimation(keyPath: "opacity")

View File

@@ -9,9 +9,9 @@
#import <Foundation/Foundation.h>
#import "CSAudioQueue.h"
#import "CSOpenGLView.h"
#import "CSStaticAnalyser.h"
#import "CSJoystickManager.h"
#import "CSScanTargetView.h"
#import "CSStaticAnalyser.h"
@class CSMachine;
@protocol CSMachineDelegate
@@ -62,14 +62,11 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
- (BOOL)isStereo;
- (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize stereo:(BOOL)stereo;
- (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
- (void)setView:(nullable CSScanTargetView *)view aspectRatio:(float)aspectRatio;
- (void)start;
- (void)stop;
- (void)updateViewForPixelSize:(CGSize)pixelSize;
- (void)drawViewForPixelSize:(CGSize)pixelSize;
- (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed;
- (void)clearAllKeys;
@@ -77,7 +74,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
- (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY;
@property (atomic, strong, nullable) CSAudioQueue *audioQueue;
@property (nonatomic, readonly, nonnull) CSOpenGLView *view;
@property (nonatomic, readonly, nonnull) CSScanTargetView *view;
@property (nonatomic, weak, nullable) id<CSMachineDelegate> delegate;
@property (nonatomic, readonly, nonnull) NSString *userDefaultsPrefix;

View File

@@ -9,8 +9,9 @@
#import "CSMachine.h"
#import "CSMachine+Target.h"
#include "CSROMFetcher.hpp"
#import "CSHighPrecisionTimer.h"
#include "CSROMFetcher.hpp"
#import "CSScanTarget+CppScanTarget.h"
#include "MediaTarget.hpp"
#include "JoystickMachine.hpp"
@@ -31,13 +32,7 @@
#include <atomic>
#include <bitset>
#import <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "../../../../Outputs/OpenGL/ScanTarget.hpp"
#include "../../../../Outputs/OpenGL/Screenshot.hpp"
@interface CSMachine() <CSOpenGLViewDisplayLinkDelegate>
@interface CSMachine() <CSScanTargetViewDisplayLinkDelegate>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
- (void)addLED:(NSString *)led;
@@ -154,7 +149,6 @@ struct ActivityObserver: public Activity::Observer {
NSMutableArray<NSString *> *_leds;
CSHighPrecisionTimer *_timer;
CGSize _pixelSize;
std::atomic_flag _isUpdating;
Time::Nanos _syncTime;
Time::Nanos _timeDiff;
@@ -165,7 +159,11 @@ struct ActivityObserver: public Activity::Observer {
NSTimer *_joystickTimer;
std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget;
// This array exists to reduce blocking on the main queue; anything that would otherwise need
// to synchronise on self in order to post input to the machine can instead synchronise on
// _inputEvents and add a block to it. The main machine execution loop promises to synchronise
// on _inputEvents very briefly at the start of every tick and execute all enqueued blocks.
NSMutableArray<dispatch_block_t> *_inputEvents;
}
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray<CSMissingROM *> *)missingROMs {
@@ -217,6 +215,8 @@ struct ActivityObserver: public Activity::Observer {
_speakerDelegate.machine = self;
_speakerDelegate.machineAccessLock = _delegateMachineAccessLock;
_inputEvents = [[NSMutableArray alloc] init];
_joystickMachine = _machine->joystick_machine();
[self updateJoystickTimer];
_isUpdating.clear();
@@ -245,11 +245,11 @@ struct ActivityObserver: public Activity::Observer {
_speakerDelegate.machine = nil;
[_delegateMachineAccessLock unlock];
[_view performWithGLContext:^{
@synchronized(self) {
self->_scanTarget.reset();
}
}];
// [_view performWithGLContext:^{
// @synchronized(self) {
// self->_scanTarget.reset();
// }
// }];
}
- (float)idealSamplingRateFromRange:(NSRange)range {
@@ -351,30 +351,10 @@ struct ActivityObserver: public Activity::Observer {
}
}
- (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio {
- (void)setView:(CSScanTargetView *)view aspectRatio:(float)aspectRatio {
_view = view;
_view.displayLinkDelegate = self;
[view performWithGLContext:^{
[self setupOutputWithAspectRatio:aspectRatio];
} flushDrawable:NO];
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
_scanTarget = std::make_unique<Outputs::Display::OpenGL::ScanTarget>();
_machine->scan_producer()->set_scan_target(_scanTarget.get());
}
- (void)updateViewForPixelSize:(CGSize)pixelSize {
// _pixelSize = pixelSize;
// @synchronized(self) {
// const auto scan_status = _machine->crt_machine()->get_scan_status();
// NSLog(@"FPS (hopefully): %0.2f [retrace: %0.4f]", 1.0f / scan_status.field_duration, scan_status.retrace_duration);
// }
}
- (void)drawViewForPixelSize:(CGSize)pixelSize {
_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
_machine->scan_producer()->set_scan_target(_view.scanTarget.scanTarget);
}
- (void)paste:(NSString *)paste {
@@ -384,26 +364,7 @@ struct ActivityObserver: public Activity::Observer {
}
- (NSBitmapImageRep *)imageRepresentation {
// Grab a screenshot.
Outputs::Display::OpenGL::Screenshot screenshot(4, 3);
// Generate an NSBitmapImageRep containing the screenshot's data.
NSBitmapImageRep *const result =
[[NSBitmapImageRep alloc]
initWithBitmapDataPlanes:NULL
pixelsWide:screenshot.width
pixelsHigh:screenshot.height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:4 * screenshot.width
bitsPerPixel:0];
memcpy(result.bitmapData, screenshot.pixel_data.data(), size_t(screenshot.width*screenshot.height*4));
return result;
return self.view.imageRepresentation;
}
- (void)applyMedia:(const Analyser::Static::Media &)media {
@@ -428,84 +389,84 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed {
auto keyboard_machine = _machine->keyboard_machine();
if(keyboard_machine && (self.inputMode != CSMachineKeyboardInputModeJoystick || !keyboard_machine->get_keyboard().is_exclusive())) {
Inputs::Keyboard::Key mapped_key = Inputs::Keyboard::Key::Help; // Make an innocuous default guess.
[self applyInputEvent:^{
auto keyboard_machine = self->_machine->keyboard_machine();
if(keyboard_machine && (self.inputMode != CSMachineKeyboardInputModeJoystick || !keyboard_machine->get_keyboard().is_exclusive())) {
Inputs::Keyboard::Key mapped_key = Inputs::Keyboard::Key::Help; // Make an innocuous default guess.
#define BIND(source, dest) case source: mapped_key = Inputs::Keyboard::Key::dest; break;
// Connect the Carbon-era Mac keyboard scancodes to Clock Signal's 'universal' enumeration in order
// to pass into the platform-neutral realm.
switch(key) {
BIND(VK_ANSI_0, k0); BIND(VK_ANSI_1, k1); BIND(VK_ANSI_2, k2); BIND(VK_ANSI_3, k3); BIND(VK_ANSI_4, k4);
BIND(VK_ANSI_5, k5); BIND(VK_ANSI_6, k6); BIND(VK_ANSI_7, k7); BIND(VK_ANSI_8, k8); BIND(VK_ANSI_9, k9);
// Connect the Carbon-era Mac keyboard scancodes to Clock Signal's 'universal' enumeration in order
// to pass into the platform-neutral realm.
switch(key) {
BIND(VK_ANSI_0, k0); BIND(VK_ANSI_1, k1); BIND(VK_ANSI_2, k2); BIND(VK_ANSI_3, k3); BIND(VK_ANSI_4, k4);
BIND(VK_ANSI_5, k5); BIND(VK_ANSI_6, k6); BIND(VK_ANSI_7, k7); BIND(VK_ANSI_8, k8); BIND(VK_ANSI_9, k9);
BIND(VK_ANSI_Q, Q); BIND(VK_ANSI_W, W); BIND(VK_ANSI_E, E); BIND(VK_ANSI_R, R); BIND(VK_ANSI_T, T);
BIND(VK_ANSI_Y, Y); BIND(VK_ANSI_U, U); BIND(VK_ANSI_I, I); BIND(VK_ANSI_O, O); BIND(VK_ANSI_P, P);
BIND(VK_ANSI_Q, Q); BIND(VK_ANSI_W, W); BIND(VK_ANSI_E, E); BIND(VK_ANSI_R, R); BIND(VK_ANSI_T, T);
BIND(VK_ANSI_Y, Y); BIND(VK_ANSI_U, U); BIND(VK_ANSI_I, I); BIND(VK_ANSI_O, O); BIND(VK_ANSI_P, P);
BIND(VK_ANSI_A, A); BIND(VK_ANSI_S, S); BIND(VK_ANSI_D, D); BIND(VK_ANSI_F, F); BIND(VK_ANSI_G, G);
BIND(VK_ANSI_H, H); BIND(VK_ANSI_J, J); BIND(VK_ANSI_K, K); BIND(VK_ANSI_L, L);
BIND(VK_ANSI_A, A); BIND(VK_ANSI_S, S); BIND(VK_ANSI_D, D); BIND(VK_ANSI_F, F); BIND(VK_ANSI_G, G);
BIND(VK_ANSI_H, H); BIND(VK_ANSI_J, J); BIND(VK_ANSI_K, K); BIND(VK_ANSI_L, L);
BIND(VK_ANSI_Z, Z); BIND(VK_ANSI_X, X); BIND(VK_ANSI_C, C); BIND(VK_ANSI_V, V);
BIND(VK_ANSI_B, B); BIND(VK_ANSI_N, N); BIND(VK_ANSI_M, M);
BIND(VK_ANSI_Z, Z); BIND(VK_ANSI_X, X); BIND(VK_ANSI_C, C); BIND(VK_ANSI_V, V);
BIND(VK_ANSI_B, B); BIND(VK_ANSI_N, N); BIND(VK_ANSI_M, M);
BIND(VK_F1, F1); BIND(VK_F2, F2); BIND(VK_F3, F3); BIND(VK_F4, F4);
BIND(VK_F5, F5); BIND(VK_F6, F6); BIND(VK_F7, F7); BIND(VK_F8, F8);
BIND(VK_F9, F9); BIND(VK_F10, F10); BIND(VK_F11, F11); BIND(VK_F12, F12);
BIND(VK_F1, F1); BIND(VK_F2, F2); BIND(VK_F3, F3); BIND(VK_F4, F4);
BIND(VK_F5, F5); BIND(VK_F6, F6); BIND(VK_F7, F7); BIND(VK_F8, F8);
BIND(VK_F9, F9); BIND(VK_F10, F10); BIND(VK_F11, F11); BIND(VK_F12, F12);
BIND(VK_ANSI_Keypad0, Keypad0); BIND(VK_ANSI_Keypad1, Keypad1); BIND(VK_ANSI_Keypad2, Keypad2);
BIND(VK_ANSI_Keypad3, Keypad3); BIND(VK_ANSI_Keypad4, Keypad4); BIND(VK_ANSI_Keypad5, Keypad5);
BIND(VK_ANSI_Keypad6, Keypad6); BIND(VK_ANSI_Keypad7, Keypad7); BIND(VK_ANSI_Keypad8, Keypad8);
BIND(VK_ANSI_Keypad9, Keypad9);
BIND(VK_ANSI_Keypad0, Keypad0); BIND(VK_ANSI_Keypad1, Keypad1); BIND(VK_ANSI_Keypad2, Keypad2);
BIND(VK_ANSI_Keypad3, Keypad3); BIND(VK_ANSI_Keypad4, Keypad4); BIND(VK_ANSI_Keypad5, Keypad5);
BIND(VK_ANSI_Keypad6, Keypad6); BIND(VK_ANSI_Keypad7, Keypad7); BIND(VK_ANSI_Keypad8, Keypad8);
BIND(VK_ANSI_Keypad9, Keypad9);
BIND(VK_ANSI_Equal, Equals); BIND(VK_ANSI_Minus, Hyphen);
BIND(VK_ANSI_RightBracket, CloseSquareBracket); BIND(VK_ANSI_LeftBracket, OpenSquareBracket);
BIND(VK_ANSI_Quote, Quote); BIND(VK_ANSI_Grave, BackTick);
BIND(VK_ANSI_Equal, Equals); BIND(VK_ANSI_Minus, Hyphen);
BIND(VK_ANSI_RightBracket, CloseSquareBracket); BIND(VK_ANSI_LeftBracket, OpenSquareBracket);
BIND(VK_ANSI_Quote, Quote); BIND(VK_ANSI_Grave, BackTick);
BIND(VK_ANSI_Semicolon, Semicolon);
BIND(VK_ANSI_Backslash, Backslash); BIND(VK_ANSI_Slash, ForwardSlash);
BIND(VK_ANSI_Comma, Comma); BIND(VK_ANSI_Period, FullStop);
BIND(VK_ANSI_Semicolon, Semicolon);
BIND(VK_ANSI_Backslash, Backslash); BIND(VK_ANSI_Slash, ForwardSlash);
BIND(VK_ANSI_Comma, Comma); BIND(VK_ANSI_Period, FullStop);
BIND(VK_ANSI_KeypadDecimal, KeypadDecimalPoint); BIND(VK_ANSI_KeypadEquals, KeypadEquals);
BIND(VK_ANSI_KeypadMultiply, KeypadAsterisk); BIND(VK_ANSI_KeypadDivide, KeypadSlash);
BIND(VK_ANSI_KeypadPlus, KeypadPlus); BIND(VK_ANSI_KeypadMinus, KeypadMinus);
BIND(VK_ANSI_KeypadClear, KeypadDelete); BIND(VK_ANSI_KeypadEnter, KeypadEnter);
BIND(VK_ANSI_KeypadDecimal, KeypadDecimalPoint); BIND(VK_ANSI_KeypadEquals, KeypadEquals);
BIND(VK_ANSI_KeypadMultiply, KeypadAsterisk); BIND(VK_ANSI_KeypadDivide, KeypadSlash);
BIND(VK_ANSI_KeypadPlus, KeypadPlus); BIND(VK_ANSI_KeypadMinus, KeypadMinus);
BIND(VK_ANSI_KeypadClear, KeypadDelete); BIND(VK_ANSI_KeypadEnter, KeypadEnter);
BIND(VK_Return, Enter); BIND(VK_Tab, Tab);
BIND(VK_Space, Space); BIND(VK_Delete, Backspace);
BIND(VK_Control, LeftControl); BIND(VK_Option, LeftOption);
BIND(VK_Command, LeftMeta); BIND(VK_Shift, LeftShift);
BIND(VK_RightControl, RightControl); BIND(VK_RightOption, RightOption);
BIND(VK_Escape, Escape); BIND(VK_CapsLock, CapsLock);
BIND(VK_Home, Home); BIND(VK_End, End);
BIND(VK_PageUp, PageUp); BIND(VK_PageDown, PageDown);
BIND(VK_Return, Enter); BIND(VK_Tab, Tab);
BIND(VK_Space, Space); BIND(VK_Delete, Backspace);
BIND(VK_Control, LeftControl); BIND(VK_Option, LeftOption);
BIND(VK_Command, LeftMeta); BIND(VK_Shift, LeftShift);
BIND(VK_RightControl, RightControl); BIND(VK_RightOption, RightOption);
BIND(VK_Escape, Escape); BIND(VK_CapsLock, CapsLock);
BIND(VK_Home, Home); BIND(VK_End, End);
BIND(VK_PageUp, PageUp); BIND(VK_PageDown, PageDown);
BIND(VK_RightShift, RightShift);
BIND(VK_Help, Help);
BIND(VK_ForwardDelete, Delete);
BIND(VK_RightShift, RightShift);
BIND(VK_Help, Help);
BIND(VK_ForwardDelete, Delete);
BIND(VK_LeftArrow, Left); BIND(VK_RightArrow, Right);
BIND(VK_DownArrow, Down); BIND(VK_UpArrow, Up);
}
BIND(VK_LeftArrow, Left); BIND(VK_RightArrow, Right);
BIND(VK_DownArrow, Down); BIND(VK_UpArrow, Up);
}
#undef BIND
// Pick an ASCII code, if any.
char pressedKey = '\0';
if(characters.length) {
unichar firstCharacter = [characters characterAtIndex:0];
if(firstCharacter < 128) {
pressedKey = (char)firstCharacter;
// Pick an ASCII code, if any.
char pressedKey = '\0';
if(characters.length) {
unichar firstCharacter = [characters characterAtIndex:0];
if(firstCharacter < 128) {
pressedKey = (char)firstCharacter;
}
}
@synchronized(self) {
if(keyboard_machine->apply_key(mapped_key, pressedKey, isPressed, self.inputMode == CSMachineKeyboardInputModeKeyboardLogical)) {
return;
}
}
}
@synchronized(self) {
if(keyboard_machine->apply_key(mapped_key, pressedKey, isPressed, self.inputMode == CSMachineKeyboardInputModeKeyboardLogical)) {
return;
}
}
}
auto joystick_machine = _machine->joystick_machine();
if(self.inputMode == CSMachineKeyboardInputModeJoystick && joystick_machine) {
@synchronized(self) {
auto joystick_machine = self->_machine->joystick_machine();
if(self.inputMode == CSMachineKeyboardInputModeJoystick && joystick_machine) {
auto &joysticks = joystick_machine->get_joysticks();
if(!joysticks.empty()) {
// Convert to a C++ bool so that the following calls are resolved correctly even if overloaded.
@@ -530,49 +491,55 @@ struct ActivityObserver: public Activity::Observer {
}
}
}
}];
}
- (void)applyInputEvent:(dispatch_block_t)event {
@synchronized(_inputEvents) {
[_inputEvents addObject:event];
}
}
- (void)clearAllKeys {
const auto keyboard_machine = _machine->keyboard_machine();
if(keyboard_machine) {
@synchronized(self) {
[self applyInputEvent:^{
keyboard_machine->get_keyboard().reset_all_keys();
}
}];
}
const auto joystick_machine = _machine->joystick_machine();
if(joystick_machine) {
@synchronized(self) {
[self applyInputEvent:^{
for(auto &joystick : joystick_machine->get_joysticks()) {
joystick->reset_all_inputs();
}
}
}];
}
const auto mouse_machine = _machine->mouse_machine();
if(mouse_machine) {
@synchronized(self) {
[self applyInputEvent:^{
mouse_machine->get_mouse().reset_all_buttons();
}
}];
}
}
- (void)setMouseButton:(int)button isPressed:(BOOL)isPressed {
auto mouse_machine = _machine->mouse_machine();
if(mouse_machine) {
@synchronized(self) {
[self applyInputEvent:^{
mouse_machine->get_mouse().set_button_pressed(button % mouse_machine->get_mouse().get_number_of_buttons(), isPressed);
}
}];
}
}
- (void)addMouseMotionX:(CGFloat)deltaX y:(CGFloat)deltaY {
auto mouse_machine = _machine->mouse_machine();
if(mouse_machine) {
@synchronized(self) {
[self applyInputEvent:^{
mouse_machine->get_mouse().move(int(deltaX), int(deltaY));
}
}];
}
}
@@ -737,11 +704,10 @@ struct ActivityObserver: public Activity::Observer {
#pragma mark - Timer
- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
- (void)scanTargetViewDisplayLinkDidFire:(CSScanTargetView *)view now:(const CVTimeStamp *)now outputTime:(const CVTimeStamp *)outputTime {
// First order of business: grab a timestamp.
const auto timeNow = Time::nanos_now();
CGSize pixelSize = view.backingSize;
BOOL isSyncLocking;
@synchronized(self) {
// Store a means to map from CVTimeStamp.hostTime to Time::Nanos;
@@ -753,9 +719,6 @@ struct ActivityObserver: public Activity::Observer {
// Store the next end-of-frame time. TODO: and start of next and implied visible duration, if raster racing?
_syncTime = int64_t(now->hostTime) + _timeDiff;
// Also crib the current view pixel size.
_pixelSize = pixelSize;
// Set the current refresh period.
_refreshPeriod = double(now->videoRefreshPeriod) / double(now->videoTimeScale);
@@ -765,9 +728,7 @@ struct ActivityObserver: public Activity::Observer {
// Draw the current output. (TODO: do this within the timer if either raster racing or, at least, sync matching).
if(!isSyncLocking) {
[self.view performWithGLContext:^{
self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
} flushDrawable:YES];
[self.view draw];
}
}
@@ -783,9 +744,16 @@ struct ActivityObserver: public Activity::Observer {
lastTime = std::max(timeNow - Time::Nanos(10'000'000'000 / TICKS), lastTime);
const auto duration = timeNow - lastTime;
CGSize pixelSize;
BOOL splitAndSync = NO;
@synchronized(self) {
// Post on input events.
@synchronized(self->_inputEvents) {
for(dispatch_block_t action: self->_inputEvents) {
action();
}
[self->_inputEvents removeAllObjects];
}
// If this tick includes vsync then inspect the machine.
if(timeNow >= self->_syncTime && lastTime < self->_syncTime) {
splitAndSync = self->_isSyncLocking = self->_scanSynchroniser.can_synchronise(self->_machine->scan_producer()->get_scan_status(), self->_refreshPeriod);
@@ -806,7 +774,6 @@ struct ActivityObserver: public Activity::Observer {
if(!splitAndSync) {
self->_machine->timed_machine()->run_for((double)duration / 1e9);
}
pixelSize = self->_pixelSize;
}
// If this was not a split-and-sync then dispatch the update request asynchronously, unless
@@ -822,13 +789,10 @@ struct ActivityObserver: public Activity::Observer {
}
if(!wasUpdating) {
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
[self.view performWithGLContext:^{
self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
if(splitAndSync) {
self->_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
}
} flushDrawable:splitAndSync];
[self.view updateBacking];
if(splitAndSync) {
[self.view draw];
}
self->_isUpdating.clear();
});
}

View File

@@ -0,0 +1,16 @@
//
// CSScanTarget+C__ScanTarget.h
// Clock Signal
//
// Created by Thomas Harte on 08/08/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#import "CSScanTarget.h"
#include "ScanTarget.hpp"
@interface CSScanTarget (CppScanTarget)
@property (nonatomic, readonly, nonnull) Outputs::Display::ScanTarget *scanTarget;
@end

View File

@@ -0,0 +1,25 @@
//
// ScanTarget.h
// Clock Signal
//
// Created by Thomas Harte on 02/08/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <MetalKit/MetalKit.h>
/*!
Provides a ScanTarget that uses Metal as its back-end.
*/
@interface CSScanTarget : NSObject <MTKViewDelegate>
- (nonnull instancetype)initWithView:(nonnull MTKView *)view;
// Draws all scans currently residing at the scan target to the backing store,
// ready for output when next requested.
- (void)updateFrameBuffer;
- (nonnull NSBitmapImageRep *)imageRepresentation;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,588 @@
//
// ScanTarget.metal
// Clock Signal
//
// Created by Thomas Harte on 04/08/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include <metal_stdlib>
using namespace metal;
struct Uniforms {
// This is used to scale scan positions, i.e. it provides the range
// for mapping from scan-style integer positions into eye space.
int2 scale;
// Applies a multiplication to all cyclesSinceRetrace values.
float cycleMultiplier;
// This provides the intended height of a scan, in eye-coordinate terms.
float lineWidth;
// Provides zoom and offset to scale the source data.
float3x3 sourceToDisplay;
// Provides conversions to and from RGB for the active colour space.
half3x3 toRGB;
half3x3 fromRGB;
// Describes the filter in use for chroma filtering; it'll be
// 15 coefficients but they're symmetrical around the centre.
half3 chromaKernel[8];
// Describes the filter in use for luma filtering; 15 coefficients
// symmetrical around the centre.
half lumaKernel[8];
// Sets the opacity at which output strips are drawn.
half outputAlpha;
// Sets the gamma power to which output colours are raised.
half outputGamma;
// Sets a brightness multiplier for output colours.
half outputMultiplier;
};
namespace {
constexpr sampler standardSampler( coord::pixel,
address::clamp_to_edge, // Although arbitrary, stick with this address mode for compatibility all the way to MTLFeatureSet_iOS_GPUFamily1_v1.
filter::nearest);
constexpr sampler linearSampler( coord::pixel,
address::clamp_to_edge, // Although arbitrary, stick with this address mode for compatibility all the way to MTLFeatureSet_iOS_GPUFamily1_v1.
filter::linear);
}
// MARK: - Structs used for receiving data from the emulation.
// This is intended to match the net effect of `Scan` as defined by the BufferingScanTarget.
struct Scan {
struct EndPoint {
uint16_t position[2];
uint16_t dataOffset;
int16_t compositeAngle;
uint16_t cyclesSinceRetrace;
} endPoints[2];
uint8_t compositeAmplitude;
uint16_t dataY;
uint16_t line;
};
// This matches the BufferingScanTarget's `Line`.
struct Line {
struct EndPoint {
uint16_t position[2];
int16_t compositeAngle;
uint16_t cyclesSinceRetrace;
} endPoints[2];
uint8_t compositeAmplitude;
uint16_t line;
};
// MARK: - Intermediate structs.
struct SourceInterpolator {
float4 position [[position]];
float2 textureCoordinates;
half unitColourPhase; // i.e. one unit per circle.
half colourPhase; // i.e. 2*pi units per circle, just regular radians.
half colourAmplitude [[flat]];
};
struct CopyInterpolator {
float4 position [[position]];
float2 textureCoordinates;
};
// MARK: - Vertex shaders.
float2 textureLocation(constant Line *line, float offset, constant Uniforms &uniforms) {
return float2(
uniforms.cycleMultiplier * mix(line->endPoints[0].cyclesSinceRetrace, line->endPoints[1].cyclesSinceRetrace, offset),
line->line + 0.5f);
}
float2 textureLocation(constant Scan *scan, float offset, constant Uniforms &) {
return float2(
mix(scan->endPoints[0].dataOffset, scan->endPoints[1].dataOffset, offset),
scan->dataY + 0.5f);
}
template <typename Input> SourceInterpolator toDisplay(
constant Uniforms &uniforms [[buffer(1)]],
constant Input *inputs [[buffer(0)]],
uint instanceID [[instance_id]],
uint vertexID [[vertex_id]]) {
SourceInterpolator output;
// Get start and end vertices in regular float2 form.
const float2 start = float2(
float(inputs[instanceID].endPoints[0].position[0]) / float(uniforms.scale.x),
float(inputs[instanceID].endPoints[0].position[1]) / float(uniforms.scale.y)
);
const float2 end = float2(
float(inputs[instanceID].endPoints[1].position[0]) / float(uniforms.scale.x),
float(inputs[instanceID].endPoints[1].position[1]) / float(uniforms.scale.y)
);
// Calculate the tangent and normal.
const float2 tangent = (end - start);
const float2 normal = float2(tangent.y, -tangent.x) / length(tangent);
// Load up the colour details.
output.colourAmplitude = float(inputs[instanceID].compositeAmplitude) / 255.0f;
output.unitColourPhase = mix(
float(inputs[instanceID].endPoints[0].compositeAngle),
float(inputs[instanceID].endPoints[1].compositeAngle),
float((vertexID&2) >> 1)
) / 64.0f;
output.colourPhase = 2.0f * 3.141592654f * output.unitColourPhase;
// Hence determine this quad's real shape, using vertexID to pick a corner.
// position2d is now in the range [0, 1].
const float2 sourcePosition = start + (float(vertexID&2) * 0.5f) * tangent + (float(vertexID&1) - 0.5f) * normal * uniforms.lineWidth;
const float2 position2d = (uniforms.sourceToDisplay * float3(sourcePosition, 1.0f)).xy;
output.position = float4(
position2d,
0.0f,
1.0f
);
output.textureCoordinates = textureLocation(&inputs[instanceID], float((vertexID&2) >> 1), uniforms);
return output;
}
// These next two assume the incoming geometry to be a four-vertex triangle strip; each instance will therefore
// produce a quad.
vertex SourceInterpolator scanToDisplay( constant Uniforms &uniforms [[buffer(1)]],
constant Scan *scans [[buffer(0)]],
uint instanceID [[instance_id]],
uint vertexID [[vertex_id]]) {
return toDisplay(uniforms, scans, instanceID, vertexID);
}
vertex SourceInterpolator lineToDisplay( constant Uniforms &uniforms [[buffer(1)]],
constant Line *lines [[buffer(0)]],
uint instanceID [[instance_id]],
uint vertexID [[vertex_id]]) {
return toDisplay(uniforms, lines, instanceID, vertexID);
}
// This assumes that it needs to generate endpoints for a line segment.
vertex SourceInterpolator scanToComposition( constant Uniforms &uniforms [[buffer(1)]],
constant Scan *scans [[buffer(0)]],
uint instanceID [[instance_id]],
uint vertexID [[vertex_id]],
texture2d<float> texture [[texture(0)]]) {
SourceInterpolator result;
// Populate result as if direct texture access were available.
result.position.x = uniforms.cycleMultiplier * mix(scans[instanceID].endPoints[0].cyclesSinceRetrace, scans[instanceID].endPoints[1].cyclesSinceRetrace, float(vertexID));
result.position.y = scans[instanceID].line;
result.position.zw = float2(0.0f, 1.0f);
result.textureCoordinates.x = mix(scans[instanceID].endPoints[0].dataOffset, scans[instanceID].endPoints[1].dataOffset, float(vertexID));
result.textureCoordinates.y = scans[instanceID].dataY;
result.unitColourPhase = mix(
float(scans[instanceID].endPoints[0].compositeAngle),
float(scans[instanceID].endPoints[1].compositeAngle),
float(vertexID)
) / 64.0f;
result.colourPhase = 2.0f * 3.141592654f * result.unitColourPhase;
result.colourAmplitude = float(scans[instanceID].compositeAmplitude) / 255.0f;
// Map position into eye space, allowing for target texture dimensions.
const float2 textureSize = float2(texture.get_width(), texture.get_height());
result.position.xy =
((result.position.xy + float2(0.0f, 0.5f)) / textureSize)
* float2(2.0f, -2.0f) + float2(-1.0f, 1.0f);
return result;
}
vertex CopyInterpolator copyVertex(uint vertexID [[vertex_id]], texture2d<float> texture [[texture(0)]]) {
CopyInterpolator vert;
const uint x = vertexID & 1;
const uint y = (vertexID >> 1) & 1;
vert.textureCoordinates = float2(
x * texture.get_width(),
y * texture.get_height()
);
vert.position = float4(
float(x) * 2.0 - 1.0,
1.0 - float(y) * 2.0,
0.0,
1.0
);
return vert;
}
// MARK: - Various input format conversion samplers.
half2 quadrature(float phase) {
return half2(cos(phase), sin(phase));
}
half4 composite(half level, half2 quadrature, half amplitude) {
return half4(
level,
half2(0.5f) + quadrature*half(0.5f),
amplitude
);
}
// The luminance formats can be sampled either in their natural format, or to the intermediate
// composite format used for composition. Direct sampling is always for final output, so the two
// 8-bit formats also provide a gamma option.
half convertLuminance1(SourceInterpolator vert [[stage_in]], texture2d<ushort> texture [[texture(0)]]) {
return clamp(half(texture.sample(standardSampler, vert.textureCoordinates).r), half(0.0f), half(1.0f));
}
half convertLuminance8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
return texture.sample(standardSampler, vert.textureCoordinates).r;
}
half convertPhaseLinkedLuminance8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
const int offset = int(vert.unitColourPhase * 4.0f) & 3;
auto sample = texture.sample(standardSampler, vert.textureCoordinates);
return sample[offset];
}
#define CompositeSet(name, type) \
fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier; \
return half4(half3(luminance), uniforms.outputAlpha); \
} \
\
fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half luminance = pow(convert##name(vert, texture) * uniforms.outputMultiplier, uniforms.outputGamma); \
return half4(half3(luminance), uniforms.outputAlpha); \
} \
\
fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<type> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half luminance = convert##name(vert, texture) * uniforms.outputMultiplier; \
return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude); \
}
CompositeSet(Luminance1, ushort);
CompositeSet(Luminance8, half);
CompositeSet(PhaseLinkedLuminance8, half);
#undef CompositeSet
// The luminance/phase format can produce either composite or S-Video.
/// @returns A 2d vector comprised where .x = luminance; .y = chroma.
half2 convertLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
const auto luminancePhase = texture.sample(standardSampler, vert.textureCoordinates).rg;
const half phaseOffset = 3.141592654 * 4.0 * luminancePhase.g;
const half rawChroma = step(luminancePhase.g, half(0.75f)) * cos(vert.colourPhase + phaseOffset);
return half2(luminancePhase.r, rawChroma);
}
fragment half4 compositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
const half2 luminanceChroma = convertLuminance8Phase8(vert, texture);
const half luminance = mix(luminanceChroma.r, luminanceChroma.g, vert.colourAmplitude);
return composite(luminance, quadrature(vert.colourPhase), vert.colourAmplitude);
}
fragment half4 sampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
const half2 luminanceChroma = convertLuminance8Phase8(vert, texture);
const half2 qam = quadrature(vert.colourPhase) * half(0.5f);
return half4(luminanceChroma.r,
half2(0.5f) + luminanceChroma.g*qam,
half(1.0f));
}
fragment half4 directCompositeSampleLuminance8Phase8(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {
const half2 luminanceChroma = convertLuminance8Phase8(vert, texture);
const half luminance = mix(luminanceChroma.r * uniforms.outputMultiplier, luminanceChroma.g, vert.colourAmplitude);
return half4(half3(luminance), uniforms.outputAlpha);
}
fragment half4 directCompositeSampleLuminance8Phase8WithGamma(SourceInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) {
const half2 luminanceChroma = convertLuminance8Phase8(vert, texture);
const half luminance = mix(pow(luminanceChroma.r * uniforms.outputMultiplier, uniforms.outputGamma), luminanceChroma.g, vert.colourAmplitude);
return half4(half3(luminance), uniforms.outputAlpha);
}
// All the RGB formats can produce RGB, composite or S-Video.
half3 convertRed8Green8Blue8(SourceInterpolator vert, texture2d<half> texture) {
return texture.sample(standardSampler, vert.textureCoordinates).rgb;
}
half3 convertRed4Green4Blue4(SourceInterpolator vert, texture2d<ushort> texture) {
const auto sample = texture.sample(standardSampler, vert.textureCoordinates).rg;
return clamp(half3(sample.r&15, (sample.g >> 4)&15, sample.g&15), half(0.0f), half(1.0f));
}
half3 convertRed2Green2Blue2(SourceInterpolator vert, texture2d<ushort> texture) {
const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r;
return clamp(half3((sample >> 4)&3, (sample >> 2)&3, sample&3), half(0.0f), half(1.0f));
}
half3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d<ushort> texture) {
const auto sample = texture.sample(standardSampler, vert.textureCoordinates).r;
return clamp(half3(sample&4, sample&2, sample&1), half(0.0f), half(1.0f));
}
#define DeclareShaders(name, pixelType) \
fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
return half4(convert##name(vert, texture), uniforms.outputAlpha); \
} \
\
fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
return half4(pow(convert##name(vert, texture), uniforms.outputGamma), uniforms.outputAlpha); \
} \
\
fragment half4 svideoSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const auto colour = uniforms.fromRGB * convert##name(vert, texture); \
const half2 qam = quadrature(vert.colourPhase); \
const half chroma = dot(colour.gb, qam); \
return half4( \
colour.r, \
half2(0.5f) + chroma*qam*half(0.5f), \
half(1.0f) \
); \
} \
\
half composite##name(SourceInterpolator vert, texture2d<pixelType> texture, constant Uniforms &uniforms, half2 colourSubcarrier) { \
const auto colour = uniforms.fromRGB * convert##name(vert, texture); \
return mix(colour.r, dot(colour.gb, colourSubcarrier), half(vert.colourAmplitude)); \
} \
\
fragment half4 compositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half2 colourSubcarrier = quadrature(vert.colourPhase); \
return composite(composite##name(vert, texture, uniforms, colourSubcarrier), colourSubcarrier, vert.colourAmplitude); \
} \
\
fragment half4 directCompositeSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half level = composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)); \
return half4(half3(level), uniforms.outputAlpha); \
} \
\
fragment half4 directCompositeSample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \
const half level = pow(composite##name(vert, texture, uniforms, quadrature(vert.colourPhase)), uniforms.outputGamma); \
return half4(half3(level), uniforms.outputAlpha); \
}
DeclareShaders(Red8Green8Blue8, half)
DeclareShaders(Red4Green4Blue4, ushort)
DeclareShaders(Red2Green2Blue2, ushort)
DeclareShaders(Red1Green1Blue1, ushort)
fragment half4 copyFragment(CopyInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
return texture.sample(standardSampler, vert.textureCoordinates);
}
fragment half4 interpolateFragment(CopyInterpolator vert [[stage_in]], texture2d<half> texture [[texture(0)]]) {
return texture.sample(linearSampler, vert.textureCoordinates);
}
fragment half4 clearFragment(constant Uniforms &uniforms [[buffer(0)]]) {
return half4(0.0, 0.0, 0.0, uniforms.outputAlpha);
}
// MARK: - Compute kernels
/// Given input pixels of the form (luminance, 0.5 + 0.5*chrominance*cos(phase), 0.5 + 0.5*chrominance*sin(phase)), applies a lowpass
/// filter to the two chrominance parts, then uses the toRGB matrix to convert to RGB and stores.
template <bool applyGamma> void filterChromaKernel( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
constexpr half4 moveToZero(0.0f, 0.5f, 0.5f, 0.0f);
const half4 rawSamples[] = {
inTexture.read(gid + uint2(0, offset)) - moveToZero,
inTexture.read(gid + uint2(1, offset)) - moveToZero,
inTexture.read(gid + uint2(2, offset)) - moveToZero,
inTexture.read(gid + uint2(3, offset)) - moveToZero,
inTexture.read(gid + uint2(4, offset)) - moveToZero,
inTexture.read(gid + uint2(5, offset)) - moveToZero,
inTexture.read(gid + uint2(6, offset)) - moveToZero,
inTexture.read(gid + uint2(7, offset)) - moveToZero,
inTexture.read(gid + uint2(8, offset)) - moveToZero,
inTexture.read(gid + uint2(9, offset)) - moveToZero,
inTexture.read(gid + uint2(10, offset)) - moveToZero,
inTexture.read(gid + uint2(11, offset)) - moveToZero,
inTexture.read(gid + uint2(12, offset)) - moveToZero,
inTexture.read(gid + uint2(13, offset)) - moveToZero,
inTexture.read(gid + uint2(14, offset)) - moveToZero,
};
#define Sample(x, y) uniforms.chromaKernel[y] * rawSamples[x].rgb
const half3 colour =
Sample(0, 0) + Sample(1, 1) + Sample(2, 2) + Sample(3, 3) + Sample(4, 4) + Sample(5, 5) + Sample(6, 6) +
Sample(7, 7) +
Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0);
#undef Sample
const half4 output = half4(uniforms.toRGB * colour * uniforms.outputMultiplier, uniforms.outputAlpha);
if(applyGamma) {
outTexture.write(pow(output, uniforms.outputGamma), gid + uint2(7, offset));
} else {
outTexture.write(output, gid + uint2(7, offset));
}
}
kernel void filterChromaKernelNoGamma( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
filterChromaKernel<false>(inTexture, outTexture, gid, uniforms, offset);
}
kernel void filterChromaKernelWithGamma( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
filterChromaKernel<true>(inTexture, outTexture, gid, uniforms, offset);
}
void setSeparatedLumaChroma(half luminance, half4 centreSample, texture2d<half, access::write> outTexture, uint2 gid, int offset) {
// The mix/steps below ensures that the absence of a colour burst leads the colour subcarrier to be discarded.
const half isColour = step(half(0.01f), centreSample.a);
const half chroma = (centreSample.r - luminance) / mix(half(1.0f), centreSample.a, isColour);
outTexture.write(half4(
luminance / mix(half(1.0f), (half(1.0f) - centreSample.a), isColour),
isColour * (centreSample.gb - half2(0.5f)) * chroma + half2(0.5f),
1.0f
),
gid + uint2(7, offset));
}
/// Given input pixels of the form:
///
/// (composite sample, cos(phase), sin(phase), colour amplitude), applies a lowpass
///
/// Filters to separate luminance, subtracts that and scales and maps the remaining chrominance in order to output
/// pixels in the form:
///
/// (luminance, 0.5 + 0.5*chrominance*cos(phase), 0.5 + 0.5*chrominance*sin(phase))
///
/// i.e. the input form for the filterChromaKernel, above].
kernel void separateLumaKernel15( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
const half4 centreSample = inTexture.read(gid + uint2(7, offset));
const half rawSamples[] = {
inTexture.read(gid + uint2(0, offset)).r, inTexture.read(gid + uint2(1, offset)).r,
inTexture.read(gid + uint2(2, offset)).r, inTexture.read(gid + uint2(3, offset)).r,
inTexture.read(gid + uint2(4, offset)).r, inTexture.read(gid + uint2(5, offset)).r,
inTexture.read(gid + uint2(6, offset)).r,
centreSample.r,
inTexture.read(gid + uint2(8, offset)).r,
inTexture.read(gid + uint2(9, offset)).r, inTexture.read(gid + uint2(10, offset)).r,
inTexture.read(gid + uint2(11, offset)).r, inTexture.read(gid + uint2(12, offset)).r,
inTexture.read(gid + uint2(13, offset)).r, inTexture.read(gid + uint2(14, offset)).r,
};
#define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x]
const half luminance =
Sample(0, 0) + Sample(1, 1) + Sample(2, 2) + Sample(3, 3) + Sample(4, 4) + Sample(5, 5) + Sample(6, 6) +
Sample(7, 7) +
Sample(8, 6) + Sample(9, 5) + Sample(10, 4) + Sample(11, 3) + Sample(12, 2) + Sample(13, 1) + Sample(14, 0);
#undef Sample
return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset);
}
kernel void separateLumaKernel9( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
const half4 centreSample = inTexture.read(gid + uint2(7, offset));
const half rawSamples[] = {
inTexture.read(gid + uint2(3, offset)).r, inTexture.read(gid + uint2(4, offset)).r,
inTexture.read(gid + uint2(5, offset)).r, inTexture.read(gid + uint2(6, offset)).r,
centreSample.r,
inTexture.read(gid + uint2(8, offset)).r, inTexture.read(gid + uint2(9, offset)).r,
inTexture.read(gid + uint2(10, offset)).r, inTexture.read(gid + uint2(11, offset)).r
};
#define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x]
const half luminance =
Sample(0, 3) + Sample(1, 4) + Sample(2, 5) + Sample(3, 6) +
Sample(4, 7) +
Sample(5, 6) + Sample(6, 5) + Sample(7, 4) + Sample(8, 3);
#undef Sample
return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset);
}
kernel void separateLumaKernel7( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
const half4 centreSample = inTexture.read(gid + uint2(7, offset));
const half rawSamples[] = {
inTexture.read(gid + uint2(4, offset)).r,
inTexture.read(gid + uint2(5, offset)).r, inTexture.read(gid + uint2(6, offset)).r,
centreSample.r,
inTexture.read(gid + uint2(8, offset)).r, inTexture.read(gid + uint2(9, offset)).r,
inTexture.read(gid + uint2(10, offset)).r
};
#define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x]
const half luminance =
Sample(0, 4) + Sample(1, 5) + Sample(2, 6) +
Sample(3, 7) +
Sample(4, 6) + Sample(5, 5) + Sample(6, 4);
#undef Sample
return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset);
}
kernel void separateLumaKernel5( texture2d<half, access::read> inTexture [[texture(0)]],
texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]],
constant Uniforms &uniforms [[buffer(0)]],
constant int &offset [[buffer(1)]]) {
const half4 centreSample = inTexture.read(gid + uint2(7, offset));
const half rawSamples[] = {
inTexture.read(gid + uint2(5, offset)).r, inTexture.read(gid + uint2(6, offset)).r,
centreSample.r,
inTexture.read(gid + uint2(8, offset)).r, inTexture.read(gid + uint2(9, offset)).r,
};
#define Sample(x, y) uniforms.lumaKernel[y] * rawSamples[x]
const half luminance =
Sample(0, 5) + Sample(1, 6) +
Sample(2, 7) +
Sample(3, 6) + Sample(4, 5);
#undef Sample
return setSeparatedLumaChroma(luminance, centreSample, outTexture, gid, offset);
}
kernel void clearKernel( texture2d<half, access::write> outTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]]) {
outTexture.write(half4(0.0f, 0.0f, 0.0f, 1.0f), gid);
}

View File

@@ -1,5 +1,5 @@
//
// CSOpenGLView.h
// CSScanTargetView.h
// Clock Signal
//
// Created by Thomas Harte on 16/07/2015.
@@ -8,63 +8,12 @@
#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <MetalKit/MetalKit.h>
@class CSOpenGLView;
@class CSScanTargetView;
@class CSScanTarget;
typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
/// Indicates that AppKit requested a redraw for some reason (mostly likely, the window is being resized). So,
/// if the delegate doesn't redraw the view, the user is likely to see a graphical flaw.
CSOpenGLViewRedrawEventAppKit,
/// Indicates that the view's display-linked timer has triggered a redraw request. So, if the delegate doesn't
/// redraw the view, the user will just see the previous drawing without interruption.
CSOpenGLViewRedrawEventTimer
};
@protocol CSOpenGLViewDelegate
/*!
Requests that the delegate produce an image of its current output state. May be called on
any queue or thread.
@param view The view making the request.
@param redrawEvent If @c YES then the delegate may decline to redraw if its output would be
identical to the previous frame. If @c NO then the delegate must draw.
*/
- (void)openGLViewRedraw:(nonnull CSOpenGLView *)view event:(CSOpenGLViewRedrawEvent)redrawEvent;
/*!
Announces receipt of a file by drag and drop to the delegate.
@param view The view making the request.
@param URL The file URL of the received file.
*/
- (void)openGLView:(nonnull CSOpenGLView *)view didReceiveFileAtURL:(nonnull NSURL *)URL;
/*!
Announces 'capture' of the mouse i.e. that the view is now preventing the mouse from exiting
the window, in order to forward continuous mouse motion.
@param view The view making the announcement.
*/
- (void)openGLViewDidCaptureMouse:(nonnull CSOpenGLView *)view;
/*!
Announces that the mouse is no longer captured.
@param view The view making the announcement.
*/
- (void)openGLViewDidReleaseMouse:(nonnull CSOpenGLView *)view;
/*!
Announces that the OS mouse cursor is now being displayed again, after having been invisible.
@param view The view making the announcement.
*/
- (void)openGLViewDidShowOSMouseCursor:(nonnull CSOpenGLView *)view;
/*!
Announces that the OS mouse cursor will now be hidden.
@param view The view making the announcement.
*/
- (void)openGLViewWillHideOSMouseCursor:(nonnull CSOpenGLView *)view;
@end
@protocol CSOpenGLViewResponderDelegate <NSObject>
@protocol CSScanTargetViewResponderDelegate <NSObject>
/*!
Supplies a keyDown event to the delegate.
@param event The @c NSEvent describing the keyDown.
@@ -111,41 +60,72 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
*/
- (void)mouseUp:(nonnull NSEvent *)event;
/*!
Announces 'capture' of the mouse i.e. that the view is now preventing the mouse from exiting
the window, in order to forward continuous mouse motion.
@param view The view making the announcement.
*/
- (void)scanTargetViewDidCaptureMouse:(nonnull CSScanTargetView *)view;
/*!
Announces that the mouse is no longer captured.
@param view The view making the announcement.
*/
- (void)scanTargetViewDidReleaseMouse:(nonnull CSScanTargetView *)view;
/*!
Announces that the OS mouse cursor is now being displayed again, after having been invisible.
@param view The view making the announcement.
*/
- (void)scanTargetViewDidShowOSMouseCursor:(nonnull CSScanTargetView *)view;
/*!
Announces that the OS mouse cursor will now be hidden.
@param view The view making the announcement.
*/
- (void)scanTargetViewWillHideOSMouseCursor:(nonnull CSScanTargetView *)view;
/*!
Announces receipt of a file by drag and drop to the delegate.
@param view The view making the request.
@param URL The file URL of the received file.
*/
- (void)scanTargetView:(nonnull CSScanTargetView *)view didReceiveFileAtURL:(nonnull NSURL *)URL;
@end
/*!
Although I'm still on the fence about this as a design decision, CSOpenGLView is itself responsible
Although I'm still on the fence about this as a design decision, CSScanTargetView is itself responsible
for creating and destroying a CVDisplayLink. There's a practical reason for this: you'll get real synchronisation
only if a link is explicitly tied to a particular display, and the CSOpenGLView therefore owns the knowledge
only if a link is explicitly tied to a particular display, and the CSScanTargetView therefore owns the knowledge
necessary to decide when to create and modify them. It doesn't currently just propagate "did change screen"-type
messages because I haven't yet found a way to track that other than polling, in which case I might as well put
that into the display link callback.
*/
@protocol CSOpenGLViewDisplayLinkDelegate
@protocol CSScanTargetViewDisplayLinkDelegate
/*!
Informs the delegate that the display link has fired.
*/
- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime;
- (void)scanTargetViewDisplayLinkDidFire:(nonnull CSScanTargetView *)view now:(nonnull const CVTimeStamp *)now outputTime:(nonnull const CVTimeStamp *)outputTime;
@end
/*!
Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset
Provides a visible scan target with a refresh-linked update timer that can forward a subset
of typical first-responder actions.
*/
@interface CSOpenGLView : NSOpenGLView
@interface CSScanTargetView : MTKView
@property (atomic, weak, nullable) id <CSOpenGLViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <CSOpenGLViewResponderDelegate> responderDelegate;
@property (atomic, weak, nullable) id <CSOpenGLViewDisplayLinkDelegate> displayLinkDelegate;
@property (nonatomic, weak, nullable) id <CSScanTargetViewResponderDelegate> responderDelegate;
@property (atomic, weak, nullable) id <CSScanTargetViewDisplayLinkDelegate> displayLinkDelegate;
/// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then
/// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate
/// then the system cursor is disabled and the mouse events defined by CSScanTargetViewResponderDelegate
/// are forwarded, unless and until the user releases the mouse using the control+command shortcut.
@property (nonatomic, assign) BOOL shouldCaptureMouse;
/// Determines whether the CSOpenGLViewResponderDelegate of this window expects to use the command
/// Determines whether the CSScanTargetViewResponderDelegate of this window expects to use the command
/// key as though it were any other key — i.e. all command combinations should be forwarded to the delegate,
/// not being allowed to trigger regular application shortcuts such as command+q or command+h.
///
@@ -162,19 +142,24 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
*/
- (void)invalidate;
/// The size in pixels of the OpenGL canvas, factoring in screen pixel density and view size in points.
@property (nonatomic, readonly) CGSize backingSize;
/*!
Locks this view's OpenGL context and makes it current, performs @c action and then unlocks
the context. @c action is performed on the calling queue.
Ensures output begins on all pending scans.
*/
- (void)performWithGLContext:(nonnull dispatch_block_t)action flushDrawable:(BOOL)flushDrawable;
- (void)performWithGLContext:(nonnull dispatch_block_t)action;
- (void)updateBacking;
/*!
Instructs that the mouse cursor, if currently captured, should be released.
*/
- (void)releaseMouse;
/*!
@returns An image of the view's current contents.
*/
- (nonnull NSBitmapImageRep *)imageRepresentation;
/*!
@returns The CSScanTarget being used for this display.
*/
@property(nonatomic, readonly, nonnull) CSScanTarget *scanTarget;
@end

View File

@@ -1,24 +1,24 @@
//
// CSOpenGLView
// CSScanTargetView
// CLK
//
// Created by Thomas Harte on 16/07/2015.
// Copyright 2015 Thomas Harte. All rights reserved.
//
#import "CSOpenGLView.h"
#import "CSScanTargetView.h"
#import "CSApplication.h"
#import "CSScanTarget.h"
@import CoreVideo;
@import GLKit;
#include <stdatomic.h>
@interface CSOpenGLView () <NSDraggingDestination, CSApplicationEventDelegate>
@interface CSScanTargetView () <NSDraggingDestination, CSApplicationEventDelegate>
@end
@implementation CSOpenGLView {
@implementation CSScanTargetView {
CVDisplayLinkRef _displayLink;
CGSize _backingSize;
NSNumber *_currentScreenNumber;
NSTrackingArea *_mouseTrackingArea;
@@ -27,20 +27,8 @@
atomic_int _isDrawingFlag;
BOOL _isInvalid;
}
- (void)prepareOpenGL {
[super prepareOpenGL];
// Prepare the atomic int.
atomic_init(&_isDrawingFlag, 0);
// Set the clear colour.
[self.openGLContext makeCurrentContext];
glClearColor(0.0, 0.0, 0.0, 1.0);
// Setup the [initial] display link.
[self setupDisplayLink];
CSScanTarget *_scanTarget;
}
- (void)setupDisplayLink {
@@ -58,17 +46,12 @@
// Set the renderer output callback function.
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
// Set the display link for the current renderer.
CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
// Activate the display link.
CVDisplayLinkStart(_displayLink);
}
static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, __unused CVOptionFlags flagsIn, __unused CVOptionFlags *flagsOut, void *displayLinkContext) {
CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext;
CSScanTargetView *const view = (__bridge CSScanTargetView *)displayLinkContext;
// Schedule an opportunity to check that the display link is still linked to the correct display.
dispatch_async(dispatch_get_main_queue(), ^{
@@ -78,7 +61,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
// Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise.
atomic_store(&view->_isDrawingFlag, 1);
[view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime];
[view.displayLinkDelegate scanTargetViewDisplayLinkDidFire:view now:now outputTime:outputTime];
/*
Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink.
Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback,
@@ -106,30 +89,12 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
// feels fine.
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
if(![_currentScreenNumber isEqual:screenNumber]) {
// Issue a reshape, in case a switch to/from a Retina display has
// happened, changing the results of -convertSizeToBacking:, etc.
[self reshape];
// Also switch display links, to make sure synchronisation is with the display
// the window is actually on, and at its rate.
[self setupDisplayLink];
}
}
- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
[self redrawWithEvent:CSOpenGLViewRedrawEventTimer];
}
- (void)drawRect:(NSRect)dirtyRect {
[self redrawWithEvent:CSOpenGLViewRedrawEventAppKit];
}
- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event {
[self performWithGLContext:^{
[self.delegate openGLViewRedraw:self event:event];
} flushDrawable:YES];
}
- (void)invalidate {
_isInvalid = YES;
[self stopDisplayLink];
@@ -160,65 +125,35 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
CVDisplayLinkRelease(_displayLink);
}
- (CGSize)backingSize {
@synchronized(self) {
return _backingSize;
}
- (CSScanTarget *)scanTarget {
return _scanTarget;
}
- (void)reshape {
[super reshape];
@synchronized(self) {
_backingSize = [self convertSizeToBacking:self.bounds.size];
}
[self performWithGLContext:^{
CGSize viewSize = [self backingSize];
glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height);
} flushDrawable:NO];
- (void)updateBacking {
[_scanTarget updateFrameBuffer];
}
- (void)awakeFromNib {
NSOpenGLPixelFormatAttribute attributes[] = {
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
// NSOpenGLPFAMultisample,
// NSOpenGLPFASampleBuffers, 1,
// NSOpenGLPFASamples, 2,
0
};
// Use the preferred device if available.
if(@available(macOS 10.15, *)) {
self.device = self.preferredDevice;
} else {
self.device = MTLCreateSystemDefaultDevice();
}
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attributes];
NSOpenGLContext *context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
// Configure for explicit drawing.
self.paused = YES;
self.enableSetNeedsDisplay = NO;
#ifdef DEBUG
// When we're using a CoreProfile context, crash if we call a legacy OpenGL function
// This will make it much more obvious where and when such a function call is made so
// that we can remove such calls.
// Without this we'd simply get GL_INVALID_OPERATION error for calling legacy functions
// but it would be more difficult to see where that function was called.
CGLEnable([context CGLContextObj], kCGLCECrashOnRemovedFunctions);
#endif
self.pixelFormat = pixelFormat;
self.openGLContext = context;
self.wantsBestResolutionOpenGLSurface = YES;
// Create the scan target.
_scanTarget = [[CSScanTarget alloc] initWithView:self];
self.delegate = _scanTarget;
// Register to receive dragged and dropped file URLs.
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
}
- (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable {
CGLLockContext([[self openGLContext] CGLContextObj]);
[self.openGLContext makeCurrentContext];
action();
CGLUnlockContext([[self openGLContext] CGLContextObj]);
if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]);
}
- (void)performWithGLContext:(nonnull dispatch_block_t)action {
[self performWithGLContext:action flushDrawable:NO];
// Setup the [initial] display link.
[self setupDisplayLink];
}
#pragma mark - NSResponder
@@ -259,12 +194,16 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
[self.responderDelegate paste:sender];
}
- (NSBitmapImageRep *)imageRepresentation {
return self.scanTarget.imageRepresentation;
}
#pragma mark - NSDraggingDestination
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) {
NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]];
[self.delegate openGLView:self didReceiveFileAtURL:URL];
[self.responderDelegate scanTargetView:self didReceiveFileAtURL:URL];
}
return YES;
}
@@ -300,13 +239,13 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(__unused NSTimer * _Nonnull timer) {
[NSCursor setHiddenUntilMouseMoves:YES];
[self.delegate openGLViewWillHideOSMouseCursor:self];
[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self];
}];
}
}
- (void)mouseEntered:(NSEvent *)event {
[self.delegate openGLViewDidShowOSMouseCursor:self];
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
[super mouseEntered:event];
[self scheduleMouseHide];
}
@@ -315,7 +254,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
[super mouseExited:event];
[_mouseHideTimer invalidate];
_mouseHideTimer = nil;
[self.delegate openGLViewWillHideOSMouseCursor:self];
[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self];
}
- (void)releaseMouse {
@@ -323,8 +262,8 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
_mouseIsCaptured = NO;
CGAssociateMouseAndMouseCursorPosition(true);
[NSCursor unhide];
[self.delegate openGLViewDidReleaseMouse:self];
[self.delegate openGLViewDidShowOSMouseCursor:self];
[self.responderDelegate scanTargetViewDidReleaseMouse:self];
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil;
}
}
@@ -336,7 +275,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
// Mouse capture is off, so don't play games with the cursor, just schedule it to
// hide in the near future.
[self scheduleMouseHide];
[self.delegate openGLViewDidShowOSMouseCursor:self];
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
} else {
if(_mouseIsCaptured) {
// Mouse capture is on, so move the cursor back to the middle of the window, and
@@ -354,7 +293,7 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
[self.responderDelegate mouseMoved:event];
} else {
[self.delegate openGLViewDidShowOSMouseCursor:self];
[self.responderDelegate scanTargetViewDidShowOSMouseCursor:self];
}
}
}
@@ -387,8 +326,8 @@ static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const
_mouseIsCaptured = YES;
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(false);
[self.delegate openGLViewWillHideOSMouseCursor:self];
[self.delegate openGLViewDidCaptureMouse:self];
[self.responderDelegate scanTargetViewWillHideOSMouseCursor:self];
[self.responderDelegate scanTargetViewDidCaptureMouse:self];
if(self.shouldUsurpCommand) {
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self;
}

View File

@@ -7,7 +7,6 @@
//
#import <XCTest/XCTest.h>
#import <OpenGL/OpenGL.h>
#include "9918.hpp"

View File

@@ -1,6 +1,9 @@
QT += core gui multimedia widgets
# Be specific about C++17 but also try the vaguer C++1z for older
# versions of Qt.
CONFIG += c++17
CONFIG += c++1z
# Permit multiple source files in different directories to have the same file name.
CONFIG += object_parallel_to_source
@@ -82,6 +85,7 @@ SOURCES += \
\
$$SRC/Outputs/*.cpp \
$$SRC/Outputs/CRT/*.cpp \
$$SRC/Outputs/ScanTargets/*.cpp \
$$SRC/Outputs/OpenGL/*.cpp \
$$SRC/Outputs/OpenGL/Primitives/*.cpp \
\
@@ -201,6 +205,7 @@ HEADERS += \
$$SRC/Outputs/*.hpp \
$$SRC/Outputs/CRT/*.hpp \
$$SRC/Outputs/CRT/Internals/*.hpp \
$$SRC/Outputs/ScanTargets/*.hpp \
$$SRC/Outputs/OpenGL/*.hpp \
$$SRC/Outputs/OpenGL/Primitives/*.hpp \
$$SRC/Outputs/Speaker/*.hpp \

View File

@@ -45,7 +45,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
MainWindow::MainWindow(const QString &fileName) {
init();
launchFile(fileName);
if(!launchFile(fileName)) {
setUIPhase(UIPhase::SelectingMachine);
}
}
void MainWindow::deleteMachine() {
@@ -210,11 +212,17 @@ void MainWindow::insertFile(const QString &fileName) {
mediaTarget->insert_media(media);
}
void MainWindow::launchFile(const QString &fileName) {
bool MainWindow::launchFile(const QString &fileName) {
targets = Analyser::Static::GetTargets(fileName.toStdString());
if(!targets.empty()) {
openFileName = QFileInfo(fileName).fileName();
launchMachine();
return true;
} else {
QMessageBox msgBox;
msgBox.setText("Unable to open file: " + fileName);
msgBox.exec();
return false;
}
}
@@ -707,6 +715,7 @@ void MainWindow::dropEvent(QDropEvent* event) {
bool foundROM = false;
const auto appDataLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString();
QString unusedRoms;
for(const auto &url: event->mimeData()->urls()) {
const char *const name = url.toLocalFile().toUtf8();
FILE *const file = fopen(name, "rb");
@@ -716,6 +725,7 @@ void MainWindow::dropEvent(QDropEvent* event) {
CRC::CRC32 generator;
const uint32_t crc = generator.compute_crc(*contents);
bool wasUsed = false;
for(const auto &rom: missingRoms) {
if(std::find(rom.crc32s.begin(), rom.crc32s.end(), crc) != rom.crc32s.end()) {
foundROM = true;
@@ -731,10 +741,22 @@ void MainWindow::dropEvent(QDropEvent* event) {
FILE *const target = fopen(destination.c_str(), "wb");
fwrite(contents->data(), 1, contents->size(), target);
fclose(target);
wasUsed = true;
}
}
if(!wasUsed) {
if(!unusedRoms.isEmpty()) unusedRoms += ", ";
unusedRoms += url.fileName();
}
}
if(!unusedRoms.isEmpty()) {
QMessageBox msgBox;
msgBox.setText("Couldn't identify ROMs: " + unusedRoms);
msgBox.exec();
}
if(foundROM) launchMachine();
} break;
}
@@ -1338,24 +1360,25 @@ void MainWindow::addActivityObserver() {
}
void MainWindow::register_led(const std::string &name) {
std::lock_guard guard(ledStatusesLock);
ledStatuses[name] = false;
updateStatusBarText();
QMetaObject::invokeMethod(this, "updateStatusBarText");
}
void MainWindow::set_led_status(const std::string &name, bool isLit) {
std::lock_guard guard(ledStatusesLock);
ledStatuses[name] = isLit;
updateStatusBarText(); // Assumption here: Qt's attempt at automatic thread confinement will work here.
QMetaObject::invokeMethod(this, "updateStatusBarText");
}
void MainWindow::updateStatusBarText() {
QString fullText;
bool isFirst = true;
std::lock_guard guard(ledStatusesLock);
for(const auto &pair: ledStatuses) {
if(!isFirst) fullText += " | ";
if(!fullText.isEmpty()) fullText += " | ";
fullText += QString::fromStdString(pair.first);
fullText += " ";
fullText += pair.second ? "" : "";
isFirst = false;
}
statusBar()->showMessage(fullText);
}

View File

@@ -5,6 +5,7 @@
#include <QMainWindow>
#include <memory>
#include <mutex>
#include <optional>
#include "audiobuffer.h"
@@ -80,6 +81,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat
private slots:
void startMachine();
void updateStatusBarText();
private:
void start_appleII();
@@ -100,7 +102,7 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat
QAction *insertAction = nullptr;
void insertFile(const QString &fileName);
void launchFile(const QString &fileName);
bool launchFile(const QString &fileName);
void launchTarget(std::unique_ptr<Analyser::Static::Target> &&);
void restoreSelections();
@@ -144,9 +146,11 @@ class MainWindow : public QMainWindow, public Outputs::Speaker::Speaker::Delegat
void register_led(const std::string &) override;
void set_led_status(const std::string &, bool) override;
std::recursive_mutex ledStatusesLock;
std::map<std::string, bool> ledStatuses;
void addActivityObserver();
void updateStatusBarText();
};
#endif // MAINWINDOW_H

View File

@@ -86,7 +86,7 @@ void ScanTargetWidget::vsync() {
const auto time_now = Time::nanos_now();
requestedRedrawTime = vsyncPredictor.suggested_draw_time();
const auto delay_time = (requestedRedrawTime - time_now) / 1'000'000;
if(delay_time > 0) {
if(delay_time > 0 && delay_time < vsyncPredictor.frame_duration()) {
QTimer::singleShot(delay_time, this, SLOT(repaint()));
} else {
requestedRedrawTime = 0;

View File

@@ -1,19 +1,19 @@
import glob
import sys
# establish UTF-8 encoding for Python 2
# Establish UTF-8 encoding for Python 2.
if sys.version_info < (3, 0):
reload(sys)
sys.setdefaultencoding('utf-8')
# create build environment
# Create build environment.
env = Environment()
# determine compiler and linker flags for SDL
# Determine compiler and linker flags for SDL.
env.ParseConfig('sdl2-config --cflags')
env.ParseConfig('sdl2-config --libs')
# gather a list of source files
# Gather a list of source files.
SOURCES = glob.glob('*.cpp')
SOURCES += glob.glob('../../Analyser/Dynamic/*.cpp')
@@ -79,6 +79,7 @@ SOURCES += glob.glob('../../Machines/ZX8081/*.cpp')
SOURCES += glob.glob('../../Outputs/*.cpp')
SOURCES += glob.glob('../../Outputs/CRT/*.cpp')
SOURCES += glob.glob('../../Outputs/ScanTargets/*.cpp')
SOURCES += glob.glob('../../Outputs/OpenGL/*.cpp')
SOURCES += glob.glob('../../Outputs/OpenGL/Primitives/*.cpp')
@@ -117,11 +118,11 @@ SOURCES += glob.glob('../../Storage/Tape/*.cpp')
SOURCES += glob.glob('../../Storage/Tape/Formats/*.cpp')
SOURCES += glob.glob('../../Storage/Tape/Parsers/*.cpp')
# add additional compiler flags
env.Append(CCFLAGS = ['--std=c++17', '-Wall', '-O2', '-DNDEBUG'])
# Add additional compiler flags; c++1z is insurance in case c++17 isn't fully implemented.
env.Append(CCFLAGS = ['--std=c++17', '--std=c++1z', '-Wall', '-O2', '-DNDEBUG'])
# add additional libraries to link against
# Add additional libraries to link against.
env.Append(LIBS = ['libz', 'pthread', 'GL'])
# build target
# Build target.
env.Program(target = 'clksignal', source = SOURCES)

View File

@@ -27,7 +27,7 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
// in NTSC and PAL TV."
time_multiplier_ = 65535 / cycles_per_line;
time_multiplier_ = 63487 / cycles_per_line; // 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted.
phase_denominator_ = int64_t(cycles_per_line) * int64_t(colour_cycle_denominator) * int64_t(time_multiplier_);
phase_numerator_ = 0;
colour_cycle_numerator_ = int64_t(colour_cycle_numerator);
@@ -194,8 +194,8 @@ Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(uint16_t data_offset
end_point.y = uint16_t(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_);
end_point.data_offset = data_offset;
// TODO: this is a workaround for the limited precision that can be posted onwards;
// it'd be better to make time_multiplier_ an explicit modal and just not divide by it.
// Ensure .composite_angle is sampled at the location indicated by .cycles_since_end_of_horizontal_retrace.
// TODO: I could supply time_multiplier_ as a modal and just not round .cycles_since_end_of_horizontal_retrace. Would that be better?
const auto lost_precision = cycles_since_horizontal_sync_ % time_multiplier_;
end_point.composite_angle = int16_t(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_) * (is_alernate_line_ ? -1 : 1);
end_point.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_);
@@ -427,7 +427,8 @@ void CRT::set_immediate_default_phase(float phase) {
void CRT::output_data(int number_of_cycles, size_t number_of_samples) {
#ifndef NDEBUG
assert(number_of_samples > 0 && number_of_samples <= allocated_data_length_);
assert(number_of_samples > 0);
assert(number_of_samples <= allocated_data_length_);
allocated_data_length_ = std::numeric_limits<size_t>::min();
#endif
scan_target_->end_data(number_of_samples);

View File

@@ -81,7 +81,7 @@ class CRT {
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
Outputs::Display::ScanTarget::Modals scan_target_modals_;
static constexpr uint8_t DefaultAmplitude = 80;
static constexpr uint8_t DefaultAmplitude = 41; // Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150.
#ifndef NDEBUG
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();

View File

@@ -50,7 +50,7 @@ void Metrics::announce_did_resize() {
frames_missed_ = frames_hit_ = 0;
}
void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::duration, bool complete) {
void Metrics::announce_draw_status(bool complete) {
if(!complete) {
++frames_missed_;
} else {
@@ -79,6 +79,10 @@ void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::d
}
}
void Metrics::announce_draw_status(size_t, std::chrono::high_resolution_clock::duration, bool complete) {
announce_draw_status(complete);
}
bool Metrics::should_lower_resolution() const {
// If less than 100 frames are on record, return no opinion; otherwise
// suggest a lower resolution if more than 10 frames in the last 100-200

View File

@@ -12,6 +12,7 @@
#include "ScanTarget.hpp"
#include <array>
#include <atomic>
#include <chrono>
namespace Outputs {
@@ -33,6 +34,9 @@ class Metrics {
/// Provides Metrics with a new data point for output speed estimation.
void announce_draw_status(size_t lines, std::chrono::high_resolution_clock::duration duration, bool complete);
/// Provides Metrics with a new data point for output speed estimation, albeit without line-specific information.
void announce_draw_status(bool complete);
/// @returns @c true if Metrics thinks a lower output buffer resolution is desirable in the abstract; @c false otherwise.
bool should_lower_resolution() const;
@@ -48,8 +52,8 @@ class Metrics {
size_t line_total_history_pointer_ = 0;
void add_line_total(int);
int frames_hit_ = 0;
int frames_missed_ = 0;
std::atomic<int> frames_hit_ = 0;
std::atomic<int> frames_missed_ = 0;
};
}

View File

@@ -16,6 +16,8 @@
#ifdef __APPLE__
#if TARGET_OS_IPHONE
#else
// These remain so that I can, at least for now, build the kiosk version under macOS.
// They can be eliminated if and when Apple fully withdraws OpenGL support.
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <OpenGL/gl3ext.h>

View File

@@ -40,11 +40,6 @@ constexpr GLenum QAMChromaTextureUnit = GL_TEXTURE2;
/// The texture unit that contains the current display.
constexpr GLenum AccumulationTextureUnit = GL_TEXTURE3;
#define TextureAddress(x, y) (((y) << 11) | (x))
#define TextureAddressGetY(v) uint16_t((v) >> 11)
#define TextureAddressGetX(v) uint16_t((v) & 0x7ff)
#define TextureSub(a, b) (((a) - (b)) & 0x3fffff)
constexpr GLint internalFormatForDepth(std::size_t depth) {
switch(depth) {
default: return GL_FALSE;
@@ -84,9 +79,8 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) :
unprocessed_line_texture_(LineBufferWidth, LineBufferHeight, UnprocessedLineBufferTextureUnit, GL_NEAREST, false),
full_display_rectangle_(-1.0f, -1.0f, 2.0f, 2.0f) {
// Ensure proper initialisation of the two atomic pointer sets.
read_pointers_.store(write_pointers_);
submit_pointers_.store(write_pointers_);
set_scan_buffer(scan_buffer_.data(), scan_buffer_.size());
set_line_buffer(line_buffer_.data(), line_metadata_buffer_.data(), line_buffer_.size());
// Allocate space for the scans and lines.
allocate_buffer(scan_buffer_, scan_buffer_name_, scan_vertex_array_);
@@ -101,265 +95,33 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) :
test_gl(glBlendFunc, GL_SRC_ALPHA, GL_CONSTANT_COLOR);
test_gl(glBlendColor, 0.4f, 0.4f, 0.4f, 1.0f);
// Establish initial state for the two atomic flags.
is_updating_.clear();
// Establish initial state for is_drawing_to_accumulation_buffer_.
is_drawing_to_accumulation_buffer_.clear();
}
ScanTarget::~ScanTarget() {
while(is_updating_.test_and_set());
glDeleteBuffers(1, &scan_buffer_name_);
glDeleteTextures(1, &write_area_texture_name_);
glDeleteVertexArrays(1, &scan_vertex_array_);
perform([=] {
glDeleteBuffers(1, &scan_buffer_name_);
glDeleteTextures(1, &write_area_texture_name_);
glDeleteVertexArrays(1, &scan_vertex_array_);
});
}
void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) {
while(is_updating_.test_and_set());
target_framebuffer_ = target_framebuffer;
is_updating_.clear();
}
void ScanTarget::set_modals(Modals modals) {
// Don't change the modals while drawing is ongoing; a previous set might be
// in the process of being established.
while(is_updating_.test_and_set());
modals_ = modals;
modals_are_dirty_ = true;
is_updating_.clear();
}
Outputs::Display::ScanTarget::Scan *ScanTarget::begin_scan() {
if(allocation_has_failed_) return nullptr;
std::lock_guard lock_guard(write_pointers_mutex_);
const auto result = &scan_buffer_[write_pointers_.scan_buffer];
const auto read_pointers = read_pointers_.load();
// Advance the pointer.
const auto next_write_pointer = decltype(write_pointers_.scan_buffer)((write_pointers_.scan_buffer + 1) % scan_buffer_.size());
// Check whether that's too many.
if(next_write_pointer == read_pointers.scan_buffer) {
allocation_has_failed_ = true;
return nullptr;
}
write_pointers_.scan_buffer = next_write_pointer;
++provided_scans_;
// Fill in extra OpenGL-specific details.
result->line = write_pointers_.line;
vended_scan_ = result;
return &result->scan;
}
void ScanTarget::end_scan() {
if(vended_scan_) {
std::lock_guard lock_guard(write_pointers_mutex_);
vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_);
vended_scan_->line = write_pointers_.line;
vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_);
vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_);
#ifdef LOG_SCANS
if(vended_scan_->scan.composite_amplitude) {
std::cout << "S: ";
std::cout << vended_scan_->scan.end_points[0].composite_angle << "/" << vended_scan_->scan.end_points[0].data_offset << "/" << vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace << " -> ";
std::cout << vended_scan_->scan.end_points[1].composite_angle << "/" << vended_scan_->scan.end_points[1].data_offset << "/" << vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace << " => ";
std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].data_offset - vended_scan_->scan.end_points[0].data_offset) * 64.0f) << "/";
std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace - vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f);
std::cout << std::endl;
}
#endif
}
vended_scan_ = nullptr;
}
uint8_t *ScanTarget::begin_data(size_t required_length, size_t required_alignment) {
assert(required_alignment);
if(allocation_has_failed_) return nullptr;
std::lock_guard lock_guard(write_pointers_mutex_);
if(write_area_texture_.empty()) {
allocation_has_failed_ = true;
return nullptr;
}
// Determine where the proposed write area would start and end.
uint16_t output_y = TextureAddressGetY(write_pointers_.write_area);
uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1;
aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment);
uint16_t end_x = aligned_start_x + uint16_t(1 + required_length);
if(end_x > WriteAreaWidth) {
output_y = (output_y + 1) % WriteAreaHeight;
aligned_start_x = uint16_t(required_alignment);
end_x = aligned_start_x + uint16_t(1 + required_length);
}
// Check whether that steps over the read pointer.
const auto end_address = TextureAddress(end_x, output_y);
const auto read_pointers = read_pointers_.load();
const auto end_distance = TextureSub(end_address, read_pointers.write_area);
const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area);
// If allocating this would somehow make the write pointer back away from the read pointer,
// there must not be enough space left.
if(end_distance < previous_distance) {
allocation_has_failed_ = true;
return nullptr;
}
// Everything checks out, note expectation of a future end_data and return the pointer.
data_is_allocated_ = true;
vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y);
assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= write_area_texture_.size());
return &write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_];
// Note state at exit:
// write_pointers_.write_area points to the first pixel the client is expected to draw to.
}
void ScanTarget::end_data(size_t actual_length) {
if(allocation_has_failed_ || !data_is_allocated_) return;
std::lock_guard lock_guard(write_pointers_mutex_);
// Bookend the start of the new data, to safeguard for precision errors in sampling.
memcpy(
&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_],
data_type_size_);
// Advance to the end of the current run.
write_pointers_.write_area += actual_length + 1;
// Also bookend the end.
memcpy(
&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_texture_[size_t(write_pointers_.write_area - 2) * data_type_size_],
data_type_size_);
// The write area was allocated in the knowledge that there's sufficient
// distance left on the current line, but there's a risk of exactly filling
// the final line, in which case this should wrap back to 0.
write_pointers_.write_area %= (write_area_texture_.size() / data_type_size_);
// Record that no further end_data calls are expected.
data_is_allocated_ = false;
}
void ScanTarget::will_change_owner() {
allocation_has_failed_ = true;
vended_scan_ = nullptr;
}
void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) {
// Forward the event to the display metrics tracker.
display_metrics_.announce_event(event);
if(event == ScanTarget::Event::EndVerticalRetrace) {
// The previous-frame-is-complete flag is subject to a two-slot queue because
// measurement for *this* frame needs to begin now, meaning that the previous
// result needs to be put somewhere — it'll be attached to the first successful
// line output.
is_first_in_frame_ = true;
previous_frame_was_complete_ = frame_is_complete_;
frame_is_complete_ = true;
}
if(output_is_visible_ == is_visible) return;
if(is_visible) {
const auto read_pointers = read_pointers_.load();
std::lock_guard lock_guard(write_pointers_mutex_);
// Commit the most recent line only if any scans fell on it.
// Otherwise there's no point outputting it, it'll contribute nothing.
if(provided_scans_) {
// Store metadata if concluding a previous line.
if(active_line_) {
line_metadata_buffer_[size_t(write_pointers_.line)].is_first_in_frame = is_first_in_frame_;
line_metadata_buffer_[size_t(write_pointers_.line)].previous_frame_was_complete = previous_frame_was_complete_;
is_first_in_frame_ = false;
}
// Attempt to allocate a new line; note allocation failure if necessary.
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line == read_pointers.line) {
allocation_has_failed_ = true;
active_line_ = nullptr;
} else {
write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)];
}
provided_scans_ = 0;
}
if(active_line_) {
active_line_->end_points[0].x = location.x;
active_line_->end_points[0].y = location.y;
active_line_->end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line_->end_points[0].composite_angle = location.composite_angle;
active_line_->line = write_pointers_.line;
active_line_->composite_amplitude = composite_amplitude;
}
} else {
if(active_line_) {
// A successfully-allocated line is ending.
active_line_->end_points[1].x = location.x;
active_line_->end_points[1].y = location.y;
active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line_->end_points[1].composite_angle = location.composite_angle;
#ifdef LOG_LINES
if(active_line_->composite_amplitude) {
std::cout << "L: ";
std::cout << active_line_->end_points[0].composite_angle << "/" << active_line_->end_points[0].cycles_since_end_of_horizontal_retrace << " -> ";
std::cout << active_line_->end_points[1].composite_angle << "/" << active_line_->end_points[1].cycles_since_end_of_horizontal_retrace << " => ";
std::cout << (active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) << "/" << (active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) << " => ";
std::cout << double(active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) / (double(active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f);
std::cout << std::endl;
}
#endif
}
// A line is complete; submit latest updates if nothing failed.
if(allocation_has_failed_) {
// Reset all pointers to where they were; this also means
// the stencil won't be properly populated.
write_pointers_ = submit_pointers_.load();
frame_is_complete_ = false;
} else {
// Advance submit pointer.
submit_pointers_.store(write_pointers_);
}
allocation_has_failed_ = false;
}
output_is_visible_ = is_visible;
perform([=] {
target_framebuffer_ = target_framebuffer;
});
}
void ScanTarget::setup_pipeline() {
const auto data_type_size = Outputs::Display::size_for_data_type(modals_.input_data_type);
auto modals = BufferingScanTarget::modals();
const auto data_type_size = Outputs::Display::size_for_data_type(modals.input_data_type);
// Ensure the lock guard here has a restricted scope; this is the only time that a thread
// other than the main owner of write_pointers_ may adjust it.
{
std::lock_guard lock_guard(write_pointers_mutex_);
if(data_type_size != data_type_size_) {
// TODO: flush output.
data_type_size_ = data_type_size;
write_area_texture_.resize(WriteAreaWidth*WriteAreaHeight*data_type_size_);
write_pointers_.scan_buffer = 0;
write_pointers_.write_area = 0;
}
// Resize the texture only if required.
const size_t required_size = WriteAreaWidth*WriteAreaHeight*data_type_size;
if(required_size != write_area_data_size()) {
write_area_texture_.resize(required_size);
set_write_area(write_area_texture_.data());
}
// Prepare to bind line shaders.
@@ -367,7 +129,7 @@ void ScanTarget::setup_pipeline() {
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Destroy or create a QAM buffer and shader, if appropriate.
const bool needs_qam_buffer = (modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::SVideo);
const bool needs_qam_buffer = (modals.display_type == DisplayType::CompositeColour || modals.display_type == DisplayType::SVideo);
if(needs_qam_buffer) {
if(!qam_chroma_texture_) {
qam_chroma_texture_ = std::make_unique<TextureTarget>(LineBufferWidth, LineBufferHeight, QAMChromaTextureUnit, GL_NEAREST, false);
@@ -386,8 +148,8 @@ void ScanTarget::setup_pipeline() {
output_shader_ = conversion_shader();
enable_vertex_attributes(ShaderType::Conversion, *output_shader_);
set_uniforms(ShaderType::Conversion, *output_shader_);
output_shader_->set_uniform("origin", modals_.visible_area.origin.x, modals_.visible_area.origin.y);
output_shader_->set_uniform("size", modals_.visible_area.size.width, modals_.visible_area.size.height);
output_shader_->set_uniform("origin", modals.visible_area.origin.x, modals.visible_area.origin.y);
output_shader_->set_uniform("size", modals.visible_area.size.width, modals.visible_area.size.height);
output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0));
output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0));
@@ -400,17 +162,14 @@ void ScanTarget::setup_pipeline() {
input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0));
}
Outputs::Display::Metrics &ScanTarget::display_metrics() {
return display_metrics_;
}
bool ScanTarget::is_soft_display_type() {
return modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::CompositeMonochrome;
const auto display_type = modals().display_type;
return display_type == DisplayType::CompositeColour || display_type == DisplayType::CompositeMonochrome;
}
void ScanTarget::update(int, int output_height) {
// If the GPU is still busy, don't wait; we'll catch it next time.
if(fence_ != nullptr) {
// if the GPU is still busy, don't wait; we'll catch it next time
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {
display_metrics_.announce_draw_status(
lines_submitted_,
@@ -420,322 +179,314 @@ void ScanTarget::update(int, int output_height) {
}
fence_ = nullptr;
}
// Update the display metrics.
display_metrics_.announce_draw_status(
lines_submitted_,
std::chrono::high_resolution_clock::now() - line_submission_begin_time_,
true);
// Spin until the is-drawing flag is reset; the wait sync above will deal
// with instances where waiting is inappropriate.
while(is_updating_.test_and_set());
// Grab the new output list.
perform([=] {
OutputArea area = get_output_area();
// Establish the pipeline if necessary.
const bool did_setup_pipeline = modals_are_dirty_;
if(modals_are_dirty_) {
setup_pipeline();
modals_are_dirty_ = false;
}
// Determine the start time of this submission group.
line_submission_begin_time_ = std::chrono::high_resolution_clock::now();
// Grab the current read and submit pointers.
const auto submit_pointers = submit_pointers_.load();
const auto read_pointers = read_pointers_.load();
// Determine how many lines are about to be submitted.
lines_submitted_ = (read_pointers.line + line_buffer_.size() - submit_pointers.line) % line_buffer_.size();
// Submit scans; only the new ones need to be communicated.
size_t new_scans = (submit_pointers.scan_buffer + scan_buffer_.size() - read_pointers.scan_buffer) % scan_buffer_.size();
if(new_scans) {
test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_);
// Map only the required portion of the buffer.
const size_t new_scans_size = new_scans * sizeof(Scan);
uint8_t *const destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
test_gl_error();
if(read_pointers.scan_buffer < submit_pointers.scan_buffer) {
memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], new_scans_size);
} else {
const size_t first_portion_length = (scan_buffer_.size() - read_pointers.scan_buffer) * sizeof(Scan);
memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], first_portion_length);
memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length);
// Establish the pipeline if necessary.
const auto new_modals = BufferingScanTarget::new_modals();
const bool did_setup_pipeline = bool(new_modals);
if(did_setup_pipeline) {
setup_pipeline();
}
// Flush and unmap the buffer.
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Determine the start time of this submission group and the number of lines it will contain.
line_submission_begin_time_ = std::chrono::high_resolution_clock::now();
lines_submitted_ = (area.end.line - area.start.line + line_buffer_.size()) % line_buffer_.size();
// Submit texture.
if(submit_pointers.write_area != read_pointers.write_area) {
test_gl(glActiveTexture, SourceDataTextureUnit);
test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_);
// Submit scans; only the new ones need to be communicated.
size_t new_scans = (area.end.scan - area.start.scan + scan_buffer_.size()) % scan_buffer_.size();
if(new_scans) {
test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_);
// Create storage for the texture if it doesn't yet exist; this was deferred until here
// because the pixel format wasn't initially known.
if(!texture_exists_) {
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
test_gl(glTexImage2D,
GL_TEXTURE_2D,
0,
internalFormatForDepth(data_type_size_),
WriteAreaWidth,
WriteAreaHeight,
0,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
nullptr);
texture_exists_ = true;
}
// Map only the required portion of the buffer.
const size_t new_scans_size = new_scans * sizeof(Scan);
uint8_t *const destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
test_gl_error();
const auto start_y = TextureAddressGetY(read_pointers.write_area);
const auto end_y = TextureAddressGetY(submit_pointers.write_area);
if(end_y >= start_y) {
// Submit the direct region from the submit pointer to the read pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, start_y,
WriteAreaWidth,
1 + end_y - start_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]);
} else {
// The circular buffer wrapped around; submit the data from the read pointer to the end of
// the buffer and from the start of the buffer to the submit pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, 0,
WriteAreaWidth,
1 + end_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[0]);
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, start_y,
WriteAreaWidth,
WriteAreaHeight - start_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]);
}
}
// Push new input to the unprocessed line buffer.
if(new_scans) {
unprocessed_line_texture_.bind_framebuffer();
// Clear newly-touched lines; that is everything from (read+1) to submit.
const uint16_t first_line_to_clear = (read_pointers.line+1)%line_buffer_.size();
const uint16_t final_line_to_clear = submit_pointers.line;
if(first_line_to_clear != final_line_to_clear) {
test_gl(glEnable, GL_SCISSOR_TEST);
// Determine the proper clear colour — this needs to be anything that describes black
// in the input colour encoding at use.
if(modals_.input_data_type == InputDataType::Luminance8Phase8) {
// Supply both a zero luminance and a colour-subcarrier-disengaging phase.
test_gl(glClearColor, 0.0f, 1.0f, 0.0f, 0.0f);
// Copy as a single chunk if possible; otherwise copy in two parts.
if(area.start.scan < area.end.scan) {
memcpy(destination, &scan_buffer_[size_t(area.start.scan)], new_scans_size);
} else {
test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
const size_t first_portion_length = (scan_buffer_.size() - area.start.scan) * sizeof(Scan);
memcpy(destination, &scan_buffer_[area.start.scan], first_portion_length);
memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length);
}
if(first_line_to_clear < final_line_to_clear) {
test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
// Flush and unmap the buffer.
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Submit texture.
if(area.start.write_area_x != area.end.write_area_x || area.start.write_area_y != area.end.write_area_y) {
test_gl(glActiveTexture, SourceDataTextureUnit);
test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_);
// Create storage for the texture if it doesn't yet exist; this was deferred until here
// because the pixel format wasn't initially known.
if(!texture_exists_) {
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
test_gl(glTexImage2D,
GL_TEXTURE_2D,
0,
internalFormatForDepth(write_area_data_size()),
WriteAreaWidth,
WriteAreaHeight,
0,
formatForDepth(write_area_data_size()),
GL_UNSIGNED_BYTE,
nullptr);
texture_exists_ = true;
}
if(area.end.write_area_y >= area.start.write_area_y) {
// Submit the direct region from the submit pointer to the read pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, area.start.write_area_y,
WriteAreaWidth,
1 + area.end.write_area_y - area.start.write_area_y,
formatForDepth(write_area_data_size()),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]);
} else {
test_gl(glScissor, 0, 0, unprocessed_line_texture_.get_width(), final_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
// The circular buffer wrapped around; submit the data from the read pointer to the end of
// the buffer and from the start of the buffer to the submit pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, area.start.write_area_y,
WriteAreaWidth,
WriteAreaHeight - area.start.write_area_y,
formatForDepth(write_area_data_size()),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]);
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, 0,
WriteAreaWidth,
1 + area.end.write_area_y,
formatForDepth(write_area_data_size()),
GL_UNSIGNED_BYTE,
&write_area_texture_[0]);
}
test_gl(glDisable, GL_SCISSOR_TEST);
}
// Apply new spans. They definitely always go to the first buffer.
test_gl(glBindVertexArray, scan_vertex_array_);
input_shader_->bind();
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans));
}
// Push new input to the unprocessed line buffer.
if(new_scans) {
unprocessed_line_texture_.bind_framebuffer();
// Logic for reducing resolution: start doing so if the metrics object reports that
// it's a good idea. Go up to a quarter of the requested resolution, subject to
// clamping at each stage. If the output resolution changes, or anything else about
// the output pipeline, just start trying the highest size again.
if(display_metrics_.should_lower_resolution() && is_soft_display_type()) {
resolution_reduction_level_ = std::min(resolution_reduction_level_+1, 4);
}
if(output_height_ != output_height || did_setup_pipeline) {
resolution_reduction_level_ = 1;
output_height_ = output_height;
}
// Clear newly-touched lines; that is everything from (read+1) to submit.
const auto first_line_to_clear = GLsizei((area.start.line+1)%line_buffer_.size());
const auto final_line_to_clear = GLsizei(area.end.line);
if(first_line_to_clear != final_line_to_clear) {
test_gl(glEnable, GL_SCISSOR_TEST);
// Ensure the accumulation buffer is properly sized, allowing for the metrics object's
// feelings about whether too high a resolution is being used.
const int framebuffer_height = std::max(output_height / resolution_reduction_level_, std::min(540, output_height));
const int proportional_width = (framebuffer_height * 4) / 3;
const bool did_create_accumulation_texture = !accumulation_texture_ || ( (accumulation_texture_->get_width() != proportional_width || accumulation_texture_->get_height() != framebuffer_height));
// Work with the accumulation_buffer_ potentially starts from here onwards; set its flag.
while(is_drawing_to_accumulation_buffer_.test_and_set());
if(did_create_accumulation_texture) {
LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height);
display_metrics_.announce_did_resize();
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(
new TextureTarget(
GLsizei(proportional_width),
GLsizei(framebuffer_height),
AccumulationTextureUnit,
GL_NEAREST,
true));
if(accumulation_texture_) {
new_framebuffer->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
test_gl(glActiveTexture, AccumulationTextureUnit);
accumulation_texture_->bind_texture();
accumulation_texture_->draw(4.0f / 3.0f);
test_gl(glClear, GL_STENCIL_BUFFER_BIT);
new_framebuffer->bind_texture();
}
accumulation_texture_ = std::move(new_framebuffer);
// In the absence of a way to resize a stencil buffer, just mark
// what's currently present as invalid to avoid an improper clear
// for this frame.
stencil_is_valid_ = false;
}
if(did_setup_pipeline || did_create_accumulation_texture) {
set_sampling_window(proportional_width, framebuffer_height, *output_shader_);
}
// Figure out how many new lines are ready.
uint16_t new_lines = (submit_pointers.line + LineBufferHeight - read_pointers.line) % LineBufferHeight;
if(new_lines) {
// Prepare to output lines.
test_gl(glBindVertexArray, line_vertex_array_);
// Bind the accumulation framebuffer, unless there's going to be QAM work first.
if(!qam_separation_shader_ || line_metadata_buffer_[read_pointers.line].is_first_in_frame) {
accumulation_texture_->bind_framebuffer();
output_shader_->bind();
// Enable blending and stenciling.
test_gl(glEnable, GL_BLEND);
test_gl(glEnable, GL_STENCIL_TEST);
}
// Set the proper stencil function regardless.
test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0));
test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR);
// Prepare to upload data that will consitute lines.
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Divide spans by which frame they're in.
uint16_t start_line = read_pointers.line;
while(new_lines) {
uint16_t end_line = (start_line + 1) % LineBufferHeight;
// Find the limit of spans to draw in this cycle.
size_t lines = 1;
while(end_line != submit_pointers.line && !line_metadata_buffer_[end_line].is_first_in_frame) {
end_line = (end_line + 1) % LineBufferHeight;
++lines;
}
// If this is start-of-frame, clear any untouched pixels and flush the stencil buffer
if(line_metadata_buffer_[start_line].is_first_in_frame) {
if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) {
full_display_rectangle_.draw(0.0f, 0.0f, 0.0f);
// Determine the proper clear colour — this needs to be anything that describes black
// in the input colour encoding at use.
if(modals().input_data_type == InputDataType::Luminance8Phase8) {
// Supply both a zero luminance and a colour-subcarrier-disengaging phase.
test_gl(glClearColor, 0.0f, 1.0f, 0.0f, 0.0f);
} else {
test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
}
stencil_is_valid_ = true;
if(first_line_to_clear < final_line_to_clear) {
test_gl(glScissor, GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
} else {
test_gl(glScissor, GLint(0), GLint(0), unprocessed_line_texture_.get_width(), final_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
test_gl(glScissor, GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
}
test_gl(glDisable, GL_SCISSOR_TEST);
}
// Apply new spans. They definitely always go to the first buffer.
test_gl(glBindVertexArray, scan_vertex_array_);
input_shader_->bind();
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans));
}
// Logic for reducing resolution: start doing so if the metrics object reports that
// it's a good idea. Go up to a quarter of the requested resolution, subject to
// clamping at each stage. If the output resolution changes, or anything else about
// the output pipeline, just start trying the highest size again.
if(display_metrics_.should_lower_resolution() && is_soft_display_type()) {
resolution_reduction_level_ = std::min(resolution_reduction_level_+1, 4);
}
if(output_height_ != output_height || did_setup_pipeline) {
resolution_reduction_level_ = 1;
output_height_ = output_height;
}
// Ensure the accumulation buffer is properly sized, allowing for the metrics object's
// feelings about whether too high a resolution is being used.
const int framebuffer_height = std::max(output_height / resolution_reduction_level_, std::min(540, output_height));
const int proportional_width = (framebuffer_height * 4) / 3;
const bool did_create_accumulation_texture = !accumulation_texture_ || ( (accumulation_texture_->get_width() != proportional_width || accumulation_texture_->get_height() != framebuffer_height));
// Work with the accumulation_buffer_ potentially starts from here onwards; set its flag.
while(is_drawing_to_accumulation_buffer_.test_and_set());
if(did_create_accumulation_texture) {
LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height);
display_metrics_.announce_did_resize();
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(
new TextureTarget(
GLsizei(proportional_width),
GLsizei(framebuffer_height),
AccumulationTextureUnit,
GL_NEAREST,
true));
if(accumulation_texture_) {
new_framebuffer->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
test_gl(glActiveTexture, AccumulationTextureUnit);
accumulation_texture_->bind_texture();
accumulation_texture_->draw(4.0f / 3.0f);
test_gl(glClear, GL_STENCIL_BUFFER_BIT);
// Rebind the program for span output.
test_gl(glBindVertexArray, line_vertex_array_);
if(!qam_separation_shader_) {
output_shader_->bind();
}
new_framebuffer->bind_texture();
}
accumulation_texture_ = std::move(new_framebuffer);
// Upload.
const auto buffer_size = lines * sizeof(Line);
if(!end_line || end_line > start_line) {
test_gl(glBufferSubData, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]);
} else {
uint8_t *destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
assert(destination);
test_gl_error();
// In the absence of a way to resize a stencil buffer, just mark
// what's currently present as invalid to avoid an improper clear
// for this frame.
stencil_is_valid_ = false;
}
const size_t buffer_length = line_buffer_.size() * sizeof(Line);
const size_t start_position = start_line * sizeof(Line);
memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position);
memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line));
if(did_setup_pipeline || did_create_accumulation_texture) {
set_sampling_window(proportional_width, framebuffer_height, *output_shader_);
}
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Produce colour information, if required.
if(qam_separation_shader_) {
qam_separation_shader_->bind();
qam_chroma_texture_->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT); // TODO: this is here as a hint that the old framebuffer doesn't need reloading;
// test whether that's a valid optimisation on desktop OpenGL.
test_gl(glDisable, GL_BLEND);
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
// Figure out how many new lines are ready.
auto new_lines = (area.end.line - area.start.line + LineBufferHeight) % LineBufferHeight;
if(new_lines) {
// Prepare to output lines.
test_gl(glBindVertexArray, line_vertex_array_);
// Bind the accumulation framebuffer, unless there's going to be QAM work first.
if(!qam_separation_shader_ || line_metadata_buffer_[area.start.line].is_first_in_frame) {
accumulation_texture_->bind_framebuffer();
output_shader_->bind();
// Enable blending and stenciling.
test_gl(glEnable, GL_BLEND);
test_gl(glEnable, GL_STENCIL_TEST);
}
// Render to the output.
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
// Set the proper stencil function regardless.
test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0));
test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR);
start_line = end_line;
new_lines -= lines;
// Prepare to upload data that will consitute lines.
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Divide spans by which frame they're in.
auto start_line = area.start.line;
while(new_lines) {
uint16_t end_line = (start_line + 1) % LineBufferHeight;
// Find the limit of spans to draw in this cycle.
size_t lines = 1;
while(end_line != area.end.line && !line_metadata_buffer_[end_line].is_first_in_frame) {
end_line = (end_line + 1) % LineBufferHeight;
++lines;
}
// If this is start-of-frame, clear any untouched pixels and flush the stencil buffer
if(line_metadata_buffer_[start_line].is_first_in_frame) {
if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) {
full_display_rectangle_.draw(0.0f, 0.0f, 0.0f);
}
stencil_is_valid_ = true;
test_gl(glClear, GL_STENCIL_BUFFER_BIT);
// Rebind the program for span output.
test_gl(glBindVertexArray, line_vertex_array_);
if(!qam_separation_shader_) {
output_shader_->bind();
}
}
// Upload.
const auto buffer_size = lines * sizeof(Line);
if(!end_line || end_line > start_line) {
test_gl(glBufferSubData, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]);
} else {
uint8_t *destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
assert(destination);
test_gl_error();
const size_t buffer_length = line_buffer_.size() * sizeof(Line);
const size_t start_position = start_line * sizeof(Line);
memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position);
memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line));
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Produce colour information, if required.
if(qam_separation_shader_) {
qam_separation_shader_->bind();
qam_chroma_texture_->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT); // TODO: this is here as a hint that the old framebuffer doesn't need reloading;
// test whether that's a valid optimisation on desktop OpenGL.
test_gl(glDisable, GL_BLEND);
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
accumulation_texture_->bind_framebuffer();
output_shader_->bind();
test_gl(glEnable, GL_BLEND);
test_gl(glEnable, GL_STENCIL_TEST);
}
// Render to the output.
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
start_line = end_line;
new_lines -= lines;
}
// Disable blending and the stencil test again.
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDisable, GL_BLEND);
}
// Disable blending and the stencil test again.
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDisable, GL_BLEND);
}
// That's it for operations affecting the accumulation buffer.
is_drawing_to_accumulation_buffer_.clear();
// That's it for operations affecting the accumulation buffer.
is_drawing_to_accumulation_buffer_.clear();
// All data now having been spooled to the GPU, update the read pointers to
// the submit pointer location.
read_pointers_.store(submit_pointers);
// Grab a fence sync object to avoid busy waiting upon the next extry into this
// function, and reset the is_updating_ flag.
fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
is_updating_.clear();
// Grab a fence sync object to avoid busy waiting upon the next extry into this
// function, and reset the is_updating_ flag.
fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
complete_output_area(area);
});
}
void ScanTarget::draw(int output_width, int output_height) {
while(is_drawing_to_accumulation_buffer_.test_and_set());
while(is_drawing_to_accumulation_buffer_.test_and_set(std::memory_order_acquire));
if(accumulation_texture_) {
// Copy the accumulation texture to the target.
@@ -748,5 +499,5 @@ void ScanTarget::draw(int output_width, int output_height) {
accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f);
}
is_drawing_to_accumulation_buffer_.clear();
is_drawing_to_accumulation_buffer_.clear(std::memory_order_release);
}

View File

@@ -11,7 +11,7 @@
#include "../Log.hpp"
#include "../DisplayMetrics.hpp"
#include "../ScanTarget.hpp"
#include "../ScanTargets/BufferingScanTarget.hpp"
#include "OpenGL.hpp"
#include "Primitives/TextureTarget.hpp"
@@ -32,12 +32,13 @@ namespace Outputs {
namespace Display {
namespace OpenGL {
/*!
Provides a ScanTarget that uses OpenGL to render its output;
this uses various internal buffers so that the only geometry
drawn to the target framebuffer is a quad.
*/
class ScanTarget: public Outputs::Display::ScanTarget {
class ScanTarget: public Outputs::Display::BufferingScanTarget { // TODO: use private inheritance and expose only display_metrics() and a custom cast?
public:
ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f);
~ScanTarget();
@@ -49,10 +50,10 @@ class ScanTarget: public Outputs::Display::ScanTarget {
/*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */
void update(int output_width, int output_height);
/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */
Metrics &display_metrics();
private:
static constexpr int LineBufferWidth = 2048;
static constexpr int LineBufferHeight = 2048;
#ifndef NDEBUG
struct OpenGLVersionDumper {
OpenGLVersionDumper() {
@@ -62,93 +63,15 @@ class ScanTarget: public Outputs::Display::ScanTarget {
} dumper_;
#endif
static constexpr int WriteAreaWidth = 2048;
static constexpr int WriteAreaHeight = 2048;
static constexpr int LineBufferWidth = 2048;
static constexpr int LineBufferHeight = 2048;
GLuint target_framebuffer_;
const float output_gamma_;
// Outputs::Display::ScanTarget finals.
void set_modals(Modals) final;
Scan *begin_scan() final;
void end_scan() final;
uint8_t *begin_data(size_t required_length, size_t required_alignment) final;
void end_data(size_t actual_length) final;
void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final;
void will_change_owner() final;
bool output_is_visible_ = false;
Metrics display_metrics_;
int resolution_reduction_level_ = 1;
int output_height_ = 0;
size_t lines_submitted_ = 0;
std::chrono::high_resolution_clock::time_point line_submission_begin_time_;
// Extends the definition of a Scan to include two extra fields,
// relevant to the way that this scan target processes video.
struct Scan {
Outputs::Display::ScanTarget::Scan scan;
/// Stores the y coordinate that this scan's data is at, within the write area texture.
uint16_t data_y;
/// Stores the y coordinate of this scan within the line buffer.
uint16_t line;
};
struct PointerSet {
// This constructor is here to appease GCC's interpretation of
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
PointerSet() noexcept {}
// The sizes below might be less hassle as something more natural like ints,
// but squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64.
int write_area = 1; // By convention this points to the vended area. Which is preceded by a guard pixel. So a sensible default construction is write_area = 1.
uint16_t scan_buffer = 0;
uint16_t line = 0;
};
/// A pointer to the next thing that should be provided to the caller for data.
PointerSet write_pointers_;
/// A mutex for gettng access to write_pointers_; access to write_pointers_,
/// data_type_size_ or write_area_texture_ is almost never contended, so this
/// is cheap for the main use case.
std::mutex write_pointers_mutex_;
/// A pointer to the final thing currently cleared for submission.
std::atomic<PointerSet> submit_pointers_;
/// A pointer to the first thing not yet submitted for display.
std::atomic<PointerSet> read_pointers_;
/// Maintains a buffer of the most recent scans.
std::array<Scan, 16384> scan_buffer_;
// Maintains a list of composite scan buffer coordinates; the Line struct
// is transported to the GPU in its entirety; the LineMetadatas live in CPU
// space only.
struct Line {
struct EndPoint {
uint16_t x, y;
uint16_t cycles_since_end_of_horizontal_retrace;
int16_t composite_angle;
} end_points[2];
uint16_t line;
uint8_t composite_amplitude;
};
struct LineMetadata {
bool is_first_in_frame;
bool previous_frame_was_complete;
};
std::array<Line, LineBufferHeight> line_buffer_;
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
// Contains the first composition of scans into lines;
// they're accumulated prior to output to allow for continuous
// application of any necessary conversions — e.g. composite processing.
@@ -164,13 +87,6 @@ class ScanTarget: public Outputs::Display::ScanTarget {
Rectangle full_display_rectangle_;
bool stencil_is_valid_ = false;
// Ephemeral state that helps in line composition.
Line *active_line_ = nullptr;
int provided_scans_ = 0;
bool is_first_in_frame_ = true;
bool frame_is_complete_ = true;
bool previous_frame_was_complete_ = true;
// OpenGL storage handles for buffer data.
GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0;
GLuint line_buffer_name_ = 0, line_vertex_array_ = 0;
@@ -178,24 +94,10 @@ class ScanTarget: public Outputs::Display::ScanTarget {
template <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name);
template <typename T> void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer);
// Uses a texture to vend write areas.
std::vector<uint8_t> write_area_texture_;
size_t data_type_size_ = 0;
GLuint write_area_texture_name_ = 0;
bool texture_exists_ = false;
// Ephemeral information for the begin/end functions.
Scan *vended_scan_ = nullptr;
int vended_write_area_pointer_ = 0;
// Track allocation failures.
bool data_is_allocated_ = false;
bool allocation_has_failed_ = false;
// Receives scan target modals.
Modals modals_;
bool modals_are_dirty_ = false;
void setup_pipeline();
enum class ShaderType {
@@ -213,14 +115,12 @@ class ScanTarget: public Outputs::Display::ScanTarget {
std::vector<std::string> bindings(ShaderType type) const;
GLsync fence_ = nullptr;
std::atomic_flag is_updating_;
std::atomic_flag is_drawing_to_accumulation_buffer_;
std::unique_ptr<Shader> input_shader_;
std::unique_ptr<Shader> output_shader_;
std::unique_ptr<Shader> qam_separation_shader_;
/*!
Produces a shader that composes fragment of the input stream to a single buffer,
normalising the data into one of four forms: RGB, 8-bit luminance,
@@ -248,6 +148,12 @@ class ScanTarget: public Outputs::Display::ScanTarget {
contrast tends to be low, such as a composite colour display.
*/
bool is_soft_display_type();
// Storage for the various buffers.
std::vector<uint8_t> write_area_texture_;
std::array<Scan, LineBufferHeight*5> scan_buffer_;
std::array<Line, LineBufferHeight> line_buffer_;
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
};
}

View File

@@ -23,14 +23,15 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const {
// converge even allowing for the fact that they may not be spaced by exactly
// the expected distance. Cf. the stencil-powered logic for making sure all
// pixels are painted only exactly once per field.
const auto modals = BufferingScanTarget::modals();
switch(type) {
case ShaderType::Composition: break;
default:
target.set_uniform("rowHeight", GLfloat(1.05f / modals_.expected_vertical_lines));
target.set_uniform("scale", GLfloat(modals_.output_scale.x), GLfloat(modals_.output_scale.y) * modals_.aspect_ratio * (3.0f / 4.0f));
target.set_uniform("phaseOffset", GLfloat(modals_.input_data_tweaks.phase_linked_luminance_offset));
target.set_uniform("rowHeight", GLfloat(1.05f / modals.expected_vertical_lines));
target.set_uniform("scale", GLfloat(modals.output_scale.x), GLfloat(modals.output_scale.y) * modals.aspect_ratio * (3.0f / 4.0f));
target.set_uniform("phaseOffset", GLfloat(modals.input_data_tweaks.phase_linked_luminance_offset));
const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator);
const float clocks_per_angle = float(modals.cycles_per_line) * float(modals.colour_cycle_denominator) / float(modals.colour_cycle_numerator);
GLfloat texture_offsets[4];
GLfloat angles[4];
for(int c = 0; c < 4; ++c) {
@@ -41,7 +42,7 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const {
target.set_uniform("textureCoordinateOffsets", 1, 4, texture_offsets);
target.set_uniform("compositeAngleOffsets", 4, 1, angles);
switch(modals_.composite_colour_space) {
switch(modals.composite_colour_space) {
case ColourSpace::YIQ: {
const GLfloat rgbToYIQ[] = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f};
const GLfloat yiqToRGB[] = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f};
@@ -61,9 +62,10 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const {
}
void ScanTarget::set_sampling_window(int output_width, int, Shader &target) {
if(modals_.display_type != DisplayType::CompositeColour) {
const float one_pixel_width = float(modals_.cycles_per_line) * modals_.visible_area.size.width / float(output_width);
const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator);
const auto modals = BufferingScanTarget::modals();
if(modals.display_type != DisplayType::CompositeColour) {
const float one_pixel_width = float(modals.cycles_per_line) * modals.visible_area.size.width / float(output_width);
const float clocks_per_angle = float(modals.cycles_per_line) * float(modals.colour_cycle_denominator) / float(modals.colour_cycle_numerator);
GLfloat texture_offsets[4];
GLfloat angles[4];
for(int c = 0; c < 4; ++c) {
@@ -191,8 +193,9 @@ std::vector<std::string> ScanTarget::bindings(ShaderType type) const {
std::string ScanTarget::sampling_function() const {
std::string fragment_shader;
const auto modals = BufferingScanTarget::modals();
if(modals_.display_type == DisplayType::SVideo) {
if(modals.display_type == DisplayType::SVideo) {
fragment_shader +=
"vec2 svideo_sample(vec2 coordinate, float angle) {";
} else {
@@ -200,8 +203,8 @@ std::string ScanTarget::sampling_function() const {
"float composite_sample(vec2 coordinate, float angle) {";
}
const bool is_svideo = modals_.display_type == DisplayType::SVideo;
switch(modals_.input_data_type) {
const bool is_svideo = modals.display_type == DisplayType::SVideo;
switch(modals.input_data_type) {
case InputDataType::Luminance1:
case InputDataType::Luminance8:
// Easy, just copy across.
@@ -255,6 +258,8 @@ std::string ScanTarget::sampling_function() const {
}
std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
const auto modals = BufferingScanTarget::modals();
// Compose a vertex shader. If the display type is RGB, generate just the proper
// geometry position, plus a solitary textureCoordinate.
//
@@ -301,7 +306,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
"out vec4 fragColour;";
if(modals_.display_type != DisplayType::RGB) {
if(modals.display_type != DisplayType::RGB) {
vertex_shader +=
"out float compositeAngle;"
"out float compositeAmplitude;"
@@ -316,7 +321,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
"uniform vec4 compositeAngleOffsets;";
}
if(modals_.display_type == DisplayType::SVideo || modals_.display_type == DisplayType::CompositeColour) {
if(modals.display_type == DisplayType::SVideo || modals.display_type == DisplayType::CompositeColour) {
vertex_shader += "out vec2 qamTextureCoordinates[4];";
fragment_shader += "in vec2 qamTextureCoordinates[4];";
}
@@ -332,7 +337,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
"gl_Position = vec4(eyePosition, 0.0, 1.0);";
// For everything other than RGB, calculate the two composite outputs.
if(modals_.display_type != DisplayType::RGB) {
if(modals.display_type != DisplayType::RGB) {
vertex_shader +=
"compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;"
"compositeAmplitude = lineCompositeAmplitude / 255.0;"
@@ -346,7 +351,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
"textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);";
if((modals_.display_type == DisplayType::SVideo) || (modals_.display_type == DisplayType::CompositeColour)) {
if((modals.display_type == DisplayType::SVideo) || (modals.display_type == DisplayType::CompositeColour)) {
vertex_shader +=
"float centreCompositeAngle = abs(mix(startCompositeAngle, endCompositeAngle, lateral)) * 4.0 / 64.0;"
"centreCompositeAngle = floor(centreCompositeAngle);"
@@ -360,7 +365,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
// Compose a fragment shader.
if(modals_.display_type != DisplayType::RGB) {
if(modals.display_type != DisplayType::RGB) {
fragment_shader +=
"uniform mat3 lumaChromaToRGB;"
"uniform mat3 rgbToLumaChroma;";
@@ -372,7 +377,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
"void main(void) {"
"vec3 fragColour3;";
switch(modals_.display_type) {
switch(modals.display_type) {
case DisplayType::CompositeColour:
fragment_shader +=
"vec4 angles = compositeAngle + compositeAngleOffsets;"
@@ -460,13 +465,13 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
}
// Apply a brightness adjustment if requested.
if(fabs(modals_.brightness - 1.0f) > 0.05f) {
fragment_shader += "fragColour3 = fragColour3 * " + std::to_string(modals_.brightness) + ";";
if(fabs(modals.brightness - 1.0f) > 0.05f) {
fragment_shader += "fragColour3 = fragColour3 * " + std::to_string(modals.brightness) + ";";
}
// Apply a gamma correction if required.
if(fabs(output_gamma_ - modals_.intended_gamma) > 0.05f) {
const float gamma_ratio = output_gamma_ / modals_.intended_gamma;
if(fabs(output_gamma_ - modals.intended_gamma) > 0.05f) {
const float gamma_ratio = output_gamma_ / modals.intended_gamma;
fragment_shader += "fragColour3 = pow(fragColour3, vec3(" + std::to_string(gamma_ratio) + "));";
}
@@ -482,41 +487,44 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
}
std::unique_ptr<Shader> ScanTarget::composition_shader() const {
const auto modals = BufferingScanTarget::modals();
const std::string vertex_shader =
"#version 150\n"
R"x(#version 150
"in float startDataX;"
"in float startClock;"
in float startDataX;
in float startClock;
"in float endDataX;"
"in float endClock;"
in float endDataX;
in float endClock;
"in float dataY;"
"in float lineY;"
in float dataY;
in float lineY;
"out vec2 textureCoordinate;"
"uniform usampler2D textureName;"
out vec2 textureCoordinate;
uniform usampler2D textureName;
"void main(void) {"
"float lateral = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
void main(void) {
float lateral = float(gl_VertexID & 1);
float longitudinal = float((gl_VertexID & 2) >> 1);
"textureCoordinate = vec2(mix(startDataX, endDataX, lateral), dataY + 0.5) / textureSize(textureName, 0);"
"vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.0);"
"gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);"
"}";
textureCoordinate = vec2(mix(startDataX, endDataX, lateral), dataY + 0.5) / textureSize(textureName, 0);
vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.0);
gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);
}
)x";
std::string fragment_shader =
"#version 150\n"
R"x(#version 150
"out vec4 fragColour;"
"in vec2 textureCoordinate;"
out vec4 fragColour;
in vec2 textureCoordinate;
"uniform usampler2D textureName;"
uniform usampler2D textureName;
"void main(void) {";
void main(void) {
)x";
switch(modals_.input_data_type) {
switch(modals.input_data_type) {
case InputDataType::Luminance1:
fragment_shader += "fragColour = textureLod(textureName, textureCoordinate, 0).rrrr;";
break;
@@ -556,7 +564,8 @@ std::unique_ptr<Shader> ScanTarget::composition_shader() const {
}
std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const {
const bool is_svideo = modals_.display_type == DisplayType::SVideo;
const auto modals = BufferingScanTarget::modals();
const bool is_svideo = modals.display_type == DisplayType::SVideo;
// Sets up texture coordinates to run between startClock and endClock, mapping to
// coordinates that correlate with four times the absolute value of the composite angle.
@@ -632,7 +641,7 @@ std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const {
sampling_function() +
"void main(void) {";
if(modals_.display_type == DisplayType::SVideo) {
if(modals.display_type == DisplayType::SVideo) {
fragment_shader +=
"fragColour = vec4(svideo_sample(textureCoordinate, compositeAngle).rgg * vec3(1.0, cos(compositeAngle), sin(compositeAngle)), 1.0);";
} else {

View File

@@ -9,6 +9,7 @@
#ifndef Outputs_Display_ScanTarget_h
#define Outputs_Display_ScanTarget_h
#include <array>
#include <cstddef>
#include <cstdint>
#include "../ClockReceiver/TimeTypes.hpp"
@@ -53,6 +54,9 @@ enum class DisplayType {
/*!
Enumerates the potential formats of input data.
All types are designed to be 1, 2 or 4 bytes per pixel; this hopefully creates appropriate alignment
on all formats.
*/
enum class InputDataType {
@@ -72,8 +76,10 @@ enum class InputDataType {
// of a colour subcarrier. So they can be used to generate a luminance signal,
// or an s-video pipeline.
Luminance8Phase8, // 2 bytes/pixel; first is luminance, second is phase.
// Phase is encoded on a 192-unit circle; anything
Luminance8Phase8, // 2 bytes/pixel; first is luminance, second is phase
// of a cosine wave.
//
// Phase is encoded on a 128-unit circle; anything
// greater than 192 implies that the colour part of
// the signal should be omitted.
@@ -86,7 +92,8 @@ enum class InputDataType {
Red8Green8Blue8, // 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant.
};
inline size_t size_for_data_type(InputDataType data_type) {
/// @returns the number of bytes per sample for data of type @c data_type.
constexpr inline size_t size_for_data_type(InputDataType data_type) {
switch(data_type) {
case InputDataType::Luminance1:
case InputDataType::Luminance8:
@@ -107,7 +114,28 @@ inline size_t size_for_data_type(InputDataType data_type) {
}
}
inline DisplayType natural_display_type_for_data_type(InputDataType data_type) {
/// @returns @c true if this data type presents normalised data, i.e. each byte holds a
/// value in the range [0, 255] representing a real number in the range [0.0, 1.0]; @c false otherwise.
constexpr inline size_t data_type_is_normalised(InputDataType data_type) {
switch(data_type) {
case InputDataType::Luminance8:
case InputDataType::Luminance8Phase8:
case InputDataType::Red8Green8Blue8:
case InputDataType::PhaseLinkedLuminance8:
return true;
default:
case InputDataType::Luminance1:
case InputDataType::Red1Green1Blue1:
case InputDataType::Red2Green2Blue2:
case InputDataType::Red4Green4Blue4:
return false;
}
}
/// @returns The 'natural' display type for data of type @c data_type. The natural display is whichever would
/// display it with the least number of conversions. Caveat: a colour display is assumed for pure-composite data types.
constexpr inline DisplayType natural_display_type_for_data_type(InputDataType data_type) {
switch(data_type) {
default:
case InputDataType::Luminance1:
@@ -126,6 +154,34 @@ inline DisplayType natural_display_type_for_data_type(InputDataType data_type) {
}
}
/// @returns A 3x3 matrix in row-major order to convert from @c colour_space to RGB.
inline std::array<float, 9> to_rgb_matrix(ColourSpace colour_space) {
const std::array<float, 9> yiq_to_rgb = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f};
const std::array<float, 9> yuv_to_rgb = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f};
switch(colour_space) {
case ColourSpace::YIQ: return yiq_to_rgb;
case ColourSpace::YUV: return yuv_to_rgb;
}
// Should be unreachable.
return std::array<float, 9>{};
}
/// @returns A 3x3 matrix in row-major order to convert to @c colour_space to RGB.
inline std::array<float, 9> from_rgb_matrix(ColourSpace colour_space) {
const std::array<float, 9> rgb_to_yiq = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f};
const std::array<float, 9> rgb_to_yuv = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f};
switch(colour_space) {
case ColourSpace::YIQ: return rgb_to_yiq;
case ColourSpace::YUV: return rgb_to_yuv;
}
// Should be unreachable.
return std::array<float, 9>{};
}
/*!
Provides an abstract target for 'scans' i.e. continuous sweeps of output data,
which are identified by 2d start and end coordinates, and the PCM-sampled data
@@ -325,22 +381,22 @@ struct ScanTarget {
struct ScanStatus {
/// The current (prediced) length of a field (including retrace).
Time::Seconds field_duration;
Time::Seconds field_duration = 0.0;
/// The difference applied to the field_duration estimate during the last field.
Time::Seconds field_duration_gradient;
Time::Seconds field_duration_gradient = 0.0;
/// The amount of time this device spends in retrace.
Time::Seconds retrace_duration;
Time::Seconds retrace_duration = 0.0;
/// The distance into the current field, from a small negative amount (in retrace) through
/// 0 (start of visible area field) to 1 (end of field).
///
/// This will increase monotonically, being a measure
/// of the current vertical position — i.e. if current_position = 0.8 then a caller can
/// conclude that the top 80% of the visible part of the display has been painted.
float current_position;
float current_position = 0.0f;
/// The total number of hsyncs so far encountered;
int hsync_count;
int hsync_count = 0;
/// @c true if retrace is currently going on; @c false otherwise.
bool is_in_retrace;
bool is_in_retrace = false;
/*!
@returns this ScanStatus, with time-relative fields scaled by dividing them by @c dividend.

View File

@@ -0,0 +1,384 @@
//
// BufferingScanTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 22/07/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#include "BufferingScanTarget.hpp"
#include <cassert>
#include <cstring>
#define TextureAddressGetY(v) uint16_t((v) >> 11)
#define TextureAddressGetX(v) uint16_t((v) & 0x7ff)
#define TextureSub(a, b) (((a) - (b)) & 0x3fffff)
#define TextureAddress(x, y) (((y) << 11) | (x))
using namespace Outputs::Display;
BufferingScanTarget::BufferingScanTarget() {
// Ensure proper initialisation of the two atomic pointer sets.
read_pointers_.store(write_pointers_, std::memory_order::memory_order_relaxed);
submit_pointers_.store(write_pointers_, std::memory_order::memory_order_relaxed);
// Establish initial state for is_updating_.
is_updating_.clear(std::memory_order::memory_order_relaxed);
}
// MARK: - Producer; pixel data.
uint8_t *BufferingScanTarget::begin_data(size_t required_length, size_t required_alignment) {
assert(required_alignment);
// Acquire the standard producer lock, nominally over write_pointers_.
std::lock_guard lock_guard(producer_mutex_);
// If allocation has already failed on this line, continue the trend.
if(allocation_has_failed_) return nullptr;
// If there isn't yet a write area then mark allocation as failed and finish.
if(!write_area_) {
allocation_has_failed_ = true;
return nullptr;
}
// Determine where the proposed write area would start and end.
uint16_t output_y = TextureAddressGetY(write_pointers_.write_area);
uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1;
aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment);
uint16_t end_x = aligned_start_x + uint16_t(1 + required_length);
if(end_x > WriteAreaWidth) {
output_y = (output_y + 1) % WriteAreaHeight;
aligned_start_x = uint16_t(required_alignment);
end_x = aligned_start_x + uint16_t(1 + required_length);
}
// Check whether that steps over the read pointer; if so then the final address will be closer
// to the write pointer than the old.
const auto end_address = TextureAddress(end_x, output_y);
const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed);
const auto end_distance = TextureSub(end_address, read_pointers.write_area);
const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area);
// Perform a quick sanity check.
assert(end_distance >= 0);
assert(previous_distance >= 0);
// If allocating this would somehow make the write pointer back away from the read pointer,
// there must not be enough space left.
if(end_distance < previous_distance) {
allocation_has_failed_ = true;
return nullptr;
}
// Everything checks out, note expectation of a future end_data and return the pointer.
assert(!data_is_allocated_);
data_is_allocated_ = true;
vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y);
assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= WriteAreaWidth*WriteAreaHeight*data_type_size_);
return &write_area_[size_t(write_pointers_.write_area) * data_type_size_];
// Note state at exit:
// write_pointers_.write_area points to the first pixel the client is expected to draw to.
}
void BufferingScanTarget::end_data(size_t actual_length) {
// Acquire the producer lock.
std::lock_guard lock_guard(producer_mutex_);
// Do nothing if no data write is actually ongoing.
if(allocation_has_failed_ || !data_is_allocated_) return;
// Bookend the start of the new data, to safeguard for precision errors in sampling.
memcpy(
&write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_[size_t(write_pointers_.write_area) * data_type_size_],
data_type_size_);
// Advance to the end of the current run.
write_pointers_.write_area += actual_length + 1;
// Also bookend the end.
memcpy(
&write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_[size_t(write_pointers_.write_area - 2) * data_type_size_],
data_type_size_);
// The write area was allocated in the knowledge that there's sufficient
// distance left on the current line, but there's a risk of exactly filling
// the final line, in which case this should wrap back to 0.
write_pointers_.write_area %= WriteAreaWidth*WriteAreaHeight;
// Record that no further end_data calls are expected.
data_is_allocated_ = false;
}
// MARK: - Producer; scans.
Outputs::Display::ScanTarget::Scan *BufferingScanTarget::begin_scan() {
std::lock_guard lock_guard(producer_mutex_);
// If there's already an allocation failure on this line, do no work.
if(allocation_has_failed_) {
vended_scan_ = nullptr;
return nullptr;
}
const auto result = &scan_buffer_[write_pointers_.scan];
const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed);
// Advance the pointer.
const auto next_write_pointer = decltype(write_pointers_.scan)((write_pointers_.scan + 1) % scan_buffer_size_);
// Check whether that's too many.
if(next_write_pointer == read_pointers.scan) {
allocation_has_failed_ = true;
vended_scan_ = nullptr;
return nullptr;
}
write_pointers_.scan = next_write_pointer;
++provided_scans_;
// Fill in extra OpenGL-specific details.
result->line = write_pointers_.line;
vended_scan_ = result;
#ifndef NDEBUG
assert(!scan_is_ongoing_);
scan_is_ongoing_ = true;
#endif
return &result->scan;
}
void BufferingScanTarget::end_scan() {
std::lock_guard lock_guard(producer_mutex_);
#ifndef NDEBUG
assert(scan_is_ongoing_);
scan_is_ongoing_ = false;
#endif
// Complete the scan only if one is afoot.
if(vended_scan_) {
vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_);
vended_scan_->line = write_pointers_.line;
vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_);
vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_);
vended_scan_ = nullptr;
}
}
// MARK: - Producer; lines.
void BufferingScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) {
std::lock_guard lock_guard(producer_mutex_);
// Forward the event to the display metrics tracker.
display_metrics_.announce_event(event);
if(event == ScanTarget::Event::EndVerticalRetrace) {
// The previous-frame-is-complete flag is subject to a two-slot queue because
// measurement for *this* frame needs to begin now, meaning that the previous
// result needs to be put somewhere — it'll be attached to the first successful
// line output, whenever that comes.
is_first_in_frame_ = true;
previous_frame_was_complete_ = frame_is_complete_;
frame_is_complete_ = true;
}
// Proceed from here only if a change in visibility has occurred.
if(output_is_visible_ == is_visible) return;
output_is_visible_ = is_visible;
#ifndef NDEBUG
assert(!scan_is_ongoing_);
#endif
if(is_visible) {
const auto read_pointers = read_pointers_.load(std::memory_order::memory_order_relaxed);
// Attempt to allocate a new line, noting allocation success or failure.
const auto next_line = uint16_t((write_pointers_.line + 1) % line_buffer_size_);
allocation_has_failed_ = next_line == read_pointers.line;
if(!allocation_has_failed_) {
// If there was space for a new line, establish its start and reset the count of provided scans.
Line &active_line = line_buffer_[size_t(write_pointers_.line)];
active_line.end_points[0].x = location.x;
active_line.end_points[0].y = location.y;
active_line.end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line.end_points[0].composite_angle = location.composite_angle;
active_line.line = write_pointers_.line;
active_line.composite_amplitude = composite_amplitude;
provided_scans_ = 0;
}
} else {
// Commit the most recent line only if any scans fell on it and all allocation was successful.
if(!allocation_has_failed_ && provided_scans_) {
const auto submit_pointers = submit_pointers_.load(std::memory_order::memory_order_relaxed);
// Store metadata.
LineMetadata &metadata = line_metadata_buffer_[size_t(write_pointers_.line)];
metadata.is_first_in_frame = is_first_in_frame_;
metadata.previous_frame_was_complete = previous_frame_was_complete_;
metadata.first_scan = submit_pointers.scan;
is_first_in_frame_ = false;
// Sanity check.
assert(((metadata.first_scan + size_t(provided_scans_)) % scan_buffer_size_) == write_pointers_.scan);
// Store actual line data.
Line &active_line = line_buffer_[size_t(write_pointers_.line)];
active_line.end_points[1].x = location.x;
active_line.end_points[1].y = location.y;
active_line.end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line.end_points[1].composite_angle = location.composite_angle;
// Advance the line pointer.
write_pointers_.line = uint16_t((write_pointers_.line + 1) % line_buffer_size_);
// Update the submit pointers with all lines, scans and data written during this line.
std::atomic_thread_fence(std::memory_order::memory_order_release);
submit_pointers_.store(write_pointers_, std::memory_order::memory_order_release);
} else {
// Something failed, or there was nothing on the line anyway, so reset all pointers to where they
// were before this line. Mark frame as incomplete if this was an allocation failure.
write_pointers_ = submit_pointers_.load(std::memory_order::memory_order_relaxed);
frame_is_complete_ &= !allocation_has_failed_;
}
// Don't permit anything to be allocated on invisible areas.
allocation_has_failed_ = true;
}
}
// MARK: - Producer; other state.
void BufferingScanTarget::will_change_owner() {
std::lock_guard lock_guard(producer_mutex_);
allocation_has_failed_ = true;
vended_scan_ = nullptr;
#ifdef DEBUG
data_is_allocated_ = false;
#endif
}
const Outputs::Display::Metrics &BufferingScanTarget::display_metrics() {
return display_metrics_;
}
void BufferingScanTarget::set_write_area(uint8_t *base) {
std::lock_guard lock_guard(producer_mutex_);
write_area_ = base;
write_pointers_ = submit_pointers_ = read_pointers_ = PointerSet();
allocation_has_failed_ = true;
vended_scan_ = nullptr;
}
size_t BufferingScanTarget::write_area_data_size() const {
// TODO: can I guarantee this is safe without requiring that set_write_area
// be within an @c perform block?
return data_type_size_;
}
void BufferingScanTarget::set_modals(Modals modals) {
perform([=] {
modals_ = modals;
modals_are_dirty_ = true;
});
}
// MARK: - Consumer.
BufferingScanTarget::OutputArea BufferingScanTarget::get_output_area() {
// The area to draw is that between the read pointers, representing wherever reading
// last stopped, and the submit pointers, representing all the new data that has been
// cleared for submission.
const auto submit_pointers = submit_pointers_.load(std::memory_order::memory_order_acquire);
const auto read_ahead_pointers = read_ahead_pointers_.load(std::memory_order::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order::memory_order_acquire);
OutputArea area;
area.start.line = read_ahead_pointers.line;
area.end.line = submit_pointers.line;
area.start.scan = read_ahead_pointers.scan;
area.end.scan = submit_pointers.scan;
area.start.write_area_x = TextureAddressGetX(read_ahead_pointers.write_area);
area.start.write_area_y = TextureAddressGetY(read_ahead_pointers.write_area);
area.end.write_area_x = TextureAddressGetX(submit_pointers.write_area);
area.end.write_area_y = TextureAddressGetY(submit_pointers.write_area);
// Update the read-ahead pointers.
read_ahead_pointers_.store(submit_pointers, std::memory_order::memory_order_relaxed);
#ifndef NDEBUG
area.counter = output_area_counter_;
++output_area_counter_;
#endif
return area;
}
void BufferingScanTarget::complete_output_area(const OutputArea &area) {
// TODO: check that this is the expected next area if in DEBUG mode.
PointerSet new_read_pointers;
new_read_pointers.line = uint16_t(area.end.line);
new_read_pointers.scan = uint16_t(area.end.scan);
new_read_pointers.write_area = TextureAddress(area.end.write_area_x, area.end.write_area_y);
read_pointers_.store(new_read_pointers, std::memory_order::memory_order_relaxed);
#ifndef NDEBUG
// This will fire if the caller is announcing completed output areas out of order.
assert(area.counter == output_area_next_returned_);
++output_area_next_returned_;
#endif
}
void BufferingScanTarget::perform(const std::function<void(void)> &function) {
while(is_updating_.test_and_set(std::memory_order_acquire));
function();
is_updating_.clear(std::memory_order_release);
}
void BufferingScanTarget::set_scan_buffer(Scan *buffer, size_t size) {
scan_buffer_ = buffer;
scan_buffer_size_ = size;
}
void BufferingScanTarget::set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size) {
line_buffer_ = line_buffer;
line_metadata_buffer_ = metadata_buffer;
line_buffer_size_ = size;
}
const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() {
if(!modals_are_dirty_) {
return nullptr;
}
modals_are_dirty_ = false;
// MAJOR SHARP EDGE HERE: assume that because the new_modals have been fetched then the caller will
// now ensure their texture buffer is appropriate. They might provide a new pointer and might now.
// But either way it's now appropriate to start treating the data size as implied by the data type.
std::lock_guard lock_guard(producer_mutex_);
data_type_size_ = Outputs::Display::size_for_data_type(modals_.input_data_type);
return &modals_;
}
const Outputs::Display::ScanTarget::Modals &BufferingScanTarget::modals() const {
return modals_;
}

View File

@@ -0,0 +1,270 @@
//
// BufferingScanTarget.hpp
// Clock Signal
//
// Created by Thomas Harte on 22/07/2020.
// Copyright © 2020 Thomas Harte. All rights reserved.
//
#ifndef BufferingScanTarget_hpp
#define BufferingScanTarget_hpp
#include "../ScanTarget.hpp"
#include "../DisplayMetrics.hpp"
#include <array>
#include <atomic>
#include <functional>
#include <mutex>
#include <vector>
namespace Outputs {
namespace Display {
/*!
Provides basic thread-safe (hopefully) circular queues for any scan target that:
* will store incoming Scans into a linear circular buffer and pack regions of
incoming pixel data into a 2048x2048 2d texture;
* will compose whole lines of content by partioning the Scans based on sync
placement and then pasting together their content;
* will process those lines as necessary to map from input format to whatever
suits the display; and
* will then output the lines.
This buffer rejects new data when full.
*/
class BufferingScanTarget: public Outputs::Display::ScanTarget {
public:
/*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */
const Metrics &display_metrics();
static constexpr int WriteAreaWidth = 2048;
static constexpr int WriteAreaHeight = 2048;
BufferingScanTarget();
// This is included because it's assumed that scan targets will want to expose one.
// It is the subclass's responsibility to post timings.
Metrics display_metrics_;
/// Extends the definition of a Scan to include two extra fields,
/// completing this scan's source data and destination locations.
struct Scan {
Outputs::Display::ScanTarget::Scan scan;
/// Stores the y coordinate for this scan's data within the write area texture.
/// Use this plus the scan's endpoints' data_offsets to locate this data in 2d.
/// Note that the data_offsets will have been adjusted to be relative to the line
/// they fall within, not the data allocation.
uint16_t data_y;
/// Stores the y coordinate assigned to this scan within the intermediate buffers.
/// Use this plus this scan's endpoints' x locations to determine where to composite
/// this data for intermediate processing.
uint16_t line;
};
/// Defines the boundaries of a complete line of video — a 2d start and end location,
/// composite phase and amplitude (if relevant), the source line in the intermediate buffer
/// plus the start and end offsets of the area that is visible from the intermediate buffer.
struct Line {
struct EndPoint {
uint16_t x, y;
int16_t composite_angle;
uint16_t cycles_since_end_of_horizontal_retrace;
} end_points[2];
uint8_t composite_amplitude;
uint16_t line;
};
/// Provides additional metadata about lines; this is separate because it's unlikely to be of
/// interest to the GPU, unlike the fields in Line.
struct LineMetadata {
/// @c true if this line was the first drawn after vertical sync; @c false otherwise.
bool is_first_in_frame;
/// @c true if this line is the first in the frame and if every single piece of output
/// from the previous frame was recorded; @c false otherwise. Data can be dropped
/// from a frame if performance problems mean that the emulated machine is running
/// more quickly than complete frames can be generated.
bool previous_frame_was_complete;
/// The index of the first scan that will appear on this line.
size_t first_scan;
};
/// Sets the area of memory to use as a scan buffer.
void set_scan_buffer(Scan *buffer, size_t size);
/// Sets the area of memory to use as line and line metadata buffers.
void set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size);
/// Sets a new base address for the texture.
/// When called this will flush all existing data and load up the
/// new data size.
void set_write_area(uint8_t *base);
/// @returns The number of bytes per input sample, as per the latest modals.
size_t write_area_data_size() const;
/// Defines a segment of data now ready for output, consisting of start and endpoints for:
///
/// (i) the region of the write area that has been modified; if the caller is using shared memory
/// for the write area then it can ignore this information;
///
/// (ii) the number of scans that have been completed; and
///
/// (iii) the number of lines that have been completed.
///
/// New write areas and scans are exposed only upon completion of the corresponding lines.
/// The values indicated by the start point are the first that should be drawn. Those indicated
/// by the end point are one after the final that should be drawn.
///
/// So e.g. start.scan = 23, end.scan = 24 means draw a single scan, index 23.
struct OutputArea {
struct Endpoint {
int write_area_x, write_area_y;
size_t scan;
size_t line;
};
Endpoint start, end;
#ifndef NDEBUG
size_t counter;
#endif
};
/// Gets the current range of content that has been posted but not yet returned by
/// a previous call to get_output_area().
///
/// Does not require the caller to be within a @c perform block.
OutputArea get_output_area();
/// Announces that the output area has now completed output, freeing up its memory for
/// further modification.
///
/// It is the caller's responsibility to ensure that the areas passed to complete_output_area
/// are those from get_output_area and are marked as completed in the same order that
/// they were originally provided.
///
/// Does not require the caller to be within a @c perform block.
void complete_output_area(const OutputArea &);
/// Performs @c action ensuring that no other @c perform actions, or any
/// change to modals, occurs simultaneously.
void perform(const std::function<void(void)> &action);
/// @returns new Modals if any have been set since the last call to get_new_modals().
/// The caller must be within a @c perform block.
const Modals *new_modals();
/// @returns the current @c Modals.
const Modals &modals() const;
private:
// ScanTarget overrides.
void set_modals(Modals) final;
Outputs::Display::ScanTarget::Scan *begin_scan() final;
void end_scan() final;
uint8_t *begin_data(size_t required_length, size_t required_alignment) final;
void end_data(size_t actual_length) final;
void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final;
void will_change_owner() final;
// Uses a texture to vend write areas.
uint8_t *write_area_ = nullptr;
size_t data_type_size_ = 0;
// Tracks changes in raster visibility in order to populate
// Lines and LineMetadatas.
bool output_is_visible_ = false;
// Track allocation failures.
bool data_is_allocated_ = false;
bool allocation_has_failed_ = false;
// Ephemeral information for the begin/end functions.
Scan *vended_scan_ = nullptr;
int vended_write_area_pointer_ = 0;
// Ephemeral state that helps in line composition.
int provided_scans_ = 0;
bool is_first_in_frame_ = true;
bool frame_is_complete_ = true;
bool previous_frame_was_complete_ = true;
// By convention everything in the PointerSet points to the next instance
// of whatever it is that will be used. So a client should start with whatever
// is pointed to by the read pointers and carry until it gets to a value that
// is equal to whatever is in the submit pointers.
struct PointerSet {
// This constructor is here to appease GCC's interpretation of
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
PointerSet() noexcept {}
// Squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64.
// Points to the vended area in the write area texture.
// The vended area is always preceded by a guard pixel, so a
// sensible default construction is write_area = 1.
int32_t write_area = 1;
// Points into the scan buffer.
uint16_t scan = 0;
// Points into the line buffer.
uint16_t line = 0;
};
/// A pointer to the final thing currently cleared for submission.
std::atomic<PointerSet> submit_pointers_;
/// A pointer to the first thing not yet submitted for display; this is
/// atomic since it also acts as the buffer into which the write_pointers_
/// may run and is therefore used by both producer and consumer.
std::atomic<PointerSet> read_pointers_;
std::atomic<PointerSet> read_ahead_pointers_;
/// This is used as a spinlock to guard `perform` calls.
std::atomic_flag is_updating_;
/// A mutex for gettng access to anything the producer modifies — i.e. the write_pointers_,
/// data_type_size_ and write_area_texture_, and all other state to do with capturing
/// data, scans and lines.
///
/// This is almost never contended. The main collision is a user-prompted change of modals while the
/// emulation thread is running.
std::mutex producer_mutex_;
/// A pointer to the next thing that should be provided to the caller for data.
PointerSet write_pointers_;
// The owner-supplied scan buffer and size.
Scan *scan_buffer_ = nullptr;
size_t scan_buffer_size_ = 0;
// The owner-supplied line buffer and size.
Line *line_buffer_ = nullptr;
LineMetadata *line_metadata_buffer_ = nullptr;
size_t line_buffer_size_ = 0;
// Current modals and whether they've yet been returned
// from a call to @c get_new_modals.
Modals modals_;
bool modals_are_dirty_ = false;
#ifndef NDEBUG
// Debug features; these amount to API validation.
bool scan_is_ongoing_ = false;
size_t output_area_counter_ = 0;
size_t output_area_next_returned_ = 0;
#endif
};
}
}
#endif /* BufferingScanTarget_hpp */

View File

@@ -10,7 +10,6 @@
#define FilteringSpeaker_h
#include "../Speaker.hpp"
#include "../../../SignalProcessing/Stepper.hpp"
#include "../../../SignalProcessing/FIRFilter.hpp"
#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Concurrency/AsyncTaskQueue.hpp"
@@ -131,7 +130,7 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
at construction, filtering it and passing it on to the speaker's delegate if there is one.
*/
void run_for(const Cycles cycles) {
const auto delegate = delegate_.load();
const auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
if(!delegate) return;
const int scale = get_scale();
@@ -198,7 +197,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
std::vector<int16_t> input_buffer_;
std::vector<int16_t> output_buffer_;
std::unique_ptr<SignalProcessing::Stepper> stepper_;
float step_rate_ = 0.0f;
float position_error_ = 0.0f;
std::unique_ptr<SignalProcessing::FIRFilter> filter_;
std::mutex filter_parameters_mutex_;
@@ -223,9 +223,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
);
number_of_taps = (number_of_taps * 2) | 1;
stepper_ = std::make_unique<SignalProcessing::Stepper>(
uint64_t(filter_parameters.input_cycles_per_second),
uint64_t(filter_parameters.output_cycles_per_second));
step_rate_ = filter_parameters.input_cycles_per_second / filter_parameters.output_cycles_per_second;
position_error_ = 0.0f;
filter_ = std::make_unique<SignalProcessing::FIRFilter>(
unsigned(number_of_taps),
@@ -304,7 +303,8 @@ template <typename SampleSource> class LowpassSpeaker: public Speaker {
// If the next loop around is going to reuse some of the samples just collected, use a memmove to
// preserve them in the correct locations (TODO: use a longer buffer to fix that?) and don't skip
// anything. Otherwise skip as required to get to the next sample batch and don't expect to reuse.
const auto steps = stepper_->step() * (SampleSource::get_is_stereo() ? 2 : 1);
const size_t steps = size_t(step_rate_ + position_error_) * (SampleSource::get_is_stereo() ? 2 : 1);
position_error_ = fmodf(step_rate_ + position_error_, 1.0f);
if(steps < input_buffer_.size()) {
auto *const input_buffer = input_buffer_.data();
std::memmove( input_buffer,

View File

@@ -45,6 +45,16 @@ class Speaker {
compute_output_rate();
}
/*!
Takes a copy of the most recent output rate provided to @c rhs.
*/
void copy_output_rate(const Speaker &rhs) {
output_cycles_per_second_ = rhs.output_cycles_per_second_;
output_buffer_size_ = rhs.output_buffer_size_;
stereo_output_.store(rhs.stereo_output_.load(std::memory_order::memory_order_relaxed), std::memory_order::memory_order_relaxed);
compute_output_rate();
}
/// Sets the output volume, in the range [0, 1].
virtual void set_output_volume(float) = 0;
@@ -79,7 +89,7 @@ class Speaker {
virtual void speaker_did_change_input_clock([[maybe_unused]] Speaker *speaker) {}
};
virtual void set_delegate(Delegate *delegate) {
delegate_ = delegate;
delegate_.store(delegate, std::memory_order::memory_order_relaxed);
}
@@ -89,7 +99,7 @@ class Speaker {
protected:
void did_complete_samples(Speaker *, const std::vector<int16_t> &buffer, bool is_stereo) {
// Test the delegate for existence again, as it may have changed.
const auto delegate = delegate_.load();
const auto delegate = delegate_.load(std::memory_order::memory_order_relaxed);
if(!delegate) return;
++completed_sample_sets_;

View File

@@ -2,9 +2,9 @@
# Clock Signal
Clock Signal ('CLK') is an emulator for tourists that seeks to be invisible. Users directly launch classic software with no emulator or per-emulated-machine learning curve.
[Releases](https://github.com/TomHarte/CLK/releases) are hosted on GitHub.
macOS and source releases are [hosted on GitHub](https://github.com/TomHarte/CLK/releases). For desktop Linux it is also available as a [Snap](https://snapcraft.io/clock-signal).
On the Mac it is a native Cocoa application; under Linux, BSD and other UNIXes and UNIX-alikes it can be built either with Qt or with SDL; the Qt build should be considered preliminary, pending a sustainable workaround for its keyboard handling.
On the Mac it is a native Cocoa and Metal application; under Linux, BSD and other UNIXes and UNIX-alikes it uses OpenGL and can be built either with Qt or with SDL.
So its aims are:
* single-click load of any piece of source media for any supported platform;
@@ -17,6 +17,7 @@ It currently contains emulations of the:
* Amstrad CPC;
* Apple II/II+ and IIe;
* Atari 2600;
* Atari ST;
* ColecoVision;
* Commodore Vic-20 (and Commodore 1540/1);
* Macintosh 512ke and Plus;
@@ -25,8 +26,6 @@ It currently contains emulations of the:
* Sega Master System; and
* Sinclair ZX80/81.
In addition, emulation of the Atari ST is experimental.
## Single-click Loading
Through the combination of static analysis and runtime analysis, CLK seeks to be able automatically to select and configure the appropriate machine to run any provided disk, tape or ROM; to issue any commands necessary to run the software contained on the disk, tape or ROM; and to provide accelerated loading where feasible.

View File

@@ -90,7 +90,7 @@ void Controller::set_drive(int index_mask) {
return;
}
ClockingHint::Preference former_prefernece = preferred_clocking();
const ClockingHint::Preference former_preference = preferred_clocking();
// Stop receiving events from the current drive.
get_drive().set_event_delegate(nullptr);
@@ -114,7 +114,7 @@ void Controller::set_drive(int index_mask) {
get_drive().set_event_delegate(this);
if(preferred_clocking() != former_prefernece) {
if(preferred_clocking() != former_preference) {
update_clocking_observer();
}
}

View File

@@ -61,6 +61,12 @@ class Disk {
@returns whether the disk image is read only. Defaults to @c true if not overridden.
*/
virtual bool get_is_read_only() = 0;
/*!
@returns @c true if the tracks at the two addresses are different. @c false if they are the same track.
This can avoid some degree of work when disk images offer sub-head-position precision.
*/
virtual bool tracks_differ(Track::Address, Track::Address) = 0;
};
}

View File

@@ -67,6 +67,12 @@ class DiskImage {
@returns whether the disk image is read only. Defaults to @c true if not overridden.
*/
virtual bool get_is_read_only() { return true; }
/*!
@returns @c true if the tracks at the two addresses are different. @c false if they are the same track.
This can avoid some degree of work when disk images offer sub-head-position precision.
*/
virtual bool tracks_differ(Track::Address lhs, Track::Address rhs) { return lhs != rhs; }
};
class DiskImageHolderBase: public Disk {
@@ -93,6 +99,7 @@ template <typename T> class DiskImageHolder: public DiskImageHolderBase {
void set_track_at_position(Track::Address address, const std::shared_ptr<Track> &track);
void flush_tracks();
bool get_is_read_only();
bool tracks_differ(Track::Address lhs, Track::Address rhs);
private:
T disk_image_;

View File

@@ -58,3 +58,7 @@ template <typename T> std::shared_ptr<Track> DiskImageHolder<T>::get_track_at_po
template <typename T> DiskImageHolder<T>::~DiskImageHolder() {
if(update_queue_) update_queue_->flush();
}
template <typename T> bool DiskImageHolder<T>::tracks_differ(Track::Address lhs, Track::Address rhs) {
return disk_image_.tracks_differ(lhs, rhs);
}

View File

@@ -112,10 +112,22 @@ int WOZ::get_head_count() {
}
long WOZ::file_offset(Track::Address address) {
// Calculate table position; if this track is defined to be unformatted, return no track.
const int table_position = address.head * (is_3_5_disk_ ? 80 : 160) +
(is_3_5_disk_ ? address.position.as_int() : address.position.as_quarter());
if(track_map_[table_position] == 0xff) return NoSuchTrack;
// Calculate table position.
int table_position;
if(!is_3_5_disk_) {
table_position = address.head * 160 + address.position.as_quarter();
} else {
if(type_ == Type::WOZ1) {
table_position = address.head * 80 + address.position.as_int();
} else {
table_position = address.head + (address.position.as_int() * 2);
}
}
// Check that this track actually exists.
if(track_map_[table_position] == 0xff) {
return NoSuchTrack;
}
// Seek to the real track.
switch(type_) {
@@ -125,9 +137,17 @@ long WOZ::file_offset(Track::Address address) {
}
}
bool WOZ::tracks_differ(Track::Address lhs, Track::Address rhs) {
const long offset1 = file_offset(lhs);
const long offset2 = file_offset(rhs);
return offset1 != offset2;
}
std::shared_ptr<Track> WOZ::get_track_at_position(Track::Address address) {
const long offset = file_offset(address);
if(offset == NoSuchTrack) return nullptr;
if(offset == NoSuchTrack) {
return nullptr;
}
// Seek to the real track.
std::vector<uint8_t> track_contents;
@@ -146,6 +166,7 @@ std::shared_ptr<Track> WOZ::get_track_at_position(Track::Address address) {
number_of_bits = std::min(file_.get16le(), uint16_t(6646*8));
break;
default:
case Type::WOZ2: {
// In WOZ 2 an extra level of indirection allows for variable track sizes.
const uint16_t starting_block = file_.get16le();
@@ -194,5 +215,12 @@ void WOZ::set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tra
}
bool WOZ::get_is_read_only() {
return file_.get_is_known_read_only() || is_read_only_ || type_ == Type::WOZ2; // WOZ 2 disks are currently read only.
/*
There is an unintended issue with the disk code that sites above here: it doesn't understand the idea
of multiple addresses mapping to the same track, yet it maintains a cache of track contents. Therefore
if a WOZ is written to, what's written will magically be exactly 1/4 track wide, not affecting its
neighbours. I've made WOZs readonly until I can correct that issue.
*/
return true;
// return file_.get_is_known_read_only() || is_read_only_ || type_ == Type::WOZ2; // WOZ 2 disks are currently read only.
}

View File

@@ -31,6 +31,7 @@ class WOZ: public DiskImage {
std::shared_ptr<Track> get_track_at_position(Track::Address address) final;
void set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tracks) final;
bool get_is_read_only() final;
bool tracks_differ(Track::Address, Track::Address) final;
private:
Storage::FileHolder file_;

View File

@@ -80,6 +80,10 @@ bool Drive::get_is_track_zero() const {
}
void Drive::step(HeadPosition offset) {
if(offset == HeadPosition(0)) {
return;
}
if(ready_type_ == ReadyType::IBMRDY) {
is_ready_ = true;
}
@@ -94,7 +98,7 @@ void Drive::step(HeadPosition offset) {
}
// If the head moved, flush the old track.
if(head_position_ != old_head_position) {
if(disk_ && disk_->tracks_differ(Track::Address(head_, head_position_), Track::Address(head_, old_head_position))) {
track_ = nullptr;
}
@@ -300,11 +304,6 @@ void Drive::get_next_event(float duration_already_passed) {
current_event_.type = Track::Event::IndexHole;
}
// Begin a 2ms period of holding the index line pulse active if this is an index pulse event.
if(current_event_.type == Track::Event::IndexHole) {
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000);
}
// divide interval, which is in terms of a single rotation of the disk, by rotation speed to
// convert it into revolutions per second; this is achieved by multiplying by rotational_multiplier_
float interval = std::max((current_event_.length - duration_already_passed) * rotational_multiplier_, 0.0f);
@@ -327,6 +326,9 @@ void Drive::process_next_event() {
is_ready_ = true;
}
cycles_since_index_hole_ = 0;
// Begin a 2ms period of holding the index line pulse active.
index_pulse_remaining_ = Cycles((get_input_clock_rate() * 2) / 1000);
}
if(
event_delegate_ &&
@@ -355,8 +357,8 @@ void Drive::setup_track() {
}
float offset = 0.0f;
const auto track_time_now = get_time_into_track();
const auto time_found = track_->seek_to(Time(track_time_now)).get<float>();
const float track_time_now = get_time_into_track();
const float time_found = track_->seek_to(track_time_now);
// `time_found` can be greater than `track_time_now` if limited precision caused rounding.
if(time_found <= track_time_now) {

View File

@@ -21,9 +21,9 @@ struct Sector {
Describes the location of a sector, implementing < to allow for use as a set key.
*/
struct Address {
struct {
union {
/// For Apple II-type sectors, provides the volume number.
uint_fast8_t volume = 0;
uint_fast8_t volume;
/// For Macintosh-type sectors, provides the format from the sector header.
uint_fast8_t format = 0;
};

View File

@@ -57,14 +57,14 @@ uint8_t unmap_five_and_three(uint8_t source) {
return five_and_three_unmapping[source - 0xab];
}
std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8> &header, const std::unique_ptr<Sector> &original) {
// There must be at least 704 bytes to decode from.
if(original->data.size() < 704) return nullptr;
std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8> *header, const std::unique_ptr<Sector> &original) {
// There must be a header and at least 704 bytes to decode from.
if(!header || original->data.size() < 704) return nullptr;
// Attempt a six-and-two unmapping of the header.
std::array<uint_fast8_t, 5> decoded_header;
for(size_t c = 0; c < decoded_header.size(); ++c) {
decoded_header[c] = unmap_six_and_two(header[c]);
decoded_header[c] = unmap_six_and_two((*header)[c]);
if(decoded_header[c] == 0xff) {
return nullptr;
}
@@ -140,29 +140,32 @@ std::unique_ptr<Sector> decode_macintosh_sector(const std::array<uint_fast8_t, 8
return sector;
}
std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8> &header, const std::unique_ptr<Sector> &original, bool is_five_and_three) {
std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8> *header, const std::unique_ptr<Sector> &original, bool is_five_and_three) {
// There must be at least 411 bytes to decode a five-and-three sector from;
// there must be only 343 if this is a six-and-two sector.
const size_t data_size = is_five_and_three ? 411 : 343;
if(original->data.size() < data_size) return nullptr;
// Check for apparent four and four encoding.
uint_fast8_t header_mask = 0xff;
for(auto c : header) header_mask &= c;
header_mask &= 0xaa;
if(header_mask != 0xaa) return nullptr;
// Allocate a sector and fill the header fields.
// Allocate a sector.
auto sector = std::make_unique<Sector>();
sector->data.resize(data_size);
sector->address.volume = ((header[0] << 1) | 1) & header[1];
sector->address.track = ((header[2] << 1) | 1) & header[3];
sector->address.sector = ((header[4] << 1) | 1) & header[5];
// If there is a header, check for apparent four and four encoding.
if(header) {
uint_fast8_t header_mask = 0xff;
for(auto c : *header) header_mask &= c;
header_mask &= 0xaa;
if(header_mask != 0xaa) return nullptr;
// Check the header checksum.
const uint_fast8_t checksum = ((header[6] << 1) | 1) & header[7];
if(checksum != (sector->address.volume^sector->address.track^sector->address.sector)) return nullptr;
// Fill the header fields.
sector->address.volume = (((*header)[0] << 1) | 1) & (*header)[1];
sector->address.track = (((*header)[2] << 1) | 1) & (*header)[3];
sector->address.sector = (((*header)[4] << 1) | 1) & (*header)[5];
// Check the header checksum.
const uint_fast8_t checksum = (((*header)[6] << 1) | 1) & (*header)[7];
if(checksum != (sector->address.volume^sector->address.track^sector->address.sector)) return nullptr;
}
// Unmap the sector contents.
for(size_t index = 0; index < data_size; ++index) {
@@ -178,9 +181,6 @@ std::unique_ptr<Sector> decode_appleii_sector(const std::array<uint_fast8_t, 8>
}
if(sector->data.back()) return nullptr;
// Having checked the checksum, remove it.
sector->data.resize(sector->data.size() - 1);
if(is_five_and_three) {
// TODO: the below is almost certainly incorrect; Beneath Apple DOS partly documents
// the process, enough to give the basic outline below of how five source bytes are
@@ -250,6 +250,7 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
size_t bit = 0;
int header_delay = 0;
bool is_five_and_three = false;
bool has_header = false;
while(bit < segment.data.size() || pointer != scanning_sentinel || header_delay) {
shift_register = uint_fast8_t((shift_register << 1) | (segment.data[bit % segment.data.size()] ? 1 : 0));
++bit;
@@ -290,6 +291,7 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
sector_location = size_t(bit % segment.data.size());
header_delay = 200; // Allow up to 200 bytes to find the body, if the
// track split comes in between.
has_header = true;
}
}
} else {
@@ -308,18 +310,19 @@ std::map<std::size_t, Sector> Storage::Encodings::AppleGCR::sectors_from_segment
pointer = scanning_sentinel;
// Potentially this is a Macintosh sector.
auto macintosh_sector = decode_macintosh_sector(header, sector);
auto macintosh_sector = decode_macintosh_sector(has_header ? &header : nullptr, sector);
if(macintosh_sector) {
result.insert(std::make_pair(sector_location, std::move(*macintosh_sector)));
continue;
}
// Apple II then?
auto appleii_sector = decode_appleii_sector(header, sector, is_five_and_three);
auto appleii_sector = decode_appleii_sector(has_header ? &header : nullptr, sector, is_five_and_three);
if(appleii_sector) {
result.insert(std::make_pair(sector_location, std::move(*appleii_sector)));
}
has_header = false;
} else {
new_sector->data.push_back(value);
}

View File

@@ -109,9 +109,9 @@ Storage::Time PCMSegmentEventSource::get_length() {
return segment_->length_of_a_bit * unsigned(segment_->data.size());
}
Storage::Time PCMSegmentEventSource::seek_to(const Time &time_from_start) {
float PCMSegmentEventSource::seek_to(float time_from_start) {
// test for requested time being beyond the end
const Time length = get_length();
const float length = get_length().get<float>();
if(time_from_start >= length) {
next_event_.type = Track::Event::IndexHole;
bit_pointer_ = segment_->data.size()+1;
@@ -122,21 +122,21 @@ Storage::Time PCMSegmentEventSource::seek_to(const Time &time_from_start) {
next_event_.type = Track::Event::FluxTransition;
// test for requested time being before the first bit
Time half_bit_length = segment_->length_of_a_bit;
half_bit_length.length >>= 1;
const float bit_length = segment_->length_of_a_bit.get<float>();
const float half_bit_length = bit_length / 2.0f;
if(time_from_start < half_bit_length) {
bit_pointer_ = 0;
return Storage::Time(0);
return 0.0f;
}
// adjust for time to get to bit zero and determine number of bits in;
// bit_pointer_ always records _the next bit_ that might trigger an event,
// so should be one beyond the one reached by a seek.
const Time relative_time = time_from_start - half_bit_length;
bit_pointer_ = 1 + (relative_time / segment_->length_of_a_bit).get<unsigned int>();
const float relative_time = time_from_start + half_bit_length; // the period [0, 0.5) should map to window 0, ending with bit 0; [0.5, 1.5) should map to window 1; etc.
bit_pointer_ = size_t(relative_time / bit_length);
// map up to the correct amount of time
return half_bit_length + segment_->length_of_a_bit * unsigned(bit_pointer_ - 1);
// Map up to the correct amount of time; this should be the start of the window that ends upon the bit at bit_pointer_.
return bit_length * float(bit_pointer_) - half_bit_length;
}
const PCMSegment &PCMSegmentEventSource::segment() const {

View File

@@ -183,7 +183,7 @@ class PCMSegmentEventSource {
@returns the time the source is now at.
*/
Time seek_to(const Time &time_from_start);
float seek_to(float time_from_start);
/*!
@returns the total length of the stream of data that the source will provide.

View File

@@ -121,17 +121,17 @@ Track::Event PCMTrack::get_next_event() {
return event;
}
Storage::Time PCMTrack::seek_to(const Time &time_since_index_hole) {
float PCMTrack::seek_to(float time_since_index_hole) {
// initial condition: no time yet accumulated, the whole thing requested yet to navigate
Storage::Time accumulated_time;
Storage::Time time_left_to_seek = time_since_index_hole;
float accumulated_time = 0.0f;
float time_left_to_seek = time_since_index_hole;
// search from the first segment
segment_pointer_ = 0;
do {
// if this segment extends beyond the amount of time left to seek, trust it to complete
// the seek
Storage::Time segment_time = segment_event_sources_[segment_pointer_].get_length();
const float segment_time = segment_event_sources_[segment_pointer_].get_length().get<float>();
if(segment_time > time_left_to_seek) {
return accumulated_time + segment_event_sources_[segment_pointer_].seek_to(time_left_to_seek);
}

View File

@@ -50,7 +50,7 @@ class PCMTrack: public Track {
// as per @c Track
Event get_next_event() final;
Time seek_to(const Time &time_since_index_hole) final;
float seek_to(float time_since_index_hole) final;
Track *clone() const final;
// Obtains a copy of this track, flattened to a single PCMSegment, which

View File

@@ -84,6 +84,13 @@ class Track {
int rhs_largest_position = rhs.position.as_largest();
return std::tie(head, largest_position) < std::tie(rhs.head, rhs_largest_position);
}
constexpr bool operator == (const Address &rhs) const {
return head == rhs.head && position == rhs.position;
}
constexpr bool operator != (const Address &rhs) const {
return head != rhs.head || position != rhs.position;
}
constexpr Address(int head, HeadPosition position) : head(head), position(position) {}
};
@@ -107,11 +114,11 @@ class Track {
virtual Event get_next_event() = 0;
/*!
Jumps to the event latest offset that is less than or equal to the input time.
Jumps to the start of the fist event that will occur after @c time_since_index_hole.
@returns the time jumped to.
*/
virtual Time seek_to(const Time &time_since_index_hole) = 0;
virtual float seek_to(float time_since_index_hole) = 0;
/*!
The virtual copy constructor pattern; returns a copy of the Track.

View File

@@ -35,7 +35,7 @@ Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track,
length_multiplier.simplify();
// start at the index hole
track_copy->seek_to(Time(0));
track_copy->seek_to(0.0f);
// grab events until the next index hole
Time time_error = Time(0);
@@ -54,7 +54,7 @@ Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track,
if(history_size) {
history_size--;
if(!history_size) {
track_copy->seek_to(Time(0));
track_copy->seek_to(0.0f);
time_error.set_zero();
result_accumulator.is_recording = true;
}

View File

@@ -17,8 +17,8 @@ Track::Event UnformattedTrack::get_next_event() {
return event;
}
Storage::Time UnformattedTrack::seek_to(const Time &) {
return Time(0);
float UnformattedTrack::seek_to(float) {
return 0.0f;
}
Track *UnformattedTrack::clone() const {

View File

@@ -20,7 +20,7 @@ namespace Disk {
class UnformattedTrack: public Track {
public:
Event get_next_event() final;
Time seek_to(const Time &time_since_index_hole) final;
float seek_to(float time_since_index_hole) final;
Track *clone() const final;
};

View File

@@ -247,10 +247,10 @@ void TZX::get_data_block(const DataBlock &data_block) {
void TZX::get_data(const Data &data) {
// Output data.
for(unsigned int c = 0; c < data.data_length; c++) {
for(decltype(data.data_length) c = 0; c < data.data_length; c++) {
uint8_t next_byte = file_.get8();
unsigned int bits = (c != data.data_length-1) ? 8 : data.number_of_bits_in_final_byte;
auto bits = (c != data.data_length-1) ? 8 : data.number_of_bits_in_final_byte;
while(bits--) {
unsigned int pulse_length = (next_byte & 0x80) ? data.length_of_one_bit_pulse : data.length_of_zero_bit_pulse;
next_byte <<= 1;

View File

@@ -77,7 +77,7 @@ class TZX: public PulseQueuedTape {
unsigned int length_of_one_bit_pulse;
unsigned int number_of_bits_in_final_byte;
unsigned int pause_after_block;
long data_length;
uint32_t data_length;
};
struct DataBlock {

View File

@@ -69,16 +69,16 @@ void TimedEventLoop::set_next_event_time_interval(Time interval) {
void TimedEventLoop::set_next_event_time_interval(float interval) {
// Calculate [interval]*[input clock rate] + [subcycles until this event]
float float_interval = interval * float(input_clock_rate_) + subcycles_until_event_;
const float float_interval = interval * float(input_clock_rate_) + subcycles_until_event_;
// So this event will fire in the integral number of cycles from now, putting us at the remainder
// number of subcycles
// This event will fire in the integral number of cycles from now, putting us at the remainder
// number of subcycles.
const Cycles::IntType addition = Cycles::IntType(float_interval);
cycles_until_event_ += addition;
subcycles_until_event_ = fmodf(float_interval, 1.0);
subcycles_until_event_ = fmodf(float_interval, 1.0f);
assert(cycles_until_event_ >= 0);
assert(subcycles_until_event_ >= 0.0);
assert(subcycles_until_event_ >= 0.0f);
}
Time TimedEventLoop::get_time_into_next_event() {

View File

@@ -103,7 +103,7 @@ namespace Storage {
private:
Cycles::IntType input_clock_rate_ = 0;
Cycles::IntType cycles_until_event_ = 0;
float subcycles_until_event_ = 0.0;
float subcycles_until_event_ = 0.0f;
};
}