// // Video.cpp // Clock Signal // // Created by Thomas Harte on 20/03/2026. // Copyright © 2026 Thomas Harte. All rights reserved. // #include "Video.hpp" #include #include 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 struct PixelGenerator { static void output( uint8_t bank0, uint8_t bank1, uint16_t *target, const std::array &palette ); }; // // 40-column, and Pages 1 and 2. // template void PixelGenerator::output( const uint8_t bank0, const uint8_t bank1, uint16_t *const target, const std::array &palette ) { const std::array colours = [&] () -> std::array { 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 struct PixelGenerator { static void output( const uint8_t bank0, const uint8_t bank1, uint16_t *const target, const std::array &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 struct PixelGenerator { static void output( const uint8_t bank0, const uint8_t bank1, uint16_t *const target, const std::array &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 struct PixelGenerator { static void output( const uint8_t bank0, const uint8_t bank1, uint16_t *const target, const std::array &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 struct PixelGenerator { static void output( uint8_t bank0, uint8_t bank1, uint16_t *const target, const std::array &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 struct PixelGenerator { static void output( uint8_t bank0, uint8_t bank1, uint16_t *const target, const std::array &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(), [&] (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(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_begin, line_end, [&](const int begin, const int end) { crt_.output_level(end - begin, mapped_border_); }); } template 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_begin, line_end, [&](const int begin, const int end) { crt_.output_level(end - begin, mapped_border_); }); Numeric::clamp(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(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::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_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; }