mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-23 05:29:23 +00:00
453 lines
16 KiB
C++
453 lines
16 KiB
C++
//
|
|
// ScanTarget.hpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 30/10/2018.
|
|
// Copyright © 2018 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include "../ClockReceiver/TimeTypes.hpp"
|
|
|
|
namespace Outputs::Display {
|
|
|
|
enum class Type {
|
|
PAL50,
|
|
PAL60,
|
|
NTSC60
|
|
};
|
|
|
|
struct Rect {
|
|
struct Point {
|
|
float x, y;
|
|
} origin;
|
|
|
|
struct {
|
|
float width, height;
|
|
} size;
|
|
|
|
constexpr Rect() : origin({0.0f, 0.0f}), size({1.0f, 1.0f}) {}
|
|
constexpr Rect(float x, float y, float width, float height) :
|
|
origin({x, y}), size({width, height}) {}
|
|
};
|
|
|
|
enum class ColourSpace {
|
|
/// YIQ is the NTSC colour space.
|
|
YIQ,
|
|
|
|
/// YUV is the PAL colour space.
|
|
YUV
|
|
};
|
|
|
|
enum class DisplayType {
|
|
RGB,
|
|
SVideo,
|
|
CompositeColour,
|
|
CompositeMonochrome
|
|
};
|
|
|
|
constexpr bool is_composite(DisplayType type) {
|
|
return type == DisplayType::CompositeColour || type == DisplayType::CompositeMonochrome;
|
|
}
|
|
|
|
/*!
|
|
Enumerates the potential formats of input data.
|
|
|
|
All types are designed to be 1, 2 or 4 bytes per pixel; this hopefully creates appropriate alignment
|
|
on all formats.
|
|
*/
|
|
enum class InputDataType {
|
|
|
|
// The luminance types can be used to feed only two video pipelines:
|
|
// black and white video, or composite colour.
|
|
|
|
Luminance1, // 1 byte/pixel; any bit set => white; no bits set => black.
|
|
Luminance8, // 1 byte/pixel; linear scale.
|
|
|
|
PhaseLinkedLuminance8, // 4 bytes/pixel; each byte is an individual 8-bit luminance
|
|
// value and which value is output is a function of
|
|
// colour subcarrier phase — byte 0 defines the first quarter
|
|
// of each colour cycle, byte 1 the next quarter, etc. This
|
|
// format is intended to permit replay of sampled original data.
|
|
|
|
// The luminance plus phase types describe a luminance and the phase offset
|
|
// of a colour subcarrier. So they can be used to generate a luminance signal,
|
|
// or an s-video pipeline.
|
|
|
|
Luminance8Phase8, // 2 bytes/pixel; first is luminance, second is phase
|
|
// of a cosine wave.
|
|
//
|
|
// Phase is encoded on a 128-unit circle; anything
|
|
// greater than 192 implies that the colour part of
|
|
// the signal should be omitted.
|
|
|
|
// The RGB types can directly feed an RGB pipeline, naturally, or can be mapped
|
|
// to phase+luminance, or just to luminance.
|
|
|
|
Red1Green1Blue1, // 1 byte/pixel; bit 0 is blue on or off, bit 1 is green, bit 2 is red.
|
|
Red2Green2Blue2, // 1 byte/pixel; bits 0 and 1 are blue, bits 2 and 3 are green, bits 4 and 5 are blue.
|
|
Red4Green4Blue4, // 2 bytes/pixel; low nibble in first byte is red, high nibble in second is green, low is blue.
|
|
// i.e. if it were a little endian word, 0xgb0r; or 0x0rgb big endian.
|
|
Red8Green8Blue8, // 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant.
|
|
};
|
|
|
|
/// @returns the number of bytes per sample for data of type @c data_type.
|
|
/// Guaranteed to be 1, 2 or 4 for valid data types.
|
|
constexpr inline size_t size_for_data_type(InputDataType data_type) {
|
|
switch(data_type) {
|
|
case InputDataType::Luminance1:
|
|
case InputDataType::Luminance8:
|
|
case InputDataType::Red1Green1Blue1:
|
|
case InputDataType::Red2Green2Blue2:
|
|
return 1;
|
|
|
|
case InputDataType::Luminance8Phase8:
|
|
case InputDataType::Red4Green4Blue4:
|
|
return 2;
|
|
|
|
case InputDataType::Red8Green8Blue8:
|
|
case InputDataType::PhaseLinkedLuminance8:
|
|
return 4;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// @returns @c true if this data type presents normalised data, i.e. each byte holds a
|
|
/// value in the range [0, 255] representing a real number in the range [0.0, 1.0]; @c false otherwise.
|
|
constexpr inline size_t data_type_is_normalised(InputDataType data_type) {
|
|
switch(data_type) {
|
|
case InputDataType::Luminance8:
|
|
case InputDataType::Luminance8Phase8:
|
|
case InputDataType::Red8Green8Blue8:
|
|
case InputDataType::PhaseLinkedLuminance8:
|
|
return true;
|
|
|
|
default:
|
|
case InputDataType::Luminance1:
|
|
case InputDataType::Red1Green1Blue1:
|
|
case InputDataType::Red2Green2Blue2:
|
|
case InputDataType::Red4Green4Blue4:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// @returns The 'natural' display type for data of type @c data_type. The natural display is whichever would
|
|
/// display it with the least number of conversions. Caveat: a colour display is assumed for pure-composite data types.
|
|
constexpr inline DisplayType natural_display_type_for_data_type(InputDataType data_type) {
|
|
switch(data_type) {
|
|
default:
|
|
case InputDataType::Luminance1:
|
|
case InputDataType::Luminance8:
|
|
case InputDataType::PhaseLinkedLuminance8:
|
|
return DisplayType::CompositeColour;
|
|
|
|
case InputDataType::Red1Green1Blue1:
|
|
case InputDataType::Red2Green2Blue2:
|
|
case InputDataType::Red4Green4Blue4:
|
|
case InputDataType::Red8Green8Blue8:
|
|
return DisplayType::RGB;
|
|
|
|
case InputDataType::Luminance8Phase8:
|
|
return DisplayType::SVideo;
|
|
}
|
|
}
|
|
|
|
/// @returns A 3x3 matrix in row-major order to convert from @c colour_space to RGB.
|
|
inline std::array<float, 9> to_rgb_matrix(ColourSpace colour_space) {
|
|
const std::array<float, 9> yiq_to_rgb = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f};
|
|
const std::array<float, 9> yuv_to_rgb = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f};
|
|
|
|
switch(colour_space) {
|
|
case ColourSpace::YIQ: return yiq_to_rgb;
|
|
case ColourSpace::YUV: return yuv_to_rgb;
|
|
}
|
|
|
|
// Should be unreachable.
|
|
return std::array<float, 9>{};
|
|
}
|
|
|
|
/// @returns A 3x3 matrix in row-major order to convert to @c colour_space to RGB.
|
|
inline std::array<float, 9> from_rgb_matrix(ColourSpace colour_space) {
|
|
const std::array<float, 9> rgb_to_yiq = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f};
|
|
const std::array<float, 9> rgb_to_yuv = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f};
|
|
|
|
switch(colour_space) {
|
|
case ColourSpace::YIQ: return rgb_to_yiq;
|
|
case ColourSpace::YUV: return rgb_to_yuv;
|
|
}
|
|
|
|
// Should be unreachable.
|
|
return std::array<float, 9>{};
|
|
}
|
|
|
|
/*!
|
|
Provides an abstract target for 'scans' i.e. continuous sweeps of output data,
|
|
which are identified by 2d start and end coordinates, and the PCM-sampled data
|
|
that is output during the sweep.
|
|
|
|
Additional information is provided to allow decoding (and/or encoding) of a
|
|
composite colour feed.
|
|
|
|
Otherwise helpful: the ScanTarget vends all allocated memory. That should allow
|
|
for use of shared memory where available.
|
|
*/
|
|
struct ScanTarget {
|
|
virtual ~ScanTarget() {}
|
|
|
|
|
|
/*
|
|
This top section of the interface deals with modal settings. A ScanTarget can
|
|
assume that the modals change very infrequently.
|
|
*/
|
|
|
|
struct Modals {
|
|
/// Describes the format of input data.
|
|
InputDataType input_data_type;
|
|
|
|
struct InputDataTweaks {
|
|
/// If using the PhaseLinkedLuminance8 data type, this value provides an offset
|
|
/// to add to phase before indexing the supplied luminances.
|
|
float phase_linked_luminance_offset = 0.0f;
|
|
|
|
} input_data_tweaks;
|
|
|
|
/// Describes the type of display that the data is being shown on.
|
|
DisplayType display_type = DisplayType::SVideo;
|
|
|
|
/// If being fed composite data, this defines the colour space in use.
|
|
ColourSpace composite_colour_space;
|
|
|
|
/// Provides an integral clock rate for the duration of "a single line", specifically
|
|
/// for an idealised line. So e.g. in NTSC this will be for the duration of 227.5
|
|
/// colour clocks, regardless of whether the source actually stretches lines to
|
|
/// 228 colour cycles, abbreviates them to 227 colour cycles, etc.
|
|
int cycles_per_line;
|
|
|
|
/// Sets a GCD for the durations of pixels coming out of this device. This with
|
|
/// the @c cycles_per_line are offered for sizing of intermediary buffers.
|
|
int clocks_per_pixel_greatest_common_divisor;
|
|
|
|
/// Provides the number of colour cycles in a line, as a quotient.
|
|
int colour_cycle_numerator, colour_cycle_denominator;
|
|
|
|
/// Provides a pre-estimate of the likely number of left-to-right scans per frame.
|
|
/// This isn't a guarantee, but it should provide a decent-enough estimate.
|
|
int expected_vertical_lines;
|
|
|
|
/// Provides an additional restriction on the section of the display that is expected
|
|
/// to contain interesting content.
|
|
Rect visible_area;
|
|
|
|
/// Describes the usual gamma of the output device these scans would appear on.
|
|
float intended_gamma = 2.2f;
|
|
|
|
/// Provides a brightness multiplier for the display output.
|
|
float brightness = 1.0f;
|
|
|
|
/// Specifies the range of values that will be output for x and y coordinates.
|
|
struct {
|
|
uint16_t x, y;
|
|
} output_scale;
|
|
|
|
/// Describes the intended display aspect ratio.
|
|
float aspect_ratio = 4.0f / 3.0f;
|
|
};
|
|
|
|
/// Sets the total format of input data.
|
|
virtual void set_modals(Modals) = 0;
|
|
|
|
|
|
/*
|
|
This second section of the interface allows provision of the streamed data, plus some control
|
|
over the streaming.
|
|
*/
|
|
|
|
/*!
|
|
Defines a scan in terms of its two endpoints.
|
|
*/
|
|
struct Scan {
|
|
struct EndPoint {
|
|
/// Provide the coordinate of this endpoint. These are fixed point, purely fractional
|
|
/// numbers, relative to the scale provided in the Modals.
|
|
uint16_t x, y;
|
|
|
|
/// Provides the offset, in samples, into the most recently allocated write area, of data
|
|
/// at this end point.
|
|
uint16_t data_offset;
|
|
|
|
/// For composite video, provides the angle of the colour subcarrier at this endpoint.
|
|
///
|
|
/// This is a slightly weird fixed point, being:
|
|
///
|
|
/// * a six-bit fractional part;
|
|
/// * a nine-bit integral part; and
|
|
/// * a sign.
|
|
///
|
|
/// Positive numbers indicate that the colour subcarrier is 'running positively' on this
|
|
/// line; i.e. it is any NTSC line or an appropriate swing PAL line, encoded as
|
|
/// x*cos(a) + y*sin(a).
|
|
///
|
|
/// Negative numbers indicate a 'negative running' colour subcarrier; i.e. it is one of
|
|
/// the phase alternated lines of PAL, encoded as x*cos(a) - y*sin(a), or x*cos(-a) + y*sin(-a),
|
|
/// whichever you prefer.
|
|
///
|
|
/// It will produce undefined behaviour if signs differ on a single scan.
|
|
int16_t composite_angle;
|
|
|
|
/// Gives the number of cycles since the most recent horizontal retrace ended.
|
|
uint16_t cycles_since_end_of_horizontal_retrace;
|
|
} end_points[2];
|
|
|
|
/// For composite video, dictates the amplitude of the colour subcarrier as a proportion of
|
|
/// the whole, as determined from the colour burst. Will be 0 if there was no colour burst.
|
|
union {
|
|
uint8_t composite_amplitude;
|
|
|
|
uint32_t padding;
|
|
};
|
|
};
|
|
|
|
/// Requests a new scan to populate.
|
|
///
|
|
/// @return A valid pointer, or @c nullptr if insufficient further storage is available.
|
|
virtual Scan *begin_scan() = 0;
|
|
|
|
/// Requests a new scan to populate.
|
|
virtual void end_scan() {}
|
|
|
|
/// Finds the first available storage of at least @c required_length pixels in size which is
|
|
/// suitably aligned for writing of @c required_alignment number of samples at a time.
|
|
///
|
|
/// Calls will be paired off with calls to @c end_data.
|
|
///
|
|
/// @returns a pointer to the allocated space if any was available; @c nullptr otherwise.
|
|
virtual uint8_t *begin_data(size_t required_length, size_t required_alignment = 1) = 0;
|
|
|
|
/// Announces that the owner is finished with the region created by the most recent @c begin_data
|
|
/// and indicates that its actual final size was @c actual_length.
|
|
///
|
|
/// It is required that every call to begin_data be paired with a call to end_data.
|
|
virtual void end_data([[maybe_unused]] size_t actual_length) {}
|
|
|
|
/// Tells the scan target that its owner is about to change; this is a hint that existing
|
|
/// data and scan allocations should be invalidated.
|
|
virtual void will_change_owner() {}
|
|
|
|
/// Acts as a fence, marking the end of an atomic set of [begin/end]_[scan/data] calls] — all future pieces of
|
|
/// data will have no relation to scans prior to the submit() and all future scans will similarly have no relation to
|
|
/// prior runs of data.
|
|
///
|
|
/// Drawing is defined to be best effort, so the scan target should either:
|
|
///
|
|
/// (i) output everything received since the previous submit; or
|
|
/// (ii) output nothing.
|
|
///
|
|
/// If there were any allocation failures — i.e. any nullptr responses to begin_data or
|
|
/// begin_scan — then (ii) is a required response. But a scan target may also need to opt for (ii)
|
|
/// for any other reason.
|
|
///
|
|
/// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for
|
|
/// as long as it feels is appropriate, subject to a @c flush.
|
|
virtual void submit() {}
|
|
|
|
|
|
/*
|
|
ScanTargets also receive notification of certain events that may be helpful in processing, particularly
|
|
for synchronising internal output to the outside world.
|
|
*/
|
|
|
|
enum class Event {
|
|
BeginHorizontalRetrace,
|
|
EndHorizontalRetrace,
|
|
|
|
BeginVerticalRetrace,
|
|
EndVerticalRetrace,
|
|
};
|
|
|
|
/*!
|
|
Provides a hint that the named event has occurred.
|
|
|
|
Guarantee:
|
|
* any announce acts as an implicit fence on data/scans, much as a submit().
|
|
|
|
Permitted ScanTarget implementation:
|
|
* ignore all output during retrace periods.
|
|
|
|
@param event The event.
|
|
@param is_visible @c true if the output stream is visible immediately after this event; @c false otherwise.
|
|
@param location The location of the event.
|
|
@param composite_amplitude The amplitude of the colour burst on this line (0, if no colour burst was found).
|
|
*/
|
|
virtual void announce([[maybe_unused]] Event event, [[maybe_unused]] bool is_visible, [[maybe_unused]] const Scan::EndPoint &location, [[maybe_unused]] uint8_t composite_amplitude) {}
|
|
};
|
|
|
|
struct ScanStatus {
|
|
/// The current (prediced) length of a field (including retrace).
|
|
Time::Seconds field_duration = 0.0;
|
|
/// The difference applied to the field_duration estimate during the last field.
|
|
Time::Seconds field_duration_gradient = 0.0;
|
|
/// The amount of time this device spends in retrace.
|
|
Time::Seconds retrace_duration = 0.0;
|
|
/// The distance into the current field, from a small negative amount (in retrace) through
|
|
/// 0 (start of visible area field) to 1 (end of field).
|
|
///
|
|
/// This will increase monotonically, being a measure
|
|
/// of the current vertical position — i.e. if current_position = 0.8 then a caller can
|
|
/// conclude that the top 80% of the visible part of the display has been painted.
|
|
float current_position = 0.0f;
|
|
/// The total number of hsyncs so far encountered;
|
|
int hsync_count = 0;
|
|
/// @c true if retrace is currently going on; @c false otherwise.
|
|
bool is_in_retrace = false;
|
|
|
|
/*!
|
|
@returns this ScanStatus, with time-relative fields scaled by dividing them by @c dividend.
|
|
*/
|
|
ScanStatus operator / (float dividend) {
|
|
const ScanStatus result = {
|
|
.field_duration = field_duration / dividend,
|
|
.field_duration_gradient = field_duration_gradient / dividend,
|
|
.retrace_duration = retrace_duration / dividend,
|
|
.current_position = current_position,
|
|
.hsync_count = hsync_count,
|
|
.is_in_retrace = is_in_retrace,
|
|
};
|
|
return result;
|
|
}
|
|
|
|
/*!
|
|
@returns this ScanStatus, with time-relative fields scaled by multiplying them by @c multiplier.
|
|
*/
|
|
ScanStatus operator * (float multiplier) {
|
|
const ScanStatus result = {
|
|
.field_duration = field_duration * multiplier,
|
|
.field_duration_gradient = field_duration_gradient * multiplier,
|
|
.retrace_duration = retrace_duration * multiplier,
|
|
.current_position = current_position,
|
|
.hsync_count = hsync_count,
|
|
.is_in_retrace = is_in_retrace,
|
|
};
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/*!
|
|
Provides a null target for scans.
|
|
*/
|
|
struct NullScanTarget: public ScanTarget {
|
|
void set_modals(Modals) override {}
|
|
Scan *begin_scan() override { return nullptr; }
|
|
uint8_t *begin_data(size_t, size_t) override { return nullptr; }
|
|
void submit() override {}
|
|
|
|
static NullScanTarget singleton;
|
|
};
|
|
|
|
}
|