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

Compare commits

..

114 Commits

Author SHA1 Message Date
Thomas Harte
25e783ff2f Merge pull request #1614 from TomHarte/6845Reading
Return 0 for write-only and nonexistent registers.
2025-10-20 13:56:27 -04:00
Thomas Harte
2eb94f1b66 Return 0 for write-only and nonexistent registers. 2025-10-20 13:26:22 -04:00
Thomas Harte
bd5a2f240d Record version number. 2025-10-19 19:51:06 -04:00
Thomas Harte
73054d971c Merge pull request #1612 from TomHarte/NoCounter
Eliminate CompileTimeCounter.
2025-10-19 19:45:52 -04:00
Thomas Harte
8c7f2491d7 Eliminate CompileTimeCounter. 2025-10-19 19:36:36 -04:00
Thomas Harte
564542420b Merge pull request #1609 from TomHarte/BBCAdvancedDiscToolkit
BBC Micro: add ADT ROM if available.
2025-10-18 21:03:07 -04:00
Thomas Harte
3f7e3e6d75 Use the ADT ROM if available. 2025-10-18 09:33:55 -04:00
Thomas Harte
6521d7d02b Add ADT 1.40 to ROM catalogue. 2025-10-18 09:15:17 -04:00
Thomas Harte
ad162a4e4a Merge pull request #1607 from TomHarte/MacAudio
Correct Mac audio buffering
2025-10-17 21:58:57 -04:00
Thomas Harte
676b1f6fdc Adopt brackets, as is now a macro. 2025-10-17 21:38:12 -04:00
Thomas Harte
406ef4e16c Record new version number. 2025-10-17 21:00:11 -04:00
Thomas Harte
217976350b Merge pull request #1606 from TomHarte/CPCAnalyser
Improve file-selection logic.
2025-10-17 18:38:33 -04:00
Thomas Harte
e8f860d6fe Improve file-selection logic. 2025-10-17 18:25:44 -04:00
Thomas Harte
859e6e2396 Merge pull request #1605 from TomHarte/CompiletimeCounter
Add compile-time counter; switch 1770 to using it for sequence points.
2025-10-17 17:44:13 -04:00
Thomas Harte
51186e615f Add warning. 2025-10-17 16:10:23 -04:00
Thomas Harte
bd8287fda3 Resolve obstructive warning. 2025-10-17 15:21:47 -04:00
Thomas Harte
287ff99bbc Use Numeric::Counter. 2025-10-17 15:06:05 -04:00
Thomas Harte
0bbfcedabb Alphabetise includes. 2025-10-17 15:05:52 -04:00
Thomas Harte
812e1e637d Eliminate magic constant. 2025-10-17 11:22:02 -04:00
Thomas Harte
f20fd38940 Introduce a compile-time counter; use it for 1770 sequencing. 2025-10-17 11:19:21 -04:00
Thomas Harte
b4cfabc005 Merge pull request #1604 from TomHarte/CPCBrightness
Apply outputMultiplier in direct RGB output.
2025-10-16 23:42:43 -04:00
Thomas Harte
c49e160501 Apply outputMultiplier in direct RGB output. 2025-10-16 22:44:40 -04:00
Thomas Harte
a0a24902d5 Merge pull request #1603 from TomHarte/JoystickDirection
Provide joystick up/down as down = positive again.
2025-10-16 21:58:22 -04:00
Thomas Harte
1047bc8a80 Provide up/down in down = positive again. 2025-10-16 21:30:49 -04:00
Thomas Harte
0eed49c4cb Merge pull request #1599 from TomHarte/AutoClip
Automatically select and zoom to 'interesting' content.
2025-10-16 21:04:23 -04:00
Thomas Harte
e7f09e2ece Fix first reading. 2025-10-16 20:51:39 -04:00
Thomas Harte
89678f1ea7 Tweak decision process, add maximum scale parameter. 2025-10-16 16:26:16 -04:00
Thomas Harte
e43ec7d549 Correct bias to the left. 2025-10-16 11:50:32 -04:00
Thomas Harte
95395132f0 Make stability threshold modal. 2025-10-16 11:29:41 -04:00
Thomas Harte
89293d8481 Add stability as a prefilter. 2025-10-16 11:26:07 -04:00
Thomas Harte
e6de24557f Set appropriate BBC bounds. 2025-10-15 23:39:55 -04:00
Thomas Harte
66d76dc36a Adjust dynamic semantics again. 2025-10-15 23:30:25 -04:00
Thomas Harte
06629def62 Restore some fixed areas, work on API. 2025-10-14 22:51:36 -04:00
Thomas Harte
97aeb5e930 Merge branch 'AutoClip' of github.com:TomHarte/CLK into AutoClip 2025-10-14 22:23:12 -04:00
Thomas Harte
bf45b6e20b Merge branch 'master' into AutoClip 2025-10-14 22:23:06 -04:00
Thomas Harte
6ad41326b0 Remove errant space. 2025-10-13 23:13:45 -04:00
Thomas Harte
2bbca3c169 Slightly beef up 8272 logging. 2025-10-13 23:09:10 -04:00
Thomas Harte
ae903b0712 Increase consts. 2025-10-13 22:53:52 -04:00
Thomas Harte
a2a7f82716 Merge branch 'master' into AutoClip 2025-10-13 22:51:21 -04:00
Thomas Harte
00456c891a Merge pull request #1601 from TomHarte/EvenShorterText
Even shorter text
2025-10-13 22:50:53 -04:00
Thomas Harte
afd5faaab1 Tweak constraints again. 2025-10-13 13:29:31 -04:00
Thomas Harte
bb33cf0f8d Shorten text even further. 2025-10-13 13:26:49 -04:00
Thomas Harte
edc510572a Reorder constraints. 2025-10-13 10:55:23 -04:00
Thomas Harte
bc6cffa95c Enable full dynamic selection again for the CPC. 2025-10-13 08:54:33 -04:00
Thomas Harte
48ed2912b0 Reenable dynamic framing. 2025-10-12 22:30:37 -04:00
Thomas Harte
a8af262c41 Avoid shadowing, use normal instance suffix. 2025-10-12 21:41:22 -04:00
Thomas Harte
dcf49933bc Merge branch 'master' into AutoClip 2025-10-12 21:32:22 -04:00
Thomas Harte
9c014001da Merge pull request #1600 from TomHarte/ShorterOpenDialogue
macOS: shorten prompt to File -> Open...
2025-10-12 21:27:07 -04:00
Thomas Harte
4f410088dd Improve constraints. 2025-10-12 21:17:07 -04:00
Thomas Harte
e1c1b66dc5 Shorten footer text. 2025-10-12 20:55:29 -04:00
Thomas Harte
23c3a1fa99 Lean further overtly towards a state machine. 2025-10-12 08:59:07 -04:00
Thomas Harte
ef6e1b2f74 Unpublish enum, simplify function names. 2025-10-11 15:07:09 -04:00
Thomas Harte
e130ae0a8a Merge branch 'AutoClip' of github.com:TomHarte/CLK into AutoClip 2025-10-10 22:27:47 -04:00
Thomas Harte
1a1e3281e4 Avoid overlong line; add consts. 2025-10-10 22:27:29 -04:00
Thomas Harte
a4e55c9362 Avoid overlong line. 2025-10-10 22:25:16 -04:00
Thomas Harte
0b4c51eebd Scale interesting rects once only. 2025-10-10 22:23:22 -04:00
Thomas Harte
1107f0d9a3 For relevant machines: pick different amounts of border to show. 2025-10-10 21:58:03 -04:00
Thomas Harte
775819432b Apply warm-up for the Apple II and ZX Spectrum. 2025-10-10 21:37:31 -04:00
Thomas Harte
a71a60937f Prewarm Macintosh; mark RAM as const. 2025-10-10 18:02:46 -04:00
Thomas Harte
5e661fe96b Add prewarming to the Oric. 2025-10-10 18:00:54 -04:00
Thomas Harte
a9f5b17fcb Eliminate frame_is_complete_, add prewalming loop. 2025-10-10 17:59:10 -04:00
Thomas Harte
b0c2b55fc9 Fix initial bounds, slightly update breathing space. 2025-10-10 15:44:54 -04:00
Thomas Harte
925832aac5 Include tolerance for interlacing. 2025-10-10 14:29:40 -04:00
Thomas Harte
994131e2ea Use stability as test for initial frame. 2025-10-10 14:18:25 -04:00
Thomas Harte
f8d27d0ae0 Remove explicit visible area declarations. 2025-10-09 22:17:02 -04:00
Thomas Harte
fc50af0e17 Adjust vertical sync test. 2025-10-09 22:16:43 -04:00
Thomas Harte
087d3535f6 Start focussing on getting a good crop for 'static' machines. 2025-10-09 18:01:46 -04:00
Thomas Harte
e9d310962f Support an asymmetric 90%. 2025-10-09 14:01:52 -04:00
Thomas Harte
0f9c89d259 Limit to 90%. 2025-10-09 13:59:03 -04:00
Thomas Harte
258c37685b Fix axis. 2025-10-09 13:53:35 -04:00
Thomas Harte
56f092a0c3 Try a rolling average of 250 frames, subject to thresholding. 2025-10-09 13:51:19 -04:00
Thomas Harte
6c3048ffbf Relax flywheel response rate again. 2025-10-08 22:12:58 -04:00
Thomas Harte
c58eba61de Extend required stability window. 2025-10-08 22:00:32 -04:00
Thomas Harte
8a54773f1b Reduce Metal buffer thrashing. 2025-10-08 21:19:31 -04:00
Thomas Harte
2c483e7b97 Avoid nullptr dereference if there is no activity observer. 2025-10-08 17:42:57 -04:00
Thomas Harte
1027e9ffdc Add but abandon first attempt at sane limits. 2025-10-08 17:34:54 -04:00
Thomas Harte
85d6957e03 Attempt to do better at startup. 2025-10-08 14:33:49 -04:00
Thomas Harte
c3609b66a9 Attempt a quick snap at startup. 2025-10-08 14:13:34 -04:00
Thomas Harte
605f4a92d7 Use animation curve, try to be fooled less at startup. 2025-10-08 12:58:12 -04:00
Thomas Harte
d395e2bc75 Introduce animated crop. 2025-10-08 12:18:04 -04:00
Thomas Harte
e6ccdc5a97 Edge towards animations. 2025-10-07 23:00:36 -04:00
Thomas Harte
a68c7aa45f Use filter, attempt to be intelligent about the border. 2025-10-07 22:56:51 -04:00
Thomas Harte
66e959ab65 Temporarily exclude borders. 2025-10-07 22:42:26 -04:00
Thomas Harte
d68b172a40 Introduce preliminary output frame filtering. 2025-10-07 22:36:36 -04:00
Thomas Harte
d3ee778265 Eliminate common black border -> blank mapping.
Will move this inside the CRT.
2025-10-07 22:10:14 -04:00
Thomas Harte
da96df7df7 Ensure OpenGL appropriately letterboxes or pillarboxes. 2025-10-07 21:37:22 -04:00
Thomas Harte
4ea82581ec Factor out zoom logic, start trying to knock OpenGL into shape. 2025-10-07 13:29:21 -04:00
Thomas Harte
4473d3400e Reformat slightly. 2025-10-07 12:52:55 -04:00
Thomas Harte
2f1f843e48 Correct origin.y minification. 2025-10-07 12:44:03 -04:00
Thomas Harte
53a3d9042e Switch to multiline strings, shorter comments. 2025-10-06 22:58:50 -04:00
Thomas Harte
6eb32f98b2 Fix rectangle union. 2025-10-06 22:50:29 -04:00
Thomas Harte
0fad97ed48 Apply different axis scales. 2025-10-06 22:36:19 -04:00
Thomas Harte
27246247a2 OpenGL: fix centring. 2025-10-06 20:58:42 -04:00
Thomas Harte
cbc96e2223 Reformat in proximity. 2025-10-06 20:45:20 -04:00
Thomas Harte
8fdf32cde8 Avoid OpenGL churn. 2025-10-06 20:43:12 -04:00
Thomas Harte
03a94e59e2 Merge branch 'master' into AutoClip 2025-10-06 20:29:08 -04:00
Thomas Harte
2c0610fef8 Accumulate union of all pixel-bearing scans. 2025-10-06 20:26:15 -04:00
Thomas Harte
60b3c51085 Merge pull request #1598 from TomHarte/DynamicViewArea
Begin move towards automatic cropping.
2025-10-06 19:07:20 -04:00
Thomas Harte
d7b5a45417 Adopt even more aggressive mixing, avoid negative. 2025-10-06 16:20:54 -04:00
Thomas Harte
e11060bde8 Further improve asserting. 2025-10-06 16:16:06 -04:00
Thomas Harte
4653de9161 Pull out and comment on mix, improve asserts. 2025-10-06 16:11:59 -04:00
Thomas Harte
1926ad9215 Normalise and slightly reformat flywheel interface. 2025-10-06 14:53:08 -04:00
Thomas Harte
33d047c703 Add a const. 2025-10-06 14:38:40 -04:00
Thomas Harte
fadda00246 Eliminate flywheel 'get's, hence normalise CRT line lengths. 2025-10-06 14:36:39 -04:00
Thomas Harte
a3fed788d8 Reduce repetition. 2025-10-06 14:27:57 -04:00
Thomas Harte
dde31e8687 Reformat inner loop. 2025-10-06 14:26:03 -04:00
Thomas Harte
190fb009bc Clean up CRT.hpp for formatting. Switch pointer to reference. 2025-10-06 13:55:03 -04:00
Thomas Harte
62574d04c6 Avoid some redundant parameter names. 2025-10-06 13:32:28 -04:00
Thomas Harte
2496257bcf Adopt normative public-then-private ordering. 2025-10-06 13:28:04 -04:00
Thomas Harte
ab73b4de6b Split off the mismatch warner. 2025-10-06 13:27:10 -04:00
Thomas Harte
6c1c32baca Move flywheels local. 2025-10-04 22:42:56 -04:00
Thomas Harte
239cc15c8f Introduce cubic timing function. 2025-10-04 22:26:09 -04:00
Thomas Harte
6b437c3907 Merge pull request #1597 from TomHarte/NewShaker
Ensure CPCShakerTests is runnable.
2025-10-03 22:33:52 -04:00
Thomas Harte
4756f63169 Ensure CPCShakerTests is runnable. 2025-10-03 22:25:16 -04:00
56 changed files with 1852 additions and 985 deletions

View File

@@ -16,9 +16,18 @@
#include <algorithm> #include <algorithm>
#include <cstring> #include <cstring>
#include <unordered_set>
namespace { namespace {
std::string rtrimmed(const std::string &input) {
auto trimmed = input;
trimmed.erase(std::find_if(trimmed.rbegin(), trimmed.rend(), [](const char ch) {
return !std::isspace(ch);
}).base(), trimmed.end());
return trimmed;
}
bool strcmp_insensitive(const char *a, const char *b) { bool strcmp_insensitive(const char *a, const char *b) {
if(std::strlen(a) != std::strlen(b)) return false; if(std::strlen(a) != std::strlen(b)) return false;
while(*a) { while(*a) {
@@ -104,58 +113,85 @@ void InspectCatalogue(
return; return;
} }
// If only one file is [potentially] BASIC, run that one; otherwise if only one has a suffix const auto run_name = [&]() -> std::optional<std::string> {
// that AMSDOS allows to be omitted, pick that one. // Collect:
int basic_files = 0; //
int implicit_suffixed_files = 0; // 1. a set of all files that can be run without specifying an extension plus their appearance counts;
// 2. a set of all BASIC file names.
std::unordered_map<std::string, int> candidates;
std::unordered_set<std::string> basic_names;
for(std::size_t c = 0; c < candidate_files.size(); c++) {
// Files with nothing but spaces in their name can't be loaded by the user, so disregard them.
if(
(candidate_files[c]->type == " " && candidate_files[c]->name == " ") ||
!is_implied_extension(candidate_files[c]->type)
) {
continue;
}
std::size_t last_basic_file = 0; ++candidates[candidate_files[c]->name];
std::size_t last_implicit_suffixed_file = 0; if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) {
basic_names.insert(candidate_files[c]->name);
for(std::size_t c = 0; c < candidate_files.size(); c++) {
// Files with nothing but spaces in their name can't be loaded by the user, so disregard them.
if(candidate_files[c]->type == " " && candidate_files[c]->name == " ")
continue;
// Check for whether this is [potentially] BASIC.
if(candidate_files[c]->data.size() >= 128 && !((candidate_files[c]->data[18] >> 1) & 7)) {
basic_files++;
last_basic_file = c;
}
// Check suffix for emptiness.
if(is_implied_extension(candidate_files[c]->type)) {
implicit_suffixed_files++;
last_implicit_suffixed_file = c;
}
}
if(basic_files == 1 || implicit_suffixed_files == 1) {
std::size_t selected_file = (basic_files == 1) ? last_basic_file : last_implicit_suffixed_file;
target->loading_command = RunCommandFor(*candidate_files[selected_file]);
return;
}
// One more guess: if only one remaining candidate file has a different name than the others,
// assume it is intended to stand out.
std::map<std::string, int> name_counts;
std::map<std::string, std::size_t> indices_by_name;
std::size_t index = 0;
for(const auto &file : candidate_files) {
name_counts[file->name]++;
indices_by_name[file->name] = index;
index++;
}
if(name_counts.size() == 2) {
for(const auto &pair : name_counts) {
if(pair.second == 1) {
target->loading_command = RunCommandFor(*candidate_files[indices_by_name[pair.first]]);
return;
} }
} }
}
// Desperation. // Only one candidate total.
target->loading_command = "cat\n"; if(candidates.size() == 1) {
return candidates.begin()->first;
}
// Only one BASIC candidate.
if(basic_names.size() == 1) {
return *basic_names.begin();
}
// Exactly two candidate names, but only one is a unique name.
if(candidates.size() == 2) {
const auto item1 = candidates.begin();
const auto item2 = std::next(item1);
if(item1->second == 1 && item2->second != 1) {
return item1->first;
}
if(item2->second == 1 && item1->second != 1) {
return item2->first;
}
}
// Remove from candidates anything that is just a suffixed version of
// another name, as long as the other name is three or more characters.
std::vector<std::string> to_remove;
for(const auto &lhs: candidates) {
const auto trimmed = rtrimmed(lhs.first);
if(trimmed.size() < 3) {
continue;
}
for(const auto &rhs: candidates) {
if(lhs.first == rhs.first) {
continue;
}
if(rhs.first.find(trimmed) == 0) {
to_remove.push_back(rhs.first);
}
}
}
for(const auto &candidate: to_remove) {
candidates.erase(candidate);
}
if(candidates.size() == 1) {
return candidates.begin()->first;
}
return {};
} ();
if(run_name.has_value()) {
target->loading_command = "run\"" + rtrimmed(*run_name) + "\n";
} else {
target->loading_command = "cat\n";
}
} }
bool CheckBootSector( bool CheckBootSector(

View File

@@ -8,8 +8,8 @@
#include "1770.hpp" #include "1770.hpp"
#include "Storage/Disk/Encodings/MFM/Constants.hpp"
#include "Outputs/Log.hpp" #include "Outputs/Log.hpp"
#include "Storage/Disk/Encodings/MFM/Constants.hpp"
namespace { namespace {
using Logger = Log::Logger<Log::Source::WDFDC>; using Logger = Log::Logger<Log::Source::WDFDC>;
@@ -133,18 +133,34 @@ void WD1770::run_for(const Cycles cycles) {
} }
} }
#include <iostream>
void WD1770::posit_event(const int new_event_type) { void WD1770::posit_event(const int new_event_type) {
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; interesting_event_mask_ = int(mask); return; case __LINE__: #define WAIT_FOR_EVENT(mask) { \
interesting_event_mask_ = int(mask); \
static constexpr int location = __COUNTER__ + 1; \
resume_point_ = location; \
return; \
case location: \
(void)0; \
}
#define WAIT_FOR_TIME(ms) \
delay_time_ = ms * 8000; \
WAIT_FOR_EVENT(Event1770::Timer);
#define WAIT_FOR_BYTES(count) \
distance_into_section_ = 0; \
WAIT_FOR_EVENT(Event::Token); \
distance_into_section_ += get_latest_token().type == Token::Byte; \
if(distance_into_section_ < count) { \
RESUME_WAIT(Event::Token); \
}
#define RESUME_WAIT(mask) interesting_event_mask_ = int(mask); return; #define RESUME_WAIT(mask) interesting_event_mask_ = int(mask); return;
#define WAIT_FOR_TIME(ms) resume_point_ = __LINE__; delay_time_ = ms * 8000; WAIT_FOR_EVENT(Event1770::Timer);
#define WAIT_FOR_BYTES(count) distance_into_section_ = 0; \ #define BEGIN_SECTION() switch(resume_point_) { default:
WAIT_FOR_EVENT(Event::Token); \ #define END_SECTION() (void)0; }
if(get_latest_token().type == Token::Byte) ++distance_into_section_; \
if(distance_into_section_ < count) { \
RESUME_WAIT(Event::Token); \
}
#define BEGIN_SECTION() switch(resume_point_) { default:
#define END_SECTION() (void)0; }
const auto READ_ID = [&] { const auto READ_ID = [&] {
if(new_event_type == int(Event::Token)) { if(new_event_type == int(Event::Token)) {
@@ -185,7 +201,7 @@ void WD1770::posit_event(const int new_event_type) {
if(new_event_type == int(Event1770::ForceInterrupt)) { if(new_event_type == int(Event1770::ForceInterrupt)) {
interesting_event_mask_ = 0; interesting_event_mask_ = 0;
resume_point_ = 0; resume_point_ = IdleResumePoint;
update_status([] (Status &status) { update_status([] (Status &status) {
status.type = Status::One; status.type = Status::One;
status.data_request = false; status.data_request = false;
@@ -216,7 +232,7 @@ void WD1770::posit_event(const int new_event_type) {
BEGIN_SECTION() BEGIN_SECTION()
// Wait for a new command, branch to the appropriate handler. // Wait for a new command, branch to the appropriate handler.
case 0: case IdleResumePoint:
wait_for_command: wait_for_command:
Logger::info().append("Idle..."); Logger::info().append("Idle...");
set_data_mode(DataMode::Scanning); set_data_mode(DataMode::Scanning);

View File

@@ -124,9 +124,12 @@ private:
}; };
void posit_event(int type); void posit_event(int type);
int interesting_event_mask_; int interesting_event_mask_;
int resume_point_ = 0;
Cycles::IntType delay_time_ = 0; Cycles::IntType delay_time_ = 0;
// Current state machine stap pointer.
static constexpr int IdleResumePoint = 0;
int resume_point_ = IdleResumePoint;
// ID buffer // ID buffer
uint8_t header_[6]; uint8_t header_[6];

View File

@@ -161,10 +161,10 @@ public:
switch(output_mode) { switch(output_mode) {
case OutputMode::PAL: case OutputMode::PAL:
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
break; break;
case OutputMode::NTSC: case OutputMode::NTSC:
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
break; break;
} }

View File

@@ -94,7 +94,13 @@ public:
if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40; if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40;
if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_; if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_;
if(selected_register_ < 12 || selected_register_ > 17) return 0xff;
// Registers below 12 are write-only; no registers are defined above position 17
// (other than the UM6845R-specific test register as per above).
//
// Per the BBC Wiki, attempting to read such a register results in 0.
if(selected_register_ < 12 || selected_register_ > 17) return 0x00;
return registers_[selected_register_]; return registers_[selected_register_];
} }
@@ -114,6 +120,7 @@ public:
case 6: layout_.vertical.displayed = value; break; case 6: layout_.vertical.displayed = value; break;
case 7: layout_.vertical.start_sync = value; break; case 7: layout_.vertical.start_sync = value; break;
case 8: case 8:
printf("Interlace mode: %d", value & 3);
switch(value & 3) { switch(value & 3) {
default: layout_.interlace_mode_ = InterlaceMode::Off; break; default: layout_.interlace_mode_ = InterlaceMode::Off; break;
case 0b01: layout_.interlace_mode_ = InterlaceMode::Sync; break; case 0b01: layout_.interlace_mode_ = InterlaceMode::Sync; break;
@@ -156,7 +163,7 @@ public:
0x7f, // Start horizontal retrace. 0x7f, // Start horizontal retrace.
0x1f, 0x7f, 0x7f, 0x1f, 0x7f, 0x7f,
0xff, 0x1f, 0x7f, 0x1f, 0xfc, 0x1f, 0x7f, 0x1f,
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask), uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask), uint8_t(RefreshAddress::Mask >> 8), uint8_t(RefreshAddress::Mask),
}; };

View File

@@ -58,15 +58,18 @@ void i8272::run_for(const Cycles cycles) {
drives_[c].step_rate_counter %= (8000 * step_rate_time_); drives_[c].step_rate_counter %= (8000 * step_rate_time_);
while(steps--) { while(steps--) {
// Perform a step. // Perform a step.
int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1; const int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1;
Logger::info().append(
"Target %d versus believed %d", drives_[c].target_head_position, drives_[c].head_position);
select_drive(c); select_drive(c);
get_drive().step(Storage::Disk::HeadPosition(direction)); get_drive().step(Storage::Disk::HeadPosition(direction));
if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction; if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction;
Logger::info().append(
"Drive %d: seeking %d but seemingly at %d", c, drives_[c].target_head_position, drives_[c].head_position);
// Check for completion. // Check for completion.
if(seek_is_satisfied(c)) { if(seek_is_satisfied(c)) {
Logger::info().append(
"Drive %d: seek satisfied", c, drives_[c].target_head_position, drives_[c].head_position);
drives_[c].phase = Drive::CompletedSeeking; drives_[c].phase = Drive::CompletedSeeking;
drives_seeking_--; drives_seeking_--;
break; break;
@@ -141,22 +144,30 @@ uint8_t i8272::read(const int address) {
} }
void i8272::posit_event(const int event_type) { void i8272::posit_event(const int event_type) {
#define BEGIN_SECTION() switch(resume_point_) { default:
#define BEGIN_SECTION() switch(resume_point_) { default: case IdleResumePoint:
#define END_SECTION() } #define END_SECTION() }
#define WAIT_FOR_EVENT(mask) resume_point_ = __LINE__; \ #define WAIT_FOR_EVENT(mask) { \
interesting_event_mask_ = int(mask); \ static constexpr int location = __COUNTER__ + 1; \
return; \ resume_point_ = location; \
case __LINE__: interesting_event_mask_ = int(mask); \
return; \
case location: \
(void)0; \
}
#define WAIT_FOR_TIME(ms) interesting_event_mask_ = int(Event8272::Timer); \ #define WAIT_FOR_TIME(ms) { \
delay_time_ = ms_to_cycles(ms); \ static constexpr int location = __COUNTER__ + 1; \
is_sleeping_ = false; \ interesting_event_mask_ = int(Event8272::Timer); \
update_clocking_observer(); \ delay_time_ = ms_to_cycles(ms); \
resume_point_ = __LINE__; \ is_sleeping_ = false; \
[[fallthrough]]; \ update_clocking_observer(); \
case __LINE__: \ resume_point_ = location; \
if(delay_time_) return; [[fallthrough]]; \
case location: \
if(delay_time_) return; \
}
#define PASTE(x, y) x##y #define PASTE(x, y) x##y
#define LABEL(x, y) PASTE(x, y) #define LABEL(x, y) PASTE(x, y)
@@ -716,7 +727,6 @@ void i8272::posit_event(const int event_type) {
// Performs sense interrupt status. // Performs sense interrupt status.
sense_interrupt_status: sense_interrupt_status:
Logger::info().append("Sense interrupt status");
{ {
// Find the first drive that is in the CompletedSeeking state. // Find the first drive that is in the CompletedSeeking state.
int found_drive = -1; int found_drive = -1;
@@ -731,12 +741,12 @@ void i8272::posit_event(const int event_type) {
if(found_drive != -1) { if(found_drive != -1) {
drives_[found_drive].phase = Drive::NotSeeking; drives_[found_drive].phase = Drive::NotSeeking;
status_.set_status0(uint8_t(found_drive | uint8_t(Status0::SeekEnded))); status_.set_status0(uint8_t(found_drive | uint8_t(Status0::SeekEnded)));
// status_.end_sense_interrupt_status(found_drive, 0);
// status_.set(Status0::SeekEnded);
result_stack_ = { drives_[found_drive].head_position, status_[0]}; result_stack_ = { drives_[found_drive].head_position, status_[0]};
Logger::info().append("Sense interrupt status: returning %02x %02x", result_stack_[0], result_stack_[1]);
} else { } else {
result_stack_ = { 0x80 }; result_stack_ = { 0x80 };
Logger::info().append("Sense interrupt status: returning %02x", result_stack_[0]);
} }
} }
goto post_result; goto post_result;

View File

@@ -72,9 +72,11 @@ private:
}; };
void posit_event(int type) final; void posit_event(int type) final;
int interesting_event_mask_ = int(Event8272::CommandByte); int interesting_event_mask_ = int(Event8272::CommandByte);
int resume_point_ = 0;
bool is_access_command_ = false; bool is_access_command_ = false;
static constexpr int IdleResumePoint = 0;
int resume_point_ = 0;
// The counter used for ::Timer events. // The counter used for ::Timer events.
Cycles::IntType delay_time_ = 0; Cycles::IntType delay_time_ = 0;

View File

@@ -74,9 +74,9 @@ TMS9918<personality>::TMS9918() {
this->crt_.set_display_type(Outputs::Display::DisplayType::RGB); this->crt_.set_display_type(Outputs::Display::DisplayType::RGB);
if constexpr (is_yamaha_vdp(personality)) { if constexpr (is_yamaha_vdp(personality)) {
this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f)); this->crt_.set_fixed_framing(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f));
} else { } else {
this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); this->crt_.set_fixed_framing(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f));
} }
// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement
@@ -683,13 +683,7 @@ void Base<personality>::output_border(int cycles, [[maybe_unused]] const uint32_
return; return;
} }
// If the border colour is 0, that can be communicated crt_.output_level<uint32_t>(cycles, border_colour);
// more efficiently as an explicit blank.
if(border_colour) {
crt_.output_level<uint32_t>(cycles, border_colour);
} else {
crt_.output_blank(cycles);
}
} }
// MARK: - External interface. // MARK: - External interface.

View File

@@ -28,7 +28,7 @@ template <typename Performer> struct TaskQueueStorage {
protected: protected:
void update() { void update() {
auto time_now = Time::nanos_now(); const auto time_now = Time::nanos_now();
performer.perform(time_now - last_fired_); performer.perform(time_now - last_fired_);
last_fired_ = time_now; last_fired_ = time_now;
} }
@@ -64,7 +64,12 @@ template <> struct TaskQueueStorage<void> {
action occupies the asynchronous thread for long enough. So it is not true that @c perform will be action occupies the asynchronous thread for long enough. So it is not true that @c perform will be
called once per action. called once per action.
*/ */
template <bool perform_automatically, bool start_immediately = true, typename Performer = void> class AsyncTaskQueue: public TaskQueueStorage<Performer> { template <
bool perform_automatically,
bool start_immediately = true,
typename Performer = void
>
class AsyncTaskQueue: public TaskQueueStorage<Performer> {
public: public:
template <typename... Args> AsyncTaskQueue(Args&&... args) : template <typename... Args> AsyncTaskQueue(Args&&... args) :
TaskQueueStorage<Performer>(std::forward<Args>(args)...) { TaskQueueStorage<Performer>(std::forward<Args>(args)...) {
@@ -84,7 +89,7 @@ public:
/// on the same thread as the performer, after the performer has been updated /// on the same thread as the performer, after the performer has been updated
/// to 'now'. /// to 'now'.
void enqueue(const std::function<void(void)> &post_action) { void enqueue(const std::function<void(void)> &post_action) {
std::lock_guard guard(condition_mutex_); const std::lock_guard guard(condition_mutex_);
actions_.push_back(post_action); actions_.push_back(post_action);
if constexpr (perform_automatically) { if constexpr (perform_automatically) {

View File

@@ -29,7 +29,7 @@ public:
/// Defines the broad type of the input. /// Defines the broad type of the input.
enum Type { enum Type {
// Half-axis inputs. // Half-axis inputs.
Up, Down, Left, Right, Down, Up, Left, Right,
// Full-axis inputs. // Full-axis inputs.
Horizontal, Vertical, Horizontal, Vertical,
// Fire buttons. // Fire buttons.
@@ -192,8 +192,8 @@ public:
const auto analogue_value = [&](const int mask) { const auto analogue_value = [&](const int mask) {
switch(mask) { switch(mask) {
default: return 0.5f; default: return 0.5f;
case 0b01: return digital_maximum(); case 0b01: return digital_minimum();
case 0b10: return digital_minimum(); case 0b10: return digital_maximum();
} }
}; };
@@ -264,10 +264,11 @@ private:
const int mask = [&] { const int mask = [&] {
switch(input.type) { switch(input.type) {
default: return 0; default: return 0;
case Input::Type::Up: return 1 << 1; case Input::Type::Up: return 1 << 0;
case Input::Type::Down: return 1 << 2; case Input::Type::Down: return 1 << 1;
case Input::Type::Left: return 1 << 2;
case Input::Type::Right: return 1 << 3; case Input::Type::Right: return 1 << 3;
case Input::Type::Left: return 1 << 4;
} }
} (); } ();
if(is_active) { if(is_active) {
@@ -279,8 +280,8 @@ private:
int digital_mask(const Input::Type axis) const { int digital_mask(const Input::Type axis) const {
switch(axis) { switch(axis) {
default: return 0; default: return 0;
case Input::Type::Horizontal: return (digital_inputs_ >> 3) & 3; case Input::Type::Horizontal: return (digital_inputs_ >> 2) & 3;
case Input::Type::Vertical: return (digital_inputs_ >> 1) & 3; case Input::Type::Vertical: return (digital_inputs_ >> 0) & 3;
} }
} }
int digital_inputs_ = 0; int digital_inputs_ = 0;

View File

@@ -28,7 +28,7 @@ struct Video {
ram_(ram), ram_(ram),
crt_(Outputs::Display::InputDataType::Red4Green4Blue4) { crt_(Outputs::Display::InputDataType::Red4Green4Blue4) {
set_clock_divider(3); set_clock_divider(3);
crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f));
crt_.set_display_type(Outputs::Display::DisplayType::RGB); crt_.set_display_type(Outputs::Display::DisplayType::RGB);
} }

View File

@@ -179,7 +179,7 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
) : ) :
audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks), delegate_(delegate) audio_(audio), video_base_(video_base), via_(via), joysticks_(joysticks), delegate_(delegate)
{ {
set_key_flag(6, run_disk); set_key_flag(uint8_t(Key::Bit3), run_disk);
} }
// CA2: key pressed; // CA2: key pressed;
@@ -226,12 +226,16 @@ struct SystemVIAPortHandler: public MOS::MOS6522::IRQDelegatePortHandler {
if(new_caps != caps_led_state_) { if(new_caps != caps_led_state_) {
caps_led_state_ = new_caps; caps_led_state_ = new_caps;
activity_observer_->set_led_status(caps_led, caps_led_state_); if(activity_observer_) {
activity_observer_->set_led_status(caps_led, caps_led_state_);
}
} }
if(new_shift != shift_led_state_) { if(new_shift != shift_led_state_) {
shift_led_state_ = new_shift; shift_led_state_ = new_shift;
activity_observer_->set_led_status(shift_led, shift_led_state_); if(activity_observer_) {
activity_observer_->set_led_status(shift_led, shift_led_state_);
}
} }
} }
} }
@@ -375,7 +379,9 @@ public:
ram_(ram), ram_(ram),
system_via_(system_via) system_via_(system_via)
{ {
crt_.set_visible_area(crt_.get_rect_for_area(30, 256, 160, 800, 4.0f / 3.0f)); crt_.set_dynamic_framing(
Outputs::Display::Rect(0.13333f, 0.06507f, 0.71579f, 0.86069f),
0.0f, 0.05f);
} }
void set_palette(const uint8_t value) { void set_palette(const uint8_t value) {
@@ -724,6 +730,14 @@ public:
install_sideways(fs_slot--, roms.find(Name::BBCMicroADFS130)->second, false); install_sideways(fs_slot--, roms.find(Name::BBCMicroADFS130)->second, false);
} }
// Install the ADT ROM if available, but don't error if it's missing. It's very optional.
if(target.has_1770dfs || target.has_adfs) {
const auto adt_rom = rom_fetcher(Request(Name::BBCMicroAdvancedDiscToolkit140));
if(const auto rom = adt_rom.find(Name::BBCMicroAdvancedDiscToolkit140); rom != adt_rom.end()) {
install_sideways(fs_slot--, rom->second, false);
}
}
// Throw sideways RAM into all unused slots. // Throw sideways RAM into all unused slots.
if(target.has_sideways_ram) { if(target.has_sideways_ram) {
for(size_t c = 0; c < 16; c++) { for(size_t c = 0; c < 16; c++) {
@@ -748,6 +762,10 @@ public:
if(!target.loading_command.empty()) { if(!target.loading_command.empty()) {
type_string(target.loading_command); type_string(target.loading_command);
} }
// Prime the display and then reset.
// run_for(Cycles(100'000'000));
// m6502_.set_power_on(true);
} }
// MARK: - 6502 bus. // MARK: - 6502 bus.

View File

@@ -20,13 +20,11 @@ VideoOutput::VideoOutput(const uint8_t *memory) :
1, 1,
Outputs::Display::Type::PAL50, Outputs::Display::Type::PAL50,
Outputs::Display::InputDataType::Red1Green1Blue1) { Outputs::Display::InputDataType::Red1Green1Blue1) {
crt_.set_visible_area(crt_.get_rect_for_area( // Default construction values leave this out of text mode, and text
312 - vsync_end, // mode uses a subregion of pixel modes.
256, crt_.set_fixed_framing([&] {
h_total - hsync_start, run_for(Cycles(10'000));
80 * 8, });
4.0f / 3.0f
));
} }
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *const scan_target) { void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *const scan_target) {

View File

@@ -46,7 +46,7 @@ Chipset::Chipset(MemoryMap &map, int input_clock_rate) :
// Very conservatively crop, to roughly the centre 88% of a frame. // Very conservatively crop, to roughly the centre 88% of a frame.
// This rectange was specifically calibrated around the default Workbench display. // This rectange was specifically calibrated around the default Workbench display.
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.055f, 0.88f, 0.88f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.055f, 0.88f, 0.88f));
} }
#undef DMA_CONSTRUCT #undef DMA_CONSTRUCT

View File

@@ -175,7 +175,9 @@ public:
interrupt_timer_(interrupt_timer) { interrupt_timer_(interrupt_timer) {
establish_palette_hits(); establish_palette_hits();
build_mode_table(); build_mode_table();
crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f)); crt_.set_dynamic_framing(
Outputs::Display::Rect(0.16842f, 0.19909f, 0.71579f, 0.67197f),
0.0f, 0.1f);
crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel, crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel,
// whereas Red2Green2Blue2 defines a range of 0-3. // whereas Red2Green2Blue2 defines a range of 0-3.
} }
@@ -377,14 +379,7 @@ private:
void output_border(const int length) { void output_border(const int length) {
assert(length >= 0); assert(length >= 0);
crt_.output_level<uint8_t>(length * 16, border_);
// A black border can be output via crt_.output_blank for a minor performance
// win; otherwise paint whatever the border colour really is.
if(border_) {
crt_.output_level<uint8_t>(length * 16, border_);
} else {
crt_.output_blank(length * 16);
}
} }
template <typename IntT> template <typename IntT>

View File

@@ -38,9 +38,13 @@ public:
Input(Input::Fire, 2), Input(Input::Fire, 2),
}) {} }) {}
void did_set_input(const Input &input, float value) final { void did_set_input(const Input &input, const float value) final {
if(!input.info.control.index && (input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical)) if(
!input.info.control.index &&
(input.type == Input::Type::Horizontal || input.type == Input::Type::Vertical)
) {
axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value; axes[(input.type == Input::Type::Horizontal) ? 0 : 1] = 1.0f - value;
}
} }
void did_set_input(const Input &input, bool value) final { void did_set_input(const Input &input, bool value) final {

View File

@@ -25,20 +25,8 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
// crt_.set_immediate_default_phase(0.5f); // crt_.set_immediate_default_phase(0.5f);
} }
void VideoBase::set_use_square_pixels(bool use_square_pixels) { void VideoBase::set_use_square_pixels(const bool use_square_pixels) {
use_square_pixels_ = use_square_pixels; use_square_pixels_ = use_square_pixels;
// HYPER-UGLY HACK. See correlated hack in the Macintosh.
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.122f, 0.75f, 0.77f));
#else
if(use_square_pixels) {
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.09f, 0.75f, 0.77f));
} else {
crt_.set_visible_area(Outputs::Display::Rect(0.128f, 0.12f, 0.75f, 0.77f));
}
#endif
if(use_square_pixels) { if(use_square_pixels) {
// From what I can make out, many contemporary Apple II monitors were // From what I can make out, many contemporary Apple II monitors were
// calibrated slightly to stretch the Apple II's display slightly wider // calibrated slightly to stretch the Apple II's display slightly wider
@@ -55,6 +43,13 @@ void VideoBase::set_use_square_pixels(bool use_square_pixels) {
crt_.set_aspect_ratio(4.0f / 3.0f); crt_.set_aspect_ratio(4.0f / 3.0f);
} }
} }
void VideoBase::establish_framing() {
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
}
bool VideoBase::get_use_square_pixels() const { bool VideoBase::get_use_square_pixels() const {
return use_square_pixels_; return use_square_pixels_;
} }

View File

@@ -35,8 +35,10 @@ class VideoBase: public VideoSwitches<Cycles> {
public: public:
VideoBase(bool is_iie, std::function<void(Cycles)> &&target); VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
void establish_framing();
/// Sets the scan target. /// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target); void set_scan_target(Outputs::Display::ScanTarget *);
/// Gets the current scan status. /// Gets the current scan status.
Outputs::Display::ScanStatus get_scaled_scan_status() const; Outputs::Display::ScanStatus get_scaled_scan_status() const;
@@ -119,8 +121,11 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
public: public:
/// Constructs an instance of the video feed; a CRT is also created. /// Constructs an instance of the video feed; a CRT is also created.
Video(BusHandler &bus_handler) : Video(BusHandler &bus_handler) :
VideoBase(is_iie, [this] (Cycles cycles) { advance(cycles); }), VideoBase(is_iie, [this] (const Cycles cycles) { advance(cycles); }),
bus_handler_(bus_handler) {} bus_handler_(bus_handler)
{
establish_framing();
}
/*! /*!
Obtains the last value the video read prior to time now+offset, according to the *current* Obtains the last value the video read prior to time now+offset, according to the *current*

View File

@@ -111,11 +111,6 @@ Video::Video() :
VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }), VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }),
crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) { crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB); crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.097f, 0.1f, 0.85f, 0.85f));
// Reduce the initial bounce by cueing up the part of the frame that initial drawing actually
// starts with. More or less.
crt_.output_blank(228*63*2);
// Establish the shift lookup table for NTSC -> RGB output. // Establish the shift lookup table for NTSC -> RGB output.
for(size_t c = 0; c < sizeof(ntsc_delay_lookup_) / sizeof(*ntsc_delay_lookup_); c++) { for(size_t c = 0; c < sizeof(ntsc_delay_lookup_) / sizeof(*ntsc_delay_lookup_); c++) {
@@ -149,8 +144,11 @@ Outputs::Display::DisplayType Video::get_display_type() const {
return crt_.get_display_type(); return crt_.get_display_type();
} }
void Video::set_internal_ram(const uint8_t *ram) { void Video::set_internal_ram(const uint8_t *const ram) {
ram_ = ram; ram_ = ram;
// crt_.set_automatic_fixed_framing([&] {
// run_for(Cycles(10'000));
// });
} }
void Video::advance(Cycles cycles) { void Video::advance(Cycles cycles) {
@@ -410,15 +408,7 @@ void Video::output_row(int row, int start, int end) {
// Output right border as far as currently known. // Output right border as far as currently known.
if(start >= start_of_right_border && start < start_of_sync) { if(start >= start_of_right_border && start < start_of_sync) {
const int end_of_period = std::min(start_of_sync, end); const int end_of_period = std::min(start_of_sync, end);
crt_.output_level<uint16_t>((end_of_period - start) * CyclesPerTick, border_colour_);
if(border_colour_) {
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
if(pixel) *pixel = border_colour_;
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
} else {
crt_.output_blank((end_of_period - start) * CyclesPerTick);
}
// There's no point updating start here; just fall // There's no point updating start here; just fall
// through to the end == FinalColumn test. // through to the end == FinalColumn test.
} }
@@ -426,15 +416,7 @@ void Video::output_row(int row, int start, int end) {
// This line is all border, all the time. // This line is all border, all the time.
if(start >= start_of_left_border && start < start_of_sync) { if(start >= start_of_left_border && start < start_of_sync) {
const int end_of_period = std::min(start_of_sync, end); const int end_of_period = std::min(start_of_sync, end);
crt_.output_level<uint16_t>((end_of_period - start) * CyclesPerTick, border_colour_);
if(border_colour_) {
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
if(pixel) *pixel = border_colour_;
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
} else {
crt_.output_blank((end_of_period - start) * CyclesPerTick);
}
start = end_of_period; start = end_of_period;
if(start == end) return; if(start == end) return;
} }

View File

@@ -39,17 +39,6 @@ Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulato
crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) { crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB); 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.
#if defined(__APPLE__) && !defined(IGNORE_APPLE)
crt_.set_visible_area(Outputs::Display::Rect(0.08f, 10.0f / 368.0f, 0.82f, 344.0f / 368.0f));
#else
crt_.set_visible_area(Outputs::Display::Rect(0.08f, -0.025f, 0.82f, 0.82f));
#endif
crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area. crt_.set_aspect_ratio(1.73f); // The Mac uses a non-standard scanning area.
} }
@@ -191,7 +180,12 @@ void Video::set_use_alternate_buffers(bool use_alternate_screen_buffer, bool use
use_alternate_audio_buffer_ = use_alternate_audio_buffer; use_alternate_audio_buffer_ = use_alternate_audio_buffer;
} }
void Video::set_ram(uint16_t *ram, uint32_t mask) { void Video::set_ram(const uint16_t *const ram, const uint32_t mask) {
ram_ = ram; ram_ = ram;
ram_mask_ = mask; ram_mask_ = mask;
// Now that RAM is assigned, the CRT cna be warmed.
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
} }

View File

@@ -57,7 +57,7 @@ public:
Provides a base address and a mask indicating which parts of the generated video and audio/drive addresses are Provides a base address and a mask indicating which parts of the generated video and audio/drive addresses are
actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff. actually decoded, accessing *word-sized memory*; e.g. for a 128kb Macintosh this should be (1 << 16) - 1 = 0xffff.
*/ */
void set_ram(uint16_t *ram, uint32_t mask); void set_ram(const uint16_t *ram, uint32_t mask);
/*! /*!
@returns @c true if the video is currently outputting a vertical sync, @c false otherwise. @returns @c true if the video is currently outputting a vertical sync, @c false otherwise.
@@ -86,7 +86,7 @@ private:
DriveSpeedAccumulator &drive_speed_accumulator_; DriveSpeedAccumulator &drive_speed_accumulator_;
Outputs::CRT::CRT crt_; Outputs::CRT::CRT crt_;
uint16_t *ram_ = nullptr; const uint16_t *ram_ = nullptr;
uint32_t ram_mask_ = 0; uint32_t ram_mask_ = 0;
HalfCycles frame_position_; HalfCycles frame_position_;

View File

@@ -13,6 +13,8 @@
#include "Machines/MachineTypes.hpp" #include "Machines/MachineTypes.hpp"
#include "Outputs/CRT/MismatchWarner.hpp"
#include "Analyser/Static/Atari2600/Target.hpp" #include "Analyser/Static/Atari2600/Target.hpp"
#include "Cartridges/Atari8k.hpp" #include "Cartridges/Atari8k.hpp"

View File

@@ -127,10 +127,9 @@ Video::Video() :
// Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's // Show a total of 260 lines; a little short for PAL but a compromise between that and the ST's
// usual output height of 200 lines. // usual output height of 200 lines.
crt_.set_visible_area(crt_.get_rect_for_area( crt_.set_fixed_framing(crt_.get_rect_for_area(
33, 260, 33, 260,
480, 1280, 480, 1280));
4.0f / 3.0f));
} }
void Video::set_ram(uint16_t *ram, size_t size) { void Video::set_ram(uint16_t *ram, size_t size) {

View File

@@ -36,12 +36,11 @@ public:
const auto visible_lines = 33 * 8; const auto visible_lines = 33 * 8;
const auto centre = eos() - vs_stop() + 104; // i.e. centre on vertical_counter_ = 104. const auto centre = eos() - vs_stop() + 104; // i.e. centre on vertical_counter_ = 104.
crt_.set_visible_area(crt_.get_rect_for_area( crt_.set_fixed_framing(crt_.get_rect_for_area(
centre - (visible_lines / 2), centre - (visible_lines / 2),
visible_lines, visible_lines,
int(HorizontalEvent::Begin40Columns) - int(HorizontalEvent::BeginSync) + int(HorizontalEvent::ScheduleCounterReset) + 1 - 8, int(HorizontalEvent::Begin40Columns) - int(HorizontalEvent::BeginSync) + int(HorizontalEvent::ScheduleCounterReset) + 1 - 8,
int(HorizontalEvent::End40Columns) - int(HorizontalEvent::Begin40Columns) + 16, int(HorizontalEvent::End40Columns) - int(HorizontalEvent::Begin40Columns) + 16
4.0f / 3.0f
)); ));
} }

View File

@@ -54,7 +54,7 @@ Nick::Nick(const uint8_t *const ram) :
set_display_type(Outputs::Display::DisplayType::RGB); set_display_type(Outputs::Display::DisplayType::RGB);
// Crop to the centre 90% of the display. // Crop to the centre 90% of the display.
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
} }
void Nick::write(const uint16_t address, const uint8_t value) { void Nick::write(const uint16_t address, const uint8_t value) {

View File

@@ -34,6 +34,11 @@ VideoOutput::VideoOutput(uint8_t *memory) :
crt_.set_input_data_type(data_type_); crt_.set_input_data_type(data_type_);
crt_.set_delegate(&frequency_mismatch_warner_); crt_.set_delegate(&frequency_mismatch_warner_);
update_crt_frequency(); update_crt_frequency();
// Prewarm CRT.
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
} }
void VideoOutput::register_crt_frequency_mismatch() { void VideoOutput::register_crt_frequency_mismatch() {
@@ -42,11 +47,7 @@ void VideoOutput::register_crt_frequency_mismatch() {
} }
void VideoOutput::update_crt_frequency() { void VideoOutput::update_crt_frequency() {
// Set the proper frequency...
crt_.set_new_display_type(64*6, crt_is_60Hz_ ? Outputs::Display::Type::PAL60 : Outputs::Display::Type::PAL50); crt_.set_new_display_type(64*6, crt_is_60Hz_ ? Outputs::Display::Type::PAL60 : Outputs::Display::Type::PAL50);
// ... but also pick an appropriate crop rectangle.
crt_.set_visible_area(crt_.get_rect_for_area(crt_is_60Hz_ ? 26 : 54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
} }
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) { void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {

View File

@@ -9,6 +9,7 @@
#pragma once #pragma once
#include "Outputs/CRT/CRT.hpp" #include "Outputs/CRT/CRT.hpp"
#include "Outputs/CRT/MismatchWarner.hpp"
#include "ClockReceiver/ClockReceiver.hpp" #include "ClockReceiver/ClockReceiver.hpp"
#include <cstdint> #include <cstdint>

View File

@@ -103,7 +103,6 @@ private:
CRTCOutputter() : CRTCOutputter() :
crt(910, 8, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red2Green2Blue2) crt(910, 8, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red2Green2Blue2)
{ {
crt.set_visible_area(Outputs::Display::Rect(0.095f, 0.095f, 0.82f, 0.82f));
crt.set_display_type(Outputs::Display::DisplayType::RGB); crt.set_display_type(Outputs::Display::DisplayType::RGB);
} }
@@ -192,11 +191,7 @@ private:
switch(output_state) { switch(output_state) {
case OutputState::Sync: crt.output_sync(count * active_clock_divider); break; case OutputState::Sync: crt.output_sync(count * active_clock_divider); break;
case OutputState::Border: case OutputState::Border:
if(active_border_colour) { crt.output_level<uint8_t>(count * active_clock_divider, active_border_colour);
crt.output_blank(count * active_clock_divider);
} else {
crt.output_level<uint8_t>(count * active_clock_divider, active_border_colour);
}
break; break;
case OutputState::ColourBurst: crt.output_colour_burst(count * active_clock_divider, colour_phase); break; case OutputState::ColourBurst: crt.output_colour_burst(count * active_clock_divider, colour_phase); break;
case OutputState::Pixels: flush_pixels(); break; case OutputState::Pixels: flush_pixels(); break;

View File

@@ -96,7 +96,6 @@ private:
// TODO: really this should be a Luminance8 and set an appropriate modal tint colour; // TODO: really this should be a Luminance8 and set an appropriate modal tint colour;
// consider whether that's worth building into the scan target. // consider whether that's worth building into the scan target.
{ {
crt.set_visible_area(Outputs::Display::Rect(0.028f, 0.025f, 0.98f, 0.98f));
crt.set_display_type(Outputs::Display::DisplayType::RGB); crt.set_display_type(Outputs::Display::DisplayType::RGB);
} }

View File

@@ -186,7 +186,9 @@ public:
InstructionSet::x86::SegmentRegisterSet<Descriptor> descriptors; InstructionSet::x86::SegmentRegisterSet<Descriptor> descriptors;
auto operator <=>(const Segments &rhs) const = default; auto operator ==(const Segments &rhs) const {
return descriptors == rhs.descriptors;
}
private: private:
void load_real(const Source segment) { void load_real(const Source segment) {

View File

@@ -27,7 +27,7 @@ Video::Video() :
// Show only the centre 80% of the TV frame. // Show only the centre 80% of the TV frame.
crt_.set_display_type(Outputs::Display::DisplayType::CompositeMonochrome); crt_.set_display_type(Outputs::Display::DisplayType::CompositeMonochrome);
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f)); crt_.set_fixed_framing(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
} }
void Video::run_for(const HalfCycles half_cycles) { void Video::run_for(const HalfCycles half_cycles) {

View File

@@ -114,7 +114,7 @@ private:
static constexpr int interrupt_duration = 64; static constexpr int interrupt_duration = 64;
public: public:
void run_for(HalfCycles duration) { void run_for(const HalfCycles duration) {
static constexpr auto timings = get_timings(); static constexpr auto timings = get_timings();
static constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1; static constexpr int sync_line = (timings.interrupt_time / timings.half_cycles_per_line) + 1;
@@ -307,7 +307,6 @@ public:
{ {
// Show only the centre 80% of the TV frame. // Show only the centre 80% of the TV frame.
crt_.set_display_type(Outputs::Display::DisplayType::RGB); crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
// Get the CRT roughly into phase. // Get the CRT roughly into phase.
// //
@@ -316,14 +315,20 @@ public:
crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time); crt_.output_blank(timings.lines_per_frame*timings.half_cycles_per_line - timings.interrupt_time);
} }
void set_video_source(const uint8_t *source) { void set_video_source(const uint8_t *const source) {
const bool is_first_set = !memory_;
memory_ = source; memory_ = source;
if(is_first_set) {
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
}
} }
/*! /*!
@returns The amount of time until the next change in the interrupt line, that being the only internally-observeable output. @returns The amount of time until the next change in the interrupt line, that being the only internally-observeable output.
*/ */
HalfCycles next_sequence_point() { HalfCycles next_sequence_point() const {
static constexpr auto timings = get_timings(); static constexpr auto timings = get_timings();
// Is the frame still ahead of this interrupt? // Is the frame still ahead of this interrupt?
@@ -352,10 +357,12 @@ public:
@returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention @returns How many cycles the [ULA/gate array] would delay the CPU for if it were to recognise that contention
needs to be applied in @c offset half-cycles from now. needs to be applied in @c offset half-cycles from now.
*/ */
HalfCycles access_delay(HalfCycles offset) const { HalfCycles access_delay(const HalfCycles offset) const {
static constexpr auto timings = get_timings(); static constexpr auto timings = get_timings();
const int delay_time = (time_into_frame_ + offset.as<int>() + timings.contention_leadin) % (timings.half_cycles_per_line * timings.lines_per_frame); const int delay_time =
assert(!(delay_time&1)); (time_into_frame_ + offset.as<int>() + timings.contention_leadin) %
(timings.half_cycles_per_line * timings.lines_per_frame);
// assert(!(delay_time&1));
// Check for a time within the no-contention window. // Check for a time within the no-contention window.
if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) { if(delay_time >= (191*timings.half_cycles_per_line + timings.contention_duration)) {
@@ -400,7 +407,7 @@ public:
written to contended memory. This is what will be returned if the floating written to contended memory. This is what will be returned if the floating
bus is accessed when the gate array isn't currently reading. bus is accessed when the gate array isn't currently reading.
*/ */
void set_last_contended_area_access([[maybe_unused]] uint8_t value) { void set_last_contended_area_access([[maybe_unused]] const uint8_t value) {
if constexpr (timing == Timing::Plus3) { if constexpr (timing == Timing::Plus3) {
last_contended_access_ = value | 1; last_contended_access_ = value | 1;
} }
@@ -409,13 +416,13 @@ public:
/*! /*!
Sets the current border colour. Sets the current border colour.
*/ */
void set_border_colour(uint8_t colour) { void set_border_colour(const uint8_t colour) {
border_byte_ = colour; border_byte_ = colour;
border_colour_ = palette[colour]; border_colour_ = palette[colour];
} }
/// Sets the scan target. /// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { void set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
crt_.set_scan_target(scan_target); crt_.set_scan_target(scan_target);
} }
@@ -425,7 +432,7 @@ public:
} }
/*! Sets the type of display the CRT will request. */ /*! Sets the type of display the CRT will request. */
void set_display_type(Outputs::Display::DisplayType type) { void set_display_type(const Outputs::Display::DisplayType type) {
crt_.set_display_type(type); crt_.set_display_type(type);
} }

View File

@@ -445,6 +445,14 @@ const std::vector<Description> &Description::all_roms() {
16_kb, 16_kb,
0xd3855588u 0xd3855588u
}, },
{
BBCMicroAdvancedDiscToolkit140,
"BBCMicro",
"the Advanced Disc Toolkit 1.40 ROM",
"ADT-1.40.rom",
16_kb,
0x8314fed0u
},
// //
// ColecoVision. // ColecoVision.

View File

@@ -84,6 +84,7 @@ enum Name {
BBCMicroMOS12, BBCMicroMOS12,
BBCMicroDFS226, BBCMicroDFS226,
BBCMicroADFS130, BBCMicroADFS130,
BBCMicroAdvancedDiscToolkit140,
// ColecoVision. // ColecoVision.
ColecoVisionBIOS, ColecoVisionBIOS,

73
Numeric/CubicCurve.hpp Normal file
View File

@@ -0,0 +1,73 @@
//
// CubicCurve.hpp
// Clock Signal
//
// Created by Thomas Harte on 04/10/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include <cassert>
namespace Numeric {
/*!
Provides a cubic Bezier-based timing function.
*/
struct CubicCurve {
CubicCurve(const float c1x, const float c1y, const float c2x, const float c2y) :
c1_(c1x, c1y), c2_(c2x, c2y)
{
assert(0.0f <= c1x); assert(c1x <= 1.0f);
assert(0.0f <= c1y); assert(c1y <= 1.0f);
assert(0.0f <= c2x); assert(c2x <= 1.0f);
assert(0.0f <= c2y); assert(c2y <= 1.0f);
}
/// @returns A standard ease-in-out animation curve.
static CubicCurve easeInOut() {
return CubicCurve(0.42f, 0.0f, 0.58f, 1.0f);
}
/// @returns The value for y given x, in range [0.0, 1.0].
float value(const float x) const {
return axis(t(x), 1);
}
private:
/// @returns The value for @c t that generates the value @c x.
float t(const float x) const {
static constexpr float Precision = 0.01f;
float bounds[2] = {0.0f, 1.0f};
const auto midpoint = [&] { return (bounds[0] + bounds[1]) * 0.5f; };
while(bounds[1] > bounds[0] + Precision) {
const float mid = midpoint();
const float value = axis(mid, 0);
if(value > x) {
bounds[1] = mid;
} else {
bounds[0] = mid;
}
}
return midpoint();
}
/// @returns The value for axis @c index at time @c t.
float axis(const float t, int index) const {
const float f1 = t * c1_[index];
const float f2 = t * c2_[index] + (1.0f - t) * c1_[index];
const float f3 = t + (1.0f - t) * c2_[index];
const float b1 = t * f2 + (1.0f - t) * f1;
const float b2 = t * f3 + (1.0f - t) * f2;
return t * b2 + (1.0f - t) * b1;
}
float c1_[2];
float c2_[2];
};
}

View File

@@ -26,6 +26,9 @@ template <typename Full, typename Half> union alignas(Full) alignas(Half) Regist
auto operator <=>(const RegisterPair &rhs) const { auto operator <=>(const RegisterPair &rhs) const {
return full <=> rhs.full; return full <=> rhs.full;
} }
auto operator ==(const RegisterPair &rhs) const {
return full == rhs.full;
}
#if TARGET_RT_BIG_ENDIAN #if TARGET_RT_BIG_ENDIAN
struct { struct {
Half high, low; Half high, low;

View File

@@ -1780,6 +1780,9 @@
4B83348E1F5DBA6E0097E338 /* 6522Storage.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 6522Storage.hpp; path = Implementation/6522Storage.hpp; sourceTree = "<group>"; }; 4B83348E1F5DBA6E0097E338 /* 6522Storage.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 6522Storage.hpp; path = Implementation/6522Storage.hpp; sourceTree = "<group>"; };
4B8334911F5E24FF0097E338 /* C1540Base.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = C1540Base.hpp; path = Implementation/C1540Base.hpp; sourceTree = "<group>"; }; 4B8334911F5E24FF0097E338 /* C1540Base.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = C1540Base.hpp; path = Implementation/C1540Base.hpp; sourceTree = "<group>"; };
4B8334941F5E25B60097E338 /* C1540.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = C1540.cpp; path = Implementation/C1540.cpp; sourceTree = "<group>"; }; 4B8334941F5E25B60097E338 /* C1540.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = C1540.cpp; path = Implementation/C1540.cpp; sourceTree = "<group>"; };
4B847B9F2E920C7500774B9B /* CubicCurve.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = CubicCurve.hpp; path = /Users/thomasharte/Projects/CLK/Numeric/CubicCurve.hpp; sourceTree = "<absolute>"; };
4B847BA42E94320B00774B9B /* MismatchWarner.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MismatchWarner.hpp; sourceTree = "<group>"; };
4B847BA62E96013000774B9B /* RectAccumulator.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = RectAccumulator.hpp; sourceTree = "<group>"; };
4B85322922778E4200F26553 /* Comparative68000.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Comparative68000.hpp; sourceTree = "<group>"; }; 4B85322922778E4200F26553 /* Comparative68000.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Comparative68000.hpp; sourceTree = "<group>"; };
4B85322E2277ABDD00F26553 /* tos100.trace.txt.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = tos100.trace.txt.gz; sourceTree = "<group>"; }; 4B85322E2277ABDD00F26553 /* tos100.trace.txt.gz */ = {isa = PBXFileReference; lastKnownFileType = archive.gzip; path = tos100.trace.txt.gz; sourceTree = "<group>"; };
4B8671EA2D8B40D8009E1610 /* Descriptors.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Descriptors.hpp; sourceTree = "<group>"; }; 4B8671EA2D8B40D8009E1610 /* Descriptors.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Descriptors.hpp; sourceTree = "<group>"; };
@@ -1797,6 +1800,7 @@
4B8805FA1DCFF807003085B1 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Parsers/Oric.hpp; sourceTree = "<group>"; }; 4B8805FA1DCFF807003085B1 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Parsers/Oric.hpp; sourceTree = "<group>"; };
4B882F582C2F9C6900D84031 /* CPCShakerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CPCShakerTests.mm; sourceTree = "<group>"; }; 4B882F582C2F9C6900D84031 /* CPCShakerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CPCShakerTests.mm; sourceTree = "<group>"; };
4B882F5A2C2F9C7700D84031 /* Shaker */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shaker; sourceTree = "<group>"; }; 4B882F5A2C2F9C7700D84031 /* Shaker */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shaker; sourceTree = "<group>"; };
4B884C6F2EA28E0F00073840 /* CompileTimeCounter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CompileTimeCounter.hpp; sourceTree = "<group>"; };
4B88559C2E8185BF00E251DD /* SizedInt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SizedInt.hpp; sourceTree = "<group>"; }; 4B88559C2E8185BF00E251DD /* SizedInt.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SizedInt.hpp; sourceTree = "<group>"; };
4B8855A22E84D51B00E251DD /* SAA5050.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAA5050.hpp; sourceTree = "<group>"; }; 4B8855A22E84D51B00E251DD /* SAA5050.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAA5050.hpp; sourceTree = "<group>"; };
4B8855A32E84D51B00E251DD /* SAA5050.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAA5050.cpp; sourceTree = "<group>"; }; 4B8855A32E84D51B00E251DD /* SAA5050.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAA5050.cpp; sourceTree = "<group>"; };
@@ -2797,6 +2801,7 @@
children = ( children = (
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */, 4B0CCC421C62D0B3001CAC5F /* CRT.cpp */,
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */, 4B0CCC431C62D0B3001CAC5F /* CRT.hpp */,
4B847BA42E94320B00774B9B /* MismatchWarner.hpp */,
4BBF99071C8FBA6F0075DAFB /* Internals */, 4BBF99071C8FBA6F0075DAFB /* Internals */,
); );
path = CRT; path = CRT;
@@ -3147,6 +3152,7 @@
4BD191D5219113B80042E144 /* OpenGL */, 4BD191D5219113B80042E144 /* OpenGL */,
4BB8616B24E22DC500A00E03 /* ScanTargets */, 4BB8616B24E22DC500A00E03 /* ScanTargets */,
4BD060A41FE49D3C006E14BE /* Speaker */, 4BD060A41FE49D3C006E14BE /* Speaker */,
4B847B9F2E920C7500774B9B /* CubicCurve.hpp */,
); );
name = Outputs; name = Outputs;
path = ../../Outputs; path = ../../Outputs;
@@ -3757,6 +3763,7 @@
4BFEA2F12682A90200EBF94C /* Sizes.hpp */, 4BFEA2F12682A90200EBF94C /* Sizes.hpp */,
4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */, 4BD9713A2BFD7E7100C907AA /* StringSimilarity.hpp */,
4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */, 4B0329202D0A78DC00C51EB5 /* UpperBound.hpp */,
4B884C6F2EA28E0F00073840 /* CompileTimeCounter.hpp */,
); );
name = Numeric; name = Numeric;
path = ../../Numeric; path = ../../Numeric;
@@ -4954,6 +4961,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */, 4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */,
4B847BA62E96013000774B9B /* RectAccumulator.hpp */,
); );
path = Internals; path = Internals;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@@ -57,7 +57,7 @@
isEnabled = "NO"> isEnabled = "NO">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument
argument = "--new=Electron" argument = "--new=electron"
isEnabled = "YES"> isEnabled = "YES">
</CommandLineArgument> </CommandLineArgument>
<CommandLineArgument <CommandLineArgument

View File

@@ -19,8 +19,8 @@ static void OSSGuard(OSGuardable guardable) {
static BOOL IsDry(int x) { return x < 2; } static BOOL IsDry(int x) { return x < 2; }
static const int MaximumBacklog = 4; #define MaximumBacklog 4
static const int NumBuffers = MaximumBacklog + 1; #define NumBuffers (MaximumBacklog + 1)
@implementation CSAudioQueue { @implementation CSAudioQueue {
AudioQueueRef _audioQueue; AudioQueueRef _audioQueue;

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24128" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24128"/>
</dependencies> </dependencies>
<objects> <objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View File

@@ -821,11 +821,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>25.10.03</string> <string>25.10.19</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>25.10.03</string> <string>25.10.19</string>
<key>LSApplicationCategoryType</key> <key>LSApplicationCategoryType</key>
<string>public.app-category.entertainment</string> <string>public.app-category.entertainment</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23727" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24128" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies> <dependencies>
<deployment identifier="macosx"/> <deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23727"/> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24128"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<objects> <objects>
@@ -17,10 +17,10 @@
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g"> <window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titleVisibility="hidden" id="QvC-M9-y7g">
<windowStyleMask key="styleMask" titled="YES" documentModal="YES"/> <windowStyleMask key="styleMask" titled="YES" documentModal="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="590" height="405"/> <rect key="contentRect" x="196" y="240" width="590" height="410"/>
<rect key="screenRect" x="0.0" y="0.0" width="1710" height="1074"/> <rect key="screenRect" x="0.0" y="0.0" width="1710" height="1074"/>
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
<rect key="frame" x="0.0" y="0.0" width="590" height="405"/> <rect key="frame" x="0.0" y="0.0" width="590" height="410"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="hKn-1l-OSN">
@@ -51,21 +51,20 @@ Gw
</button> </button>
<textField focusRingType="none" horizontalHuggingPriority="1" verticalHuggingPriority="1" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9YM-5x-pc0"> <textField focusRingType="none" horizontalHuggingPriority="1" verticalHuggingPriority="1" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9YM-5x-pc0">
<rect key="frame" x="18" y="14" width="405" height="32"/> <rect key="frame" x="18" y="14" width="405" height="32"/>
<textFieldCell key="cell" allowsUndo="NO" sendsActionOnEndEditing="YES" id="xTm-Oy-oz5"> <textFieldCell key="cell" allowsUndo="NO" sendsActionOnEndEditing="YES" title="If you use File -&gt; Open..., the emulator will select and configure a machine for you." id="xTm-Oy-oz5">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<string key="title">If you use File -&gt; Open... to select a disk, tape or cartridge directly, the emulator will select and configure a machine for you.</string>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell> </textFieldCell>
</textField> </textField>
<scrollView borderType="line" autohidesScrollers="YES" horizontalLineScroll="24" horizontalPageScroll="10" verticalLineScroll="24" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z5Q-Bs-hJj"> <scrollView borderType="line" autohidesScrollers="YES" horizontalLineScroll="24" horizontalPageScroll="10" verticalLineScroll="24" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="z5Q-Bs-hJj">
<rect key="frame" x="20" y="50" width="130" height="306"/> <rect key="frame" x="20" y="53" width="130" height="308"/>
<clipView key="contentView" id="O8s-Vw-9yQ"> <clipView key="contentView" id="O8s-Vw-9yQ">
<rect key="frame" x="1" y="1" width="128" height="304"/> <rect key="frame" x="1" y="1" width="128" height="306"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="none" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="24" id="3go-Eb-GOy"> <tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="none" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" rowHeight="24" id="3go-Eb-GOy">
<rect key="frame" x="0.0" y="0.0" width="128" height="304"/> <rect key="frame" x="0.0" y="0.0" width="128" height="306"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/> <size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
@@ -105,7 +104,7 @@ Gw
</scroller> </scroller>
</scrollView> </scrollView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VAc-6N-O7q"> <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="VAc-6N-O7q">
<rect key="frame" x="18" y="364" width="554" height="21"/> <rect key="frame" x="18" y="369" width="554" height="21"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Choose a machine" id="32m-Vs-dPO"> <textFieldCell key="cell" lineBreakMode="clipping" title="Choose a machine" id="32m-Vs-dPO">
<font key="font" textStyle="title2" name=".SFNS-Regular"/> <font key="font" textStyle="title2" name=".SFNS-Regular"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -113,16 +112,16 @@ Gw
</textFieldCell> </textFieldCell>
</textField> </textField>
<tabView type="noTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c"> <tabView type="noTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="VUb-QG-x7c">
<rect key="frame" x="154" y="47" width="420" height="310"/> <rect key="frame" x="154" y="50" width="420" height="312"/>
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<tabViewItems> <tabViewItems>
<tabViewItem label="Amiga" identifier="amiga" id="JmB-OF-xcM"> <tabViewItem label="Amiga" identifier="amiga" id="JmB-OF-xcM">
<view key="view" id="5zS-Nj-Ynx"> <view key="view" id="5zS-Nj-Ynx">
<rect key="frame" x="10" y="7" width="400" height="274"/> <rect key="frame" x="10" y="7" width="400" height="292"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews> <subviews>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qfH-1l-GXp"> <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="qfH-1l-GXp">
<rect key="frame" x="110" y="230" width="80" height="25"/> <rect key="frame" x="110" y="248" width="80" height="25"/>
<popUpButtonCell key="cell" type="push" title="512 kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="512" imageScaling="axesIndependently" inset="2" selectedItem="Zev-ku-jDG" id="vdO-VR-mUx"> <popUpButtonCell key="cell" type="push" title="512 kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="512" imageScaling="axesIndependently" inset="2" selectedItem="Zev-ku-jDG" id="vdO-VR-mUx">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/> <font key="font" metaFont="message"/>
@@ -136,7 +135,7 @@ Gw
</popUpButtonCell> </popUpButtonCell>
</popUpButton> </popUpButton>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P6K-dt-stj"> <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="P6K-dt-stj">
<rect key="frame" x="18" y="236" width="89" height="16"/> <rect key="frame" x="18" y="254" width="89" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Chip Memory:" id="FIO-ZR-rsA"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Chip Memory:" id="FIO-ZR-rsA">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -144,7 +143,7 @@ Gw
</textFieldCell> </textFieldCell>
</textField> </textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YD0-OJ-2bY"> <textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="YD0-OJ-2bY">
<rect key="frame" x="18" y="206" width="87" height="16"/> <rect key="frame" x="18" y="224" width="87" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Fast Memory:" id="Rpz-39-jyt"> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Fast Memory:" id="Rpz-39-jyt">
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
@@ -152,7 +151,7 @@ Gw
</textFieldCell> </textFieldCell>
</textField> </textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="af8-pF-qc9"> <popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="af8-pF-qc9">
<rect key="frame" x="108" y="200" width="72" height="25"/> <rect key="frame" x="108" y="218" width="72" height="25"/>
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="zV7-V8-c7s" id="39D-ms-pf9"> <popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="zV7-V8-c7s" id="39D-ms-pf9">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="message"/> <font key="font" metaFont="message"/>
@@ -1142,26 +1141,26 @@ Gw
</tabView> </tabView>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="JQy-Cj-AOK" firstAttribute="centerY" secondItem="hKn-1l-OSN" secondAttribute="centerY" id="1AC-HX-EbU"/>
<constraint firstAttribute="trailing" secondItem="VAc-6N-O7q" secondAttribute="trailing" constant="20" symbolic="YES" id="3Kw-kW-eiL"/> <constraint firstAttribute="trailing" secondItem="VAc-6N-O7q" secondAttribute="trailing" constant="20" symbolic="YES" id="3Kw-kW-eiL"/>
<constraint firstItem="9YM-5x-pc0" firstAttribute="centerY" secondItem="JQy-Cj-AOK" secondAttribute="centerY" id="5hN-Ed-w9y"/> <constraint firstItem="9YM-5x-pc0" firstAttribute="centerY" secondItem="JQy-Cj-AOK" secondAttribute="centerY" id="5hN-Ed-w9y"/>
<constraint firstAttribute="trailing" secondItem="VUb-QG-x7c" secondAttribute="trailing" constant="20" symbolic="YES" id="Bem-7v-AY6"/> <constraint firstAttribute="trailing" secondItem="VUb-QG-x7c" secondAttribute="trailing" constant="20" symbolic="YES" id="Bem-7v-AY6"/>
<constraint firstAttribute="bottom" secondItem="JQy-Cj-AOK" secondAttribute="bottom" constant="20" symbolic="YES" id="Kvh-1K-iI8"/>
<constraint firstItem="VAc-6N-O7q" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" symbolic="YES" id="MUn-yE-cae"/> <constraint firstItem="VAc-6N-O7q" firstAttribute="top" secondItem="EiT-Mj-1SZ" secondAttribute="top" constant="20" symbolic="YES" id="MUn-yE-cae"/>
<constraint firstItem="9YM-5x-pc0" firstAttribute="top" secondItem="VUb-QG-x7c" secondAttribute="bottom" constant="5" id="Z5h-ey-NTi"/>
<constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="3go-Eb-GOy" secondAttribute="top" id="dTe-ji-4be"/> <constraint firstItem="VUb-QG-x7c" firstAttribute="top" secondItem="3go-Eb-GOy" secondAttribute="top" id="dTe-ji-4be"/>
<constraint firstItem="z5Q-Bs-hJj" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="dqq-yW-rJM"/> <constraint firstItem="z5Q-Bs-hJj" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="dqq-yW-rJM"/>
<constraint firstItem="hKn-1l-OSN" firstAttribute="leading" secondItem="JQy-Cj-AOK" secondAttribute="trailing" constant="12" symbolic="YES" id="f3Q-Om-rI0"/> <constraint firstItem="hKn-1l-OSN" firstAttribute="leading" secondItem="JQy-Cj-AOK" secondAttribute="trailing" constant="12" symbolic="YES" id="f3Q-Om-rI0"/>
<constraint firstItem="9YM-5x-pc0" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="g8a-if-Ogh"/> <constraint firstItem="9YM-5x-pc0" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="g8a-if-Ogh"/>
<constraint firstItem="VAc-6N-O7q" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="hgA-S3-Cfr"/> <constraint firstItem="VAc-6N-O7q" firstAttribute="leading" secondItem="EiT-Mj-1SZ" secondAttribute="leading" constant="20" symbolic="YES" id="hgA-S3-Cfr"/>
<constraint firstItem="9YM-5x-pc0" firstAttribute="top" secondItem="VUb-QG-x7c" secondAttribute="bottom" constant="8" symbolic="YES" id="m4I-kp-lss"/>
<constraint firstAttribute="trailing" secondItem="hKn-1l-OSN" secondAttribute="trailing" constant="20" symbolic="YES" id="mJg-vd-ddP"/> <constraint firstAttribute="trailing" secondItem="hKn-1l-OSN" secondAttribute="trailing" constant="20" symbolic="YES" id="mJg-vd-ddP"/>
<constraint firstItem="VUb-QG-x7c" firstAttribute="bottom" secondItem="3go-Eb-GOy" secondAttribute="bottom" id="me6-Ky-BNl"/>
<constraint firstItem="JQy-Cj-AOK" firstAttribute="leading" secondItem="9YM-5x-pc0" secondAttribute="trailing" constant="8" symbolic="YES" id="nlb-iJ-JUF"/> <constraint firstItem="JQy-Cj-AOK" firstAttribute="leading" secondItem="9YM-5x-pc0" secondAttribute="trailing" constant="8" symbolic="YES" id="nlb-iJ-JUF"/>
<constraint firstItem="VUb-QG-x7c" firstAttribute="leading" secondItem="z5Q-Bs-hJj" secondAttribute="trailing" constant="8" symbolic="YES" id="pMs-AZ-t0T"/> <constraint firstItem="VUb-QG-x7c" firstAttribute="leading" secondItem="z5Q-Bs-hJj" secondAttribute="trailing" constant="8" symbolic="YES" id="pMs-AZ-t0T"/>
<constraint firstItem="z5Q-Bs-hJj" firstAttribute="top" secondItem="VAc-6N-O7q" secondAttribute="bottom" constant="8" symbolic="YES" id="pRW-Ol-ekG"/> <constraint firstItem="z5Q-Bs-hJj" firstAttribute="top" secondItem="VAc-6N-O7q" secondAttribute="bottom" constant="8" symbolic="YES" id="pRW-Ol-ekG"/>
<constraint firstAttribute="bottom" secondItem="hKn-1l-OSN" secondAttribute="bottom" constant="20" symbolic="YES" id="rG2-Ea-klR"/> <constraint firstAttribute="bottom" secondItem="hKn-1l-OSN" secondAttribute="bottom" constant="20" symbolic="YES" id="rG2-Ea-klR"/>
<constraint firstItem="9YM-5x-pc0" firstAttribute="top" secondItem="z5Q-Bs-hJj" secondAttribute="bottom" constant="7" id="tc4-vn-MvM"/>
</constraints> </constraints>
</view> </view>
<point key="canvasLocation" x="-320" y="106.5"/> <point key="canvasLocation" x="-320" y="79"/>
</window> </window>
<customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target"> <customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target">
<connections> <connections>

View File

@@ -13,6 +13,7 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <cmath> #include <cmath>
#include <optional>
#include "BufferingScanTarget.hpp" #include "BufferingScanTarget.hpp"
#include "FIRFilter.hpp" #include "FIRFilter.hpp"
@@ -80,9 +81,9 @@
3) I also initially didn't want to have a finalied-line texture, but processing costs changed my mind on that. 3) I also initially didn't want to have a finalied-line texture, but processing costs changed my mind on that.
If you accept that output will be fixed precision, anyway. In that case, processing for a typical NTSC frame If you accept that output will be fixed precision, anyway. In that case, processing for a typical NTSC frame
in its original resolution means applying filtering (i.e. at least 15 samples per pixel) likely between in its original resolution means applying filtering (i.e. at least 15 samples per pixel) likely between
218,400 and 273,000 times per output frame, then upscaling from there at 1 sample per pixel. Count the second 218,400 and 273,000 times per output frame, then upscaling from there at 1 sample per pixel. Count the
sample twice for the original store and you're talking between 16*218,400 = 3,494,400 to 16*273,000 = 4,368,000 second sample twice for the original store and you're talking between 16*218,400 = 3,494,400 to
total pixel accesses. Though that's not a perfect way to measure cost, roll with it. 16*273,000 = 4,368,000 total pixel accesses. Though that's not a perfect way to measure cost, roll with it.
On my 4k monitor, doing it at actual output resolution would instead cost 3840*2160*15 = 124,416,000 total On my 4k monitor, doing it at actual output resolution would instead cost 3840*2160*15 = 124,416,000 total
accesses. Which doesn't necessarily mean "more than 28 times as much", but does mean "a lot more". accesses. Which doesn't necessarily mean "more than 28 times as much", but does mean "a lot more".
@@ -130,10 +131,12 @@ constexpr size_t NumBufferedLines = 500;
constexpr size_t NumBufferedScans = NumBufferedLines * 4; constexpr size_t NumBufferedScans = NumBufferedLines * 4;
/// The shared resource options this app would most favour; applied as widely as possible. /// The shared resource options this app would most favour; applied as widely as possible.
constexpr MTLResourceOptions SharedResourceOptionsStandard = MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeShared; constexpr MTLResourceOptions SharedResourceOptionsStandard =
MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeShared;
/// The shared resource options used for the write-area texture; on macOS it can't be MTLResourceStorageModeShared so this is a carve-out. /// The shared resource options used for the write-area texture; on macOS it can't be MTLResourceStorageModeShared so this is a carve-out.
constexpr MTLResourceOptions SharedResourceOptionsTexture = MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeManaged; constexpr MTLResourceOptions SharedResourceOptionsTexture =
MTLResourceCPUCacheModeWriteCombined | MTLResourceStorageModeManaged;
#define uniforms() reinterpret_cast<Uniforms *>(_uniformsBuffer.contents) #define uniforms() reinterpret_cast<Uniforms *>(_uniformsBuffer.contents)
@@ -281,6 +284,9 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
// The output view and its aspect ratio. // The output view and its aspect ratio.
__weak MTKView *_view; __weak MTKView *_view;
CGFloat _viewAspectRatio; // To avoid accessing .bounds away from the main thread. CGFloat _viewAspectRatio; // To avoid accessing .bounds away from the main thread.
// Previously set modals, to avoid unnecessary buffer churn.
std::optional<Outputs::Display::ScanTarget::Modals> _priorModals;
} }
- (nonnull instancetype)initWithView:(nonnull MTKView *)view { - (nonnull instancetype)initWithView:(nonnull MTKView *)view {
@@ -307,8 +313,15 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
// Install all that storage in the buffering scan target. // Install all that storage in the buffering scan target.
_scanTarget.set_write_area(reinterpret_cast<uint8_t *>(_writeAreaBuffer.contents)); _scanTarget.set_write_area(reinterpret_cast<uint8_t *>(_writeAreaBuffer.contents));
_scanTarget.set_line_buffer(reinterpret_cast<BufferingScanTarget::Line *>(_linesBuffer.contents), _lineMetadataBuffer, NumBufferedLines); _scanTarget.set_line_buffer(
_scanTarget.set_scan_buffer(reinterpret_cast<BufferingScanTarget::Scan *>(_scansBuffer.contents), NumBufferedScans); reinterpret_cast<BufferingScanTarget::Line *>(_linesBuffer.contents),
_lineMetadataBuffer,
NumBufferedLines
);
_scanTarget.set_scan_buffer(
reinterpret_cast<BufferingScanTarget::Scan *>(_scansBuffer.contents),
NumBufferedScans
);
// Generate copy and clear pipelines. // Generate copy and clear pipelines.
id<MTLLibrary> library = [_view.device newDefaultLibrary]; id<MTLLibrary> library = [_view.device newDefaultLibrary];
@@ -466,12 +479,19 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
id<MTLLibrary> library = [_view.device newDefaultLibrary]; id<MTLLibrary> library = [_view.device newDefaultLibrary];
// Ensure finalised line texture is initially clear. // Ensure finalised line texture is initially clear.
id<MTLComputePipelineState> clearPipeline = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:@"clearKernel"] error:nil]; id<MTLComputePipelineState> clearPipeline =
[_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:@"clearKernel"] error:nil];
id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer]; id<MTLCommandBuffer> commandBuffer = [_commandQueue commandBuffer];
id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder]; id<MTLComputeCommandEncoder> computeEncoder = [commandBuffer computeCommandEncoder];
[computeEncoder setTexture:texture atIndex:0]; [computeEncoder setTexture:texture atIndex:0];
[self dispatchComputeCommandEncoder:computeEncoder pipelineState:clearPipeline width:texture.width height:texture.height offsetBuffer:[self bufferForOffset:0]]; [self
dispatchComputeCommandEncoder:computeEncoder
pipelineState:clearPipeline
width:texture.width
height:texture.height
offsetBuffer:[self bufferForOffset:0]
];
[computeEncoder endEncoding]; [computeEncoder endEncoding];
[commandBuffer commit]; [commandBuffer commit];
@@ -513,8 +533,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
_finalisedLineTexture = [_view.device newTextureWithDescriptor:lineTextureDescriptor]; _finalisedLineTexture = [_view.device newTextureWithDescriptor:lineTextureDescriptor];
[self clearTexture:_finalisedLineTexture]; [self clearTexture:_finalisedLineTexture];
NSString *const kernelFunction = [self shouldApplyGamma] ? @"filterChromaKernelWithGamma" : @"filterChromaKernelNoGamma"; NSString *const kernelFunction =
_finalisedLineState = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:kernelFunction] error:nil]; [self shouldApplyGamma] ? @"filterChromaKernelWithGamma" : @"filterChromaKernelNoGamma";
_finalisedLineState =
[_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:kernelFunction] error:nil];
} }
// A luma separation texture will exist only for composite colour. // A luma separation texture will exist only for composite colour.
@@ -532,7 +554,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
case 5: kernelFunction = @"separateLumaKernel5"; break; case 5: kernelFunction = @"separateLumaKernel5"; break;
} }
_separatedLumaState = [_view.device newComputePipelineStateWithFunction:[library newFunctionWithName:kernelFunction] error:nil]; _separatedLumaState =
[_view.device
newComputePipelineStateWithFunction:[library newFunctionWithName:kernelFunction]
error:nil];
} }
} else { } else {
_separatedLumaTexture = nil; _separatedLumaTexture = nil;
@@ -553,8 +578,7 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
sourceToDisplay = recentre * sourceToDisplay; sourceToDisplay = recentre * sourceToDisplay;
} }
// Convert from the internal [0, 1] to centred [-1, 1] (i.e. Metal's eye coordinates, though also appropriate // Convert from the internal [0, 1] to centred [-1, 1].
// for the zooming step that follows).
{ {
simd::float3x3 convertToEye; simd::float3x3 convertToEye;
convertToEye.columns[0][0] = 2.0f; convertToEye.columns[0][0] = 2.0f;
@@ -565,13 +589,10 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
sourceToDisplay = convertToEye * sourceToDisplay; sourceToDisplay = convertToEye * sourceToDisplay;
} }
// Determine the correct zoom level. This is a combination of (i) the necessary horizontal stretch to produce a proper // Determine correct zoom, combining (i) the necessary horizontal stretch for aspect ratio; and
// aspect ratio; and (ii) the necessary zoom from there to either fit the visible area width or height as per a decision // (ii) the necessary zoom to fit either the visible area width or height.
// on letterboxing or pillarboxing.
const float aspectRatioStretch = float(modals.aspect_ratio / _viewAspectRatio); const float aspectRatioStretch = float(modals.aspect_ratio / _viewAspectRatio);
const float fitWidthZoom = 1.0f / (float(modals.visible_area.size.width) * aspectRatioStretch); const float zoom = modals.visible_area.appropriate_zoom(aspectRatioStretch);
const float fitHeightZoom = 1.0f / float(modals.visible_area.size.height);
const float zoom = std::min(fitWidthZoom, fitHeightZoom);
// Convert from there to the proper aspect ratio by stretching or compressing width. // Convert from there to the proper aspect ratio by stretching or compressing width.
// After this the output is exactly centred, filling the vertical space and being as wide or slender as it likes. // After this the output is exactly centred, filling the vertical space and being as wide or slender as it likes.
@@ -617,240 +638,247 @@ using BufferingScanTarget = Outputs::Display::BufferingScanTarget;
const float displayGamma = 2.2f; // This is assumed. const float displayGamma = 2.2f; // This is assumed.
uniforms()->outputGamma = __fp16(displayGamma / modals.intended_gamma); uniforms()->outputGamma = __fp16(displayGamma / modals.intended_gamma);
if(
!_priorModals ||
// _priorModals->display_type != modals.display_type ||
// Generate input texture. _priorModals->input_data_type != modals.input_data_type
// ) {
MTLPixelFormat pixelFormat; //
_bytesPerInputPixel = size_for_data_type(modals.input_data_type); // Generate input texture.
if(data_type_is_normalised(modals.input_data_type)) { //
switch(_bytesPerInputPixel) { MTLPixelFormat pixelFormat;
default: _bytesPerInputPixel = size_for_data_type(modals.input_data_type);
case 1: pixelFormat = MTLPixelFormatR8Unorm; break; if(data_type_is_normalised(modals.input_data_type)) {
case 2: pixelFormat = MTLPixelFormatRG8Unorm; break; switch(_bytesPerInputPixel) {
case 4: pixelFormat = MTLPixelFormatRGBA8Unorm; break; default:
} case 1: pixelFormat = MTLPixelFormatR8Unorm; break;
} else { case 2: pixelFormat = MTLPixelFormatRG8Unorm; break;
switch(_bytesPerInputPixel) { case 4: pixelFormat = MTLPixelFormatRGBA8Unorm; break;
default:
case 1: pixelFormat = MTLPixelFormatR8Uint; break;
case 2: pixelFormat = MTLPixelFormatRG8Uint; break;
case 4: pixelFormat = MTLPixelFormatRGBA8Uint; break;
}
}
MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:pixelFormat
width:BufferingScanTarget::WriteAreaWidth
height:BufferingScanTarget::WriteAreaHeight
mipmapped:NO];
textureDescriptor.resourceOptions = SharedResourceOptionsTexture;
if(@available(macOS 10.14, *)) {
textureDescriptor.allowGPUOptimizedContents = NO;
}
// TODO: the call below is the only reason why this project now requires macOS 10.13; is it all that helpful versus just uploading each frame?
const NSUInteger bytesPerRow = BufferingScanTarget::WriteAreaWidth * _bytesPerInputPixel;
_writeAreaTexture = [_writeAreaBuffer
newTextureWithDescriptor:textureDescriptor
offset:0
bytesPerRow:bytesPerRow];
_totalTextureBytes = bytesPerRow * BufferingScanTarget::WriteAreaHeight;
//
// Generate scan pipeline.
//
id<MTLLibrary> library = [_view.device newDefaultLibrary];
MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// Occasions when the composition buffer isn't required are slender: the output must be neither RGB nor composite monochrome.
const bool isComposition =
modals.display_type != Outputs::Display::DisplayType::RGB &&
modals.display_type != Outputs::Display::DisplayType::CompositeMonochrome;
const bool isSVideoOutput = modals.display_type == Outputs::Display::DisplayType::SVideo;
if(!isComposition) {
_pipeline = Pipeline::DirectToDisplay;
} else {
_pipeline = isSVideoOutput ? Pipeline::SVideo : Pipeline::CompositeColour;
}
struct FragmentSamplerDictionary {
/// Fragment shader that outputs to the composition buffer for composite processing.
NSString *const compositionComposite;
/// Fragment shader that outputs to the composition buffer for S-Video processing.
NSString *const compositionSVideo;
/// Fragment shader that outputs directly as monochrome composite.
NSString *const directComposite;
/// Fragment shader that outputs directly as monochrome composite, with gamma correction.
NSString *const directCompositeWithGamma;
/// Fragment shader that outputs directly as RGB.
NSString *const directRGB;
/// Fragment shader that outputs directly as RGB, with gamma correction.
NSString *const directRGBWithGamma;
};
const FragmentSamplerDictionary samplerDictionary[8] = {
// Composite formats.
{@"compositeSampleLuminance1", nil, @"sampleLuminance1", @"sampleLuminance1", @"sampleLuminance1", @"sampleLuminance1"},
{@"compositeSampleLuminance8", nil, @"sampleLuminance8", @"sampleLuminance8WithGamma", @"sampleLuminance8", @"sampleLuminance8WithGamma"},
{@"compositeSamplePhaseLinkedLuminance8", nil, @"samplePhaseLinkedLuminance8", @"samplePhaseLinkedLuminance8WithGamma", @"samplePhaseLinkedLuminance8", @"samplePhaseLinkedLuminance8WithGamma"},
// S-Video formats.
{@"compositeSampleLuminance8Phase8", @"sampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8WithGamma", @"directCompositeSampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8WithGamma"},
// RGB formats.
{@"compositeSampleRed1Green1Blue1", @"svideoSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1WithGamma", @"sampleRed1Green1Blue1", @"sampleRed1Green1Blue1"},
{@"compositeSampleRed2Green2Blue2", @"svideoSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2WithGamma", @"sampleRed2Green2Blue2", @"sampleRed2Green2Blue2WithGamma"},
{@"compositeSampleRed4Green4Blue4", @"svideoSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4WithGamma", @"sampleRed4Green4Blue4", @"sampleRed4Green4Blue4WithGamma"},
{@"compositeSampleRed8Green8Blue8", @"svideoSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8WithGamma", @"sampleRed8Green8Blue8", @"sampleRed8Green8Blue8WithGamma"},
};
#ifndef NDEBUG
// Do a quick check that all the shaders named above are defined in the Metal code. I don't think this is possible at compile time.
for(int c = 0; c < 8; ++c) {
#define Test(x) if(samplerDictionary[c].x) assert([library newFunctionWithName:samplerDictionary[c].x]);
Test(compositionComposite);
Test(compositionSVideo);
Test(directComposite);
Test(directCompositeWithGamma);
Test(directRGB);
Test(directRGBWithGamma);
#undef Test
}
#endif
uniforms()->cyclesMultiplier = 1.0f;
if(_pipeline != Pipeline::DirectToDisplay) {
// Pick a suitable cycle multiplier.
const float minimumSize = 4.0f * float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator);
while(uniforms()->cyclesMultiplier * modals.cycles_per_line < minimumSize) {
uniforms()->cyclesMultiplier += 1.0f;
if(uniforms()->cyclesMultiplier * modals.cycles_per_line > 2048) {
uniforms()->cyclesMultiplier -= 1.0f;
break;
} }
}
// Create suitable filters.
_lineBufferPixelsPerLine = NSUInteger(modals.cycles_per_line) * NSUInteger(uniforms()->cyclesMultiplier);
const float colourCyclesPerLine = float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator);
// Compute radians per pixel.
const float radiansPerPixel = (colourCyclesPerLine * 3.141592654f * 2.0f) / float(_lineBufferPixelsPerLine);
// Generate the chrominance filter.
{
simd::float3 firCoefficients[8];
const auto chromaCoefficients = boxCoefficients(radiansPerPixel, 3.141592654f * 2.0f);
_chromaKernelSize = 15;
for(size_t c = 0; c < 8; ++c) {
// Bit of a fix here: if the pipeline is for composite then assume that chroma separation wasn't
// perfect and deemphasise the colour.
firCoefficients[c].y = firCoefficients[c].z = (isSVideoOutput ? 2.0f : 1.25f) * chromaCoefficients[c];
firCoefficients[c].x = 0.0f;
if(fabsf(chromaCoefficients[c]) < 0.01f) {
_chromaKernelSize -= 2;
}
}
firCoefficients[7].x = 1.0f;
// Luminance will be very soft as a result of the separation phase; apply a sharpen filter to try to undo that.
//
// This is applied separately in order to partition three parts of the signal rather than two:
//
// 1) the luminance;
// 2) not the luminance:
// 2a) the chrominance; and
// 2b) some noise.
//
// There are real numerical hazards here given the low number of taps I am permitting to be used, so the sharpen
// filter below is just one that I found worked well. Since all numbers are fixed, the actual cutoff frequency is
// going to be a function of the input clock, which is a bit phoney but the best way to stay safe within the
// PCM sampling limits.
if(!isSVideoOutput) {
SignalProcessing::FIRFilter sharpenFilter(15, 1368, 60.0f, 227.5f);
const auto sharpen = sharpenFilter.get_coefficients();
size_t sharpenFilterSize = 15;
bool isStart = true;
for(size_t c = 0; c < 8; ++c) {
firCoefficients[c].x = sharpen[c];
if(fabsf(sharpen[c]) > 0.01f) isStart = false;
if(isStart) sharpenFilterSize -= 2;
}
_chromaKernelSize = std::max(_chromaKernelSize, sharpenFilterSize);
}
// Convert to half-size floats.
for(size_t c = 0; c < 8; ++c) {
uniforms()->chromaKernel[c] = firCoefficients[c];
}
}
// Generate the luminance separation filter and determine its required size.
{
auto *const filter = uniforms()->lumaKernel;
const auto coefficients = boxCoefficients(radiansPerPixel, 3.141592654f);
_lumaKernelSize = 15;
for(size_t c = 0; c < 8; ++c) {
filter[c] = __fp16(coefficients[c]);
if(fabsf(coefficients[c]) < 0.01f) {
_lumaKernelSize -= 2;
}
}
}
}
// Update intermediate storage.
[self updateModalBuffers];
if(_pipeline != Pipeline::DirectToDisplay) {
// Create the composition render pass.
pipelineDescriptor.colorAttachments[0].pixelFormat = _compositionTexture.pixelFormat;
pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToComposition"];
pipelineDescriptor.fragmentFunction =
[library newFunctionWithName:isSVideoOutput ? samplerDictionary[int(modals.input_data_type)].compositionSVideo : samplerDictionary[int(modals.input_data_type)].compositionComposite];
_composePipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
_compositionRenderPass = [[MTLRenderPassDescriptor alloc] init];
_compositionRenderPass.colorAttachments[0].texture = _compositionTexture;
_compositionRenderPass.colorAttachments[0].loadAction = MTLLoadActionClear;
_compositionRenderPass.colorAttachments[0].storeAction = MTLStoreActionStore;
_compositionRenderPass.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 0.3);
}
// Build the output pipeline.
pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
pipelineDescriptor.vertexFunction = [library newFunctionWithName:_pipeline == Pipeline::DirectToDisplay ? @"scanToDisplay" : @"lineToDisplay"];
if(_pipeline != Pipeline::DirectToDisplay) {
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"interpolateFragment"];
} else {
const bool isRGBOutput = modals.display_type == Outputs::Display::DisplayType::RGB;
NSString *shaderName;
if(isRGBOutput) {
shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directRGBWithGamma : samplerDictionary[int(modals.input_data_type)].directRGB;
} else { } else {
shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directCompositeWithGamma : samplerDictionary[int(modals.input_data_type)].directComposite; switch(_bytesPerInputPixel) {
default:
case 1: pixelFormat = MTLPixelFormatR8Uint; break;
case 2: pixelFormat = MTLPixelFormatRG8Uint; break;
case 4: pixelFormat = MTLPixelFormatRGBA8Uint; break;
}
} }
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:shaderName]; MTLTextureDescriptor *const textureDescriptor = [MTLTextureDescriptor
texture2DDescriptorWithPixelFormat:pixelFormat
width:BufferingScanTarget::WriteAreaWidth
height:BufferingScanTarget::WriteAreaHeight
mipmapped:NO];
textureDescriptor.resourceOptions = SharedResourceOptionsTexture;
if(@available(macOS 10.14, *)) {
textureDescriptor.allowGPUOptimizedContents = NO;
}
// TODO: the call below is the only reason why this project now requires macOS 10.13;
// is it all that helpful versus just uploading each frame?
const NSUInteger bytesPerRow = BufferingScanTarget::WriteAreaWidth * _bytesPerInputPixel;
_writeAreaTexture = [_writeAreaBuffer
newTextureWithDescriptor:textureDescriptor
offset:0
bytesPerRow:bytesPerRow];
_totalTextureBytes = bytesPerRow * BufferingScanTarget::WriteAreaHeight;
//
// Generate scan pipeline.
//
id<MTLLibrary> library = [_view.device newDefaultLibrary];
MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
// Occasions when the composition buffer isn't required are slender: the output must be neither RGB
// nor composite monochrome.
const bool isComposition =
modals.display_type != Outputs::Display::DisplayType::RGB &&
modals.display_type != Outputs::Display::DisplayType::CompositeMonochrome;
const bool isSVideoOutput = modals.display_type == Outputs::Display::DisplayType::SVideo;
if(!isComposition) {
_pipeline = Pipeline::DirectToDisplay;
} else {
_pipeline = isSVideoOutput ? Pipeline::SVideo : Pipeline::CompositeColour;
}
struct FragmentSamplerDictionary {
/// Fragment shader that outputs to the composition buffer for composite processing.
NSString *const compositionComposite;
/// Fragment shader that outputs to the composition buffer for S-Video processing.
NSString *const compositionSVideo;
/// Fragment shader that outputs directly as monochrome composite.
NSString *const directComposite;
/// Fragment shader that outputs directly as monochrome composite, with gamma correction.
NSString *const directCompositeWithGamma;
/// Fragment shader that outputs directly as RGB.
NSString *const directRGB;
/// Fragment shader that outputs directly as RGB, with gamma correction.
NSString *const directRGBWithGamma;
};
const FragmentSamplerDictionary samplerDictionary[8] = {
// Composite formats.
{@"compositeSampleLuminance1", nil, @"sampleLuminance1", @"sampleLuminance1", @"sampleLuminance1", @"sampleLuminance1"},
{@"compositeSampleLuminance8", nil, @"sampleLuminance8", @"sampleLuminance8WithGamma", @"sampleLuminance8", @"sampleLuminance8WithGamma"},
{@"compositeSamplePhaseLinkedLuminance8", nil, @"samplePhaseLinkedLuminance8", @"samplePhaseLinkedLuminance8WithGamma", @"samplePhaseLinkedLuminance8", @"samplePhaseLinkedLuminance8WithGamma"},
// S-Video formats.
{@"compositeSampleLuminance8Phase8", @"sampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8WithGamma", @"directCompositeSampleLuminance8Phase8", @"directCompositeSampleLuminance8Phase8WithGamma"},
// RGB formats.
{@"compositeSampleRed1Green1Blue1", @"svideoSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1", @"directCompositeSampleRed1Green1Blue1WithGamma", @"sampleRed1Green1Blue1", @"sampleRed1Green1Blue1"},
{@"compositeSampleRed2Green2Blue2", @"svideoSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2", @"directCompositeSampleRed2Green2Blue2WithGamma", @"sampleRed2Green2Blue2", @"sampleRed2Green2Blue2WithGamma"},
{@"compositeSampleRed4Green4Blue4", @"svideoSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4", @"directCompositeSampleRed4Green4Blue4WithGamma", @"sampleRed4Green4Blue4", @"sampleRed4Green4Blue4WithGamma"},
{@"compositeSampleRed8Green8Blue8", @"svideoSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8", @"directCompositeSampleRed8Green8Blue8WithGamma", @"sampleRed8Green8Blue8", @"sampleRed8Green8Blue8WithGamma"},
};
#ifndef NDEBUG
// Do a quick check that all the shaders named above are defined in the Metal code. I don't think this is possible at compile time.
for(int c = 0; c < 8; ++c) {
#define Test(x) if(samplerDictionary[c].x) assert([library newFunctionWithName:samplerDictionary[c].x]);
Test(compositionComposite);
Test(compositionSVideo);
Test(directComposite);
Test(directCompositeWithGamma);
Test(directRGB);
Test(directRGBWithGamma);
#undef Test
}
#endif
uniforms()->cyclesMultiplier = 1.0f;
if(_pipeline != Pipeline::DirectToDisplay) {
// Pick a suitable cycle multiplier.
const float minimumSize = 4.0f * float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator);
while(uniforms()->cyclesMultiplier * modals.cycles_per_line < minimumSize) {
uniforms()->cyclesMultiplier += 1.0f;
if(uniforms()->cyclesMultiplier * modals.cycles_per_line > 2048) {
uniforms()->cyclesMultiplier -= 1.0f;
break;
}
}
// Create suitable filters.
_lineBufferPixelsPerLine = NSUInteger(modals.cycles_per_line) * NSUInteger(uniforms()->cyclesMultiplier);
const float colourCyclesPerLine = float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator);
// Compute radians per pixel.
const float radiansPerPixel = (colourCyclesPerLine * 3.141592654f * 2.0f) / float(_lineBufferPixelsPerLine);
// Generate the chrominance filter.
{
simd::float3 firCoefficients[8];
const auto chromaCoefficients = boxCoefficients(radiansPerPixel, 3.141592654f * 2.0f);
_chromaKernelSize = 15;
for(size_t c = 0; c < 8; ++c) {
// Bit of a fix here: if the pipeline is for composite then assume that chroma separation wasn't
// perfect and deemphasise the colour.
firCoefficients[c].y = firCoefficients[c].z = (isSVideoOutput ? 2.0f : 1.25f) * chromaCoefficients[c];
firCoefficients[c].x = 0.0f;
if(fabsf(chromaCoefficients[c]) < 0.01f) {
_chromaKernelSize -= 2;
}
}
firCoefficients[7].x = 1.0f;
// Luminance will be very soft as a result of the separation phase; apply a sharpen filter to try to undo that.
//
// This is applied separately in order to partition three parts of the signal rather than two:
//
// 1) the luminance;
// 2) not the luminance:
// 2a) the chrominance; and
// 2b) some noise.
//
// There are real numerical hazards here given the low number of taps I am permitting to be used, so the sharpen
// filter below is just one that I found worked well. Since all numbers are fixed, the actual cutoff frequency is
// going to be a function of the input clock, which is a bit phoney but the best way to stay safe within the
// PCM sampling limits.
if(!isSVideoOutput) {
SignalProcessing::FIRFilter sharpenFilter(15, 1368, 60.0f, 227.5f);
const auto sharpen = sharpenFilter.get_coefficients();
size_t sharpenFilterSize = 15;
bool isStart = true;
for(size_t c = 0; c < 8; ++c) {
firCoefficients[c].x = sharpen[c];
if(fabsf(sharpen[c]) > 0.01f) isStart = false;
if(isStart) sharpenFilterSize -= 2;
}
_chromaKernelSize = std::max(_chromaKernelSize, sharpenFilterSize);
}
// Convert to half-size floats.
for(size_t c = 0; c < 8; ++c) {
uniforms()->chromaKernel[c] = firCoefficients[c];
}
}
// Generate the luminance separation filter and determine its required size.
{
auto *const filter = uniforms()->lumaKernel;
const auto coefficients = boxCoefficients(radiansPerPixel, 3.141592654f);
_lumaKernelSize = 15;
for(size_t c = 0; c < 8; ++c) {
filter[c] = __fp16(coefficients[c]);
if(fabsf(coefficients[c]) < 0.01f) {
_lumaKernelSize -= 2;
}
}
}
}
// Update intermediate storage.
[self updateModalBuffers];
if(_pipeline != Pipeline::DirectToDisplay) {
// Create the composition render pass.
pipelineDescriptor.colorAttachments[0].pixelFormat = _compositionTexture.pixelFormat;
pipelineDescriptor.vertexFunction = [library newFunctionWithName:@"scanToComposition"];
pipelineDescriptor.fragmentFunction =
[library newFunctionWithName:isSVideoOutput ? samplerDictionary[int(modals.input_data_type)].compositionSVideo : samplerDictionary[int(modals.input_data_type)].compositionComposite];
_composePipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
_compositionRenderPass = [[MTLRenderPassDescriptor alloc] init];
_compositionRenderPass.colorAttachments[0].texture = _compositionTexture;
_compositionRenderPass.colorAttachments[0].loadAction = MTLLoadActionClear;
_compositionRenderPass.colorAttachments[0].storeAction = MTLStoreActionStore;
_compositionRenderPass.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 0.5, 0.5, 0.3);
}
// Build the output pipeline.
pipelineDescriptor.colorAttachments[0].pixelFormat = _view.colorPixelFormat;
pipelineDescriptor.vertexFunction = [library newFunctionWithName:_pipeline == Pipeline::DirectToDisplay ? @"scanToDisplay" : @"lineToDisplay"];
if(_pipeline != Pipeline::DirectToDisplay) {
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:@"interpolateFragment"];
} else {
const bool isRGBOutput = modals.display_type == Outputs::Display::DisplayType::RGB;
NSString *shaderName;
if(isRGBOutput) {
shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directRGBWithGamma : samplerDictionary[int(modals.input_data_type)].directRGB;
} else {
shaderName = [self shouldApplyGamma] ? samplerDictionary[int(modals.input_data_type)].directCompositeWithGamma : samplerDictionary[int(modals.input_data_type)].directComposite;
}
pipelineDescriptor.fragmentFunction = [library newFunctionWithName:shaderName];
}
// Enable blending.
pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// Set stencil format.
pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8;
// Finish.
_outputPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
} }
_priorModals = modals;
// Enable blending.
pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
// Set stencil format.
pipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormatStencil8;
// Finish.
_outputPipeline = [_view.device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:nil];
} }
- (void)outputFrom:(size_t)start to:(size_t)end commandBuffer:(id<MTLCommandBuffer>)commandBuffer { - (void)outputFrom:(size_t)start to:(size_t)end commandBuffer:(id<MTLCommandBuffer>)commandBuffer {

View File

@@ -351,11 +351,11 @@ half3 convertRed1Green1Blue1(SourceInterpolator vert, texture2d<ushort> texture)
#define DeclareShaders(name, pixelType) \ #define DeclareShaders(name, pixelType) \
fragment half4 sample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ 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); \ return half4(convert##name(vert, texture) * uniforms.outputMultiplier, uniforms.outputAlpha); \
} \ } \
\ \
fragment half4 sample##name##WithGamma(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ 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); \ return half4(pow(convert##name(vert, texture) * uniforms.outputMultiplier, uniforms.outputGamma), uniforms.outputAlpha); \
} \ } \
\ \
fragment half4 svideoSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \ fragment half4 svideoSample##name(SourceInterpolator vert [[stage_in]], texture2d<pixelType> texture [[texture(0)]], constant Uniforms &uniforms [[buffer(0)]]) { \

View File

@@ -160,7 +160,7 @@ private:
@interface CPCShakerTests : XCTestCase @interface CPCShakerTests : XCTestCase
@end @end
@implementation CPCShakerTests {} @implementation CPCShakerTests
- (void)testCSLPath:(NSString *)path name:(NSString *)name { - (void)testCSLPath:(NSString *)path name:(NSString *)name {
using namespace Storage::Automation; using namespace Storage::Automation;

View File

@@ -8,32 +8,50 @@
#include "CRT.hpp" #include "CRT.hpp"
#include "Outputs/Log.hpp"
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cmath> #include <cmath>
#include <cstdarg> #include <cstdarg>
using namespace Outputs::CRT; using namespace Outputs::CRT;
using Logger = Log::Logger<Log::Source::CRT>;
void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Display::ColourSpace colour_space, int colour_cycle_numerator, int colour_cycle_denominator, int vertical_sync_half_lines, bool should_alternate) { // MARK: - Input timing setup.
static constexpr int millisecondsHorizontalRetraceTime = 7; // Source: Dictionary of Video and Television Technology, p. 234. void CRT::set_new_timing(
static constexpr int scanlinesVerticalRetraceTime = 8; // Source: ibid. const int cycles_per_line,
const int height_of_display,
const Outputs::Display::ColourSpace colour_space,
const int colour_cycle_numerator,
const int colour_cycle_denominator,
const int vertical_sync_half_lines,
const bool should_alternate
) {
static constexpr int HorizontalRetraceMs = 7; // Source: Dictionary of Video and Television Technology, p. 234.
static constexpr int VerticalRetraceLines = 8; // Source: ibid.
// To quote: // To quote:
// //
// "retrace interval; The interval of time for the return of the blanked scanning beam of // "retrace interval; The interval of time for the return of the blanked scanning beam of
// a TV picture tube or camera tube to the starting point of a line or field. It is about // a TV picture tube or camera tube to the starting point of a line or field. It is about
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace // 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
// in NTSC and PAL TV." // in NTSC and PAL TV."
const bool is_first_set = time_multiplier_ == 0;
// 63475 = 65535 * 31/32, i.e. the same 1/32 error as below is permitted.
time_multiplier_ = 63487 / 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_denominator_ = int64_t(cycles_per_line) * int64_t(colour_cycle_denominator) * int64_t(time_multiplier_);
phase_numerator_ = 0; phase_numerator_ = 0;
colour_cycle_numerator_ = int64_t(colour_cycle_numerator); colour_cycle_numerator_ = int64_t(colour_cycle_numerator);
phase_alternates_ = should_alternate; phase_alternates_ = should_alternate;
should_be_alternate_line_ &= phase_alternates_; should_be_alternate_line_ &= phase_alternates_;
cycles_per_line_ = cycles_per_line; cycles_per_line_ = cycles_per_line;
const int multiplied_cycles_per_line = cycles_per_line * time_multiplier_; const int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
// Allow sync to be detected (and acted upon) a line earlier than the specified requirement, // Allow sync to be detected (and acted upon) a line earlier than the specified requirement,
@@ -41,79 +59,87 @@ void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Di
// the gist for simple debugging. // the gist for simple debugging.
sync_capacitor_charge_threshold_ = ((vertical_sync_half_lines - 2) * cycles_per_line) >> 1; sync_capacitor_charge_threshold_ = ((vertical_sync_half_lines - 2) * cycles_per_line) >> 1;
// Create the two flywheels: // Horizontal flywheel: has an ideal period of `multiplied_cycles_per_line`, will accept syncs
// // within 1/32nd of that (i.e. tolerates 3.125% error) and takes HorizontalRetraceMs
// The horizontal flywheel has an ideal period of `multiplied_cycles_per_line`, will accept syncs
// within 1/32nd of that (i.e. tolerates 3.125% error) and takes millisecondsHorizontalRetraceTime
// to retrace. // to retrace.
// horizontal_flywheel_ =
// The vertical slywheel has an ideal period of `multiplied_cycles_per_line * height_of_display`, Flywheel(
// will accept syncs within 1/8th of that (i.e. tolerates 12.5% error) and takes scanlinesVerticalRetraceTime multiplied_cycles_per_line,
(HorizontalRetraceMs * multiplied_cycles_per_line) >> 6,
multiplied_cycles_per_line >> 5
);
// Vertical flywheel: has an ideal period of `multiplied_cycles_per_line * height_of_display`,
// will accept syncs within 1/8th of that (i.e. tolerates 12.5% error) and takes VerticalRetraceLines
// to retrace. // to retrace.
horizontal_flywheel_ = std::make_unique<Flywheel>(multiplied_cycles_per_line, (millisecondsHorizontalRetraceTime * multiplied_cycles_per_line) >> 6, multiplied_cycles_per_line >> 5); vertical_flywheel_ =
vertical_flywheel_ = std::make_unique<Flywheel>(multiplied_cycles_per_line * height_of_display, scanlinesVerticalRetraceTime * multiplied_cycles_per_line, (multiplied_cycles_per_line * height_of_display) >> 3); Flywheel(
multiplied_cycles_per_line * height_of_display,
VerticalRetraceLines * multiplied_cycles_per_line,
(multiplied_cycles_per_line * height_of_display) >> 3
);
// Figure out the divisor necessary to get the horizontal flywheel into a 16-bit range. // Figure out the divisor necessary to get the horizontal flywheel into a 16-bit range.
const int real_clock_scan_period = vertical_flywheel_->get_scan_period(); const int real_clock_scan_period = vertical_flywheel_.scan_period();
vertical_flywheel_output_divider_ = (real_clock_scan_period + 65534) / 65535; vertical_flywheel_output_divider_ = (real_clock_scan_period + 65534) / 65535;
// Communicate relevant fields to the scan target. // Communicate relevant fields to the scan target.
scan_target_modals_.cycles_per_line = cycles_per_line; scan_target_modals_.cycles_per_line = cycles_per_line;
scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_->get_scan_period()); scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_.scan_period());
scan_target_modals_.output_scale.y = uint16_t(real_clock_scan_period / vertical_flywheel_output_divider_); scan_target_modals_.output_scale.y = uint16_t(real_clock_scan_period / vertical_flywheel_output_divider_);
scan_target_modals_.expected_vertical_lines = height_of_display; scan_target_modals_.expected_vertical_lines = height_of_display;
scan_target_modals_.composite_colour_space = colour_space; scan_target_modals_.composite_colour_space = colour_space;
scan_target_modals_.colour_cycle_numerator = colour_cycle_numerator; scan_target_modals_.colour_cycle_numerator = colour_cycle_numerator;
scan_target_modals_.colour_cycle_denominator = colour_cycle_denominator; scan_target_modals_.colour_cycle_denominator = colour_cycle_denominator;
// Default crop: middle 90%.
if(is_first_set) {
scan_target_modals_.visible_area = posted_rect_ = Display::Rect(
0.05f, 0.05f, 0.9f, 0.9f
);
}
scan_target_->set_modals(scan_target_modals_);
const float stability_threshold = 1.0f / scan_target_modals_.expected_vertical_lines;
rect_accumulator_.set_stability_threshold(stability_threshold);
}
void CRT::set_dynamic_framing(
Outputs::Display::Rect initial,
float max_centre_offset_x,
float max_centre_offset_y,
float maximum_scale,
float minimum_scale
) {
framing_ = Framing::Dynamic;
framing_bounds_ = initial;
framing_bounds_ = initial;
framing_bounds_.scale(maximum_scale / framing_bounds_.size.width, maximum_scale / framing_bounds_.size.height);
minimum_scale_ = minimum_scale;
max_offsets_[0] = max_centre_offset_x;
max_offsets_[1] = max_centre_offset_y;
posted_rect_ = scan_target_modals_.visible_area = initial;
has_first_reading_ = true;
}
void CRT::set_fixed_framing(const std::function<void()> &advance) {
framing_ = Framing::CalibratingAutomaticFixed;
while(framing_ == Framing::CalibratingAutomaticFixed) {
advance();
}
}
void CRT::set_fixed_framing(const Display::Rect frame) {
framing_ = Framing::Static;
scan_target_modals_.visible_area = frame;
scan_target_->set_modals(scan_target_modals_); scan_target_->set_modals(scan_target_modals_);
} }
void CRT::set_scan_target(Outputs::Display::ScanTarget *scan_target) { void CRT::set_new_display_type(const int cycles_per_line, const Outputs::Display::Type displayType) {
scan_target_ = scan_target;
if(!scan_target_) scan_target_ = &Outputs::Display::NullScanTarget::singleton;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_data_type(Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_aspect_ratio(float aspect_ratio) {
scan_target_modals_.aspect_ratio = aspect_ratio;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_visible_area(Outputs::Display::Rect visible_area) {
scan_target_modals_.visible_area = visible_area;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_display_type(Outputs::Display::DisplayType display_type) {
scan_target_modals_.display_type = display_type;
scan_target_->set_modals(scan_target_modals_);
}
Outputs::Display::DisplayType CRT::get_display_type() const {
return scan_target_modals_.display_type;
}
void CRT::set_phase_linked_luminance_offset(float offset) {
scan_target_modals_.input_data_tweaks.phase_linked_luminance_offset = offset;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_input_data_type(Outputs::Display::InputDataType input_data_type) {
scan_target_modals_.input_data_type = input_data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_brightness(float brightness) {
scan_target_modals_.brightness = brightness;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displayType) {
switch(displayType) { switch(displayType) {
case Outputs::Display::Type::PAL50: case Outputs::Display::Type::PAL50:
case Outputs::Display::Type::PAL60: case Outputs::Display::Type::PAL60:
@@ -142,7 +168,7 @@ void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displ
} }
} }
void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_first_sample) { void CRT::set_composite_function_type(const CompositeSourceType type, const float offset_of_first_sample) {
if(type == DiscreteFourSamplesPerCycle) { if(type == DiscreteFourSamplesPerCycle) {
colour_burst_phase_adjustment_ = uint8_t(offset_of_first_sample * 256.0f) & 63; colour_burst_phase_adjustment_ = uint8_t(offset_of_first_sample * 256.0f) & 63;
} else { } else {
@@ -150,125 +176,194 @@ void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_
} }
} }
void CRT::set_input_gamma(float gamma) { // MARK: - Constructors.
scan_target_modals_.intended_gamma = gamma;
scan_target_->set_modals(scan_target_modals_);
}
CRT::CRT( int cycles_per_line, CRT::CRT() : animation_curve_(Numeric::CubicCurve::easeInOut()) {}
int clocks_per_pixel_greatest_common_divisor,
int height_of_display, CRT::CRT(
Outputs::Display::ColourSpace colour_space, const int cycles_per_line,
int colour_cycle_numerator, int colour_cycle_denominator, const int clocks_per_pixel_greatest_common_divisor,
int vertical_sync_half_lines, const int height_of_display,
bool should_alternate, const Outputs::Display::ColourSpace colour_space,
Outputs::Display::InputDataType data_type) { const int colour_cycle_numerator, int colour_cycle_denominator,
const int vertical_sync_half_lines,
const bool should_alternate,
const Outputs::Display::InputDataType data_type
) : CRT() {
scan_target_modals_.input_data_type = data_type; scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator, vertical_sync_half_lines, should_alternate); set_new_timing(
cycles_per_line,
height_of_display,
colour_space,
colour_cycle_numerator,
colour_cycle_denominator,
vertical_sync_half_lines,
should_alternate
);
} }
CRT::CRT( int cycles_per_line, CRT::CRT(
int clocks_per_pixel_greatest_common_divisor, const int cycles_per_line,
Outputs::Display::Type display_type, const int clocks_per_pixel_greatest_common_divisor,
Outputs::Display::InputDataType data_type) { const Outputs::Display::Type display_type,
const Outputs::Display::InputDataType data_type
) : CRT() {
scan_target_modals_.input_data_type = data_type; scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_display_type(cycles_per_line, display_type); set_new_display_type(cycles_per_line, display_type);
} }
CRT::CRT(int cycles_per_line, CRT::CRT(
int clocks_per_pixel_greatest_common_divisor, const int cycles_per_line,
int height_of_display, const int clocks_per_pixel_greatest_common_divisor,
int vertical_sync_half_lines, const int height_of_display,
Outputs::Display::InputDataType data_type) { const int vertical_sync_half_lines,
const Outputs::Display::InputDataType data_type
) : CRT() {
scan_target_modals_.input_data_type = data_type; scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor; scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(cycles_per_line, height_of_display, Outputs::Display::ColourSpace::YIQ, 1, 1, vertical_sync_half_lines, false); set_new_timing(
cycles_per_line,
height_of_display,
Outputs::Display::ColourSpace::YIQ,
1,
1,
vertical_sync_half_lines,
false
);
} }
// Use some from-thin-air arbitrary constants for default timing, otherwise passing // Use some from-thin-air arbitrary constants for default timing, otherwise passing
// construction off to one of the other constructors. // construction off to one of the other constructors.
CRT::CRT(Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {} CRT::CRT(const Outputs::Display::InputDataType data_type) : CRT(100, 1, 100, 1, data_type) {}
// MARK: - Sync loop // MARK: - Sync loop
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced) { void CRT::advance_cycles(
return vertical_flywheel_->get_next_event_in_period(vsync_is_requested, cycles_to_run_for, cycles_advanced); int number_of_cycles,
} bool hsync_requested,
bool vsync_requested,
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced) { const Scan::Type type,
return horizontal_flywheel_->get_next_event_in_period(hsync_is_requested, cycles_to_run_for, cycles_advanced); const int number_of_samples
} ) {
Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(uint16_t data_offset) {
Display::ScanTarget::Scan::EndPoint end_point;
// Clamp the available range on endpoints. These will almost always be within range, but may go
// out during times of resync.
end_point.x = uint16_t(std::min(horizontal_flywheel_->get_current_output_position(), 65535));
end_point.y = uint16_t(std::min(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_, 65535));
end_point.data_offset = data_offset;
// 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_alternate_line_ ? -1 : 1);
end_point.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_);
return end_point;
}
void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples) {
number_of_cycles *= time_multiplier_; number_of_cycles *= time_multiplier_;
const bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data)); const bool is_output_run = type == Scan::Type::Level || type == Scan::Type::Data;
const auto total_cycles = number_of_cycles; const auto total_cycles = number_of_cycles;
bool did_output = false; bool did_output = false;
const auto end_point = [&] {
return this->end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
};
using EndPoint = Outputs::Display::ScanTarget::Scan::EndPoint;
EndPoint start_point;
while(number_of_cycles) { while(number_of_cycles) {
// Get time until next horizontal and vertical sync generator events. // Get time until next horizontal and vertical sync generator events.
int time_until_vertical_sync_event, time_until_horizontal_sync_event; const auto vertical_event = vertical_flywheel_.next_event_in_period(vsync_requested, number_of_cycles);
const Flywheel::SyncEvent next_vertical_sync_event = get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event); assert(vertical_event.second >= 0 && vertical_event.second <= number_of_cycles);
const Flywheel::SyncEvent next_horizontal_sync_event = get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
const auto horizontal_event = horizontal_flywheel_.next_event_in_period(hsync_requested, vertical_event.second);
assert(horizontal_event.second >= 0 && horizontal_event.second <= vertical_event.second);
// Whichever event is scheduled to happen first is the one to advance to. // Whichever event is scheduled to happen first is the one to advance to.
const int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event); const int next_run_length = horizontal_event.second;
// Request each sync at most once.
hsync_requested = false; hsync_requested = false;
vsync_requested = false; vsync_requested = false;
// Determine whether to output any data for this portion of the output; if so then grab somewhere to put it. // Determine whether to output any data for this portion of the output; if so then grab somewhere to put it.
const bool is_output_segment = ((is_output_run && next_run_length) && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace()); const bool is_output_segment =
is_output_run &&
next_run_length &&
!horizontal_flywheel_.is_in_retrace() &&
!vertical_flywheel_.is_in_retrace();
Outputs::Display::ScanTarget::Scan *const next_scan = is_output_segment ? scan_target_->begin_scan() : nullptr; Outputs::Display::ScanTarget::Scan *const next_scan = is_output_segment ? scan_target_->begin_scan() : nullptr;
did_output |= is_output_segment; did_output |= is_output_segment;
// If outputting, store the start location and scan constants. // If outputting, store the start location and scan constants.
if(next_scan) { if(next_scan) {
next_scan->end_points[0] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)); next_scan->end_points[0] = end_point();
next_scan->composite_amplitude = colour_burst_amplitude_; next_scan->composite_amplitude = colour_burst_amplitude_;
} else if(is_output_segment && is_calibrating(framing_)) {
start_point = end_point();
} }
// Advance time: that'll affect both the colour subcarrier position and the number of cycles left to run. // Advance time: that'll affect both the colour subcarrier and the number of cycles left to run.
phase_numerator_ += next_run_length * colour_cycle_numerator_; phase_numerator_ += next_run_length * colour_cycle_numerator_;
number_of_cycles -= next_run_length; number_of_cycles -= next_run_length;
cycles_since_horizontal_sync_ += next_run_length; cycles_since_horizontal_sync_ += next_run_length;
// React to the incoming event. // React to the incoming event.
horizontal_flywheel_->apply_event(next_run_length, (next_run_length == time_until_horizontal_sync_event) ? next_horizontal_sync_event : Flywheel::SyncEvent::None); horizontal_flywheel_.apply_event(
vertical_flywheel_->apply_event(next_run_length, (next_run_length == time_until_vertical_sync_event) ? next_vertical_sync_event : Flywheel::SyncEvent::None); next_run_length,
next_run_length == horizontal_event.second ? horizontal_event.first : Flywheel::SyncEvent::None
);
// End the scan if necessary. const auto active_vertical_event =
if(next_scan) { next_run_length == vertical_event.second ? vertical_event.first : Flywheel::SyncEvent::None;
next_scan->end_points[1] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)); vertical_flywheel_.apply_event(next_run_length, active_vertical_event);
scan_target_->end_scan();
if(active_vertical_event == Flywheel::SyncEvent::StartRetrace) {
if(is_calibrating(framing_)) {
active_rect_.origin.x /= scan_target_modals_.output_scale.x;
active_rect_.size.width /= scan_target_modals_.output_scale.x;
active_rect_.origin.y /= scan_target_modals_.output_scale.y;
active_rect_.size.height /= scan_target_modals_.output_scale.y;
border_rect_.origin.x /= scan_target_modals_.output_scale.x;
border_rect_.size.width /= scan_target_modals_.output_scale.x;
border_rect_.origin.y /= scan_target_modals_.output_scale.y;
border_rect_.size.height /= scan_target_modals_.output_scale.y;
}
if(
captures_in_rect_ > 5 &&
active_rect_.size.width > 0.05f &&
active_rect_.size.height > 0.05f &&
vertical_flywheel_.was_stable()
) {
if(!level_changes_in_frame_) {
posit(active_rect_);
} else if(level_changes_in_frame_ < 20) {
posit(active_rect_ * 0.9f + border_rect_ * 0.1f);
} else {
posit(active_rect_ * 0.3f + border_rect_ * 0.7f);
}
}
level_changes_in_frame_ = 0;
if(is_calibrating(framing_)) {
border_rect_ = active_rect_ = Display::Rect(65536.0f, 65536.0f, 0.0f, 0.0f);
captures_in_rect_ = 0;
}
} }
// Announce horizontal retrace events. // End the scan if necessary.
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event != Flywheel::SyncEvent::None) { const auto posit_scan = [&](const EndPoint &start, const EndPoint &end) {
++captures_in_rect_;
border_rect_.expand(start.x, end.x, start.y, end.y);
if(number_of_samples > 1) {
active_rect_.expand(start.x, end.x, start.y, end.y);
}
};
if(next_scan) {
next_scan->end_points[1] = end_point();
if(is_calibrating(framing_)) posit_scan(next_scan->end_points[0], next_scan->end_points[1]);
scan_target_->end_scan();
} else if(is_output_segment && is_calibrating(framing_)) {
posit_scan(start_point, end_point());
}
using Event = Outputs::Display::ScanTarget::Event;
// Announce horizontal sync events.
if(next_run_length == horizontal_event.second && horizontal_event.first != Flywheel::SyncEvent::None) {
// Reset the cycles-since-sync counter if this is the end of retrace. // Reset the cycles-since-sync counter if this is the end of retrace.
if(next_horizontal_sync_event == Flywheel::SyncEvent::EndRetrace) { if(horizontal_event.first == Flywheel::SyncEvent::EndRetrace) {
cycles_since_horizontal_sync_ = 0; cycles_since_horizontal_sync_ = 0;
// This is unnecessary, strictly speaking, but seeks to help ScanTargets fit as // This is unnecessary, strictly speaking, but seeks to help ScanTargets fit as
@@ -279,39 +374,43 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
// Announce event. // Announce event.
const auto event = const auto event =
(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) horizontal_event.first == Flywheel::SyncEvent::StartRetrace
? Outputs::Display::ScanTarget::Event::BeginHorizontalRetrace : Outputs::Display::ScanTarget::Event::EndHorizontalRetrace; ? Event::BeginHorizontalRetrace : Event::EndHorizontalRetrace;
scan_target_->announce( scan_target_->announce(
event, event,
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()), !(horizontal_flywheel_.is_in_retrace() || vertical_flywheel_.is_in_retrace()),
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)), end_point(),
colour_burst_amplitude_); colour_burst_amplitude_);
// If retrace is starting, update phase if required and mark no colour burst spotted yet. // If retrace is starting, update phase if required and mark no colour burst spotted yet.
if(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) { if(horizontal_event.first == Flywheel::SyncEvent::StartRetrace) {
should_be_alternate_line_ ^= phase_alternates_; should_be_alternate_line_ ^= phase_alternates_;
colour_burst_amplitude_ = 0; colour_burst_amplitude_ = 0;
} }
} }
// Also announce vertical retrace events. // Announce vertical sync events.
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event != Flywheel::SyncEvent::None) { if(next_run_length == vertical_event.second && vertical_event.first != Flywheel::SyncEvent::None) {
const auto event = const auto event =
(next_vertical_sync_event == Flywheel::SyncEvent::StartRetrace) vertical_event.first == Flywheel::SyncEvent::StartRetrace
? Outputs::Display::ScanTarget::Event::BeginVerticalRetrace : Outputs::Display::ScanTarget::Event::EndVerticalRetrace; ? Event::BeginVerticalRetrace : Event::EndVerticalRetrace;
scan_target_->announce( scan_target_->announce(
event, event,
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()), !(horizontal_flywheel_.is_in_retrace() || vertical_flywheel_.is_in_retrace()),
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)), end_point(),
colour_burst_amplitude_); colour_burst_amplitude_);
} }
// if this is vertical retrace then advance a field // At vertical retrace advance a field.
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event == Flywheel::SyncEvent::EndRetrace) { if(next_run_length == vertical_event.second && vertical_event.first == Flywheel::SyncEvent::EndRetrace) {
if(delegate_) { if(delegate_) {
frames_since_last_delegate_call_++; ++frames_since_last_delegate_call_;
if(frames_since_last_delegate_call_ == 20) { if(frames_since_last_delegate_call_ == 20) {
delegate_->crt_did_end_batch_of_frames(*this, frames_since_last_delegate_call_, vertical_flywheel_->get_and_reset_number_of_surprises()); delegate_->crt_did_end_batch_of_frames(
*this,
frames_since_last_delegate_call_,
vertical_flywheel_.get_and_reset_number_of_surprises()
);
frames_since_last_delegate_call_ = 0; frames_since_last_delegate_call_ = 0;
} }
} }
@@ -323,16 +422,123 @@ void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_
} }
} }
// MARK: - stream feeding methods Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(const uint16_t data_offset) {
// 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_;
const auto composite_angle =
(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_)
* (is_alternate_line_ ? -1 : 1);
void CRT::output_scan(const Scan *const scan) { return Display::ScanTarget::Scan::EndPoint{
assert(scan->number_of_cycles >= 0); // Clamp the available range on endpoints. These will almost always be within range, but may go
// out during times of resync.
.x = uint16_t(std::min(horizontal_flywheel_.current_output_position(), 65535)),
.y = uint16_t(
std::min(vertical_flywheel_.current_output_position() / vertical_flywheel_output_divider_, 65535)
),
.data_offset = data_offset,
.composite_angle = int16_t(composite_angle),
.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_),
};
}
void CRT::posit(Display::Rect rect) {
// Scale and push a rect.
const auto set_rect = [&](const Display::Rect &rect) {
scan_target_modals_.visible_area = rect;
scan_target_->set_modals(scan_target_modals_);
};
// Get current interpolation between previous_posted_rect_ and posted_rect_.
const auto current_rect = [&] {
const auto animation_time = animation_curve_.value(float(animation_step_) / float(AnimationSteps));
return
previous_posted_rect_ * (1.0f - animation_time) +
posted_rect_ * animation_time;
};
// Zoom out very slightly if there's space; this avoids a cramped tight crop.
if(rect.size.width < 0.95 && rect.size.height < 0.95) {
rect.scale(1.02f, 1.02f);
}
// Static framing: don't evaluate.
if(framing_ == Framing::Static) {
return;
}
// Border reactive: take frame as gospel.
if(framing_ == Framing::BorderReactive) {
if(rect != posted_rect_) {
previous_posted_rect_ = current_rect();
posted_rect_ = rect;
animation_step_ = 0;
}
}
// Continue with any ongoing animation.
if(animation_step_ < AnimationSteps) {
set_rect(current_rect());
++animation_step_;
if(animation_step_ == AnimationSteps) {
if(framing_ == Framing::CalibratingAutomaticFixed) {
framing_ =
border_rect_ != active_rect_ ?
Framing::BorderReactive : Framing::Static;
return;
}
}
}
if(!has_first_reading_) {
rect_accumulator_.posit(rect);
if(const auto reading = rect_accumulator_.first_reading(); reading.has_value()) {
previous_posted_rect_ = posted_rect_;
posted_rect_ = *reading;
animation_step_ = 0;
has_first_reading_ = true;
Logger::info().append("First reading is (%0.5ff, %0.5ff, %0.5ff, %0.5ff)",
posted_rect_.origin.x, posted_rect_.origin.y,
posted_rect_.size.width, posted_rect_.size.height);
}
return;
}
// Constrain to permitted bounds.
framing_bounds_.constrain(rect, max_offsets_[0], max_offsets_[1]);
// Constrain to minimum scale.
rect.scale(
rect.size.width > minimum_scale_ ? 1.0f : minimum_scale_ / rect.size.width,
rect.size.height > minimum_scale_ ? 1.0f : minimum_scale_ / rect.size.height
);
const auto output_frame = rect_accumulator_.posit(rect);
if(output_frame && *output_frame != posted_rect_) {
previous_posted_rect_ = current_rect();
posted_rect_ = *output_frame;
animation_step_ = 0;
}
}
// MARK: - Stream feeding.
void CRT::output_scan(const Scan &scan) {
assert(scan.number_of_cycles >= 0);
// Simplified colour burst logic: if it's within the back porch we'll take it. // Simplified colour burst logic: if it's within the back porch we'll take it.
if(scan->type == Scan::Type::ColourBurst) { if(scan.type == Scan::Type::ColourBurst) {
if(!colour_burst_amplitude_ && horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6) { if(
!colour_burst_amplitude_ &&
horizontal_flywheel_.current_time() < (horizontal_flywheel_.standard_period() * 12) >> 6
) {
// Load phase_numerator_ as a fixed-point quantity in the range [0, 255]. // Load phase_numerator_ as a fixed-point quantity in the range [0, 255].
phase_numerator_ = scan->phase; phase_numerator_ = scan.phase;
if(colour_burst_phase_adjustment_ != 0xff) if(colour_burst_phase_adjustment_ != 0xff)
phase_numerator_ = (phase_numerator_ & ~63) + colour_burst_phase_adjustment_; phase_numerator_ = (phase_numerator_ & ~63) + colour_burst_phase_adjustment_;
@@ -340,21 +546,21 @@ void CRT::output_scan(const Scan *const scan) {
phase_numerator_ = (phase_numerator_ * phase_denominator_) >> 8; phase_numerator_ = (phase_numerator_ * phase_denominator_) >> 8;
// Crib the colour burst amplitude. // Crib the colour burst amplitude.
colour_burst_amplitude_ = scan->amplitude; colour_burst_amplitude_ = scan.amplitude;
} }
} }
// TODO: inspect raw data for potential colour burst if required; the DPLL and some zero crossing logic // TODO: inspect raw data for potential colour burst if required; the DPLL and some zero crossing logic
// will probably be sufficient but some test data would be helpful // will probably be sufficient but some test data would be helpful
// sync logic: mark whether this is currently sync and check for a leading edge // sync logic: mark whether this is currently sync and check for a leading edge
const bool this_is_sync = (scan->type == Scan::Type::Sync); const bool this_is_sync = scan.type == Scan::Type::Sync;
const bool is_leading_edge = (!is_receiving_sync_ && this_is_sync); const bool is_leading_edge = !is_receiving_sync_ && this_is_sync;
is_receiving_sync_ = this_is_sync; is_receiving_sync_ = this_is_sync;
// Horizontal sync is recognised on any leading edge that is not 'near' the expected vertical sync; // Horizontal sync is recognised on any leading edge that is not 'near' the expected vertical sync;
// the second limb is to avoid slightly horizontal sync shifting from the common pattern of // the second limb is to avoid slightly horizontal sync shifting from the common pattern of
// equalisation pulses as the inverse of ordinary horizontal sync. // equalisation pulses as the inverse of ordinary horizontal sync.
bool hsync_requested = is_leading_edge && !vertical_flywheel_->is_near_expected_sync(); bool hsync_requested = is_leading_edge && !vertical_flywheel_.is_near_expected_sync();
if(this_is_sync) { if(this_is_sync) {
// If this is sync then either begin or continue a sync accumulation phase. // If this is sync then either begin or continue a sync accumulation phase.
@@ -363,7 +569,7 @@ void CRT::output_scan(const Scan *const scan) {
} else { } else {
// If this is not sync then check how long it has been since sync. If it's more than // If this is not sync then check how long it has been since sync. If it's more than
// half a line then end sync accumulation and zero out the accumulating count. // half a line then end sync accumulation and zero out the accumulating count.
cycles_since_sync_ += scan->number_of_cycles; cycles_since_sync_ += scan.number_of_cycles;
if(cycles_since_sync_ > (cycles_per_line_ >> 2)) { if(cycles_since_sync_ > (cycles_per_line_ >> 2)) {
cycles_of_sync_ = 0; cycles_of_sync_ = 0;
is_accumulating_sync_ = false; is_accumulating_sync_ = false;
@@ -371,19 +577,19 @@ void CRT::output_scan(const Scan *const scan) {
} }
} }
int number_of_cycles = scan->number_of_cycles; int number_of_cycles = scan.number_of_cycles;
bool vsync_requested = false; bool vsync_requested = false;
// If sync is being accumulated then accumulate it; if it crosses the vertical sync threshold then // If sync is being accumulated then accumulate it; if it crosses the vertical sync threshold then
// divide this line at the crossing point and indicate vertical sync there. // divide this line at the crossing point and indicate vertical sync there.
if(is_accumulating_sync_ && !is_refusing_sync_) { if(is_accumulating_sync_ && !is_refusing_sync_) {
cycles_of_sync_ += scan->number_of_cycles; cycles_of_sync_ += scan.number_of_cycles;
if(this_is_sync && cycles_of_sync_ >= sync_capacitor_charge_threshold_) { if(this_is_sync && cycles_of_sync_ >= sync_capacitor_charge_threshold_) {
const int overshoot = std::min(cycles_of_sync_ - sync_capacitor_charge_threshold_, number_of_cycles); const int overshoot = std::min(cycles_of_sync_ - sync_capacitor_charge_threshold_, number_of_cycles);
if(overshoot) { if(overshoot) {
number_of_cycles -= overshoot; number_of_cycles -= overshoot;
advance_cycles(number_of_cycles, hsync_requested, false, scan->type, 0); advance_cycles(number_of_cycles, hsync_requested, false, scan.type, 0);
hsync_requested = false; hsync_requested = false;
number_of_cycles = overshoot; number_of_cycles = overshoot;
} }
@@ -393,77 +599,86 @@ void CRT::output_scan(const Scan *const scan) {
} }
} }
advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan->type, scan->number_of_samples); advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan.type, scan.number_of_samples);
} }
/* /*
These all merely channel into advance_cycles, supplying appropriate arguments These all merely channel into advance_cycles, supplying appropriate arguments
*/ */
void CRT::output_sync(int number_of_cycles) { void CRT::output_sync(const int number_of_cycles) {
Scan scan; output_scan(Scan{
scan.type = Scan::Type::Sync; .type = Scan::Type::Sync,
scan.number_of_cycles = number_of_cycles; .number_of_cycles = number_of_cycles,
output_scan(&scan); });
} }
void CRT::output_blank(int number_of_cycles) { void CRT::output_blank(const int number_of_cycles) {
Scan scan; output_scan(Scan{
scan.type = Scan::Type::Blank; .type = Scan::Type::Blank,
scan.number_of_cycles = number_of_cycles; .number_of_cycles = number_of_cycles,
output_scan(&scan); });
} }
void CRT::output_level(int number_of_cycles) { void CRT::output_level(const int number_of_cycles) {
scan_target_->end_data(1); scan_target_->end_data(1);
Scan scan; output_scan(Scan{
scan.type = Scan::Type::Level; .type = Scan::Type::Level,
scan.number_of_cycles = number_of_cycles; .number_of_cycles = number_of_cycles,
scan.number_of_samples = 1; .number_of_samples = 1,
output_scan(&scan); });
} }
void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line, uint8_t amplitude) { void CRT::output_colour_burst(
Scan scan; const int number_of_cycles,
scan.type = Scan::Type::ColourBurst; const uint8_t phase,
scan.number_of_cycles = number_of_cycles; const bool is_alternate_line,
scan.phase = phase; const uint8_t amplitude
scan.amplitude = amplitude >> 1; ) {
is_alternate_line_ = is_alternate_line; is_alternate_line_ = is_alternate_line;
output_scan(&scan); output_scan(Scan{
.type = Scan::Type::ColourBurst,
.number_of_cycles = number_of_cycles,
.phase = phase,
.amplitude = uint8_t(amplitude >> 1),
});
} }
void CRT::output_default_colour_burst(int number_of_cycles, uint8_t amplitude) { void CRT::output_default_colour_burst(const int number_of_cycles, const uint8_t amplitude) {
// TODO: avoid applying a rounding error here? // TODO: avoid applying a rounding error here?
output_colour_burst(number_of_cycles, uint8_t((phase_numerator_ * 256) / phase_denominator_), should_be_alternate_line_, amplitude); output_colour_burst(
number_of_cycles,
uint8_t((phase_numerator_ * 256) / phase_denominator_),
should_be_alternate_line_,
amplitude
);
} }
void CRT::set_immediate_default_phase(float phase) { void CRT::set_immediate_default_phase(const float phase) {
phase = fmodf(phase, 1.0f); phase_numerator_ = int(std::fmod(phase, 1.0f) * float(phase_denominator_));
phase_numerator_ = int(phase * float(phase_denominator_));
} }
void CRT::output_data(int number_of_cycles, size_t number_of_samples) { void CRT::output_data(const int number_of_cycles, const size_t number_of_samples) {
#ifndef NDEBUG #ifndef NDEBUG
// assert(number_of_samples > 0); // assert(number_of_samples > 0);
// assert(number_of_samples <= allocated_data_length_); // assert(number_of_samples <= allocated_data_length_);
// allocated_data_length_ = std::numeric_limits<size_t>::min(); // allocated_data_length_ = std::numeric_limits<size_t>::min();
#endif #endif
scan_target_->end_data(number_of_samples); scan_target_->end_data(number_of_samples);
Scan scan; output_scan(Scan{
scan.type = Scan::Type::Data; .type = Scan::Type::Data,
scan.number_of_cycles = number_of_cycles; .number_of_cycles = number_of_cycles,
scan.number_of_samples = int(number_of_samples); .number_of_samples = int(number_of_samples),
output_scan(&scan); });
} }
// MARK: - Getters. // MARK: - Getters.
Outputs::Display::Rect CRT::get_rect_for_area( Outputs::Display::Rect CRT::get_rect_for_area(
int first_line_after_sync, [[maybe_unused]] int first_line_after_sync,
int number_of_lines, [[maybe_unused]] int number_of_lines,
int first_cycle_after_sync, [[maybe_unused]] int first_cycle_after_sync,
int number_of_cycles, [[maybe_unused]] int number_of_cycles
float aspect_ratio
) const { ) const {
assert(number_of_cycles > 0); assert(number_of_cycles > 0);
assert(number_of_lines > 0); assert(number_of_lines > 0);
@@ -478,8 +693,8 @@ Outputs::Display::Rect CRT::get_rect_for_area(
number_of_lines += 4; number_of_lines += 4;
// Determine prima facie x extent. // Determine prima facie x extent.
const int horizontal_period = horizontal_flywheel_->get_standard_period(); const int horizontal_period = horizontal_flywheel_.standard_period();
const int horizontal_scan_period = horizontal_flywheel_->get_scan_period(); const int horizontal_scan_period = horizontal_flywheel_.scan_period();
const int horizontal_retrace_period = horizontal_period - horizontal_scan_period; const int horizontal_retrace_period = horizontal_period - horizontal_scan_period;
// Ensure requested range is within visible region. // Ensure requested range is within visible region.
@@ -490,8 +705,8 @@ Outputs::Display::Rect CRT::get_rect_for_area(
float width = float(number_of_cycles) / float(horizontal_scan_period); float width = float(number_of_cycles) / float(horizontal_scan_period);
// Determine prima facie y extent. // Determine prima facie y extent.
const int vertical_period = vertical_flywheel_->get_standard_period(); const int vertical_period = vertical_flywheel_.standard_period();
const int vertical_scan_period = vertical_flywheel_->get_scan_period(); const int vertical_scan_period = vertical_flywheel_.scan_period();
const int vertical_retrace_period = vertical_period - vertical_scan_period; const int vertical_retrace_period = vertical_period - vertical_scan_period;
// Ensure range is visible. // Ensure range is visible.
@@ -504,34 +719,67 @@ Outputs::Display::Rect CRT::get_rect_for_area(
number_of_lines * horizontal_period number_of_lines * horizontal_period
) / horizontal_period; ) / horizontal_period;
float start_y = const float start_y =
float(first_line_after_sync * horizontal_period - vertical_retrace_period) / float(first_line_after_sync * horizontal_period - vertical_retrace_period) /
float(vertical_scan_period); float(vertical_scan_period);
float height = float(number_of_lines * horizontal_period) / vertical_scan_period; const float height = float(number_of_lines * horizontal_period) / vertical_scan_period;
// Pick a zoom that includes the entire requested visible area given the aspect ratio constraints.
const float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f);
const float ideal_width = height * adjusted_aspect_ratio;
if(ideal_width > width) {
start_x -= (ideal_width - width) * 0.5f;
width = ideal_width;
} else {
float ideal_height = width / adjusted_aspect_ratio;
start_y -= (ideal_height - height) * 0.5f;
height = ideal_height;
}
// TODO: apply absolute clipping constraints now.
return Outputs::Display::Rect(start_x, start_y, width, height); return Outputs::Display::Rect(start_x, start_y, width, height);
} }
Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const { Outputs::Display::ScanStatus CRT::get_scaled_scan_status() const {
Outputs::Display::ScanStatus status; return Outputs::Display::ScanStatus{
status.field_duration = float(vertical_flywheel_->get_locked_period()) / float(time_multiplier_); .field_duration = float(vertical_flywheel_.locked_period()) / float(time_multiplier_),
status.field_duration_gradient = float(vertical_flywheel_->get_last_period_adjustment()) / float(time_multiplier_); .field_duration_gradient = float(vertical_flywheel_.last_period_adjustment()) / float(time_multiplier_),
status.retrace_duration = float(vertical_flywheel_->get_retrace_period()) / float(time_multiplier_); .retrace_duration = float(vertical_flywheel_.retrace_period()) / float(time_multiplier_),
status.current_position = float(vertical_flywheel_->get_current_phase()) / float(vertical_flywheel_->get_locked_scan_period()); .current_position = float(vertical_flywheel_.current_phase()) / float(vertical_flywheel_.locked_scan_period()),
status.hsync_count = vertical_flywheel_->get_number_of_retraces(); .hsync_count = vertical_flywheel_.number_of_retraces(),
return status; };
}
// MARK: - ScanTarget passthroughs.
void CRT::set_scan_target(Outputs::Display::ScanTarget *const scan_target) {
scan_target_ = scan_target;
if(!scan_target_) scan_target_ = &Outputs::Display::NullScanTarget::singleton;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_data_type(const Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_aspect_ratio(const float aspect_ratio) {
scan_target_modals_.aspect_ratio = aspect_ratio;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_display_type(const Outputs::Display::DisplayType display_type) {
scan_target_modals_.display_type = display_type;
scan_target_->set_modals(scan_target_modals_);
}
Outputs::Display::DisplayType CRT::get_display_type() const {
return scan_target_modals_.display_type;
}
void CRT::set_phase_linked_luminance_offset(const float offset) {
scan_target_modals_.input_data_tweaks.phase_linked_luminance_offset = offset;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_input_data_type(const Outputs::Display::InputDataType input_data_type) {
scan_target_modals_.input_data_type = input_data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_brightness(const float brightness) {
scan_target_modals_.brightness = brightness;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_input_gamma(const float gamma) {
scan_target_modals_.intended_gamma = gamma;
scan_target_->set_modals(scan_target_modals_);
} }

View File

@@ -10,11 +10,16 @@
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <functional>
#include <limits> #include <limits>
#include <memory> #include <memory>
#include <optional>
#include "Outputs/ScanTarget.hpp" #include "Outputs/ScanTarget.hpp"
#include "Internals/Flywheel.hpp" #include "Outputs/CRT/Internals/Flywheel.hpp"
#include "Outputs/CRT/Internals/RectAccumulator.hpp"
#include "Numeric/CubicCurve.hpp"
namespace Outputs::CRT { namespace Outputs::CRT {
@@ -43,9 +48,8 @@ static constexpr bool AlternatesPhase = false;
} }
class CRT; class CRT;
struct Delegate { struct Delegate {
virtual void crt_did_end_batch_of_frames(CRT &, int number_of_frames, int number_of_unexpected_vertical_syncs) = 0; virtual void crt_did_end_batch_of_frames(CRT &, int frames, int unexpected_vertical_syncs) = 0;
}; };
/*! Models a class 2d analogue output device, accepting a serial stream of data including syncs /*! Models a class 2d analogue output device, accepting a serial stream of data including syncs
@@ -54,60 +58,6 @@ struct Delegate {
colour phase for colour composite video. colour phase for colour composite video.
*/ */
class CRT { class CRT {
private:
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
// precision across the line.
int time_multiplier_ = 1;
// Two flywheels regulate scanning; the vertical will have a range much greater than the horizontal;
// the output divider is what that'll need to be divided by to reduce it into a 16-bit range as
// posted on to the scan target.
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
int vertical_flywheel_output_divider_ = 1;
int cycles_since_horizontal_sync_ = 0;
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
struct Scan {
enum Type {
Sync, Level, Data, Blank, ColourBurst
} type = Scan::Blank;
int number_of_cycles = 0, number_of_samples = 0;
uint8_t phase = 0, amplitude = 0;
};
void output_scan(const Scan *scan);
uint8_t colour_burst_amplitude_ = 30;
int colour_burst_phase_adjustment_ = 0xff;
int64_t phase_denominator_ = 1;
int64_t phase_numerator_ = 0;
int64_t colour_cycle_numerator_ = 1;
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples);
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
Delegate *delegate_ = nullptr;
int frames_since_last_delegate_call_ = 0;
bool is_receiving_sync_ = false; // @c true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync); @c false otherwise.
bool is_accumulating_sync_ = false; // @c true if a sync level has triggered the suspicion that a vertical sync might be in progress; @c false otherwise.
bool is_refusing_sync_ = false; // @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync.
int sync_capacitor_charge_threshold_ = 0; // Charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync.
int cycles_of_sync_ = 0; // The number of cycles since the potential vertical sync began.
int cycles_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync.
int cycles_per_line_ = 1;
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
Outputs::Display::ScanTarget::Modals scan_target_modals_;
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();
#endif
public: public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency. /*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
The requested number of buffers, each with the requested number of bytes per pixel, The requested number of buffers, each with the requested number of bytes per pixel,
@@ -129,18 +79,16 @@ public:
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside), @param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
in multiples of half a line. in multiples of half a line.
@param data_type The format that the caller will use for input data.
*/ */
CRT(int cycles_per_line, CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor, int clocks_per_pixel_greatest_common_divisor,
int height_of_display, int height_of_display,
Outputs::Display::ColourSpace colour_space, Outputs::Display::ColourSpace,
int colour_cycle_numerator, int colour_cycle_numerator,
int colour_cycle_denominator, int colour_cycle_denominator,
int vertical_sync_half_lines, int vertical_sync_half_lines,
bool should_alternate, bool should_alternate,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType);
/*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has /*! Constructs a monitor-style CRT — one that will take only an RGB or monochrome signal, and therefore has
no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level. no colour space or colour subcarrier frequency. This monitor will automatically map colour bursts to the black level.
@@ -149,15 +97,15 @@ public:
int clocks_per_pixel_greatest_common_divisor, int clocks_per_pixel_greatest_common_divisor,
int height_of_display, int height_of_display,
int vertical_sync_half_lines, int vertical_sync_half_lines,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType);
/*! Exactly identical to calling the designated constructor with colour subcarrier information /*! Exactly identical to calling the designated constructor with colour subcarrier information
looked up by display type. looked up by display type.
*/ */
CRT(int cycles_per_line, CRT(int cycles_per_line,
int minimum_cycles_per_pixel, int minimum_cycles_per_pixel,
Outputs::Display::Type display_type, Outputs::Display::Type,
Outputs::Display::InputDataType data_type); Outputs::Display::InputDataType);
/*! Constructs a CRT with no guaranteed expectations as to input signal other than data type; /*! Constructs a CRT with no guaranteed expectations as to input signal other than data type;
this allows for callers that intend to rely on @c set_new_timing. this allows for callers that intend to rely on @c set_new_timing.
@@ -169,7 +117,7 @@ public:
void set_new_timing( void set_new_timing(
int cycles_per_line, int cycles_per_line,
int height_of_display, int height_of_display,
Outputs::Display::ColourSpace colour_space, Outputs::Display::ColourSpace,
int colour_cycle_numerator, int colour_cycle_numerator,
int colour_cycle_denominator, int colour_cycle_denominator,
int vertical_sync_half_lines, int vertical_sync_half_lines,
@@ -179,15 +127,15 @@ public:
as though the new timing had been provided at construction. */ as though the new timing had been provided at construction. */
void set_new_display_type( void set_new_display_type(
int cycles_per_line, int cycles_per_line,
Outputs::Display::Type display_type); Outputs::Display::Type);
/*! Changes the type of data being supplied as input. /*! Changes the type of data being supplied as input.
*/ */
void set_new_data_type(Outputs::Display::InputDataType data_type); void set_new_data_type(Outputs::Display::InputDataType);
/*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default. /*! Sets the CRT's intended aspect ratio — 4.0/3.0 by default.
*/ */
void set_aspect_ratio(float aspect_ratio); void set_aspect_ratio(float);
/*! Output at the sync level. /*! Output at the sync level.
@@ -213,7 +161,10 @@ public:
@param number_of_cycles The number of cycles to repeat the output for. @param number_of_cycles The number of cycles to repeat the output for.
*/ */
template <typename IntT> template <typename IntT>
void output_level(int number_of_cycles, IntT value) { void output_level(const int number_of_cycles, const IntT value) {
level_changes_in_frame_ += value != last_level_;
last_level_ = value;
auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1)); auto colour_pointer = reinterpret_cast<IntT *>(begin_data(1));
if(colour_pointer) *colour_pointer = value; if(colour_pointer) *colour_pointer = value;
output_level(number_of_cycles); output_level(number_of_cycles);
@@ -230,7 +181,7 @@ public:
*/ */
void output_data(int number_of_cycles, size_t number_of_samples); void output_data(int number_of_cycles, size_t number_of_samples);
/*! A shorthand form for output_data that assumes the number of cycles to output for is the same as the number of samples. */ /*! A shorthand form for @c output_data that assumes the number of cycles to output for is the same as the number of samples. */
void output_data(int number_of_cycles) { void output_data(int number_of_cycles) {
output_data(number_of_cycles, size_t(number_of_cycles)); output_data(number_of_cycles, size_t(number_of_cycles));
} }
@@ -245,7 +196,12 @@ public:
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the @param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
positive portion of the wave. positive portion of the wave.
*/ */
void output_colour_burst(int number_of_cycles, uint8_t phase, bool is_alternate_line = false, uint8_t amplitude = DefaultAmplitude); void output_colour_burst(
int number_of_cycles,
uint8_t phase,
bool is_alternate_line = false,
uint8_t amplitude = DefaultAmplitude
);
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude. /*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.
@@ -284,7 +240,7 @@ public:
} }
/*! Sets the gamma exponent for the simulated screen. */ /*! Sets the gamma exponent for the simulated screen. */
void set_input_gamma(float gamma); void set_input_gamma(float);
enum CompositeSourceType { enum CompositeSourceType {
/// The composite function provides continuous output. /// The composite function provides continuous output.
@@ -306,16 +262,35 @@ public:
*/ */
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f); void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
/*! Nominates a section of the display to crop to for output. */ /*!
void set_visible_area(Outputs::Display::Rect visible_area); Indicates that the CRT should adjust the visible frame dynamically.
@param initial Indicates the initial view rectangle
*/
void set_dynamic_framing(
Outputs::Display::Rect initial,
float max_centre_offset_x,
float max_centre_offset_y,
float maximum_scale = 0.95f,
float minimum_scale = 0.6f
);
/*!
Indicates that the CRT can calculate ideal framing once up front, and then merely scale between differing amounts of border.
It will use @c advance to request advances in input until it has managed to pick a suitable window.
*/
void set_fixed_framing(const std::function<void()> &advance);
/*! Indicates that the CRT shall use only exactly the bounds specified. */
void set_fixed_framing(Outputs::Display::Rect);
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */ /*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
Outputs::Display::Rect get_rect_for_area( Outputs::Display::Rect get_rect_for_area(
int first_line_after_sync, int first_line_after_sync,
int number_of_lines, int number_of_lines,
int first_cycle_after_sync, int first_cycle_after_sync,
int number_of_cycles, int number_of_cycles) const;
float aspect_ratio) const;
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */ /*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
inline void set_delegate(Delegate *delegate) { inline void set_delegate(Delegate *delegate) {
@@ -345,51 +320,118 @@ public:
/*! Sets the output brightness. */ /*! Sets the output brightness. */
void set_brightness(float); void set_brightness(float);
};
/*!
Provides a CRT delegate that will will observe sync mismatches and, when an appropriate threshold is crossed,
ask its receiver to try a different display frequency.
*/
template <typename Receiver> class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate {
public:
CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {}
void crt_did_end_batch_of_frames(Outputs::CRT::CRT &, int number_of_frames, int number_of_unexpected_vertical_syncs) final {
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_frames = number_of_frames;
frame_records_[frame_record_pointer_ % frame_records_.size()].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
++frame_record_pointer_;
if(frame_record_pointer_*2 >= frame_records_.size()*3) {
int total_number_of_frames = 0;
int total_number_of_unexpected_vertical_syncs = 0;
for(const auto &record: frame_records_) {
total_number_of_frames += record.number_of_frames;
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
}
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
reset();
receiver_.register_crt_frequency_mismatch();
}
}
}
void reset() {
for(auto &record: frame_records_) {
record.number_of_frames = 0;
record.number_of_unexpected_vertical_syncs = 0;
}
}
private: private:
Receiver &receiver_; CRT();
struct FrameRecord {
int number_of_frames = 0; // Incoming clock lengths are multiplied by @c time_multiplier_ to increase precision across the line.
int number_of_unexpected_vertical_syncs = 0; int time_multiplier_ = 0;
// Two flywheels regulate scanning; the vertical with a range much greater than the horizontal.
Flywheel horizontal_flywheel_, vertical_flywheel_;
// A divider to reduce the vertcial flywheel into a 16-bit range.
int vertical_flywheel_output_divider_ = 1;
int cycles_since_horizontal_sync_ = 0;
/// Samples the flywheels to generate a raster endpoint, tagging it with the specified @c data_offset.
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
struct Scan {
enum Type {
Sync, Level, Data, Blank, ColourBurst
} type = Scan::Blank;
int number_of_cycles = 0, number_of_samples = 0;
uint8_t phase = 0, amplitude = 0;
}; };
std::array<FrameRecord, 4> frame_records_; void output_scan(const Scan &scan);
size_t frame_record_pointer_ = 0;
uint8_t colour_burst_amplitude_ = 30;
int colour_burst_phase_adjustment_ = 0xff;
int64_t phase_denominator_ = 1;
int64_t phase_numerator_ = 0;
int64_t colour_cycle_numerator_ = 1;
bool is_alternate_line_ = false, phase_alternates_ = false, should_be_alternate_line_ = false;
void advance_cycles(
int number_of_cycles,
bool hsync_requested,
bool vsync_requested,
const Scan::Type,
int number_of_samples);
Delegate *delegate_ = nullptr;
int frames_since_last_delegate_call_ = 0;
// @c true exactly if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync).
bool is_receiving_sync_ = false;
// @c true exactly if a sync level has triggered the suspicion that a vertical sync might be in progress.
bool is_accumulating_sync_ = false;
// @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion
// of an ongoing vertical sync. Used to let horizontal sync free-run during vertical
bool is_refusing_sync_ = false;
// Charges up during sync; depletes otherwise. Triggrs vertical sync upon hitting a required threshold.
int sync_capacitor_charge_threshold_ = 0;
// Number of cycles since sync began, while sync lasts.
int cycles_of_sync_ = 0;
// Number of cycles sync last ended. Used to defeat the prospect of vertical sync.
int cycles_since_sync_ = 0;
int cycles_per_line_ = 1;
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
Outputs::Display::ScanTarget::Modals scan_target_modals_;
// Based upon a black level to maximum excursion and positive burst peak of: NTSC: 882 & 143; PAL: 933 & 150.
static constexpr uint8_t DefaultAmplitude = 41;
// Accumulator for interesting detail from this frame.
Outputs::Display::Rect active_rect_;
Outputs::Display::Rect border_rect_;
int captures_in_rect_ = 0;
int level_changes_in_frame_ = 0;
uint32_t last_level_ = 0;
// Current state of cropping rectangle, including any ongoing animation.
Outputs::Display::Rect posted_rect_;
Outputs::Display::Rect previous_posted_rect_;
Numeric::CubicCurve animation_curve_;
static constexpr int AnimationSteps = 100;
int animation_step_ = AnimationSteps;
// Configured cropping options.
enum class Framing {
CalibratingAutomaticFixed,
Dynamic,
Static,
BorderReactive,
};
static constexpr bool is_calibrating(const Framing framing) {
return framing < Framing::Static;
}
RectAccumulator rect_accumulator_;
Framing framing_ = Framing::CalibratingAutomaticFixed;
bool has_first_reading_ = false;
void posit(Display::Rect);
// Affecting dynamic framing.
Outputs::Display::Rect framing_bounds_;
float minimum_scale_ = 0.85f;
float max_offsets_[2]{};
#ifndef NDEBUG
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
#endif
}; };
} }

View File

@@ -8,9 +8,12 @@
#pragma once #pragma once
#include <algorithm>
#include <cassert> #include <cassert>
#include <cmath>
#include <cstdlib> #include <cstdlib>
#include <cstdint> #include <cstdint>
#include <utility>
namespace Outputs::CRT { namespace Outputs::CRT {
@@ -29,49 +32,59 @@ struct Flywheel {
@param retrace_time The amount of time it takes to complete a retrace. @param retrace_time The amount of time it takes to complete a retrace.
@param sync_error_window The permitted deviation of sync timings from the norm. @param sync_error_window The permitted deviation of sync timings from the norm.
*/ */
Flywheel(int standard_period, int retrace_time, int sync_error_window) : Flywheel(const int standard_period, const int retrace_time, const int sync_error_window) noexcept :
standard_period_(standard_period), standard_period_(standard_period),
retrace_time_(retrace_time), retrace_time_(retrace_time),
sync_error_window_(sync_error_window), sync_error_window_(sync_error_window),
counter_before_retrace_(standard_period - retrace_time), counter_before_retrace_(standard_period - retrace_time),
expected_next_sync_(standard_period) {} expected_next_sync_(standard_period) {}
Flywheel() = default;
enum SyncEvent { enum SyncEvent {
/// Indicates that no synchronisation events will occur in the queried window.
None, None,
/// Indicates that the next synchronisation event will be a transition into retrce.
StartRetrace, StartRetrace,
/// Indicates that the next synchronisation event will be a transition out of retrace.
EndRetrace EndRetrace
}; };
/*! /*!
Asks the flywheel for the first synchronisation event that will occur in a given time period,
indicating whether a synchronisation request occurred at the start of the query window.
@param sync_is_requested @c true indicates that the flywheel should act as though having @param sync_is_requested @c true indicates that the flywheel should act as though having
received a synchronisation request now; @c false indicates no such event was detected. received a synchronisation request now; @c false indicates no such event was detected.
@param cycles_to_run_for The number of cycles to look ahead. @param cycles_to_run_for The maximum number of cycles to look ahead.
@param cycles_advanced After this method has completed, contains the amount of time until @returns A pair of the next synchronisation event and number of cycles until it occurs.
the returned synchronisation event.
@returns The next synchronisation event.
*/ */
inline SyncEvent get_next_event_in_period(bool sync_is_requested, int cycles_to_run_for, int *cycles_advanced) { std::pair<SyncEvent, int> next_event_in_period(
const bool sync_is_requested,
const int cycles_to_run_for
) {
// Calculates the next expected value for an event given the current expectation and the actual value.
//
// In practice this is a weighted mix of the two values, with the exact weighting affecting how
// quickly the flywheel adjusts to new input. It's a IIR lowpass filter.
constexpr auto mix = [](const int expected, const int actual) {
return (expected + actual) >> 1;
};
// A debugging helper.
constexpr auto require_positive = [](const int value) {
assert(value >= 0);
return value;
};
// If sync is signalled _now_, consider adjusting expected_next_sync_. // If sync is signalled _now_, consider adjusting expected_next_sync_.
if(sync_is_requested) { if(sync_is_requested) {
const auto last_sync = expected_next_sync_; const auto last_sync = expected_next_sync_;
if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) { if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) {
const int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_; const int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_;
expected_next_sync_ = (3*expected_next_sync_ + time_now) >> 2; expected_next_sync_ = mix(expected_next_sync_, time_now);
} else { } else {
++number_of_surprises_; ++number_of_surprises_;
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) { if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) {
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ + sync_error_window_) >> 2; expected_next_sync_ = mix(expected_next_sync_, standard_period_ + sync_error_window_);
} else { } else {
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ - sync_error_window_) >> 2; expected_next_sync_ = mix(expected_next_sync_, standard_period_ - sync_error_window_);
} }
} }
last_adjustment_ = expected_next_sync_ - last_sync; last_adjustment_ = expected_next_sync_ - last_sync;
@@ -82,18 +95,23 @@ struct Flywheel {
// End an ongoing retrace? // End an ongoing retrace?
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) { if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) {
proposed_sync_time = retrace_time_ - counter_; proposed_sync_time = require_positive(retrace_time_ - counter_);
proposed_event = SyncEvent::EndRetrace; proposed_event = SyncEvent::EndRetrace;
} }
// Start a retrace? // Start a retrace?
if(counter_ + proposed_sync_time >= expected_next_sync_) { if(counter_ + proposed_sync_time >= expected_next_sync_) {
proposed_sync_time = expected_next_sync_ - counter_; // A change in expectations above may have moved the expected sync time to before now.
// If so, just start sync ASAP.
proposed_sync_time = std::max(0, expected_next_sync_ - counter_);
proposed_event = SyncEvent::StartRetrace; proposed_event = SyncEvent::StartRetrace;
} }
*cycles_advanced = proposed_sync_time; return std::make_pair(proposed_event, proposed_sync_time);
return proposed_event; }
bool was_stable() const {
return std::abs(last_adjustment_) < (sync_error_window_ / 250);
} }
/*! /*!
@@ -104,7 +122,7 @@ struct Flywheel {
@param event The synchronisation event to apply after that period. @param event The synchronisation event to apply after that period.
*/ */
inline void apply_event(int cycles_advanced, SyncEvent event) { void apply_event(const int cycles_advanced, const SyncEvent event) {
// In debug builds, perform a sanity check for counter overflow. // In debug builds, perform a sanity check for counter overflow.
#ifndef NDEBUG #ifndef NDEBUG
const int old_counter = counter_; const int old_counter = counter_;
@@ -127,7 +145,7 @@ struct Flywheel {
@returns The current output position. @returns The current output position.
*/ */
inline int get_current_output_position() const { int current_output_position() const {
if(counter_ < retrace_time_) { if(counter_ < retrace_time_) {
const int retrace_distance = int((int64_t(counter_) * int64_t(standard_period_)) / int64_t(retrace_time_)); const int retrace_distance = int((int64_t(counter_) * int64_t(standard_period_)) / int64_t(retrace_time_));
if(retrace_distance > counter_before_retrace_) return 0; if(retrace_distance > counter_before_retrace_) return 0;
@@ -139,60 +157,60 @@ struct Flywheel {
/*! /*!
Returns the current 'phase' — 0 is the start of the display; a count up to 0 from a negative number represents Returns the current 'phase' — 0 is the start of the display; a count up to 0 from a negative number represents
the retrace period and it will then count up to get_locked_scan_period(). the retrace period and it will then count up to @c locked_scan_period().
@returns The current output position. @returns The current output position.
*/ */
inline int get_current_phase() const { int current_phase() const {
return counter_ - retrace_time_; return counter_ - retrace_time_;
} }
/*! /*!
@returns the amount of time since retrace last began. Time then counts monotonically up from zero. @returns the amount of time since retrace last began. Time then counts monotonically up from zero.
*/ */
inline int get_current_time() const { int current_time() const {
return counter_; return counter_;
} }
/*! /*!
@returns whether the output is currently retracing. @returns whether the output is currently retracing.
*/ */
inline bool is_in_retrace() const { bool is_in_retrace() const {
return counter_ < retrace_time_; return counter_ < retrace_time_;
} }
/*! /*!
@returns the expected length of the scan period (excluding retrace). @returns the expected length of the scan period (excluding retrace).
*/ */
inline int get_scan_period() const { int scan_period() const {
return standard_period_ - retrace_time_; return standard_period_ - retrace_time_;
} }
/*! /*!
@returns the actual length of the scan period (excluding retrace). @returns the actual length of the scan period (excluding retrace).
*/ */
inline int get_locked_scan_period() const { int locked_scan_period() const {
return expected_next_sync_ - retrace_time_; return expected_next_sync_ - retrace_time_;
} }
/*! /*!
@returns the expected length of a complete scan and retrace cycle. @returns the expected length of a complete scan and retrace cycle.
*/ */
inline int get_standard_period() const { int standard_period() const {
return standard_period_; return standard_period_;
} }
/*! /*!
@returns the actual current period for a complete scan (including retrace). @returns the actual current period for a complete scan (including retrace).
*/ */
inline int get_locked_period() const { int locked_period() const {
return expected_next_sync_; return expected_next_sync_;
} }
/*! /*!
@returns the amount by which the @c locked_period was adjusted, the last time that an adjustment was applied. @returns the amount by which the @c locked_period was adjusted, the last time that an adjustment was applied.
*/ */
inline int get_last_period_adjustment() const { int last_period_adjustment() const {
return last_adjustment_; return last_adjustment_;
} }
@@ -200,7 +218,7 @@ struct Flywheel {
@returns the number of synchronisation events that have seemed surprising since the last time this method was called; @returns the number of synchronisation events that have seemed surprising since the last time this method was called;
a low number indicates good synchronisation. a low number indicates good synchronisation.
*/ */
inline int get_and_reset_number_of_surprises() { int get_and_reset_number_of_surprises() {
const int result = number_of_surprises_; const int result = number_of_surprises_;
number_of_surprises_ = 0; number_of_surprises_ = 0;
return result; return result;
@@ -209,37 +227,37 @@ struct Flywheel {
/*! /*!
@returns A count of the number of retraces so far performed. @returns A count of the number of retraces so far performed.
*/ */
inline int get_number_of_retraces() const { int number_of_retraces() const {
return number_of_retraces_; return number_of_retraces_;
} }
/*! /*!
@returns The amount of time this flywheel spends in retrace, as supplied at construction. @returns The amount of time this flywheel spends in retrace, as supplied at construction.
*/ */
inline int get_retrace_period() const { int retrace_period() const {
return retrace_time_; return retrace_time_;
} }
/*! /*!
@returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent. @returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent.
*/ */
inline bool is_near_expected_sync() const { bool is_near_expected_sync() const {
return return
(counter_ < (standard_period_ / 100)) || (counter_ < (standard_period_ / 100)) ||
(counter_ >= expected_next_sync_ - (standard_period_ / 100)); (counter_ >= expected_next_sync_ - (standard_period_ / 100));
} }
private: private:
const int standard_period_; // The idealised length of time between syncs. int standard_period_; // Idealised length of time between syncs.
const int retrace_time_; // A constant indicating the amount of time it takes to perform a retrace. int retrace_time_; // Amount of time it takes to perform a retrace.
const int sync_error_window_; // A constant indicating the window either side of the next expected sync in which we'll accept other syncs. int sync_error_window_; // The window either side of the next expected sync in which other syncs are accepted.
int counter_ = 0; // Time since the _start_ of the last sync. int counter_ = 0; // Time since the _start_ of the last sync.
int counter_before_retrace_; // The value of _counter immediately before retrace began. int counter_before_retrace_; // Value of counter_ immediately before retrace began.
int expected_next_sync_; // Our current expection of when the next sync will be encountered (which implies velocity). int expected_next_sync_; // Current expection of when the next sync will occur (implying velocity).
int number_of_surprises_ = 0; // A count of the surprising syncs. int number_of_surprises_ = 0; // Count of surprising syncs.
int number_of_retraces_ = 0; // A count of the number of retraces to date. int number_of_retraces_ = 0; // Numer of retraces to date.
int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync. int last_adjustment_ = 0; // The amount by which expected_next_sync_ was adjusted at the last sync.

View File

@@ -0,0 +1,119 @@
//
// RectAccumulator.hpp
// Clock Signal
//
// Created by Thomas Harte on 07/10/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Outputs/ScanTarget.hpp"
#include <algorithm>
#include <array>
#include <numeric>
#include <optional>
namespace Outputs::CRT {
struct RectAccumulator {
std::optional<Display::Rect> posit(const Display::Rect &rect) {
stable_filter_.push_back(rect);
if(stable_filter_.full() && stable_filter_.stable(stability_threshold_)) {
first_reading_ = stable_filter_.join();
candidates_.push_back(*first_reading_);
stable_filter_.reset();
if(candidates_.full()) {
return candidates_.join();
}
}
return std::nullopt;
}
std::optional<Display::Rect> first_reading() {
if(did_first_read_) {
return std::nullopt;
}
did_first_read_ = first_reading_.has_value();
return first_reading_;
}
void set_stability_threshold(const float stability_threshold) {
stability_threshold_ = stability_threshold;
}
private:
template <size_t n>
struct RectHistory {
void push_back(const Display::Rect &rect) {
stream_[stream_pointer_] = rect;
pushes_ = std::min(pushes_ + 1, int(n));
++stream_pointer_;
if(stream_pointer_ == n) stream_pointer_ = 0;
}
Display::Rect join() const {
return std::accumulate(
stream_.begin() + 1,
stream_.end(),
*stream_.begin(),
[](const Display::Rect &lhs, const Display::Rect &rhs) {
return lhs | rhs;
}
);
}
bool stable(const float threshold) const {
if(!full()) {
return false;
}
return std::all_of(
stream_.begin() + 1,
stream_.end(),
[&](const Display::Rect &rhs) {
return rhs.equal(stream_[0], threshold);
}
);
}
const Display::Rect &any() const {
return stream_[0];
}
bool full() const {
return pushes_ == int(n);
}
void reset() {
pushes_ = 0;
}
private:
std::array<Display::Rect, n> stream_;
size_t stream_pointer_ = 0;
int pushes_ = 0;
};
// Use the union of "a prolonged period" to figure out what should currently be visible.
//
// Rects graduate to candidates only after exiting the stable filter, so the true number of
// frames considered at any given time is the product of the two sizes.
static constexpr int CandidateHistorySize = 150;
RectHistory<CandidateHistorySize> candidates_;
// At startup, look for a small number of sequential but consistent frames.
static constexpr int StableFilterSize = 4;
RectHistory<StableFilterSize> stable_filter_;
std::optional<Display::Rect> first_reading_;
bool did_first_read_ = false;
float stability_threshold_ = 0.0f;
};
}

View File

@@ -0,0 +1,69 @@
//
// MismatchWarner.hpp
// Clock Signal
//
// Created by Thomas Harte on 06/10/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "CRT.hpp"
namespace Outputs::CRT {
/*!
Provides a CRT delegate that will will observe sync mismatches and, when an appropriate threshold is crossed,
ask its receiver to try a different display frequency.
*/
template <typename Receiver>
class CRTFrequencyMismatchWarner: public Outputs::CRT::Delegate {
public:
CRTFrequencyMismatchWarner(Receiver &receiver) : receiver_(receiver) {}
void crt_did_end_batch_of_frames(
Outputs::CRT::CRT &,
const int number_of_frames,
const int number_of_unexpected_vertical_syncs
) final {
auto &record = frame_records_[frame_record_pointer_ & (NumberOfFrameRecords - 1)];
record.number_of_frames = number_of_frames;
record.number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
++frame_record_pointer_;
check_for_mismatch();
}
void reset() {
frame_records_ = std::array<FrameRecord, NumberOfFrameRecords>{};
}
private:
Receiver &receiver_;
struct FrameRecord {
int number_of_frames = 0;
int number_of_unexpected_vertical_syncs = 0;
};
void check_for_mismatch() {
if(frame_record_pointer_ * 2 >= NumberOfFrameRecords * 3) {
int total_number_of_frames = 0;
int total_number_of_unexpected_vertical_syncs = 0;
for(const auto &record: frame_records_) {
total_number_of_frames += record.number_of_frames;
total_number_of_unexpected_vertical_syncs += record.number_of_unexpected_vertical_syncs;
}
if(total_number_of_unexpected_vertical_syncs >= total_number_of_frames >> 1) {
reset();
receiver_.register_crt_frequency_mismatch();
}
}
}
static constexpr int NumberOfFrameRecords = 4;
static_assert(!(NumberOfFrameRecords & (NumberOfFrameRecords - 1)));
int frame_record_pointer_ = 0;
std::array<FrameRecord, NumberOfFrameRecords> frame_records_;
};
}

View File

@@ -37,6 +37,7 @@ enum class Source {
BBCMicro, BBCMicro,
CommodoreStaticAnalyser, CommodoreStaticAnalyser,
CMOSRTC, CMOSRTC,
CRT,
DirectAccessDevice, DirectAccessDevice,
Enterprise, Enterprise,
Floppy, Floppy,
@@ -127,6 +128,7 @@ constexpr const char *prefix(const Source source) {
case Source::BBCMicro: return "BBC"; case Source::BBCMicro: return "BBC";
case Source::CommodoreStaticAnalyser: return "Commodore Static Analyser"; case Source::CommodoreStaticAnalyser: return "Commodore Static Analyser";
case Source::CMOSRTC: return "CMOSRTC"; case Source::CMOSRTC: return "CMOSRTC";
case Source::CRT: return "CRT";
case Source::DirectAccessDevice: return "Direct Access Device"; case Source::DirectAccessDevice: return "Direct Access Device";
case Source::Enterprise: return "Enterprise"; case Source::Enterprise: return "Enterprise";
case Source::Floppy: return "Floppy"; case Source::Floppy: return "Floppy";

View File

@@ -116,12 +116,12 @@ void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) {
} }
void ScanTarget::setup_pipeline() { void ScanTarget::setup_pipeline() {
auto modals = BufferingScanTarget::modals(); const auto modals = BufferingScanTarget::modals();
const auto data_type_size = Outputs::Display::size_for_data_type(modals.input_data_type); const auto data_type_size = Outputs::Display::size_for_data_type(modals.input_data_type);
// Resize the texture only if required. // Resize the texture only if required.
const size_t required_size = WriteAreaWidth*WriteAreaHeight*data_type_size; const size_t required_size = WriteAreaWidth*WriteAreaHeight*data_type_size;
if(required_size != write_area_data_size()) { if(required_size != write_area_texture_.size()) {
write_area_texture_.resize(required_size); write_area_texture_.resize(required_size);
set_write_area(write_area_texture_.data()); set_write_area(write_area_texture_.data());
} }
@@ -131,37 +131,69 @@ void ScanTarget::setup_pipeline() {
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_); test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Destroy or create a QAM buffer and shader, if appropriate. // 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); if(!existing_modals_ || existing_modals_->display_type != modals.display_type) {
if(needs_qam_buffer) { const bool needs_qam_buffer =
if(!qam_chroma_texture_) { modals.display_type == DisplayType::CompositeColour ||
qam_chroma_texture_ = std::make_unique<TextureTarget>(LineBufferWidth, LineBufferHeight, QAMChromaTextureUnit, GL_NEAREST, false); 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
);
}
qam_separation_shader_ = qam_separation_shader();
enable_vertex_attributes(ShaderType::QAMSeparation, *qam_separation_shader_);
qam_separation_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0));
} else {
qam_chroma_texture_.reset();
qam_separation_shader_.reset();
} }
qam_separation_shader_ = qam_separation_shader(); // Establish an output shader.
enable_vertex_attributes(ShaderType::QAMSeparation, *qam_separation_shader_); output_shader_ = conversion_shader();
set_uniforms(ShaderType::QAMSeparation, *qam_separation_shader_); enable_vertex_attributes(ShaderType::Conversion, *output_shader_);
qam_separation_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0)); set_uniforms(ShaderType::Conversion, *output_shader_);
} else {
qam_chroma_texture_.reset(); output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0));
qam_separation_shader_.reset(); output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0));
} }
// Establish an output shader. if(qam_separation_shader_) {
output_shader_ = conversion_shader(); set_uniforms(ShaderType::QAMSeparation, *qam_separation_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); // Select whichever of a letterbox or pillarbox avoids cropping.
output_shader_->set_uniform("size", modals.visible_area.size.width, modals.visible_area.size.height); constexpr float output_ratio = 4.0f / 3.0f;
output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0)); const float aspect_ratio_stretch = modals.aspect_ratio / output_ratio;
output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0));
auto adjusted_rect = modals.visible_area;
const float letterbox_scale = adjusted_rect.size.height / (adjusted_rect.size.width * aspect_ratio_stretch);
if(letterbox_scale > 1.0f) {
adjusted_rect.scale(letterbox_scale, 1.0f);
} else {
adjusted_rect.scale(1.0f, 1.0f / letterbox_scale);
}
// Provide to shader.
output_shader_->set_uniform("origin", adjusted_rect.origin.x, adjusted_rect.origin.y);
output_shader_->set_uniform("size", 1.0f / adjusted_rect.size.width, 1.0f / adjusted_rect.size.height);
// Establish an input shader. // Establish an input shader.
input_shader_ = composition_shader(); if(!existing_modals_ || existing_modals_->input_data_type != modals.input_data_type) {
test_gl(glBindVertexArray, scan_vertex_array_); input_shader_ = composition_shader();
test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_); test_gl(glBindVertexArray, scan_vertex_array_);
enable_vertex_attributes(ShaderType::Composition, *input_shader_); test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_);
set_uniforms(ShaderType::Composition, *input_shader_); enable_vertex_attributes(ShaderType::Composition, *input_shader_);
input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0)); set_uniforms(ShaderType::Composition, *input_shader_);
input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0));
}
existing_modals_ = modals;
} }
bool ScanTarget::is_soft_display_type() { bool ScanTarget::is_soft_display_type() {

View File

@@ -98,6 +98,7 @@ private:
bool texture_exists_ = false; bool texture_exists_ = false;
// Receives scan target modals. // Receives scan target modals.
std::optional<ScanTarget::Modals> existing_modals_;
void setup_pipeline(); void setup_pipeline();
enum class ShaderType { enum class ShaderType {

View File

@@ -25,7 +25,7 @@ void ScanTarget::set_uniforms(ShaderType type, Shader &target) const {
case ShaderType::Composition: break; case ShaderType::Composition: break;
default: default:
target.set_uniform("rowHeight", GLfloat(1.05f / modals.expected_vertical_lines)); 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("scale", GLfloat(modals.output_scale.x), GLfloat(modals.output_scale.y));
target.set_uniform("phaseOffset", GLfloat(modals.input_data_tweaks.phase_linked_luminance_offset)); 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);
@@ -273,54 +273,58 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
// //
// If the display type is S-Video, generate three textureCoordinates, at // If the display type is S-Video, generate three textureCoordinates, at
// -45, 0, +45. // -45, 0, +45.
std::string vertex_shader = std::string vertex_shader = R"glsl(
"#version 150\n" #version 150
"uniform vec2 scale;" uniform vec2 scale;
"uniform float rowHeight;" uniform float rowHeight;
"in vec2 startPoint;" in vec2 startPoint;
"in vec2 endPoint;" in vec2 endPoint;
"in float startClock;" in float startClock;
"in float startCompositeAngle;" in float startCompositeAngle;
"in float endClock;" in float endClock;
"in float endCompositeAngle;" in float endCompositeAngle;
"in float lineY;" in float lineY;
"in float lineCompositeAmplitude;" in float lineCompositeAmplitude;
"uniform sampler2D textureName;" uniform sampler2D textureName;
"uniform sampler2D qamTextureName;" uniform sampler2D qamTextureName;
"uniform vec2 origin;" uniform vec2 origin;
"uniform vec2 size;" uniform vec2 size;
"uniform float textureCoordinateOffsets[4];" uniform float textureCoordinateOffsets[4];
"out vec2 textureCoordinates[4];"; out vec2 textureCoordinates[4];
)glsl";
std::string fragment_shader = std::string fragment_shader = R"glsl(
"#version 150\n" #version 150
"uniform sampler2D textureName;" uniform sampler2D textureName;
"uniform sampler2D qamTextureName;" uniform sampler2D qamTextureName;
"in vec2 textureCoordinates[4];" in vec2 textureCoordinates[4];
"out vec4 fragColour;"; out vec4 fragColour;
)glsl";
if(modals.display_type != DisplayType::RGB) { if(modals.display_type != DisplayType::RGB) {
vertex_shader += vertex_shader += R"glsl(
"out float compositeAngle;" out float compositeAngle;
"out float compositeAmplitude;" out float compositeAmplitude;
"out float oneOverCompositeAmplitude;" out float oneOverCompositeAmplitude;
"uniform float angleOffsets[4];"; uniform float angleOffsets[4];
fragment_shader += )glsl";
"in float compositeAngle;" fragment_shader += R"glsl(
"in float compositeAmplitude;" in float compositeAngle;
"in float oneOverCompositeAmplitude;" in float compositeAmplitude;
in float oneOverCompositeAmplitude;
"uniform vec4 compositeAngleOffsets;"; uniform vec4 compositeAngleOffsets;
)glsl";
} }
if(modals.display_type == DisplayType::SVideo || modals.display_type == DisplayType::CompositeColour) { if(modals.display_type == DisplayType::SVideo || modals.display_type == DisplayType::CompositeColour) {
@@ -329,38 +333,42 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
} }
// Add the code to generate a proper output position; this applies to all display types. // Add the code to generate a proper output position; this applies to all display types.
vertex_shader += vertex_shader += R"glsl(
"void main(void) {" void main(void) {
"float lateral = float(gl_VertexID & 1);" float lateral = float(gl_VertexID & 1);
"float longitudinal = float((gl_VertexID & 2) >> 1);" float longitudinal = float((gl_VertexID & 2) >> 1);
"vec2 centrePoint = mix(startPoint, vec2(endPoint.x, startPoint.y), lateral) / scale;" vec2 centrePoint = mix(startPoint, vec2(endPoint.x, startPoint.y), lateral) / scale;
"vec2 height = normalize(vec2(endPoint.x, startPoint.y) - startPoint).yx * (longitudinal - 0.5) * rowHeight;" vec2 height = normalize(vec2(endPoint.x, startPoint.y) - startPoint).yx * (longitudinal - 0.5) * rowHeight;
"vec2 eyePosition = vec2(-1.0, 1.0) + vec2(2.0, -2.0) * (((centrePoint + height) - origin) / size);" vec2 eyePosition = vec2(-1.0, 1.0) + vec2(2.0, -2.0) * ((centrePoint + height) - origin) * size;
"gl_Position = vec4(eyePosition, 0.0, 1.0);"; gl_Position = vec4(eyePosition, 0.0, 1.0);
)glsl";
// For everything other than RGB, calculate the two composite outputs. // For everything other than RGB, calculate the two composite outputs.
if(modals.display_type != DisplayType::RGB) { if(modals.display_type != DisplayType::RGB) {
vertex_shader += vertex_shader += R"glsl(
"compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;" compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;
"compositeAmplitude = lineCompositeAmplitude / 255.0;" compositeAmplitude = lineCompositeAmplitude / 255.0;
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.95, lineCompositeAmplitude));"; oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.95, lineCompositeAmplitude));
)glsl";
} }
vertex_shader += vertex_shader += R"glsl(
"float centreClock = mix(startClock, endClock, lateral);" float centreClock = mix(startClock, endClock, lateral);
"textureCoordinates[0] = vec2(centreClock + textureCoordinateOffsets[0], lineY + 0.5) / textureSize(textureName, 0);" textureCoordinates[0] = vec2(centreClock + textureCoordinateOffsets[0], lineY + 0.5) / textureSize(textureName, 0);
"textureCoordinates[1] = vec2(centreClock + textureCoordinateOffsets[1], lineY + 0.5) / textureSize(textureName, 0);" textureCoordinates[1] = vec2(centreClock + textureCoordinateOffsets[1], lineY + 0.5) / textureSize(textureName, 0);
"textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);" textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);
"textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);"; textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);
)glsl";
if((modals.display_type == DisplayType::SVideo) || (modals.display_type == DisplayType::CompositeColour)) { if((modals.display_type == DisplayType::SVideo) || (modals.display_type == DisplayType::CompositeColour)) {
vertex_shader += vertex_shader += R"glsl(
"float centreCompositeAngle = abs(mix(startCompositeAngle, endCompositeAngle, lateral)) * 4.0 / 64.0;" float centreCompositeAngle = abs(mix(startCompositeAngle, endCompositeAngle, lateral)) * 4.0 / 64.0;
"centreCompositeAngle = floor(centreCompositeAngle);" centreCompositeAngle = floor(centreCompositeAngle);
"qamTextureCoordinates[0] = vec2(centreCompositeAngle - 1.5, lineY + 0.5) / textureSize(textureName, 0);" qamTextureCoordinates[0] = vec2(centreCompositeAngle - 1.5, lineY + 0.5) / textureSize(textureName, 0);
"qamTextureCoordinates[1] = vec2(centreCompositeAngle - 0.5, lineY + 0.5) / textureSize(textureName, 0);" qamTextureCoordinates[1] = vec2(centreCompositeAngle - 0.5, lineY + 0.5) / textureSize(textureName, 0);
"qamTextureCoordinates[2] = vec2(centreCompositeAngle + 0.5, lineY + 0.5) / textureSize(textureName, 0);" qamTextureCoordinates[2] = vec2(centreCompositeAngle + 0.5, lineY + 0.5) / textureSize(textureName, 0);
"qamTextureCoordinates[3] = vec2(centreCompositeAngle + 1.5, lineY + 0.5) / textureSize(textureName, 0);"; qamTextureCoordinates[3] = vec2(centreCompositeAngle + 1.5, lineY + 0.5) / textureSize(textureName, 0);
)glsl";
} }
vertex_shader += "}"; vertex_shader += "}";
@@ -381,7 +389,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
switch(modals.display_type) { switch(modals.display_type) {
case DisplayType::CompositeColour: case DisplayType::CompositeColour:
fragment_shader += R"x( fragment_shader += R"glsl(
vec4 angles = compositeAngle + compositeAngleOffsets; vec4 angles = compositeAngle + compositeAngleOffsets;
// Sample four times over, at proper angle offsets. // Sample four times over, at proper angle offsets.
@@ -416,7 +424,7 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
// Apply a colour space conversion to get RGB. // Apply a colour space conversion to get RGB.
fragColour3 = lumaChromaToRGB * vec3(luminance / (1.0 - compositeAmplitude), channels); fragColour3 = lumaChromaToRGB * vec3(luminance / (1.0 - compositeAmplitude), channels);
} }
)x"; )glsl";
break; break;
case DisplayType::CompositeMonochrome: case DisplayType::CompositeMonochrome:
@@ -492,8 +500,8 @@ std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
std::unique_ptr<Shader> ScanTarget::composition_shader() const { std::unique_ptr<Shader> ScanTarget::composition_shader() const {
const auto modals = BufferingScanTarget::modals(); const auto modals = BufferingScanTarget::modals();
const std::string vertex_shader = const std::string vertex_shader = R"glsl(
R"x(#version 150 #version 150
in float startDataX; in float startDataX;
in float startClock; in float startClock;
@@ -515,7 +523,7 @@ std::unique_ptr<Shader> ScanTarget::composition_shader() const {
vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.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); gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);
} }
)x"; )glsl";
std::string fragment_shader = std::string fragment_shader =
R"x(#version 150 R"x(#version 150

View File

@@ -10,6 +10,7 @@
#include "ClockReceiver/TimeTypes.hpp" #include "ClockReceiver/TimeTypes.hpp"
#include <algorithm>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
@@ -25,15 +26,153 @@ enum class Type {
struct Rect { struct Rect {
struct Point { struct Point {
float x, y; float x, y;
auto operator <=>(const Point &) const = default;
} origin; } origin;
struct { struct Size {
float width, height; float width, height;
auto operator <=>(const Size &) const = default;
} size; } size;
constexpr Rect() : origin({0.0f, 0.0f}), size({1.0f, 1.0f}) {} auto operator <=>(const Rect &) const = default;
constexpr Rect(float x, float y, float width, float height) :
bool equal(const Rect &rhs, const float tolerance) const {
const auto compare = [=](const float left, const float right) {
if(left < right - tolerance || left > right + tolerance) return false;
return true;
};
return
compare(origin.x, rhs.origin.x) &&
compare(origin.y, rhs.origin.y) &&
compare(size.width, rhs.size.width) &&
compare(size.height, rhs.size.height);
}
constexpr Rect() noexcept : origin({0.0f, 0.0f}), size({1.0f, 1.0f}) {}
constexpr Rect(const float x, const float y, const float width, const float height) noexcept :
origin({x, y}), size({width, height}) {} origin({x, y}), size({width, height}) {}
bool empty() const {
return size.width == 0.0f || size.height == 0.0f;
}
void expand(const float min_x, const float max_x, const float min_y, const float max_y) {
origin.x = std::min(origin.x, min_x);
size.width = std::max(size.width, max_x - origin.x);
origin.y = std::min(origin.y, min_y);
size.height = std::max(size.height, max_y - origin.y);
}
/// Scales a rectange around its centre.
void scale(const float scale_x, const float scale_y) {
const float centre[] = {
origin.x + size.width * 0.5f,
origin.y + size.height * 0.5f,
};
size.width *= scale_x;
size.height *= scale_y;
origin.x = centre[0] - size.width * 0.5f;
origin.y = centre[1] - size.height * 0.5f;
}
float appropriate_zoom(const float aspect_ratio_stretch) const {
const float width_zoom = 1.0f / (size.width * aspect_ratio_stretch);
const float height_zoom = 1.0f / size.height;
return std::min(width_zoom, height_zoom);
}
Rect operator *(const float multiplier) const {
return Rect(
origin.x * multiplier,
origin.y * multiplier,
size.width * multiplier,
size.height * multiplier
);
}
Rect operator +(const Rect &rhs) const {
return Rect(
origin.x + rhs.origin.x,
origin.y + rhs.origin.y,
size.width + rhs.size.width,
size.height + rhs.size.height
);
}
/// Scale towards the origin.
Rect operator /(const float multiplier) const {
return Rect(
origin.x / multiplier,
origin.y / multiplier,
size.width / multiplier,
size.height / multiplier
);
}
/// Perform the union.
Rect operator |(const Rect &rhs) const {
const auto left = std::min(origin.x, rhs.origin.x);
const auto top = std::min(origin.y, rhs.origin.y);
return Rect(
left,
top,
std::max(origin.x + size.width - left, rhs.origin.x + rhs.size.width - left),
std::max(origin.y + size.height - top, rhs.origin.y + rhs.size.height - top)
);
}
/// Perform the intersection.
Rect operator &(const Rect &rhs) const {
const auto left = std::max(origin.x, rhs.origin.x);
const auto top = std::max(origin.y, rhs.origin.y);
return Rect(
left,
top,
std::min(origin.x + size.width - left, rhs.origin.x + rhs.size.width - left),
std::min(origin.y + size.height - top, rhs.origin.y + rhs.size.height - top)
);
}
void constrain(Rect &rhs, const float max_centre_offset_x, const float max_centre_offset_y) const {
// Push left and up if out of bounds on the right or bottom.
if(rhs.origin.x + rhs.size.width > origin.x + size.width) {
rhs.origin.x -= origin.x + size.width - rhs.size.width;
}
if(rhs.origin.y + rhs.size.height > origin.y + size.height) {
rhs.origin.y -= origin.x + size.height - rhs.size.height;
}
// Push down and right if out of bounds on the left or top.
rhs.origin.x = std::max(rhs.origin.x, origin.x);
rhs.origin.y = std::max(rhs.origin.y, origin.y);
// If the other rectangle is _still_ too large then it's not solveable by
// moving it around; just shrink it.
if(rhs.origin.x + rhs.size.width > origin.x + size.width) {
rhs.size.width = size.width;
}
if(rhs.origin.y + rhs.size.height > origin.y + size.height) {
rhs.size.height = size.height;
}
// Expand if necessary to include at least the same visible area but to align centres.
const auto apply_centre = [](float &origin, float &size, const float target, const float max) {
const auto offset = origin + size*0.5f - target;
if(offset < -max) {
size -= (offset + max) * 2.0f;
} else if(offset > max) {
const auto adjustment = offset - max;
size += adjustment * 2.0f;
origin -= adjustment * 2.0f;
}
};
apply_centre(rhs.origin.x, rhs.size.width, origin.x + size.width * 0.5f, max_centre_offset_x);
apply_centre(rhs.origin.y, rhs.size.height, origin.y + size.height * 0.5f, max_centre_offset_y);
}
}; };
enum class ColourSpace { enum class ColourSpace {

View File

@@ -382,8 +382,7 @@ const Outputs::Display::ScanTarget::Modals *BufferingScanTarget::new_modals() {
modals_are_dirty_.store(false, std::memory_order_relaxed); modals_are_dirty_.store(false, std::memory_order_relaxed);
// MAJOR SHARP EDGE HERE: assume that because the new_modals have been fetched then the caller will // 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. // now ensure their texture buffer is appropriate and swt the data size implied by the data type.
// 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_); std::lock_guard lock_guard(producer_mutex_);
data_type_size_ = Outputs::Display::size_for_data_type(modals_.input_data_type); data_type_size_ = Outputs::Display::size_for_data_type(modals_.input_data_type);
assert((data_type_size_ == 1) || (data_type_size_ == 2) || (data_type_size_ == 4)); assert((data_type_size_ == 1) || (data_type_size_ == 2) || (data_type_size_ == 4));