From aab2dd68b68cd91ff359412652345df65ad31d47 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 20:18:29 -0400 Subject: [PATCH] Substitute in a real-time video generator. --- Machines/Acorn/Electron/Electron.cpp | 88 ++-- Machines/Acorn/Electron/Video.cpp | 718 +++++++++++---------------- Machines/Acorn/Electron/Video.hpp | 153 +++--- 3 files changed, 420 insertions(+), 539 deletions(-) diff --git a/Machines/Acorn/Electron/Electron.cpp b/Machines/Acorn/Electron/Electron.cpp index eb69eced0..e94f2b306 100644 --- a/Machines/Acorn/Electron/Electron.cpp +++ b/Machines/Acorn/Electron/Electron.cpp @@ -213,21 +213,48 @@ template class ConcreteMachine: } forceinline Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) { - unsigned int cycles = 1; + Cycles cycles{1}; + + if(address < 0x8000) { + cycles = video_.ram_delay(); + } else { + if((address & 0xff00) == 0xfe00) { + cycles = video_.io_delay(); + } + } + + if(const auto video_interrupts = video_.run_for(cycles); video_interrupts) { + signal_interrupt(video_interrupts); + } + + cycles_since_audio_update_ += cycles; + if(cycles_since_audio_update_ > Cycles(16384)) update_audio(); + tape_.run_for(cycles); + + if(typer_) typer_->run_for(cycles); + if(plus3_) plus3_->run_for(cycles * 4); + if(shift_restart_counter_) { + shift_restart_counter_ -= cycles.as(); + if(shift_restart_counter_ <= 0) { + shift_restart_counter_ = 0; + m6502_.set_power_on(true); + set_key_state(KeyShift, true); + is_holding_shift_ = true; + } + } + + if constexpr (has_scsi_bus) { + if(scsi_is_clocked_) { + scsi_bus_.run_for(cycles); + } + } if(address < 0x8000) { if(isReadOperation(operation)) { *value = ram_[address]; } else { - if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) { - video_.flush(); - } ram_[address] = *value; } - - // For the entire frame, RAM is accessible only on odd cycles; in modes below 4 - // it's also accessible only outside of the pixel regions. - cycles += video_.last_valid()->get_cycles_until_next_ram_availability(video_.time_since_flush().template as() + 1); } else { switch(address & 0xff0f) { case 0xfe00: @@ -265,8 +292,7 @@ template class ConcreteMachine: case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b: case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f: if(!isReadOperation(operation)) { - video_->write(address, *value); - video_access_range_ = video_.last_valid()->get_memory_access_range(); + video_.write(address, *value); } break; case 0xfe04: @@ -472,39 +498,10 @@ template class ConcreteMachine: } } - if(video_ += Cycles(int(cycles))) { - signal_interrupt(video_.last_valid()->get_interrupts()); - } - - cycles_since_audio_update_ += Cycles(int(cycles)); - if(cycles_since_audio_update_ > Cycles(16384)) update_audio(); - tape_.run_for(Cycles(int(cycles))); - - if(typer_) typer_->run_for(Cycles(int(cycles))); - if(plus3_) plus3_->run_for(Cycles(4*int(cycles))); - if(shift_restart_counter_) { - shift_restart_counter_ -= cycles; - if(shift_restart_counter_ <= 0) { - shift_restart_counter_ = 0; - m6502_.set_power_on(true); - set_key_state(KeyShift, true); - is_holding_shift_ = true; - } - } - - if constexpr (has_scsi_bus) { - if(scsi_is_clocked_) { - scsi_bus_.run_for(Cycles(int(cycles))); - } - } - - return Cycles(int(cycles)); + return cycles; } void flush_output(int outputs) final { - if(outputs & Output::Video) { - video_.flush(); - } if(outputs & Output::Audio) { update_audio(); audio_queue_.perform(); @@ -512,19 +509,19 @@ template class ConcreteMachine: } void set_scan_target(Outputs::Display::ScanTarget *scan_target) final { - video_.last_valid()->set_scan_target(scan_target); + video_.set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const final { - return video_.last_valid()->get_scaled_scan_status(); + return video_.get_scaled_scan_status(); } void set_display_type(Outputs::Display::DisplayType display_type) final { - video_.last_valid()->set_display_type(display_type); + video_.set_display_type(display_type); } Outputs::Display::DisplayType get_display_type() const final { - return video_.last_valid()->get_display_type(); + return video_.get_display_type(); } Outputs::Speaker::Speaker *get_speaker() final { @@ -735,7 +732,6 @@ template class ConcreteMachine: // Counters related to simultaneous subsystems Cycles cycles_since_audio_update_ = 0; - VideoOutput::Range video_access_range_ = {0, 0xffff}; // Tape Tape tape_; @@ -771,7 +767,7 @@ template class ConcreteMachine: } // Outputs - JustInTimeActor video_; + VideoOutput video_; Concurrency::AsyncTaskQueue audio_queue_; SoundGenerator sound_generator_; diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 71ba7dc2c..14a8c4cae 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -38,15 +38,13 @@ namespace { // MARK: - Lifecycle -VideoOutput::VideoOutput(uint8_t *memory) : +VideoOutput::VideoOutput(const uint8_t *memory) : ram_(memory), crt_(crt_cycles_per_line, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1) { memset(palette_, 0xf, sizeof(palette_)); - setup_screen_map(); - setup_base_address(); // TODO: as implied below, I've introduced a clock's latency into the graphics pipeline somehow. Investigate. crt_.set_visible_area(crt_.get_rect_for_area(first_graphics_line - 1, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f)); @@ -70,215 +68,218 @@ Outputs::Display::DisplayType VideoOutput::get_display_type() const { // MARK: - Display update methods -void VideoOutput::start_pixel_line() { - current_pixel_line_ = (current_pixel_line_+1)&255; - if(!current_pixel_line_) { - start_line_address_ = start_screen_address_; - current_character_row_ = 0; - is_blank_line_ = false; - } else { - bool mode_has_blank_lines = (screen_mode_ == 6) || (screen_mode_ == 3); - is_blank_line_ = (mode_has_blank_lines && ((current_character_row_ > 7 && current_character_row_ < 10) || (current_pixel_line_ > 249))); +//void VideoOutput::start_pixel_line() { +// current_pixel_line_ = (current_pixel_line_+1)&255; +// if(!current_pixel_line_) { +// start_line_address_ = start_screen_address_; +// current_character_row_ = 0; +// is_blank_line_ = false; +// } else { +// bool mode_has_blank_lines = (screen_mode_ == 6) || (screen_mode_ == 3); +// is_blank_line_ = (mode_has_blank_lines && ((current_character_row_ > 7 && current_character_row_ < 10) || (current_pixel_line_ > 249))); +// +// if(!is_blank_line_) { +// start_line_address_++; +// +// if(current_character_row_ > 7) { +// start_line_address_ += ((screen_mode_ < 4) ? 80 : 40) * 8 - 8; +// current_character_row_ = 0; +// } +// } +// } +// current_screen_address_ = start_line_address_; +// current_pixel_column_ = 0; +// initial_output_target_ = current_output_target_ = nullptr; +//} +// +//void VideoOutput::end_pixel_line() { +// const int data_length = int(current_output_target_ - initial_output_target_); +// if(data_length) { +// crt_.output_data(data_length * current_output_divider_, size_t(data_length)); +// } +// current_character_row_++; +//} +// +//void VideoOutput::output_pixels(int number_of_cycles) { +// if(!number_of_cycles) return; +// +// if(is_blank_line_) { +// crt_.output_blank(number_of_cycles * crt_cycles_multiplier); +// } else { +// int divider = 1; +// switch(screen_mode_) { +// case 0: case 3: divider = 1; break; +// case 1: case 4: case 6: divider = 2; break; +// case 2: case 5: divider = 4; break; +// } +// +// if(!initial_output_target_ || divider != current_output_divider_) { +// const int data_length = int(current_output_target_ - initial_output_target_); +// if(data_length) { +// crt_.output_data(data_length * current_output_divider_, size_t(data_length)); +// } +// current_output_divider_ = divider; +// initial_output_target_ = current_output_target_ = crt_.begin_data(size_t(640 / current_output_divider_), size_t(8 / divider)); +// } +// +//#define get_pixel() \ +// if(current_screen_address_&32768) {\ +// current_screen_address_ = (screen_mode_base_address_ + current_screen_address_)&32767;\ +// }\ +// last_pixel_byte_ = ram_[current_screen_address_];\ +// current_screen_address_ = current_screen_address_+8 +// +// switch(screen_mode_) { +// case 0: case 3: +// if(initial_output_target_) { +// while(number_of_cycles--) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.eighty1bpp[last_pixel_byte_]; +// current_output_target_ += 8; +// current_pixel_column_++; +// } +// } else current_output_target_ += 8*number_of_cycles; +// break; +// +// case 1: +// if(initial_output_target_) { +// while(number_of_cycles--) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.eighty2bpp[last_pixel_byte_]; +// current_output_target_ += 4; +// current_pixel_column_++; +// } +// } else current_output_target_ += 4*number_of_cycles; +// break; +// +// case 2: +// if(initial_output_target_) { +// while(number_of_cycles--) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.eighty4bpp[last_pixel_byte_]; +// current_output_target_ += 2; +// current_pixel_column_++; +// } +// } else current_output_target_ += 2*number_of_cycles; +// break; +// +// case 4: case 6: +// if(initial_output_target_) { +// if(current_pixel_column_&1) { +// last_pixel_byte_ <<= 4; +// *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; +// current_output_target_ += 4; +// +// number_of_cycles--; +// current_pixel_column_++; +// } +// while(number_of_cycles > 1) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; +// current_output_target_ += 4; +// +// last_pixel_byte_ <<= 4; +// *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; +// current_output_target_ += 4; +// +// number_of_cycles -= 2; +// current_pixel_column_+=2; +// } +// if(number_of_cycles) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; +// current_output_target_ += 4; +// current_pixel_column_++; +// } +// } else current_output_target_ += 4 * number_of_cycles; +// break; +// +// case 5: +// if(initial_output_target_) { +// if(current_pixel_column_&1) { +// last_pixel_byte_ <<= 2; +// *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; +// current_output_target_ += 2; +// +// number_of_cycles--; +// current_pixel_column_++; +// } +// while(number_of_cycles > 1) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; +// current_output_target_ += 2; +// +// last_pixel_byte_ <<= 2; +// *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; +// current_output_target_ += 2; +// +// number_of_cycles -= 2; +// current_pixel_column_+=2; +// } +// if(number_of_cycles) { +// get_pixel(); +// *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; +// current_output_target_ += 2; +// current_pixel_column_++; +// } +// } else current_output_target_ += 2*number_of_cycles; +// break; +// } +// +//#undef get_pixel +// } +//} - if(!is_blank_line_) { - start_line_address_++; +Electron::Interrupt VideoOutput::run_for(const Cycles cycles) { + Electron::Interrupt interrupts{}; +// int number_of_cycles = int(cycles.as_integral()); +// const auto start_position = output_position_; +// output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame; +// +// const auto test_range = [&](int start, int end) { +// if( +// (start < real_time_clock_interrupt_1 && end >= real_time_clock_interrupt_1) || +// (start < real_time_clock_interrupt_2 && end >= real_time_clock_interrupt_2) +// ) { +// interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock); +// } +// +// if( +// (start < display_end_interrupt_1 && end >= display_end_interrupt_1) || +// (start < display_end_interrupt_2 && end >= display_end_interrupt_2) +// ) { +// interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd); +// } +// }; +// +// if(output_position_ >= start_position) { +// test_range(start_position, output_position_); +// } else { +// test_range(start_position, cycles_per_frame); +// test_range(0, output_position_); +// } +// +// while(number_of_cycles) { +// int draw_action_length = screen_map_[screen_map_pointer_].length; +// int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_); +// if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(time_left_in_action); +// +// number_of_cycles -= time_left_in_action; +// cycles_into_draw_action_ += time_left_in_action; +// if(cycles_into_draw_action_ == draw_action_length) { +// switch(screen_map_[screen_map_pointer_].type) { +// case DrawAction::Sync: crt_.output_sync(draw_action_length * crt_cycles_multiplier); break; +// case DrawAction::ColourBurst: crt_.output_default_colour_burst(draw_action_length * crt_cycles_multiplier); break; +// case DrawAction::Blank: crt_.output_blank(draw_action_length * crt_cycles_multiplier); break; +// case DrawAction::Pixels: end_pixel_line(); break; +// } +// screen_map_pointer_ = (screen_map_pointer_ + 1) % screen_map_.size(); +// cycles_into_draw_action_ = 0; +// if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) start_pixel_line(); +// } +// } - if(current_character_row_ > 7) { - start_line_address_ += ((screen_mode_ < 4) ? 80 : 40) * 8 - 8; - current_character_row_ = 0; - } - } - } - current_screen_address_ = start_line_address_; - current_pixel_column_ = 0; - initial_output_target_ = current_output_target_ = nullptr; -} - -void VideoOutput::end_pixel_line() { - const int data_length = int(current_output_target_ - initial_output_target_); - if(data_length) { - crt_.output_data(data_length * current_output_divider_, size_t(data_length)); - } - current_character_row_++; -} - -void VideoOutput::output_pixels(int number_of_cycles) { - if(!number_of_cycles) return; - - if(is_blank_line_) { - crt_.output_blank(number_of_cycles * crt_cycles_multiplier); - } else { - int divider = 1; - switch(screen_mode_) { - case 0: case 3: divider = 1; break; - case 1: case 4: case 6: divider = 2; break; - case 2: case 5: divider = 4; break; - } - - if(!initial_output_target_ || divider != current_output_divider_) { - const int data_length = int(current_output_target_ - initial_output_target_); - if(data_length) { - crt_.output_data(data_length * current_output_divider_, size_t(data_length)); - } - current_output_divider_ = divider; - initial_output_target_ = current_output_target_ = crt_.begin_data(size_t(640 / current_output_divider_), size_t(8 / divider)); - } - -#define get_pixel() \ - if(current_screen_address_&32768) {\ - current_screen_address_ = (screen_mode_base_address_ + current_screen_address_)&32767;\ - }\ - last_pixel_byte_ = ram_[current_screen_address_];\ - current_screen_address_ = current_screen_address_+8 - - switch(screen_mode_) { - case 0: case 3: - if(initial_output_target_) { - while(number_of_cycles--) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.eighty1bpp[last_pixel_byte_]; - current_output_target_ += 8; - current_pixel_column_++; - } - } else current_output_target_ += 8*number_of_cycles; - break; - - case 1: - if(initial_output_target_) { - while(number_of_cycles--) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.eighty2bpp[last_pixel_byte_]; - current_output_target_ += 4; - current_pixel_column_++; - } - } else current_output_target_ += 4*number_of_cycles; - break; - - case 2: - if(initial_output_target_) { - while(number_of_cycles--) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.eighty4bpp[last_pixel_byte_]; - current_output_target_ += 2; - current_pixel_column_++; - } - } else current_output_target_ += 2*number_of_cycles; - break; - - case 4: case 6: - if(initial_output_target_) { - if(current_pixel_column_&1) { - last_pixel_byte_ <<= 4; - *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 4; - - number_of_cycles--; - current_pixel_column_++; - } - while(number_of_cycles > 1) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 4; - - last_pixel_byte_ <<= 4; - *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 4; - - number_of_cycles -= 2; - current_pixel_column_+=2; - } - if(number_of_cycles) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 4; - current_pixel_column_++; - } - } else current_output_target_ += 4 * number_of_cycles; - break; - - case 5: - if(initial_output_target_) { - if(current_pixel_column_&1) { - last_pixel_byte_ <<= 2; - *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 2; - - number_of_cycles--; - current_pixel_column_++; - } - while(number_of_cycles > 1) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 2; - - last_pixel_byte_ <<= 2; - *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 2; - - number_of_cycles -= 2; - current_pixel_column_+=2; - } - if(number_of_cycles) { - get_pixel(); - *reinterpret_cast(current_output_target_) = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 2; - current_pixel_column_++; - } - } else current_output_target_ += 2*number_of_cycles; - break; - } - -#undef get_pixel - } -} - -void VideoOutput::run_for(const Cycles cycles) { - int number_of_cycles = int(cycles.as_integral()); - const auto start_position = output_position_; - output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame; - - const auto test_range = [&](int start, int end) { - if( - (start < real_time_clock_interrupt_1 && end >= real_time_clock_interrupt_1) || - (start < real_time_clock_interrupt_2 && end >= real_time_clock_interrupt_2) - ) { - interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::RealTimeClock); - } - - if( - (start < display_end_interrupt_1 && end >= display_end_interrupt_1) || - (start < display_end_interrupt_2 && end >= display_end_interrupt_2) - ) { - interrupts_ = Electron::Interrupt(interrupts_ | Electron::Interrupt::DisplayEnd); - } - }; - - if(output_position_ >= start_position) { - test_range(start_position, output_position_); - } else { - test_range(start_position, cycles_per_frame); - test_range(0, output_position_); - } - - while(number_of_cycles) { - int draw_action_length = screen_map_[screen_map_pointer_].length; - int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_); - if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(time_left_in_action); - - number_of_cycles -= time_left_in_action; - cycles_into_draw_action_ += time_left_in_action; - if(cycles_into_draw_action_ == draw_action_length) { - switch(screen_map_[screen_map_pointer_].type) { - case DrawAction::Sync: crt_.output_sync(draw_action_length * crt_cycles_multiplier); break; - case DrawAction::ColourBurst: crt_.output_default_colour_burst(draw_action_length * crt_cycles_multiplier); break; - case DrawAction::Blank: crt_.output_blank(draw_action_length * crt_cycles_multiplier); break; - case DrawAction::Pixels: end_pixel_line(); break; - } - screen_map_pointer_ = (screen_map_pointer_ + 1) % screen_map_.size(); - cycles_into_draw_action_ = 0; - if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) start_pixel_line(); - } - } + return interrupts; } // MARK: - Register hub @@ -286,226 +287,97 @@ void VideoOutput::run_for(const Cycles cycles) { void VideoOutput::write(int address, uint8_t value) { switch(address & 0xf) { case 0x02: - start_screen_address_ = (start_screen_address_ & 0xfe00) | uint16_t((value & 0xe0) << 1); - if(!start_screen_address_) start_screen_address_ |= 0x8000; + screen_base = + (screen_base & 0b0111'1110'0000'0000) | + ((value << 1) & 0b0000'0001'1100'0000); break; case 0x03: - start_screen_address_ = (start_screen_address_ & 0x01ff) | uint16_t((value & 0x3f) << 9); - if(!start_screen_address_) start_screen_address_ |= 0x8000; + screen_base = + (screen_base & 0b0111'1110'0000'0000) | + ((value << 1) & 0b0000'0001'1100'0000); break; case 0x07: { - // update screen mode - uint8_t new_screen_mode = (value >> 3)&7; - if(new_screen_mode == 7) new_screen_mode = 4; - if(new_screen_mode != screen_mode_) { - screen_mode_ = new_screen_mode; - setup_base_address(); + uint8_t mode = (value >> 3)&7; + mode_40 = mode >= 4; + mode_text = mode == 3 || mode == 6; + + switch(mode) { + case 0: + case 1: + case 2: mode_base = 0x3000; break; + case 3: mode_base = 0x4000; break; + case 6: mode_base = 0x6000; break; + default: mode_base = 0x5800; break; } - } - break; + + switch(mode) { + default: mode_bpp = Bpp::One; break; + case 1: + case 5: mode_bpp = Bpp::Two; break; + case 2: mode_bpp = Bpp::Four; break; + } + } break; case 0x08: case 0x09: case 0x0a: case 0x0b: case 0x0c: case 0x0d: case 0x0e: case 0x0f: { - constexpr int registers[4][4] = { - {10, 8, 2, 0}, - {14, 12, 6, 4}, - {15, 13, 7, 5}, - {11, 9, 3, 1}, - }; - const int index = (address >> 1)&3; - const uint8_t colour = ~value; - if(address&1) { - palette_[registers[index][0]] = (palette_[registers[index][0]]&3) | ((colour >> 1)&4); - palette_[registers[index][1]] = (palette_[registers[index][1]]&3) | ((colour >> 0)&4); - palette_[registers[index][2]] = (palette_[registers[index][2]]&3) | ((colour << 1)&4); - palette_[registers[index][3]] = (palette_[registers[index][3]]&3) | ((colour << 2)&4); - - palette_[registers[index][2]] = (palette_[registers[index][2]]&5) | ((colour >> 4)&2); - palette_[registers[index][3]] = (palette_[registers[index][3]]&5) | ((colour >> 3)&2); - } else { - palette_[registers[index][0]] = (palette_[registers[index][0]]&6) | ((colour >> 7)&1); - palette_[registers[index][1]] = (palette_[registers[index][1]]&6) | ((colour >> 6)&1); - palette_[registers[index][2]] = (palette_[registers[index][2]]&6) | ((colour >> 5)&1); - palette_[registers[index][3]] = (palette_[registers[index][3]]&6) | ((colour >> 4)&1); - - palette_[registers[index][0]] = (palette_[registers[index][0]]&5) | ((colour >> 2)&2); - palette_[registers[index][1]] = (palette_[registers[index][1]]&5) | ((colour >> 1)&2); - } - - // regenerate all palette tables for now - for(int byte = 0; byte < 256; byte++) { - uint8_t *target = reinterpret_cast(&palette_tables_.forty1bpp[byte]); - target[0] = palette_[(byte&0x80) >> 4]; - target[1] = palette_[(byte&0x40) >> 3]; - target[2] = palette_[(byte&0x20) >> 2]; - target[3] = palette_[(byte&0x10) >> 1]; - - target = reinterpret_cast(&palette_tables_.eighty2bpp[byte]); - target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)]; - target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]; - target[2] = palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)]; - target[3] = palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]; - - target = reinterpret_cast(&palette_tables_.eighty1bpp[byte]); - target[0] = palette_[(byte&0x80) >> 4]; - target[1] = palette_[(byte&0x40) >> 3]; - target[2] = palette_[(byte&0x20) >> 2]; - target[3] = palette_[(byte&0x10) >> 1]; - target[4] = palette_[(byte&0x08) >> 0]; - target[5] = palette_[(byte&0x04) << 1]; - target[6] = palette_[(byte&0x02) << 2]; - target[7] = palette_[(byte&0x01) << 3]; - - target = reinterpret_cast(&palette_tables_.forty2bpp[byte]); - target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)]; - target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]; - - target = reinterpret_cast(&palette_tables_.eighty4bpp[byte]); - target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)]; - target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]; - } - } - break; +// constexpr int registers[4][4] = { +// {10, 8, 2, 0}, +// {14, 12, 6, 4}, +// {15, 13, 7, 5}, +// {11, 9, 3, 1}, +// }; +// const int index = (address >> 1)&3; +// const uint8_t colour = ~value; +// if(address&1) { +// palette_[registers[index][0]] = (palette_[registers[index][0]]&3) | ((colour >> 1)&4); +// palette_[registers[index][1]] = (palette_[registers[index][1]]&3) | ((colour >> 0)&4); +// palette_[registers[index][2]] = (palette_[registers[index][2]]&3) | ((colour << 1)&4); +// palette_[registers[index][3]] = (palette_[registers[index][3]]&3) | ((colour << 2)&4); +// +// palette_[registers[index][2]] = (palette_[registers[index][2]]&5) | ((colour >> 4)&2); +// palette_[registers[index][3]] = (palette_[registers[index][3]]&5) | ((colour >> 3)&2); +// } else { +// palette_[registers[index][0]] = (palette_[registers[index][0]]&6) | ((colour >> 7)&1); +// palette_[registers[index][1]] = (palette_[registers[index][1]]&6) | ((colour >> 6)&1); +// palette_[registers[index][2]] = (palette_[registers[index][2]]&6) | ((colour >> 5)&1); +// palette_[registers[index][3]] = (palette_[registers[index][3]]&6) | ((colour >> 4)&1); +// +// palette_[registers[index][0]] = (palette_[registers[index][0]]&5) | ((colour >> 2)&2); +// palette_[registers[index][1]] = (palette_[registers[index][1]]&5) | ((colour >> 1)&2); +// } +// +// // regenerate all palette tables for now +// for(int byte = 0; byte < 256; byte++) { +// uint8_t *target = reinterpret_cast(&palette_tables_.forty1bpp[byte]); +// target[0] = palette_[(byte&0x80) >> 4]; +// target[1] = palette_[(byte&0x40) >> 3]; +// target[2] = palette_[(byte&0x20) >> 2]; +// target[3] = palette_[(byte&0x10) >> 1]; +// +// target = reinterpret_cast(&palette_tables_.eighty2bpp[byte]); +// target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)]; +// target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]; +// target[2] = palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)]; +// target[3] = palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]; +// +// target = reinterpret_cast(&palette_tables_.eighty1bpp[byte]); +// target[0] = palette_[(byte&0x80) >> 4]; +// target[1] = palette_[(byte&0x40) >> 3]; +// target[2] = palette_[(byte&0x20) >> 2]; +// target[3] = palette_[(byte&0x10) >> 1]; +// target[4] = palette_[(byte&0x08) >> 0]; +// target[5] = palette_[(byte&0x04) << 1]; +// target[6] = palette_[(byte&0x02) << 2]; +// target[7] = palette_[(byte&0x01) << 3]; +// +// target = reinterpret_cast(&palette_tables_.forty2bpp[byte]); +// target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)]; +// target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]; +// +// target = reinterpret_cast(&palette_tables_.eighty4bpp[byte]); +// target[0] = palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)]; +// target[1] = palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]; +// } + } break; } } -void VideoOutput::setup_base_address() { - switch(screen_mode_) { - case 0: case 1: case 2: screen_mode_base_address_ = 0x3000; break; - case 3: screen_mode_base_address_ = 0x4000; break; - case 4: case 5: screen_mode_base_address_ = 0x5800; break; - case 6: screen_mode_base_address_ = 0x6000; break; - } -} - -// MARK: - Interrupts - -Cycles VideoOutput::next_sequence_point() { - if(output_position_ < real_time_clock_interrupt_1) { - return real_time_clock_interrupt_1 - output_position_; - } - - if(output_position_ < display_end_interrupt_1) { - return display_end_interrupt_1 - output_position_; - } - - if(output_position_ < real_time_clock_interrupt_2) { - return real_time_clock_interrupt_2 - output_position_; - } - - if(output_position_ < display_end_interrupt_2) { - return display_end_interrupt_2 - output_position_; - } - - return real_time_clock_interrupt_1 + cycles_per_frame - output_position_; -} - -Electron::Interrupt VideoOutput::get_interrupts() { - const auto interrupts = interrupts_; - interrupts_ = Electron::Interrupt(0); - return interrupts; -} - -// MARK: - RAM timing and access information - -unsigned int VideoOutput::get_cycles_until_next_ram_availability(int from_time) { - unsigned int result = 0; - int position = (output_position_ + from_time) % cycles_per_frame; - - // Apply the standard cost of aligning to the available 1Mhz of RAM bandwidth. - result += 1 + (position&1); - - // In Modes 0-3 there is also a complete block on any access while pixels are being fetched. - if(screen_mode_ < 4) { - const int current_column = graphics_column(position + (position&1)); - int current_line = graphics_line(position); - if(current_column < 80 && current_line < 256) { - // Mode 3 is a further special case: in 'every ten line block', the final two aren't painted, - // so the CPU is allowed access. But the offset of the ten-line blocks depends on when the - // user switched into Mode 3, so that needs to be calculated relative to current output. - if(screen_mode_ == 3) { - // Get the line the display was on. - int output_position_line = graphics_line(output_position_); - - int implied_row; - if(current_line >= output_position_line) { - // Get the number of lines since then if still in the same frame. - int lines_since_output_position = current_line - output_position_line; - - // Therefore get the character row at the proposed time, modulo 10. - implied_row = (current_character_row_ + lines_since_output_position) % 10; - } else { - // If the frame has rolled over, the implied row is just related to the current line. - implied_row = current_line % 10; - } - - // Mode 3 ends after 250 lines, not the usual 256. - if(implied_row < 8 && current_line < 250) result += unsigned(80 - current_column); - } - else result += unsigned(80 - current_column); - } - } - return result; -} - -VideoOutput::Range VideoOutput::get_memory_access_range() { - // This can't be more specific than this without applying a lot more thought because of mixed modes: - // suppose a program runs half the screen in an 80-column mode then switches to 40 columns. Then the - // real end address will be at 128*80 + 128*40 after the original base, subject to wrapping that depends - // on where the overflow occurred. Assuming accesses may run from the lowest possible position through to - // the end of RAM is good enough for 95% of use cases however. - VideoOutput::Range range; - range.low_address = std::min(start_screen_address_, screen_mode_base_address_); - range.high_address = 0x8000; - return range; -} - -// MARK: - The screen map - -void VideoOutput::setup_screen_map() { - /* - - Odd field: Even field: - - |--S--| -S-| - |--S--| |--S--| - |-S-B-| = 3 |--S--| = 2.5 - |--B--| |--B--| - |--P--| |--P--| - |--B--| = 312 |--B--| = 312.5 - |-B- - - */ - for(int c = 0; c < 2; c++) { - if(c&1) { - screen_map_.emplace_back(DrawAction::Sync, (cycles_per_line * 5) >> 1); - screen_map_.emplace_back(DrawAction::Blank, cycles_per_line >> 1); - } else { - screen_map_.emplace_back(DrawAction::Blank, cycles_per_line >> 1); - screen_map_.emplace_back(DrawAction::Sync, (cycles_per_line * 5) >> 1); - } - for(int l = 0; l < first_graphics_line - 3; l++) emplace_blank_line(); - for(int l = 0; l < 256; l++) emplace_pixel_line(); - for(int l = 256 + first_graphics_line; l < 312; l++) emplace_blank_line(); - if(c&1) emplace_blank_line(); - } -} - -void VideoOutput::emplace_blank_line() { - screen_map_.emplace_back(DrawAction::Sync, 9); - screen_map_.emplace_back(DrawAction::ColourBurst, 24 - 9); - screen_map_.emplace_back(DrawAction::Blank, 128 - 24); -} - -void VideoOutput::emplace_pixel_line() { - // output format is: - // 9 cycles: sync - // ... to 24 cycles: colour burst - // ... to first_graphics_cycle: blank - // ... for 80 cycles: pixels - // ... until end of line: blank - screen_map_.emplace_back(DrawAction::Sync, 9); - screen_map_.emplace_back(DrawAction::ColourBurst, 24 - 9); - screen_map_.emplace_back(DrawAction::Blank, first_graphics_cycle - 24); - screen_map_.emplace_back(DrawAction::Pixels, 80); - screen_map_.emplace_back(DrawAction::Blank, 48 - first_graphics_cycle); -} diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 40ac8cf05..574fa4675 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -30,10 +30,7 @@ class VideoOutput { The pointer supplied should be to address 0 in the unexpanded Electron's memory map. */ - VideoOutput(uint8_t *memory); - - /// Produces the next @c cycles of video output. - void run_for(const Cycles cycles); + VideoOutput(const uint8_t *memory); /// Sets the destination for output. void set_scan_target(Outputs::Display::ScanTarget *scan_target); @@ -47,70 +44,40 @@ class VideoOutput { /// Gets the type of output. Outputs::Display::DisplayType get_display_type() const; + /// Produces the next @c cycles of video output. + /// + /// @returns a bit mask of all interrupts triggered. + Electron::Interrupt run_for(const Cycles cycles); + + /// @returns The number of 2Mhz cycles that will pass before completion of an attempted + /// IO [/1Mhz] access that is first signalled in the upcoming cycle. + Cycles io_delay() { + return 2 + ((h_count >> 3)&1); + } + + /// @returns The number of 2Mhz cycles that will pass before completion of an attempted + /// RAM access that is first signalled in the upcoming cycle. + Cycles ram_delay() { + if(!mode_40 && !in_blank()) { + return 2 + h_active - h_count; + } + return io_delay(); + } + /*! Writes @c value to the register at @c address. May mutate the results of @c get_next_interrupt, @c get_cycles_until_next_ram_availability and @c get_memory_access_range. */ void write(int address, uint8_t value); - /*! - @returns the next interrupt that should be generated as a result of the video hardware. - The time until signalling returned is the number of cycles after the final one triggered - by the most recent call to @c run_for. - - This result may be mutated by calls to @c write. - */ - Cycles next_sequence_point(); - - /*! - @returns a bit mask of all interrupts that have been triggered since the last call to get_interrupt(). - */ - Electron::Interrupt get_interrupts(); - /*! @returns the number of cycles after (final cycle of last run_for batch + @c from_time) before the video circuits will allow the CPU to access RAM. */ unsigned int get_cycles_until_next_ram_availability(int from_time); - struct Range { - uint16_t low_address, high_address; - }; - /*! - @returns the range of addresses that the video might read from. - */ - Range get_memory_access_range(); - private: - inline void start_pixel_line(); - inline void end_pixel_line(); - inline void output_pixels(int number_of_cycles); - inline void setup_base_address(); - - int output_position_ = 0; - - uint8_t palette_[16]; - uint8_t screen_mode_ = 6; - uint16_t screen_mode_base_address_ = 0; - uint16_t start_screen_address_ = 0; - - uint8_t *ram_; - struct { - uint32_t forty1bpp[256]; - uint16_t forty2bpp[256]; - uint64_t eighty1bpp[256]; - uint32_t eighty2bpp[256]; - uint16_t eighty4bpp[256]; - } palette_tables_; - - // Display generation. - uint16_t start_line_address_ = 0; - uint16_t current_screen_address_ = 0; - int current_pixel_line_ = -1; - int current_pixel_column_ = 0; - int current_character_row_ = 0; - uint8_t last_pixel_byte_ = 0; - bool is_blank_line_ = false; + const uint8_t *ram_ = nullptr; // CRT output uint8_t *current_output_target_ = nullptr; @@ -118,21 +85,67 @@ class VideoOutput { int current_output_divider_ = 1; Outputs::CRT::CRT crt_; - struct DrawAction { - enum Type { - Sync, ColourBurst, Blank, Pixels - } type; - int length; - DrawAction(Type type, int length) : type(type), length(length) {} - }; - std::vector screen_map_; - void setup_screen_map(); - void emplace_blank_line(); - void emplace_pixel_line(); - std::size_t screen_map_pointer_ = 0; - int cycles_into_draw_action_ = 0; + // Palette. + uint8_t palette_[16]; - Electron::Interrupt interrupts_ = Electron::Interrupt(0); -}; + // User-selected base address; constrained to a 64-byte boundary by the setter. + uint16_t screen_base; + // Parameters implied by mode selection. + uint16_t mode_base = 0; + bool mode_40 = true; + bool mode_text = false; + enum class Bpp { + One, Two, Four + } mode_bpp = Bpp::One; + + // Frame position. + int v_count = 0; + int h_count = 0; + bool field = false; + + // Current working address. + uint16_t row_addr = 0; // Address, sans character row, adopted at the start of a row. + uint16_t byte_addr = 0; // Current working address, incremented as the raster moves across the line. + int char_row = 0; // Character row; 0–9 in text mode, 0–7 in graphics. + + // Sync states. + bool vsync_int = false; // True => vsync active. + bool hsync_int = false; // True => hsync active. + + // Horizontal timing parameters; all in terms of the 16Mhz pixel clock but conveniently all + // divisible by 8, so it's safe to count time with a 2Mhz input. + static constexpr int h_active = 640; + static constexpr int hsync_start = 768; + static constexpr int hsync_end = 832; + static constexpr int h_reset_addr = 1016; + static constexpr int h_total = 1024; // Minor digression from the FPGA original here; + // in this implementation the value is tested + // _after_ position increment rather than before/instead. + // So it needs to be one higher. Which is baked into + // the constant to emphasise the all-divisible-by-8 property. + static constexpr int h_half = h_total / 2; + + // Vertical timing parameters; all in terms of lines. As per the horizontal parameters above, + // lines begin with their first visible pixel (or the equivalent position). + static constexpr int v_active_gph = 256; + static constexpr int v_active_txt = 250; + static constexpr int v_disp_gph = v_active_gph - 1; + static constexpr int v_disp_txt = v_active_txt - 1; + static constexpr int vsync_start = 274; + static constexpr int vsync_end = 276; + static constexpr int v_rtc = 99; + + // Various signals that it was convenient to factor out. + int v_total() const { + return field ? 312 : 311; + } + + bool last_line() const { + return char_row == (mode_text ? 9 : 7); + } + + bool in_blank() const { + return h_count >= h_active || (mode_text && v_count >= v_active_txt) || (!mode_text && v_count >= v_active_gph) || char_row >= 8; + }}; }