// // Video.hpp // Clock Signal // // Created by Thomas Harte on 20/03/2024. // Copyright © 2024 Thomas Harte. All rights reserved. // #pragma once #include "../../../Outputs/Log.hpp" #include "../../../Outputs/CRT/CRT.hpp" #include #include #include #include #include namespace Archimedes { template struct Video { Video(InterruptObserverT &interrupt_observer, ClockRateObserverT &clock_rate_observer, SoundT &sound, const uint8_t *ram) : interrupt_observer_(interrupt_observer), clock_rate_observer_(clock_rate_observer), sound_(sound), ram_(ram), crt_(Outputs::Display::InputDataType::Red4Green4Blue4) { set_clock_divider(3); crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f)); crt_.set_display_type(Outputs::Display::DisplayType::RGB); } static constexpr uint16_t colour(uint32_t value) { uint8_t packed[2]{}; packed[0] = value & 0xf; packed[1] = (value & 0xf0) | ((value & 0xf00) >> 8); #if TARGET_RT_BIG_ENDIAN return static_cast(packed[1] | (packed[0] << 8)); #else return static_cast(packed[0] | (packed[1] << 8)); #endif }; static constexpr uint16_t high_spread[] = { colour(0b0000'0000'0000), colour(0b0000'0000'1000), colour(0b0000'0100'0000), colour(0b0000'0100'1000), colour(0b0000'1000'0000), colour(0b0000'1000'1000), colour(0b0000'1100'0000), colour(0b0000'1100'1000), colour(0b1000'0000'0000), colour(0b1000'0000'1000), colour(0b1000'0100'0000), colour(0b1000'0100'1000), colour(0b1000'1000'0000), colour(0b1000'1000'1000), colour(0b1000'1100'0000), colour(0b1000'1100'1000), }; void write(uint32_t value) { const auto target = (value >> 24) & 0xfc; const auto timing_value = [](uint32_t value) -> uint32_t { return (value >> 14) & 0x3ff; }; switch(target) { case 0x00: case 0x04: case 0x08: case 0x0c: case 0x10: case 0x14: case 0x18: case 0x1c: case 0x20: case 0x24: case 0x28: case 0x2c: case 0x30: case 0x34: case 0x38: case 0x3c: colours_[target >> 2] = colour(value); break; case 0x40: border_colour_ = colour(value); break; case 0x44: case 0x48: case 0x4c: cursor_colours_[(target - 0x40) >> 2] = colour(value); break; case 0x80: horizontal_timing_.period = timing_value(value); break; case 0x84: horizontal_timing_.sync_width = timing_value(value); break; case 0x88: horizontal_timing_.border_start = timing_value(value); break; case 0x8c: horizontal_timing_.display_start = timing_value(value); break; case 0x90: horizontal_timing_.display_end = timing_value(value); break; case 0x94: horizontal_timing_.border_end = timing_value(value); break; case 0x98: horizontal_timing_.cursor_start = (value >> 13) & 0x7ff; cursor_shift_ = (value >> 11) & 3; break; case 0x9c: horizontal_timing_.interlace_sync_position = timing_value(value); break; case 0xa0: vertical_timing_.period = timing_value(value); break; case 0xa4: vertical_timing_.sync_width = timing_value(value); break; case 0xa8: vertical_timing_.border_start = timing_value(value); break; case 0xac: vertical_timing_.display_start = timing_value(value); break; case 0xb0: vertical_timing_.display_end = timing_value(value); break; case 0xb4: vertical_timing_.border_end = timing_value(value); break; case 0xb8: vertical_timing_.cursor_start = timing_value(value); break; case 0xbc: vertical_timing_.cursor_end = timing_value(value); break; case 0xe0: // Set pixel rate. This is the value that a 24Mhz clock should be divided // by to get half the pixel rate. switch(value & 0b11) { case 0b00: set_clock_divider(6); break; // i.e. pixel clock = 8Mhz. case 0b01: set_clock_divider(4); break; // 12Mhz. case 0b10: set_clock_divider(3); break; // 16Mhz. case 0b11: set_clock_divider(2); break; // 24Mhz. } // Set colour depth. colour_depth_ = Depth((value >> 2) & 0b11); // Crib interlace-enable. vertical_timing_.is_interlaced = value & (1 << 6); break; // // Sound parameters. // case 0x60: case 0x64: case 0x68: case 0x6c: case 0x70: case 0x74: case 0x78: case 0x7c: { const uint8_t channel = ((value >> 26) + 7) & 7; sound_.set_stereo_image(channel, value & 7); } break; case 0xc0: sound_.set_frequency(value & 0xff); break; default: logger.error().append("TODO: unrecognised VIDC write of %08x", value); break; } } void tick() { // Pick new horizontal state, possibly rolling over into the vertical. horizontal_state_.increment_position(horizontal_timing_); if(horizontal_state_.did_restart()) { end_horizontal(); const auto old_phase = vertical_state_.phase(); vertical_state_.increment_position(vertical_timing_); const auto phase = vertical_state_.phase(); if(phase != old_phase) { // Copy frame and cursor start addresses into counters at the // start of the first vertical display line. if(phase == Phase::Display) { address_ = frame_start_; cursor_address_ = cursor_start_; // Accumulate a count of how many times the processor has tried // to update the visible buffer more than once in a frame; this // will usually indicate that the software being run isn't properly // synchronised to actual machine speed. ++frame_starts_; if(frame_start_sets_ > 10) { overages_ += frame_start_sets_ > frame_starts_; frame_start_sets_ = frame_starts_ = 0; } } if(old_phase == Phase::Display) { entered_flyback_ = true; interrupt_observer_.update_interrupts(); } } // Determine which next 8 bytes will be the cursor image for this line. // Pragmatically, updating cursor_address_ once per line avoids probable // errors in getting it to appear appropriately over both pixels and border. if(vertical_state_.cursor_active) { uint8_t *cursor_pixel = cursor_image_.data(); for(int byte = 0; byte < 8; byte ++) { cursor_pixel[0] = (ram_[cursor_address_] >> 0) & 3; cursor_pixel[1] = (ram_[cursor_address_] >> 2) & 3; cursor_pixel[2] = (ram_[cursor_address_] >> 4) & 3; cursor_pixel[3] = (ram_[cursor_address_] >> 6) & 3; cursor_pixel += 4; ++cursor_address_; } } cursor_pixel_ = 32; } // Fetch if relevant display signals are active. if(vertical_state_.display_active() && horizontal_state_.display_active()) { const auto next_byte = [&]() { const auto next = ram_[address_]; ++address_; // `buffer_end_` is the final address that a 16-byte block will be fetched from; // the +16 here papers over the fact that I'm not accurately implementing DMA. if(address_ == buffer_end_ + 16) { address_ = buffer_start_; } bitmap_queue_[bitmap_queue_pointer_ & 7] = next; ++bitmap_queue_pointer_; }; switch(colour_depth_) { case Depth::EightBPP: next_byte(); next_byte(); break; case Depth::FourBPP: next_byte(); break; case Depth::TwoBPP: if(!(pixel_count_&3)) next_byte(); break; case Depth::OneBPP: if(!(pixel_count_&7)) next_byte(); break; } } // Move along line. switch(vertical_state_.phase()) { case Phase::Sync: tick_horizontal(); break; case Phase::Blank: tick_horizontal(); break; case Phase::Border: tick_horizontal(); break; case Phase::Display: tick_horizontal(); break; case Phase::StartInterlacedSync: tick_horizontal(); break; case Phase::EndInterlacedSync: tick_horizontal(); break; } ++time_in_phase_; } /// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise. bool interrupt() { // Guess: edge triggered? const bool interrupt = entered_flyback_; entered_flyback_ = false; return interrupt; } bool flyback_active() const { return vertical_state_.phase() != Phase::Display; } void set_frame_start(uint32_t address) { frame_start_ = address; ++frame_start_sets_; } void set_buffer_start(uint32_t address) { buffer_start_ = address; } void set_buffer_end(uint32_t address) { buffer_end_ = address; } void set_cursor_start(uint32_t address) { cursor_start_ = address; } Outputs::CRT::CRT &crt() { return crt_; } const Outputs::CRT::CRT &crt() const { return crt_; } int clock_divider() const { return static_cast(clock_divider_); } int frame_rate_overages() const { return overages_; } void set_dma_enabled(bool dma_enabled) { dma_enabled_ = dma_enabled; } // // The following is provided for input automation; // it does not correlate with real hardware functionality. // std::optional> cursor_location() { if( !dma_enabled_ || vertical_timing_.cursor_end <= vertical_timing_.cursor_start || horizontal_timing_.cursor_start >= (horizontal_timing_.period * 2) ) { return std::nullopt; } const auto horizontal_start = horizontal_timing_.display_start + horizontal_state_.output_latency(colour_depth_); return std::make_pair( int(horizontal_timing_.cursor_start) + 6 - int(horizontal_start * 2), int(vertical_timing_.cursor_start) - int(vertical_timing_.display_start)); } private: Log::Logger logger; InterruptObserverT &interrupt_observer_; ClockRateObserverT &clock_rate_observer_; SoundT &sound_; // In the current version of this code, video DMA occurrs costlessly, // being deferred to the component itself. const uint8_t *ram_ = nullptr; Outputs::CRT::CRT crt_; bool dma_enabled_ = false; // Horizontal and vertical timing. struct Timing { uint32_t period = 0; uint32_t sync_width = 0; uint32_t border_start = 0; uint32_t border_end = 0; uint32_t display_start = 0; uint32_t display_end = 0; uint32_t cursor_start = 0; uint32_t cursor_end = 0; uint32_t interlace_sync_position = 0; bool is_interlaced = false; }; uint32_t cursor_shift_ = 0; Timing horizontal_timing_, vertical_timing_; enum class Depth { OneBPP = 0b00, TwoBPP = 0b01, FourBPP = 0b10, EightBPP = 0b11, }; // Current video state. enum class Phase { Sync, Blank, Border, Display, StartInterlacedSync, EndInterlacedSync, }; template struct State { uint32_t position = 0; uint32_t display_start = 0; uint32_t display_end = 0; bool is_odd_iteration_ = false; void increment_position(const Timing &timing) { const auto previous_override = interlace_override_; if constexpr (is_vertical) { interlace_override_ = Phase::Sync; // i.e. no override. } if(position == timing.sync_width) { state |= SyncEnded; if(is_vertical && timing.is_interlaced && is_odd_iteration_ && previous_override == Phase::Sync) { --position; interlace_override_ = Phase::EndInterlacedSync; } } if(position == timing.display_start) { state |= DisplayStarted; display_start = position; } if(position == timing.display_end) { state |= DisplayEnded; display_end = position; } if(position == timing.border_start) state |= BorderStarted; if(position == timing.border_end) state |= BorderEnded; cursor_active |= position == timing.cursor_start; cursor_active &= position != timing.cursor_end; if(position == timing.period) { state = DidRestart; position = 0; is_odd_iteration_ ^= true; // Both display start and end need to be seeded as bigger than can be reached, // while having some overhead for addition. display_end = display_start = std::numeric_limits::max() >> 1; // Possibly label the next as a start-of-interlaced-sync. if(is_vertical && timing.is_interlaced && is_odd_iteration_) { interlace_override_ = Phase::StartInterlacedSync; } } else { ++position; if(position == 1024) position = 0; } } bool is_outputting(Depth depth) const { const auto latency = output_latency(depth); return position >= display_start + latency && position < display_end + latency; } uint32_t output_cycle(Depth depth) const { return position - display_start - output_latency(depth); } static constexpr uint32_t output_latencies[] = { 19 >> 1, // 1 bpp. 11 >> 1, // 2 bpp. 7 >> 1, // 4 bpp. 5 >> 1 // 8 bpp. }; uint32_t output_latency(Depth depth) const { return output_latencies[static_cast(depth)]; } static constexpr uint8_t SyncEnded = 0x1; static constexpr uint8_t BorderStarted = 0x2; static constexpr uint8_t BorderEnded = 0x4; static constexpr uint8_t DisplayStarted = 0x8; static constexpr uint8_t DisplayEnded = 0x10; static constexpr uint8_t DidRestart = 0x20; uint8_t state = 0; Phase interlace_override_ = Phase::Sync; bool cursor_active = false; bool did_restart() { const bool result = state & DidRestart; state &= ~DidRestart; return result; } bool display_active() const { return (state & DisplayStarted) && !(state & DisplayEnded); } Phase phase(Phase horizontal_fallback = Phase::Border) const { if(is_vertical && interlace_override_ != Phase::Sync) { return interlace_override_; } // TODO: turn the following logic into a lookup table. if(!(state & SyncEnded)) { return Phase::Sync; } if(!(state & BorderStarted) || (state & BorderEnded)) { return Phase::Blank; } if constexpr (!is_vertical) { return horizontal_fallback; } if(!(state & DisplayStarted) || (state & DisplayEnded)) { return Phase::Border; } return Phase::Display; } }; State horizontal_state_; State vertical_state_; int time_in_phase_ = 0; Phase phase_; uint16_t phased_border_colour_; int pixel_count_ = 0; int display_area_start_ = 0; uint16_t *pixels_ = nullptr; // It is elsewhere assumed that this size is a multiple of 8. static constexpr size_t PixelBufferSize = 256; // Programmer-set addresses. uint32_t buffer_start_ = 0; uint32_t buffer_end_ = 0; uint32_t frame_start_ = 0; uint32_t cursor_start_ = 0; int frame_start_sets_ = 0; int frame_starts_ = 0; int overages_ = 0; // Ephemeral address state. uint32_t address_ = 0; // Horizontal cursor output state. uint32_t cursor_address_ = 0; int cursor_pixel_ = 0; std::array cursor_image_; // Colour palette, converted to internal format. uint16_t border_colour_; std::array colours_{}; std::array cursor_colours_{}; // An interrupt flag; more closely related to the interface by which // my implementation of the IOC picks up an interrupt request than // to hardware. bool entered_flyback_ = false; // The divider that would need to be applied to a 24Mhz clock to // get half the current pixel clock; counting is in units of half // the pixel clock because that's the fidelity at which the programmer // places horizontal events — display start, end, sync period, etc. uint32_t clock_divider_ = 0; Depth colour_depth_; // A temporary buffer that holds video contents during the latency // period between their generation and their output. uint8_t bitmap_queue_[8]; int bitmap_queue_pointer_ = 0; void set_clock_divider(uint32_t divider) { if(divider == clock_divider_) { return; } clock_divider_ = divider; const auto cycles_per_line = static_cast(24'000'000 / (divider * 312 * 50)); crt_.set_new_timing( cycles_per_line, 312, /* Height of display. */ Outputs::CRT::PAL::ColourSpace, Outputs::CRT::PAL::ColourCycleNumerator, Outputs::CRT::PAL::ColourCycleDenominator, Outputs::CRT::PAL::VerticalSyncLength, Outputs::CRT::PAL::AlternatesPhase); clock_rate_observer_.update_clock_rates(); } void flush_pixels() { crt_.output_data(time_in_phase_, static_cast(pixel_count_)); time_in_phase_ = 0; pixel_count_ = 0; pixels_ = nullptr; } void set_phase(Phase phase) { if(time_in_phase_) { switch(phase_) { default: crt_.output_sync(time_in_phase_); break; case Phase::Blank: crt_.output_blank(time_in_phase_); break; case Phase::Border: crt_.output_level(time_in_phase_, phased_border_colour_); break; case Phase::Display: flush_pixels(); break; } } phase_ = phase; time_in_phase_ = 0; phased_border_colour_ = border_colour_; pixel_count_ = 0; } void end_horizontal() { set_phase(Phase::Sync); display_area_start_ = -1; bitmap_queue_pointer_ = 0; } template void tick_horizontal() { // Sync lines: obey nothing. All sync, all the time. if constexpr (vertical_phase == Phase::Sync) { return; } // Start interlaced sync lines: do blank from horizontal sync up to the programmed // cutoff, then do sync. if constexpr (vertical_phase == Phase::StartInterlacedSync) { if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) { set_phase(Phase::Blank); } if(phase_ == Phase::Blank && horizontal_state_.position == horizontal_timing_.interlace_sync_position) { set_phase(Phase::Sync); } return; } // End interlaced sync lines: do sync up to the programmed cutoff, then do blank. if constexpr (vertical_phase == Phase::EndInterlacedSync) { if(phase_ == Phase::Sync && horizontal_state_.position == horizontal_timing_.interlace_sync_position) { set_phase(Phase::Blank); } return; } // Blank lines: obey only the transition from sync to non-sync. if constexpr (vertical_phase == Phase::Blank) { if(phase_ == Phase::Sync && horizontal_state_.phase() != Phase::Sync) { set_phase(Phase::Blank); } return; } // Border lines: ignore display phases; also reset the border phase if the colour changes. if constexpr (vertical_phase == Phase::Border) { const auto phase = horizontal_state_.phase(Phase::Border); if(phase != phase_ || (phase_ == Phase::Border && border_colour_ != phased_border_colour_)) { set_phase(phase); } return; } if constexpr (vertical_phase != Phase::Display) { // Should be impossible. assert(false); } // Some timing facts, to explain what would otherwise be magic constants. static constexpr int CursorDelay = 5; // The cursor will appear six pixels after its programmed trigger point. // ... BUT! Border and display are currently a pixel early. So move the // cursor for alignment. // Deal with sync and blank via set_phase(); collapse display and border into Phase::Display. const auto phase = horizontal_state_.phase(Phase::Display); if(phase != phase_) set_phase(phase); // Update cursor pixel counter if applicable; this might mean triggering it // and it might just mean advancing it if it has already been triggered. cursor_pixel_ += 2; if(vertical_state_.cursor_active) { const auto pixel_position = horizontal_state_.position << 1; if(pixel_position <= horizontal_timing_.cursor_start && (pixel_position + 2) > horizontal_timing_.cursor_start) { cursor_pixel_ = int(horizontal_timing_.cursor_start) - int(pixel_position) - CursorDelay; } } // If this is not [collapsed] Phase::Display, just stop here. if(phase_ != Phase::Display) return; // Display phase: maintain an output buffer (if available). if(pixel_count_ == PixelBufferSize) flush_pixels(); if(!pixel_count_) pixels_ = reinterpret_cast(crt_.begin_data(PixelBufferSize)); // Output. if(pixels_) { // Paint the border colour for potential painting over. if(horizontal_state_.is_outputting(colour_depth_)) { const auto source = horizontal_state_.output_cycle(colour_depth_); // TODO: all below should be delayed an extra pixel. As should the border, actually. Fix up externally? switch(colour_depth_) { case Depth::EightBPP: { const uint8_t *bitmap = &bitmap_queue_[(source << 1) & 7]; pixels_[0] = (colours_[bitmap[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[0] >> 4]; pixels_[1] = (colours_[bitmap[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[bitmap[1] >> 4]; } break; case Depth::FourBPP: pixels_[0] = colours_[bitmap_queue_[source & 7] & 0xf]; pixels_[1] = colours_[bitmap_queue_[source & 7] >> 4]; break; case Depth::TwoBPP: { uint8_t &bitmap = bitmap_queue_[(source >> 1) & 7]; pixels_[0] = colours_[bitmap & 3]; pixels_[1] = colours_[(bitmap >> 2) & 3]; bitmap >>= 4; } break; case Depth::OneBPP: { uint8_t &bitmap = bitmap_queue_[(source >> 2) & 7]; pixels_[0] = colours_[bitmap & 1]; pixels_[1] = colours_[(bitmap >> 1) & 1]; bitmap >>= 2; } break; } } else { pixels_[0] = pixels_[1] = border_colour_; } // Overlay cursor if applicable. if(cursor_pixel_ < 32) { if(cursor_pixel_ >= 0) { const auto pixel = cursor_image_[static_cast(cursor_pixel_)]; if(pixel) { pixels_[0] = cursor_colours_[pixel]; } } if(cursor_pixel_ >= -1 && cursor_pixel_ < 31) { const auto pixel = cursor_image_[static_cast(cursor_pixel_ + 1)]; if(pixel) { pixels_[1] = cursor_colours_[pixel]; } } } pixels_ += 2; } pixel_count_ += 2; } }; }