1
0
mirror of https://github.com/TomHarte/CLK.git synced 2026-04-22 08:16:42 +00:00

Start focussing on getting a good crop for 'static' machines.

This commit is contained in:
Thomas Harte
2025-10-09 18:01:46 -04:00
parent e9d310962f
commit 087d3535f6
4 changed files with 137 additions and 64 deletions
+111 -59
View File
@@ -37,6 +37,8 @@ void CRT::set_new_timing(
// 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;
@@ -86,6 +88,18 @@ void CRT::set_new_timing(
scan_target_modals_.composite_colour_space = colour_space;
scan_target_modals_.colour_cycle_numerator = colour_cycle_numerator;
scan_target_modals_.colour_cycle_denominator = colour_cycle_denominator;
// Default crop: middle 90%.
if(is_first_set) {
posted_rect_ = Display::Rect(
0.05f * scan_target_modals_.output_scale.x,
0.05f * scan_target_modals_.output_scale.y,
0.9f * scan_target_modals_.output_scale.x,
0.9f * scan_target_modals_.output_scale.y
);
scan_target_modals_.visible_area = posted_rect_;
}
scan_target_->set_modals(scan_target_modals_);
}
@@ -128,6 +142,8 @@ void CRT::set_composite_function_type(const CompositeSourceType type, const floa
// MARK: - Constructors.
CRT::CRT() : animation_curve_(Numeric::CubicCurve::easeInOut()) {}
CRT::CRT(
const int cycles_per_line,
const int clocks_per_pixel_greatest_common_divisor,
@@ -137,7 +153,7 @@ CRT::CRT(
const int vertical_sync_half_lines,
const bool should_alternate,
const Outputs::Display::InputDataType data_type
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
) : CRT() {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(
@@ -156,7 +172,7 @@ CRT::CRT(
const int clocks_per_pixel_greatest_common_divisor,
const Outputs::Display::Type display_type,
const Outputs::Display::InputDataType data_type
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
) : CRT() {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_display_type(cycles_per_line, display_type);
@@ -168,7 +184,7 @@ CRT::CRT(
const int height_of_display,
const int vertical_sync_half_lines,
const Outputs::Display::InputDataType data_type
) : animation_curve_(Numeric::CubicCurve::easeInOut()) {
) : CRT() {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(
@@ -251,62 +267,8 @@ void CRT::advance_cycles(
vertical_flywheel_.apply_event(next_run_length, active_vertical_event);
if(active_vertical_event == Flywheel::SyncEvent::StartRetrace) {
if(frame_is_complete_ && captures_in_rect_ > 5) {
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;
};
const auto set_rect = [&](const Display::Rect &rect) {
scan_target_modals_.visible_area = rect;
scan_target_modals_.visible_area.origin.x /= scan_target_modals_.output_scale.x;
scan_target_modals_.visible_area.size.width /= scan_target_modals_.output_scale.x;
scan_target_modals_.visible_area.origin.y /= scan_target_modals_.output_scale.y;
scan_target_modals_.visible_area.size.height /= scan_target_modals_.output_scale.y;
scan_target_->set_modals(scan_target_modals_);
};
// Zoom out very slightly if there's space; this avoids a cramped tight crop.
if(
active_rect_.size.width < 0.95 * scan_target_modals_.output_scale.x &&
active_rect_.size.height < 0.95 * scan_target_modals_.output_scale.y
) {
active_rect_.scale(1.02f, 1.02f);
}
// Limit visibility to the central 90% of the display regardless.
const auto Middle95 = Display::Rect(
0.05f * scan_target_modals_.output_scale.x,
0.075f * scan_target_modals_.output_scale.y,
0.90f * scan_target_modals_.output_scale.x,
0.90f * scan_target_modals_.output_scale.y);
const auto output_frame = rect_accumulator_.posit(active_rect_ & Middle95);
// if(!posted_rect_.has_value()) {
// // Startup condition; accept any reasonable frame if the accumulator is just getting started.
// const auto any_frame = rect_accumulator_.any_union();
// if(any_frame) {
// posted_rect_ = any_frame;
// set_rect(*posted_rect_);
// }
// } else {
if(output_frame && (!posted_rect_ || *output_frame != *posted_rect_)) {
previous_posted_rect_ = posted_rect_ ? current_rect() : *output_frame;
posted_rect_ = *output_frame;
animation_step_ = 0;
}
if(animation_step_ < AnimationSteps) {
set_rect(current_rect());
++animation_step_;
}
// TODO: probably something more gradated.
// E.g. if there is at least one level colour change, zoom out a little bit.
// Otherwise zoom in somewhere closer.
levels_are_interesting_ = level_changes_in_frame_ >= 5;
// }
if(frame_is_complete_ && captures_in_rect_ > 5 && vertical_flywheel_.was_stable()) {
posit(active_rect_);
}
active_rect_ = Display::Rect(65536.0f, 65536.0f, 0.0f, 0.0f);
frame_is_complete_ = true;
@@ -427,6 +389,96 @@ Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(const uint16_t data_
};
}
void CRT::posit(Display::Rect rect) {
// Static framing: don't evaluate.
if(framing_ == Framing::Static) {
return;
}
// Zoom out very slightly if there's space; this avoids a cramped tight crop.
if(
rect.size.width < 0.95 * scan_target_modals_.output_scale.x &&
rect.size.height < 0.95 * scan_target_modals_.output_scale.y
) {
rect.scale(1.02f, 1.02f);
}
// Scale and push a rect.
const auto set_rect = [&](const Display::Rect &rect) {
scan_target_modals_.visible_area = rect;
scan_target_modals_.visible_area.origin.x /= scan_target_modals_.output_scale.x;
scan_target_modals_.visible_area.size.width /= scan_target_modals_.output_scale.x;
scan_target_modals_.visible_area.origin.y /= scan_target_modals_.output_scale.y;
scan_target_modals_.visible_area.size.height /= scan_target_modals_.output_scale.y;
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;
};
// Continue with any ongoing animation.
if(animation_step_ < AnimationSteps) {
set_rect(current_rect());
++animation_step_;
if(animation_step_ == AnimationSteps) {
if(framing_ == Framing::AutomaticFixed) {
framing_ = Framing::Static;
return;
}
}
}
// TODO: possibly start with this approach in dynamic land, too?
if(framing_ == Framing::AutomaticFixed) {
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;
return;
}
}
// If here: apply dynamic logic.
// Limit visibility to the central 90% of the display regardless.
/* const auto Middle95 = Display::Rect(
0.05f * scan_target_modals_.output_scale.x,
0.075f * scan_target_modals_.output_scale.y,
0.90f * scan_target_modals_.output_scale.x,
0.90f * scan_target_modals_.output_scale.y);
const auto output_frame = rect_accumulator_.posit(active_rect_ & Middle95);
// if(!posted_rect_.has_value()) {
// // Startup condition; accept any reasonable frame if the accumulator is just getting started.
// const auto any_frame = rect_accumulator_.any_union();
// if(any_frame) {
// posted_rect_ = any_frame;
// set_rect(*posted_rect_);
// }
// } else {
if(output_frame && (!posted_rect_ || *output_frame != *posted_rect_)) {
previous_posted_rect_ = posted_rect_ ? current_rect() : *output_frame;
posted_rect_ = *output_frame;
animation_step_ = 0;
}
// TODO: probably something more gradated.
// E.g. if there is at least one level colour change, zoom out a little bit.
// Otherwise zoom in somewhere closer.
levels_are_interesting_ = level_changes_in_frame_ >= 5;
// } */
}
// MARK: - Stream feeding.
void CRT::output_scan(const Scan &scan) {
+14 -3
View File
@@ -47,11 +47,16 @@ static constexpr bool AlternatesPhase = false;
}
class CRT;
struct Delegate {
virtual void crt_did_end_batch_of_frames(CRT &, int frames, int unexpected_vertical_syncs) = 0;
};
enum class Framing {
AutomaticFixed,
DynamicInRange,
Static,
};
/*! Models a class 2d analogue output device, accepting a serial stream of data including syncs
and generating the proper set of output spans. Attempts to act and react exactly as a real
TV would have to things like irregular or off-spec sync, and includes logic properly to track
@@ -265,6 +270,8 @@ public:
/*! Nominates a section of the display to crop to for output. */
void set_visible_area(Outputs::Display::Rect);
void set_framing(Framing, Outputs::Display::Rect seed, Outputs::Display::Rect maximum);
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
Outputs::Display::Rect get_rect_for_area(
int first_line_after_sync,
@@ -303,8 +310,10 @@ public:
void set_brightness(float);
private:
CRT();
// Incoming clock lengths are multiplied by @c time_multiplier_ to increase precision across the line.
int time_multiplier_ = 1;
int time_multiplier_ = 0;
// Two flywheels regulate scanning; the vertical with a range much greater than the horizontal.
Flywheel horizontal_flywheel_, vertical_flywheel_;
@@ -376,7 +385,7 @@ private:
bool frame_is_complete_ = false;
// Current state of cropping rectangle as communicated onwards.
std::optional<Outputs::Display::Rect> posted_rect_;
Outputs::Display::Rect posted_rect_;
Outputs::Display::Rect previous_posted_rect_;
Numeric::CubicCurve animation_curve_;
@@ -387,7 +396,9 @@ private:
int level_changes_in_frame_ = 0;
uint32_t last_level_ = 0;
Framing framing_ = Framing::AutomaticFixed;
RectAccumulator rect_accumulator_;
void posit(Display::Rect);
#ifndef NDEBUG
size_t allocated_data_length_ = std::numeric_limits<size_t>::min();
+5
View File
@@ -10,6 +10,7 @@
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdlib>
#include <cstdint>
#include <utility>
@@ -109,6 +110,10 @@ struct Flywheel {
return std::make_pair(proposed_event, proposed_sync_time);
}
bool was_stable() const {
return std::abs(last_adjustment_) < 10; // TODO: don't hard code this nonsense.
}
/*!
Advances a nominated amount of time, applying a previously returned synchronisation event
at the end of that period.
+7 -2
View File
@@ -26,6 +26,11 @@ struct RectAccumulator {
return std::nullopt;
}
std::optional<Display::Rect> first_reading() const {
if(candidates_.pushes() != 2) return std::nullopt;
return candidates_.join(2);
}
private:
template <size_t n>
struct RectHistory {
@@ -36,10 +41,10 @@ private:
if(stream_pointer_ == n) stream_pointer_ = 0;
}
Display::Rect join() const {
Display::Rect join(int limit = 0) const {
return std::accumulate(
stream_.begin() + 1,
stream_.end(),
limit > 0 ? stream_.begin() + limit : stream_.end(),
stream_[0],
[](const Display::Rect &lhs, const Display::Rect &rhs) {
return lhs | rhs;