Files

490 lines
13 KiB
C++

//
// Video.cpp
// Clock Signal
//
// Created by Thomas Harte on 20/03/2026.
// Copyright © 2026 Thomas Harte. All rights reserved.
//
#include "Video.hpp"
#include <algorithm>
#include <bit>
using namespace Thomson;
namespace {
constexpr uint16_t rgb(const uint16_t code) {
#ifdef TARGET_RT_BIG_ENDIAN
return code;
#else
return std::rotl(code, 8);
#endif
}
// Per https://pulkomandy.tk/wiki/doku.php?id=documentations:devices:gate.arrays#video_generation
constexpr uint16_t mo5_palette[] = {
rgb(0x000), rgb(0xf55), rgb(0x0f0), rgb(0xff0), rgb(0x55f), rgb(0xf0f), rgb(0x5ff), rgb(0xfff),
rgb(0xaaa), rgb(0xfaa), rgb(0xafa), rgb(0xffa), rgb(0x5af), rgb(0xfaf), rgb(0xaff), rgb(0xfa5),
};
// MARK: - Pixel generators.
// TODO: all of the below ignore the bit ordering. Don't.
template <PixelMode, DotClock, BitOrdering>
struct PixelGenerator {
static void output(
uint8_t bank0,
uint8_t bank1,
uint16_t *target,
const std::array<uint16_t, 16> &palette
);
};
//
// 40-column, and Pages 1 and 2.
//
template <PixelMode mode, DotClock clock, BitOrdering ordering>
void PixelGenerator<mode, clock, ordering>::output(
const uint8_t bank0,
const uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
const std::array<uint16_t, 2> colours = [&] () -> std::array<uint16_t, 2> {
switch(mode) {
case PixelMode::Page1:
return {
palette[8],
palette[9],
};
case PixelMode::Page2:
return {
palette[8],
palette[10],
};
case PixelMode::Columns40:
return {
palette[bank1 & 0xf],
palette[bank1 >> 4],
};
default:
__builtin_unreachable();
}
} ();
const uint8_t pixels = mode == PixelMode::Page2 ? bank1 : bank0;
target[0] = colours[(pixels >> 7) & 1];
target[1] = colours[(pixels >> 6) & 1];
target[2] = colours[(pixels >> 5) & 1];
target[3] = colours[(pixels >> 4) & 1];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
target[4] = colours[(pixels >> 3) & 1];
target[5] = colours[(pixels >> 2) & 1];
target[6] = colours[(pixels >> 1) & 1];
target[7] = colours[(pixels >> 0) & 1];
if constexpr (pixels_per_column(clock) == 8) {
return;
}
std::fill_n(&target[8], 8, colours[0]);
}
//
// 80-column.
//
template <DotClock clock, BitOrdering ordering>
struct PixelGenerator<PixelMode::Columns80, clock, ordering> {
static void output(
const uint8_t bank0,
const uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
target[0] = palette[(bank0 >> 7) & 1];
target[1] = palette[(bank1 >> 7) & 1];
target[2] = palette[(bank0 >> 6) & 1];
target[3] = palette[(bank1 >> 6) & 1];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
target[4] = palette[(bank0 >> 5) & 1];
target[5] = palette[(bank1 >> 5) & 1];
target[6] = palette[(bank0 >> 4) & 1];
target[7] = palette[(bank1 >> 4) & 1];
if constexpr (pixels_per_column(clock) == 8) {
return;
}
target[8] = palette[(bank0 >> 3) & 1];
target[9] = palette[(bank1 >> 3) & 1];
target[10] = palette[(bank0 >> 2) & 1];
target[11] = palette[(bank1 >> 2) & 1];
target[12] = palette[(bank0 >> 1) & 1];
target[13] = palette[(bank1 >> 1) & 1];
target[14] = palette[(bank0 >> 0) & 1];
target[15] = palette[(bank1 >> 0) & 1];
}
};
//
// 2bpp.
//
template <DotClock clock, BitOrdering ordering>
struct PixelGenerator<PixelMode::Bitmap4, clock, ordering> {
static void output(
const uint8_t bank0,
const uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
target[0] = palette[(bank0 >> 6) & 0x03];
target[1] = palette[(bank0 >> 4) & 0x03];
target[2] = palette[(bank0 >> 2) & 0x03];
target[3] = palette[(bank0 >> 0) & 0x03];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
target[4] = palette[(bank1 >> 6) & 0x03];
target[5] = palette[(bank1 >> 4) & 0x03];
target[6] = palette[(bank1 >> 2) & 0x03];
target[7] = palette[(bank1 >> 0) & 0x03];
if constexpr (pixels_per_column(clock) == 8) {
return;
}
std::fill_n(&target[8], 8, palette[0]);
}
};
//
// 4bpp.
//
template <DotClock clock, BitOrdering ordering>
struct PixelGenerator<PixelMode::Bitmap16, clock, ordering> {
static void output(
const uint8_t bank0,
const uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
target[0] = palette[(bank0 >> 4) & 0x0f];
target[1] = palette[(bank0 >> 0) & 0x0f];
target[2] = palette[(bank1 >> 4) & 0x0f];
target[3] = palette[(bank1 >> 0) & 0x0f];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
std::fill_n(&target[4], pixels_per_column(clock) - 4, palette[0]);
}
};
//
// Overprint.
//
template <DotClock clock, BitOrdering ordering>
struct PixelGenerator<PixelMode::Overprint, clock, ordering> {
static void output(
uint8_t bank0,
uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
const uint16_t colours[] = {
palette[8],
palette[10],
palette[9],
};
// Bank 0 appears on top of bank 1.
bank1 &= ~bank0;
uint32_t spread = uint32_t((bank0 << 16) | bank1); // -> 00000000 aaaaaaaa 00000000 bbbbbbbb
spread = (spread | (spread << 8)) & 0x0f0f0f0f; // -> 0000aaaa 0000aaaa 0000bbbb 0000bbbb
spread = (spread | (spread << 4)) & 0x33333333; // -> 00aa00aa 00aa00aa 00bb00bb 00bb00bb
spread = (spread | (spread << 2)) & 0x55555555; // -> 0a0a0a0a 0a0a0a0a 0b0b0b0b 0b0b0b0b
const uint16_t mix = uint16_t((spread >> 15) | spread); // -> abababab abababab
target[0] = colours[(mix >> 14) & 3];
target[1] = colours[(mix >> 12) & 3];
target[2] = colours[(mix >> 10) & 3];
target[3] = colours[(mix >> 8) & 3];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
target[0] = colours[(mix >> 6) & 3];
target[1] = colours[(mix >> 4) & 3];
target[2] = colours[(mix >> 2) & 3];
target[3] = colours[(mix >> 0) & 3];
if constexpr (pixels_per_column(clock) == 8) {
return;
}
std::fill_n(&target[8], 8, palette[8]);
}
};
//
// Triple overprint.
//
template <DotClock clock, BitOrdering ordering>
struct PixelGenerator<PixelMode::TripleOverprint, clock, ordering> {
static void output(
uint8_t bank0,
uint8_t bank1,
uint16_t *const target,
const std::array<uint16_t, 16> &palette
) {
// Stated rule per https://pulkomandy.tk/wiki/doku.php?id=documentations:devices:gate.arrays is as below:
//
// * Colors 0-7 all become color 0
// * Colors 12-15 all become color 12
// * Color 11 becomes color 10.
const uint16_t colours[16] = {
palette[0], palette[0], palette[0], palette[0], palette[0], palette[0], palette[0],
palette[8],
palette[9],
palette[10],
palette[10],
palette[12], palette[12], palette[12], palette[12]
};
target[0] = colours[(bank0 >> 4) & 0x0f];
target[1] = colours[(bank0 >> 0) & 0x0f];
target[2] = colours[(bank1 >> 4) & 0x0f];
target[3] = colours[(bank1 >> 0) & 0x0f];
if constexpr (pixels_per_column(clock) == 4) {
return;
}
std::fill_n(&target[4], pixels_per_column(clock) - 4, palette[0]);
}
};
}
Video::Video(const uint8_t *const pixels, const uint8_t *const attributes) :
pixels_(pixels), attributes_(attributes),
crt_(
Line::TotalCycles,
1,
Outputs::Display::Type::SECAM,
Outputs::Display::InputDataType::Red4Green4Blue4
) {
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
// Set default palette.
std::copy(std::begin(mo5_palette), std::end(mo5_palette), std::begin(mapped_palette_));
}
void Video::run_for(const Cycles cycles) {
position_.advance(
cycles.as<int>(),
[&] (const int line, const int begin, const int end) {
if(line >= Frame::VerticalSyncLine && line < Frame::VerticalSyncLine + Frame::VerticalSyncLength) {
vsync_line(begin, end);
} else if(line >= Frame::TotalPixelLines) {
border_line(begin, end);
} else {
#define Opt(x) case x: pixel_line<x>(begin, end); break;
#define Opt2(x) Opt(x + 0x00); Opt(x + 0x01);
#define Opt4(x) Opt2(x + 0x00); Opt2(x + 0x02);
#define Opt8(x) Opt4(x + 0x00); Opt4(x + 0x04);
#define Opt16(x) Opt8(x + 0x00); Opt8(x + 0x08);
#define Opt32(x) Opt16(x + 0x00); Opt16(x + 0x10);
#define Opt64(x) Opt32(x + 0x00); Opt32(x + 0x20);
#define Opt128(x) Opt64(x + 0x00); Opt64(x + 0x40);
switch(mode_ & 0x7f) {
Opt128(0);
}
#undef Opt
#undef Opt2
#undef Opt4
#undef Opt8
#undef Opt16
#undef Opt32
#undef Opt64
#undef Opt128
}
},
[&] {
source_address_ = 0;
}
);
}
uint8_t Video::vertical_state() const {
// This is 0xa7e7 in the MO6 documentation, i.e.
//
// b7: 0 = in upper or lower edge; 1 = in pixel zone (instantaneous)
// b6: latched version of b7 (but latched by what event?)
// b5: 0 = outside window; 1 = inside window (i.e. probably horizontal as well?)
static constexpr int StartPixelRegion = Line::EndOfLeftBorder;
static constexpr int EndPixelRegion = (Frame::TotalPixelLines - 1) * Line::TotalCycles + Line::EndOfPixels;
const bool b7 = position_.absolute() >= StartPixelRegion && position_.absolute() < EndPixelRegion;
const bool b5 = b7 && position_.subsegment() >= Line::EndOfLeftBorder && position_.subsegment() < Line::EndOfPixels;
return
0x01 | // Leave lower bit for population elsewhere
(b5 ? 0x20 : 0x00) |
(b7 ? 0x80 : 0x00);
}
uint8_t Video::horizontal_state() const {
// 0xa7e4 in MO6 terms, so:
//
// flying spot is:
// b7: [LT3] 0 on left-hand edge; 1 on right-hand edge.
// b6: [IMIL or possibly /INIL] 0 outside window; 1 inside window.
return 0x00;
}
void Video::vsync_line(const int, const int line_end) {
// TODO: resolve coupling here.
// Supplying sync as it comes to the CRT seems to trigger a fault in retrace start time.
if(line_end == Line::TotalCycles) {
crt_.output_sync(Line::TotalCycles); //line_end - line_begin);
}
}
void Video::border_line(const int line_begin, const int line_end) {
Numeric::clamp<0, Line::EndOfSync>(line_begin, line_end, [&](const int begin, const int end) {
crt_.output_sync(end - begin);
});
Numeric::clamp<Line::EndOfSync, Line::TotalCycles>(line_begin, line_end, [&](const int begin, const int end) {
crt_.output_level(end - begin, mapped_border_);
});
}
template <uint8_t mode>
void Video::pixel_line(const int line_begin, const int line_end) {
Numeric::clamp<0, Line::EndOfSync>(line_begin, line_end, [&](const int begin, const int end) {
crt_.output_sync(end - begin);
});
Numeric::clamp<Line::EndOfSync, Line::EndOfLeftBorder>(line_begin, line_end, [&](const int begin, const int end) {
crt_.output_level(end - begin, mapped_border_);
});
Numeric::clamp<Line::EndOfLeftBorder, Line::EndOfPixels>(line_begin, line_end, [&](const int begin, const int end) {
const auto flush_pixels = [&] {
crt_.output_data(Line::EndOfPixels - output_begin_column_, size_t(output_ - output_begin_));
output_ = output_begin_ = nullptr;
};
// Flush existing pixels if the pixel rate has changed.
static constexpr auto pixel_rate = pixels_per_column(mode);
if(output_ && pixel_rate != output_pixel_rate_) {
flush_pixels();
}
// Try to grab a buffer if without one.
if(!output_) {
output_ = output_begin_ = reinterpret_cast<uint16_t *>(crt_.begin_data(size_t(pixel_rate * 40)));
output_begin_column_ = begin;
output_pixel_rate_ = pixel_rate;
}
if(output_) {
for(int c = begin; c < end; c++) {
PixelGenerator<pixel_mode(mode), dot_clock(mode), bit_ordering(mode)>::output(
pixels_[source_address_],
attributes_[source_address_],
output_,
mapped_palette_
);
++source_address_;
output_ += pixel_rate;
}
if(end == Line::EndOfPixels) {
flush_pixels();
}
} else {
source_address_ += end - begin;
crt_.output_data(end - begin, 1);
}
});
Numeric::clamp<Line::EndOfPixels, Line::TotalCycles>(line_begin, line_end, [&](const int begin, const int end) {
crt_.output_level(end - begin, mapped_border_);
});
}
Cycles Video::next_sequence_point() const {
// Pulse the interrupt output for 8 cycles, arbitrarily. The real number seems to be undocumented, and it
// doesn't actually make much concrete difference in concrete terms because this is fed into edge-detection via
// the PIA. Knowing the real number would only fix the case where the detected transition is switched to
// trailing-edge during the pulse, which probably doesn't happen in any real software.
//
// Would be nice to know the real number though.
if(position_.absolute() < IRQ::Cycle) return IRQ::Cycle - position_.absolute();
if(position_.absolute() < IRQ::Cycle + IRQ::Length) return IRQ::Cycle + IRQ::Length - position_.absolute();
return Frame::TotalCycles - position_.absolute() + IRQ::Cycle;
}
bool Video::irq() const {
return position_.absolute() >= IRQ::Cycle && position_.absolute() < (IRQ::Cycle + IRQ::Length);
}
void Video::set_border_colour(const uint8_t colour) {
border_ = colour & 0xf;
update_mapped_border();
}
uint8_t Video::palette_index() const {
return palette_index_.get();
}
void Video::set_palette_index(const uint8_t index) {
palette_index_ = index;
}
uint8_t Video::palette() {
const auto result = palette_[palette_index_.get()];
++palette_index_;
return result;
}
void Video::set_palette(const uint8_t value) {
// Update internal store.
const auto index = palette_index_.get();
++palette_index_;
palette_[index] = value;
// Update output colour.
const auto rg = palette_[index & ~1];
const auto b = palette_[index | 1];
mapped_palette_[index >> 1] = rgb(uint16_t(
((rg & 0x0f) << 8) |
(rg & 0xf0) |
(b & 0x0f)
));
update_mapped_border();
}
void Video::set_mode(const uint8_t value) {
mode_ = value;
}