mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
114 Commits
2025-10-03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e783ff2f | ||
|
|
2eb94f1b66 | ||
|
|
bd5a2f240d | ||
|
|
73054d971c | ||
|
|
8c7f2491d7 | ||
|
|
564542420b | ||
|
|
3f7e3e6d75 | ||
|
|
6521d7d02b | ||
|
|
ad162a4e4a | ||
|
|
676b1f6fdc | ||
|
|
406ef4e16c | ||
|
|
217976350b | ||
|
|
e8f860d6fe | ||
|
|
859e6e2396 | ||
|
|
51186e615f | ||
|
|
bd8287fda3 | ||
|
|
287ff99bbc | ||
|
|
0bbfcedabb | ||
|
|
812e1e637d | ||
|
|
f20fd38940 | ||
|
|
b4cfabc005 | ||
|
|
c49e160501 | ||
|
|
a0a24902d5 | ||
|
|
1047bc8a80 | ||
|
|
0eed49c4cb | ||
|
|
e7f09e2ece | ||
|
|
89678f1ea7 | ||
|
|
e43ec7d549 | ||
|
|
95395132f0 | ||
|
|
89293d8481 | ||
|
|
e6de24557f | ||
|
|
66d76dc36a | ||
|
|
06629def62 | ||
|
|
97aeb5e930 | ||
|
|
bf45b6e20b | ||
|
|
6ad41326b0 | ||
|
|
2bbca3c169 | ||
|
|
ae903b0712 | ||
|
|
a2a7f82716 | ||
|
|
00456c891a | ||
|
|
afd5faaab1 | ||
|
|
bb33cf0f8d | ||
|
|
edc510572a | ||
|
|
bc6cffa95c | ||
|
|
48ed2912b0 | ||
|
|
a8af262c41 | ||
|
|
dcf49933bc | ||
|
|
9c014001da | ||
|
|
4f410088dd | ||
|
|
e1c1b66dc5 | ||
|
|
23c3a1fa99 | ||
|
|
ef6e1b2f74 | ||
|
|
e130ae0a8a | ||
|
|
1a1e3281e4 | ||
|
|
a4e55c9362 | ||
|
|
0b4c51eebd | ||
|
|
1107f0d9a3 | ||
|
|
775819432b | ||
|
|
a71a60937f | ||
|
|
5e661fe96b | ||
|
|
a9f5b17fcb | ||
|
|
b0c2b55fc9 | ||
|
|
925832aac5 | ||
|
|
994131e2ea | ||
|
|
f8d27d0ae0 | ||
|
|
fc50af0e17 | ||
|
|
087d3535f6 | ||
|
|
e9d310962f | ||
|
|
0f9c89d259 | ||
|
|
258c37685b | ||
|
|
56f092a0c3 | ||
|
|
6c3048ffbf | ||
|
|
c58eba61de | ||
|
|
8a54773f1b | ||
|
|
2c483e7b97 | ||
|
|
1027e9ffdc | ||
|
|
85d6957e03 | ||
|
|
c3609b66a9 | ||
|
|
605f4a92d7 | ||
|
|
d395e2bc75 | ||
|
|
e6ccdc5a97 | ||
|
|
a68c7aa45f | ||
|
|
66e959ab65 | ||
|
|
d68b172a40 | ||
|
|
d3ee778265 | ||
|
|
da96df7df7 | ||
|
|
4ea82581ec | ||
|
|
4473d3400e | ||
|
|
2f1f843e48 | ||
|
|
53a3d9042e | ||
|
|
6eb32f98b2 | ||
|
|
0fad97ed48 | ||
|
|
27246247a2 | ||
|
|
cbc96e2223 | ||
|
|
8fdf32cde8 | ||
|
|
03a94e59e2 | ||
|
|
2c0610fef8 | ||
|
|
60b3c51085 | ||
|
|
d7b5a45417 | ||
|
|
e11060bde8 | ||
|
|
4653de9161 | ||
|
|
1926ad9215 | ||
|
|
33d047c703 | ||
|
|
fadda00246 | ||
|
|
a3fed788d8 | ||
|
|
dde31e8687 | ||
|
|
190fb009bc | ||
|
|
62574d04c6 | ||
|
|
2496257bcf | ||
|
|
ab73b4de6b | ||
|
|
6c1c32baca | ||
|
|
239cc15c8f | ||
|
|
6b437c3907 | ||
|
|
4756f63169 |
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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_;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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*
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ enum Name {
|
|||||||
BBCMicroMOS12,
|
BBCMicroMOS12,
|
||||||
BBCMicroDFS226,
|
BBCMicroDFS226,
|
||||||
BBCMicroADFS130,
|
BBCMicroADFS130,
|
||||||
|
BBCMicroAdvancedDiscToolkit140,
|
||||||
|
|
||||||
// ColecoVision.
|
// ColecoVision.
|
||||||
ColecoVisionBIOS,
|
ColecoVisionBIOS,
|
||||||
|
|||||||
73
Numeric/CubicCurve.hpp
Normal file
73
Numeric/CubicCurve.hpp
Normal 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];
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 -> 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 -> 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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)]]) { \
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
119
Outputs/CRT/Internals/RectAccumulator.hpp
Normal file
119
Outputs/CRT/Internals/RectAccumulator.hpp
Normal 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
69
Outputs/CRT/MismatchWarner.hpp
Normal file
69
Outputs/CRT/MismatchWarner.hpp
Normal 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_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user