1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-23 05:29:23 +00:00
CLK/Outputs/ScanTarget.hpp
2024-01-16 23:34:46 -05:00

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;
};
}