diff --git a/Machines/Acorn/Archimedes/Video.hpp b/Machines/Acorn/Archimedes/Video.hpp index b14f5b544..cca4420df 100644 --- a/Machines/Acorn/Archimedes/Video.hpp +++ b/Machines/Acorn/Archimedes/Video.hpp @@ -12,6 +12,7 @@ #include "../../../Outputs/CRT/CRT.hpp" #include +#include #include #include @@ -26,7 +27,7 @@ struct Video { ram_(ram), crt_(Outputs::Display::InputDataType::Red4Green4Blue4) { set_clock_divider(3); - crt_.set_visible_area(Outputs::Display::Rect(0.06f, 0.07f, 0.9f, 0.9f)); + crt_.set_visible_area(Outputs::Display::Rect(0.041f, 0.04f, 0.95f, 0.95f)); crt_.set_display_type(Outputs::Display::DisplayType::RGB); } @@ -92,8 +93,6 @@ struct Video { case 0xbc: vertical_timing_.cursor_end = timing_value(value); break; case 0xe0: - logger.error().append("TODO: video control: %08x", value); - // Set pixel rate. This is the value that a 24Mhz clock should be divided // by to get half the pixel rate. switch(value & 0b11) { @@ -131,15 +130,15 @@ struct Video { 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_); - pixel_count_ = 0; const auto phase = vertical_state_.phase(); if(phase != old_phase) { - // I don't have good information on this; first guess: copy frame and - // cursor start addresses into counters at the start of the first vertical - // display line. + // 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_; @@ -167,57 +166,9 @@ struct Video { cursor_pixel_ = 32; } - // Accumulate total phase. - ++time_in_phase_; - - // Determine current output phase. - Phase new_phase; - switch(vertical_state_.phase()) { - case Phase::Sync: new_phase = Phase::Sync; break; - case Phase::Blank: new_phase = Phase::Blank; break; - case Phase::Border: - new_phase = horizontal_state_.phase() == Phase::Display ? Phase::Border : horizontal_state_.phase(); - break; - case Phase::Display: - new_phase = horizontal_state_.phase(); - break; - } - - const auto flush_pixels = [&]() { - const auto duration = static_cast(time_in_phase_); - crt_.output_data(duration, static_cast(time_in_phase_) * 2); - time_in_phase_ = 0; - pixels_ = nullptr; - }; - - // Possibly output something. - if(new_phase != phase_) { - if(time_in_phase_) { - const auto duration = static_cast(time_in_phase_); - - switch(phase_) { - case Phase::Sync: crt_.output_sync(duration); break; - case Phase::Blank: crt_.output_blank(duration); break; - case Phase::Display: flush_pixels(); break; - case Phase::Border: crt_.output_level(duration, border_colour_); break; - } - time_in_phase_ = 0; - } - phase_ = new_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. - 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); - } - } - - // Grab some more pixels if appropriate. + // Fetch if relevant display signals are active. if(vertical_state_.display_active() && horizontal_state_.display_active()) { - const auto next_byte = [&]() -> uint8_t { + const auto next_byte = [&]() { const auto next = ram_[address_]; ++address_; @@ -226,98 +177,26 @@ struct Video { if(address_ == buffer_end_ + 16) { address_ = buffer_start_; } - return next; + bitmap_queue_[bitmap_queue_pointer_ & 7] = next; + ++bitmap_queue_pointer_; }; switch(colour_depth_) { - case Depth::EightBPP: - pixel_data_[0] = next_byte(); - pixel_data_[1] = next_byte(); - break; - case Depth::FourBPP: - pixel_data_[0] = next_byte(); - break; - case Depth::TwoBPP: - if(!(pixel_count_&1)) { - pixel_data_[0] = next_byte(); - } - break; - case Depth::OneBPP: - if(!(pixel_count_&3)) { - pixel_data_[0] = next_byte(); - } - break; - } - ++pixel_count_; - } - - if(phase_ == Phase::Display) { - if(pixels_ && time_in_phase_ == PixelBufferSize/2) { - flush_pixels(); - } - - if(!pixels_) { - if(time_in_phase_) { - flush_pixels(); - } - - pixels_ = reinterpret_cast(crt_.begin_data(PixelBufferSize)); - } - - if(pixels_) { - // Each tick in here is two ticks of the pixel clock, so: - // - // 8bpp mode: output two bytes; - // 4bpp mode: output one byte; - // 2bpp mode: output one byte every second tick; - // 1bpp mode: output one byte every fourth tick. - switch(colour_depth_) { - case Depth::EightBPP: - pixels_[0] = (colours_[pixel_data_[0] & 0xf] & colour(0b0111'0011'0111)) | high_spread[pixel_data_[0] >> 4]; - pixels_[1] = (colours_[pixel_data_[1] & 0xf] & colour(0b0111'0011'0111)) | high_spread[pixel_data_[1] >> 4]; - break; - - case Depth::FourBPP: - pixels_[0] = colours_[pixel_data_[0] & 0xf]; - pixels_[1] = colours_[pixel_data_[0] >> 4]; - break; - - case Depth::TwoBPP: - pixels_[0] = colours_[pixel_data_[0] & 3]; - pixels_[1] = colours_[(pixel_data_[0] >> 2) & 3]; - pixel_data_[0] >>= 4; - break; - - case Depth::OneBPP: - pixels_[0] = colours_[pixel_data_[0] & 1]; - pixels_[1] = colours_[(pixel_data_[0] >> 1) & 1]; - pixel_data_[0] >>= 2; - break; - } - - // Overlay cursor if applicable. - // TODO: pull this so far out that the cursor can display over the border, too. - 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_ < 31) { - const auto pixel = cursor_image_[static_cast(cursor_pixel_ + 1)]; - if(pixel) { - pixels_[1] = cursor_colours_[pixel]; - } - } - } - - pixels_ += 2; + 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; } } - // Advance cursor position. - if(cursor_pixel_ < 32) cursor_pixel_ += 2; + // 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; + } + ++time_in_phase_; } /// @returns @c true if a vertical retrace interrupt has been signalled since the last call to @c interrupt(); @c false otherwise. @@ -369,32 +248,61 @@ private: 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, }; + template struct State { uint32_t position = 0; + uint32_t display_start = 0; + uint32_t display_end = 0; void increment_position(const Timing &timing) { - ++position; - if(position == 1024) position = 0; - - if(position == timing.period) { - state = DidRestart; - position = 0; - } - if(position == timing.sync_width) state |= SyncEnded; - if(position == timing.display_start) state |= DisplayStarted; - if(position == timing.display_end) state |= DisplayEnded; + 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; + + // 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; + } else { + ++position; + if(position == 1024) position = 0; + } } + bool is_outputting(Depth depth) const { + return position >= display_start + output_latencies[static_cast(depth)] && position < display_end + output_latencies[static_cast(depth)]; + } + + uint32_t output_cycle(Depth depth) const { + return position - display_start - output_latencies[static_cast(depth)]; + } + + static constexpr uint32_t output_latencies[] = { + 19 >> 1, // 1 bpp. + 11 >> 1, // 2 bpp. + 7 >> 1, // 4 bpp. + 5 >> 1 // 8 bpp. + }; + static constexpr uint8_t SyncEnded = 0x1; static constexpr uint8_t BorderStarted = 0x2; static constexpr uint8_t BorderEnded = 0x4; @@ -415,28 +323,37 @@ private: return (state & DisplayStarted) && !(state & DisplayEnded); } - Phase phase() const { - // TODO: turn the following logic into a 32-entry lookup table. + Phase phase(Phase horizontal_fallback = Phase::Border) const { + // 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_, vertical_state_; - Phase phase_ = Phase::Sync; - uint32_t time_in_phase_ = 0; - uint32_t pixel_count_ = 0; + 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 = 320; + static constexpr size_t PixelBufferSize = 256; // Programmer-set addresses. uint32_t buffer_start_ = 0; @@ -452,9 +369,6 @@ private: int cursor_pixel_ = 0; std::array cursor_image_; - // Ephemeral graphics data. - uint8_t pixel_data_[2]{}; - // Colour palette, converted to internal format. uint16_t border_colour_; std::array colours_{}; @@ -470,13 +384,12 @@ private: // 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_; - enum class Depth { - OneBPP = 0b00, - TwoBPP = 0b01, - FourBPP = 0b10, - EightBPP = 0b11, - } 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_) { @@ -495,6 +408,149 @@ private: 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_) { + case Phase::Sync: 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; + } + + // 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; + } }; }