From aab2dd68b68cd91ff359412652345df65ad31d47 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 20:18:29 -0400 Subject: [PATCH 01/13] 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; + }}; } From 59530a12fd7242bc3b8bcabd3c3188a387bcecff Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 20:21:46 -0400 Subject: [PATCH 02/13] Sub in basic transliteration of hoglet's FPGA. --- Machines/Acorn/Electron/Electron.cpp | 2 +- Machines/Acorn/Electron/Video.cpp | 139 ++++++++++++++++++--------- Machines/Acorn/Electron/Video.hpp | 2 +- 3 files changed, 93 insertions(+), 50 deletions(-) diff --git a/Machines/Acorn/Electron/Electron.cpp b/Machines/Acorn/Electron/Electron.cpp index e94f2b306..ab19216bf 100644 --- a/Machines/Acorn/Electron/Electron.cpp +++ b/Machines/Acorn/Electron/Electron.cpp @@ -683,7 +683,7 @@ template class ConcreteMachine: speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(SoundGenerator::clock_rate_divider))); } - inline void signal_interrupt(Interrupt interrupt) { + inline void signal_interrupt(uint8_t interrupt) { if(!interrupt) { return; } diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 14a8c4cae..39f66c5b6 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -230,54 +230,97 @@ Outputs::Display::DisplayType VideoOutput::get_display_type() const { // } //} -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(); -// } -// } +uint8_t VideoOutput::run_for(const Cycles cycles) { + uint8_t interrupts{}; + + int number_of_cycles = cycles.as(); + while(number_of_cycles--) { + // Horizontal and vertical counter updates. + const bool is_v_end = v_count == v_total(); + h_count += 8; + if(h_count == h_total) { + h_count = 0; + ++v_count; + + if(is_v_end) { + v_count = 0; + field = !field; + } + } + + // Test for interrupts. + if(v_count == v_rtc && ((!field && !h_count) || (field && h_count == h_half))) { + interrupts |= static_cast(Interrupt::RealTimeClock); + } + if(h_count == hsync_start && ((v_count == v_disp_gph && !mode_text) or (v_count == v_disp_txt && mode_text))) { + interrupts |= static_cast(Interrupt::DisplayEnd); + } + + // Update syncs. + if(!field) { + if(!h_count && v_count == vsync_start) { + vsync_int = true; + } else if(h_count == h_half && v_count == vsync_end) { + vsync_int = false; + } + } else { + if(h_count == h_half && v_count == vsync_start) { + vsync_int = true; + } else if(!h_count && v_count == vsync_end + 1) { + vsync_int = false; + } + } + + const auto h_sync_last = hsync_int; + if(h_count == hsync_start) { + hsync_int = true; + } else if(h_count == hsync_end) { + hsync_int = false; + } + + // Update character row on the trailing edge of hsync. + if(h_count == hsync_end) { + if(is_v_end) { + char_row = 0; + } else { + char_row = last_line() ? 0 : char_row + 1; + } + } + + // Disable the top bit of the char_row counter outside of text mode. + if(!mode_text) { + char_row &= 7; + } + + // Latch video address at frame start. + if(h_count == h_reset_addr && is_v_end) { + row_addr = byte_addr = screen_base; + } + + // Copy byte_addr back into row_addr if a new character row has begun. + if(hsync_int) { + if(last_line()) { + row_addr = byte_addr; + } else { + byte_addr = row_addr; + } + } + + // Increment the byte address across the line. + // (slghtly pained logic here because the input clock is still at the pixel rate, not the byte rate) + if(h_count < h_active) { + if( + (!mode_40 && !(h_count & 0x7)) || + (mode_40 && ((h_count & 0xf) == 0x8)) + ) { + byte_addr += 8; + + if(!(byte_addr & 0b0111'1000'0000'0000)) { + byte_addr = mode_base | (byte_addr & 0x0000'0111'1111'1111); + } + } + } + } return interrupts; } diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 574fa4675..814c83de7 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -47,7 +47,7 @@ class VideoOutput { /// Produces the next @c cycles of video output. /// /// @returns a bit mask of all interrupts triggered. - Electron::Interrupt run_for(const Cycles cycles); + uint8_t 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. From 5ca1659bcc6d6b57f207f68943863d6d8923b3c3 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 20:36:27 -0400 Subject: [PATCH 03/13] Do just enough to get 1bpp fixed-palette pixels. --- Machines/Acorn/Electron/Video.cpp | 80 ++++++++++++++++++++----------- Machines/Acorn/Electron/Video.hpp | 6 +++ 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 39f66c5b6..56289d878 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -12,42 +12,18 @@ using namespace Electron; -#define graphics_line(v) ((((v) >> 7) - first_graphics_line + field_divider_line) % field_divider_line) -#define graphics_column(v) ((((v) & 127) - first_graphics_cycle + 128) & 127) - -namespace { - constexpr int cycles_per_line = 128; - constexpr int lines_per_frame = 625; - constexpr int cycles_per_frame = lines_per_frame * cycles_per_line; - constexpr int crt_cycles_multiplier = 8; - constexpr int crt_cycles_per_line = crt_cycles_multiplier * cycles_per_line; - - constexpr int field_divider_line = 312; // i.e. the line, simultaneous with which, the first field's sync ends. So if - // the first line with pixels in field 1 is the 20th in the frame, the first line - // with pixels in field 2 will be 20+field_divider_line - constexpr int first_graphics_line = 31; - constexpr int first_graphics_cycle = 33; - - constexpr int display_end_interrupt_line = 256; - - constexpr int real_time_clock_interrupt_1 = 16704; - constexpr int real_time_clock_interrupt_2 = 56704; - constexpr int display_end_interrupt_1 = (first_graphics_line + display_end_interrupt_line)*cycles_per_line; - constexpr int display_end_interrupt_2 = (first_graphics_line + field_divider_line + display_end_interrupt_line)*cycles_per_line; -} - // MARK: - Lifecycle VideoOutput::VideoOutput(const uint8_t *memory) : ram_(memory), - crt_(crt_cycles_per_line, + crt_(128, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1) { memset(palette_, 0xf, sizeof(palette_)); // 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)); +// 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)); } void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) { @@ -55,7 +31,7 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) { } Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const { - return crt_.get_scaled_scan_status() / float(crt_cycles_multiplier); + return crt_.get_scaled_scan_status();// / float(crt_cycles_multiplier); } void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) { @@ -271,7 +247,6 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } } - const auto h_sync_last = hsync_int; if(h_count == hsync_start) { hsync_int = true; } else if(h_count == hsync_end) { @@ -306,6 +281,53 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } } + // Determine current output item. + OutputStage stage; + if(vsync_int || hsync_int) { + stage = OutputStage::Sync; + } else if(in_blank()) { + stage = OutputStage::Blank; + } else { + stage = OutputStage::Pixels; + } + + if(stage != output_) { + switch(output_) { + case OutputStage::Sync: crt_.output_sync(output_length_); break; + case OutputStage::Blank: crt_.output_blank(output_length_); break; + case OutputStage::Pixels: + if(current_output_target_) { + crt_.output_data( + output_length_, + static_cast(current_output_target_ - initial_output_target_) + ); + } else { + crt_.output_data(output_length_); + } + break; + } + output_length_ = 0; + output_ = stage; + + if(stage == OutputStage::Pixels) { + initial_output_target_ = current_output_target_ = crt_.begin_data(mode_40 ? 320 : 640); + } + } + ++output_length_; + if(output_ == OutputStage::Pixels && (!mode_40 || h_count & 8) && current_output_target_) { + const uint8_t data = ram_[byte_addr | char_row]; + + current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; + current_output_target_[1] = (data & 0x40) ? 0xff : 0x00; + current_output_target_[2] = (data & 0x20) ? 0xff : 0x00; + current_output_target_[3] = (data & 0x10) ? 0xff : 0x00; + current_output_target_[4] = (data & 0x08) ? 0xff : 0x00; + current_output_target_[5] = (data & 0x04) ? 0xff : 0x00; + current_output_target_[6] = (data & 0x02) ? 0xff : 0x00; + current_output_target_[7] = (data & 0x01) ? 0xff : 0x00; + current_output_target_ += 8; + } + // Increment the byte address across the line. // (slghtly pained logic here because the input clock is still at the pixel rate, not the byte rate) if(h_count < h_active) { @@ -319,7 +341,7 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { byte_addr = mode_base | (byte_addr & 0x0000'0111'1111'1111); } } - } + } } return interrupts; diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 814c83de7..caf717e09 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -80,6 +80,12 @@ class VideoOutput { const uint8_t *ram_ = nullptr; // CRT output + enum class OutputStage { + Sync, Blank, Pixels + }; + OutputStage output_; + int output_length_ = 0; + uint8_t *current_output_target_ = nullptr; uint8_t *initial_output_target_ = nullptr; int current_output_divider_ = 1; From 88248d7062ead8b378fd589bfe9550e13cf30510 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 20:55:26 -0400 Subject: [PATCH 04/13] Fix base address, delays. --- Machines/Acorn/Electron/Video.cpp | 12 ++++++------ Machines/Acorn/Electron/Video.hpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 56289d878..d840cdacc 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -310,7 +310,7 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { output_ = stage; if(stage == OutputStage::Pixels) { - initial_output_target_ = current_output_target_ = crt_.begin_data(mode_40 ? 320 : 640); + initial_output_target_ = current_output_target_ = crt_.begin_data(640);//crt_.begin_data(mode_40 ? 320 : 640); } } ++output_length_; @@ -352,15 +352,15 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { void VideoOutput::write(int address, uint8_t value) { switch(address & 0xf) { case 0x02: - screen_base = - (screen_base & 0b0111'1110'0000'0000) | - ((value << 1) & 0b0000'0001'1100'0000); - break; - case 0x03: screen_base = (screen_base & 0b0111'1110'0000'0000) | ((value << 1) & 0b0000'0001'1100'0000); break; + case 0x03: + screen_base = + ((value << 9) & 0b0111'1110'0000'0000) | + (screen_base & 0b0000'0001'1100'0000); + break; case 0x07: { uint8_t mode = (value >> 3)&7; mode_40 = mode >= 4; diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index caf717e09..f908b25ea 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -59,7 +59,7 @@ class VideoOutput { /// 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 2 + ((h_active - h_count) >> 3); } return io_delay(); } From f10702b3ca065bdfc8aded96d7e4b46a10145e89 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 21:01:30 -0400 Subject: [PATCH 05/13] Edge towards proper serialisation. --- Machines/Acorn/Electron/Video.cpp | 41 ++++++++++++++++++++++--------- Machines/Acorn/Electron/Video.hpp | 3 ++- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index d840cdacc..f30790cd9 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -283,15 +283,17 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // Determine current output item. OutputStage stage; + int screen_pitch = screen_pitch_; if(vsync_int || hsync_int) { stage = OutputStage::Sync; } else if(in_blank()) { stage = OutputStage::Blank; } else { stage = OutputStage::Pixels; + screen_pitch = (mode_40 ? 320 : 640) / static_cast(mode_bpp); } - if(stage != output_) { + if(stage != output_ || screen_pitch != screen_pitch_) { switch(output_) { case OutputStage::Sync: crt_.output_sync(output_length_); break; case OutputStage::Blank: crt_.output_blank(output_length_); break; @@ -308,24 +310,41 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } output_length_ = 0; output_ = stage; + screen_pitch_ = screen_pitch; if(stage == OutputStage::Pixels) { - initial_output_target_ = current_output_target_ = crt_.begin_data(640);//crt_.begin_data(mode_40 ? 320 : 640); + initial_output_target_ = current_output_target_ = crt_.begin_data(static_cast(screen_pitch_)); } } ++output_length_; if(output_ == OutputStage::Pixels && (!mode_40 || h_count & 8) && current_output_target_) { const uint8_t data = ram_[byte_addr | char_row]; - current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; - current_output_target_[1] = (data & 0x40) ? 0xff : 0x00; - current_output_target_[2] = (data & 0x20) ? 0xff : 0x00; - current_output_target_[3] = (data & 0x10) ? 0xff : 0x00; - current_output_target_[4] = (data & 0x08) ? 0xff : 0x00; - current_output_target_[5] = (data & 0x04) ? 0xff : 0x00; - current_output_target_[6] = (data & 0x02) ? 0xff : 0x00; - current_output_target_[7] = (data & 0x01) ? 0xff : 0x00; - current_output_target_ += 8; + switch(mode_bpp) { + case Bpp::One: + current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; + current_output_target_[1] = (data & 0x40) ? 0xff : 0x00; + current_output_target_[2] = (data & 0x20) ? 0xff : 0x00; + current_output_target_[3] = (data & 0x10) ? 0xff : 0x00; + current_output_target_[4] = (data & 0x08) ? 0xff : 0x00; + current_output_target_[5] = (data & 0x04) ? 0xff : 0x00; + current_output_target_[6] = (data & 0x02) ? 0xff : 0x00; + current_output_target_[7] = (data & 0x01) ? 0xff : 0x00; + current_output_target_ += 8; + break; + case Bpp::Two: + current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; + current_output_target_[1] = (data & 0x20) ? 0xff : 0x00; + current_output_target_[2] = (data & 0x08) ? 0xff : 0x00; + current_output_target_[3] = (data & 0x02) ? 0xff : 0x00; + current_output_target_ += 4; + break; + case Bpp::Four: + current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; + current_output_target_[1] = (data & 0x08) ? 0xff : 0x00; + current_output_target_ += 2; + break; + } } // Increment the byte address across the line. diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index f908b25ea..0bb40e8a2 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -85,6 +85,7 @@ class VideoOutput { }; OutputStage output_; int output_length_ = 0; + int screen_pitch_ = 0; uint8_t *current_output_target_ = nullptr; uint8_t *initial_output_target_ = nullptr; @@ -102,7 +103,7 @@ class VideoOutput { bool mode_40 = true; bool mode_text = false; enum class Bpp { - One, Two, Four + One = 1, Two = 2, Four = 4 } mode_bpp = Bpp::One; // Frame position. From d8b6d87a1c61ce073a4b666531a66aae8d3dbf66 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 21:36:05 -0400 Subject: [PATCH 06/13] Attempt colour. --- Machines/Acorn/Electron/Video.cpp | 287 +++++------------------------- Machines/Acorn/Electron/Video.hpp | 21 ++- 2 files changed, 67 insertions(+), 241 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index f30790cd9..2d116fdb0 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -31,7 +31,7 @@ void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) { } Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const { - return crt_.get_scaled_scan_status();// / float(crt_cycles_multiplier); + return crt_.get_scaled_scan_status(); } void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) { @@ -42,170 +42,6 @@ Outputs::Display::DisplayType VideoOutput::get_display_type() const { return crt_.get_display_type(); } -// 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))); -// -// 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 -// } -//} - uint8_t VideoOutput::run_for(const Cycles cycles) { uint8_t interrupts{}; @@ -322,26 +158,26 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { switch(mode_bpp) { case Bpp::One: - current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; - current_output_target_[1] = (data & 0x40) ? 0xff : 0x00; - current_output_target_[2] = (data & 0x20) ? 0xff : 0x00; - current_output_target_[3] = (data & 0x10) ? 0xff : 0x00; - current_output_target_[4] = (data & 0x08) ? 0xff : 0x00; - current_output_target_[5] = (data & 0x04) ? 0xff : 0x00; - current_output_target_[6] = (data & 0x02) ? 0xff : 0x00; - current_output_target_[7] = (data & 0x01) ? 0xff : 0x00; + current_output_target_[0] = palette1bpp_[(data >> 7) & 1]; + current_output_target_[1] = palette1bpp_[(data >> 6) & 1]; + current_output_target_[2] = palette1bpp_[(data >> 5) & 1]; + current_output_target_[3] = palette1bpp_[(data >> 4) & 1]; + current_output_target_[4] = palette1bpp_[(data >> 3) & 1]; + current_output_target_[5] = palette1bpp_[(data >> 2) & 1]; + current_output_target_[6] = palette1bpp_[(data >> 1) & 1]; + current_output_target_[7] = palette1bpp_[(data >> 0) & 1]; current_output_target_ += 8; break; case Bpp::Two: - current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; - current_output_target_[1] = (data & 0x20) ? 0xff : 0x00; - current_output_target_[2] = (data & 0x08) ? 0xff : 0x00; - current_output_target_[3] = (data & 0x02) ? 0xff : 0x00; + current_output_target_[0] = palette2bpp_[((data >> 6) & 2) | ((data >> 3) & 1)]; + current_output_target_[1] = palette2bpp_[((data >> 5) & 2) | ((data >> 2) & 1)]; + current_output_target_[2] = palette2bpp_[((data >> 4) & 2) | ((data >> 1) & 1)]; + current_output_target_[3] = palette2bpp_[((data >> 3) & 2) | ((data >> 0) & 1)]; current_output_target_ += 4; break; case Bpp::Four: - current_output_target_[0] = (data & 0x80) ? 0xff : 0x00; - current_output_target_[1] = (data & 0x08) ? 0xff : 0x00; + current_output_target_[0] = palette4bpp_[((data >> 4) & 8) | ((data >> 3) & 4) | ((data >> 2) & 2) | ((data >> 1) & 1)]; + current_output_target_[1] = palette4bpp_[((data >> 3) & 8) | ((data >> 2) & 4) | ((data >> 1) & 2) | ((data >> 0) & 1)]; current_output_target_ += 2; break; } @@ -369,7 +205,8 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // MARK: - Register hub void VideoOutput::write(int address, uint8_t value) { - switch(address & 0xf) { + address &= 0xf; + switch(address) { case 0x02: screen_base = (screen_base & 0b0111'1110'0000'0000) | @@ -403,65 +240,37 @@ void VideoOutput::write(int address, uint8_t value) { } 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)]; -// } + palette_[address - 8] = ~value; + + if(address <= 0x09) { + palette1bpp_[0] = palette_entry<1, 0, 1, 4, 0, 4>(); + palette1bpp_[1] = palette_entry<1, 2, 0, 6, 0, 2>(); + + palette2bpp_[0] = palette_entry<1, 0, 1, 4, 0, 4>(); + palette2bpp_[1] = palette_entry<1, 1, 1, 5, 0, 5>(); + palette2bpp_[2] = palette_entry<1, 2, 0, 6, 0, 2>(); + palette2bpp_[3] = palette_entry<1, 3, 0, 3, 0, 7>(); + } + + palette4bpp_[0] = palette_entry<1, 0, 1, 4, 0, 4>(); + palette4bpp_[2] = palette_entry<1, 1, 1, 5, 0, 5>(); + palette4bpp_[8] = palette_entry<1, 2, 0, 6, 0, 2>(); + palette4bpp_[10] = palette_entry<1, 3, 0, 3, 0, 7>(); + + palette4bpp_[4] = palette_entry<3, 0, 3, 4, 2, 4>(); + palette4bpp_[6] = palette_entry<3, 1, 3, 5, 2, 5>(); + palette4bpp_[12] = palette_entry<3, 2, 2, 6, 2, 2>(); + palette4bpp_[14] = palette_entry<3, 3, 2, 3, 2, 7>(); + + palette4bpp_[1] = palette_entry<5, 0, 5, 4, 4, 4>(); + palette4bpp_[3] = palette_entry<5, 1, 5, 5, 4, 5>(); + palette4bpp_[9] = palette_entry<5, 2, 4, 6, 4, 2>(); + palette4bpp_[11] = palette_entry<5, 3, 4, 3, 4, 7>(); + + palette4bpp_[1] = palette_entry<7, 0, 7, 4, 6, 4>(); + palette4bpp_[3] = palette_entry<7, 1, 7, 5, 6, 5>(); + palette4bpp_[9] = palette_entry<7, 2, 6, 6, 6, 2>(); + palette4bpp_[11] = palette_entry<7, 3, 6, 3, 6, 7>(); } break; } } - diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 0bb40e8a2..7959b6222 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -92,8 +92,25 @@ class VideoOutput { int current_output_divider_ = 1; Outputs::CRT::CRT crt_; - // Palette. - uint8_t palette_[16]; + // Palettes. + uint8_t palette_[8]; + uint8_t palette1bpp_[2]; + uint8_t palette2bpp_[4]; + uint8_t palette4bpp_[16]; + + template + uint8_t channel() { + if constexpr (source_bit < target_bit) { + return (palette_[index] << (target_bit - source_bit)) & (1 << target_bit); + } else { + return (palette_[index] >> (source_bit - target_bit)) & (1 << target_bit); + } + } + + template + uint8_t palette_entry() { + return channel() | channel() | channel(); + } // User-selected base address; constrained to a 64-byte boundary by the setter. uint16_t screen_base; From b8f4385501363cda162e061f017c36c569aee198 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 21:47:13 -0400 Subject: [PATCH 07/13] Fix palette generation. --- Machines/Acorn/Electron/Video.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 2d116fdb0..d840c1864 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -248,28 +248,28 @@ void VideoOutput::write(int address, uint8_t value) { palette2bpp_[0] = palette_entry<1, 0, 1, 4, 0, 4>(); palette2bpp_[1] = palette_entry<1, 1, 1, 5, 0, 5>(); - palette2bpp_[2] = palette_entry<1, 2, 0, 6, 0, 2>(); + palette2bpp_[2] = palette_entry<1, 2, 0, 2, 0, 6>(); palette2bpp_[3] = palette_entry<1, 3, 0, 3, 0, 7>(); } palette4bpp_[0] = palette_entry<1, 0, 1, 4, 0, 4>(); palette4bpp_[2] = palette_entry<1, 1, 1, 5, 0, 5>(); - palette4bpp_[8] = palette_entry<1, 2, 0, 6, 0, 2>(); + palette4bpp_[8] = palette_entry<1, 2, 0, 2, 0, 6>(); palette4bpp_[10] = palette_entry<1, 3, 0, 3, 0, 7>(); palette4bpp_[4] = palette_entry<3, 0, 3, 4, 2, 4>(); palette4bpp_[6] = palette_entry<3, 1, 3, 5, 2, 5>(); - palette4bpp_[12] = palette_entry<3, 2, 2, 6, 2, 2>(); + palette4bpp_[12] = palette_entry<3, 2, 2, 2, 2, 6>(); palette4bpp_[14] = palette_entry<3, 3, 2, 3, 2, 7>(); - palette4bpp_[1] = palette_entry<5, 0, 5, 4, 4, 4>(); - palette4bpp_[3] = palette_entry<5, 1, 5, 5, 4, 5>(); - palette4bpp_[9] = palette_entry<5, 2, 4, 6, 4, 2>(); - palette4bpp_[11] = palette_entry<5, 3, 4, 3, 4, 7>(); + palette4bpp_[5] = palette_entry<5, 0, 5, 4, 4, 4>(); + palette4bpp_[7] = palette_entry<5, 1, 5, 5, 4, 5>(); + palette4bpp_[13] = palette_entry<5, 2, 4, 2, 4, 6>(); + palette4bpp_[15] = palette_entry<5, 3, 4, 3, 4, 7>(); palette4bpp_[1] = palette_entry<7, 0, 7, 4, 6, 4>(); palette4bpp_[3] = palette_entry<7, 1, 7, 5, 6, 5>(); - palette4bpp_[9] = palette_entry<7, 2, 6, 6, 6, 2>(); + palette4bpp_[9] = palette_entry<7, 2, 6, 2, 6, 6>(); palette4bpp_[11] = palette_entry<7, 3, 6, 3, 6, 7>(); } break; } From 8feb8aaadc8d2f92adf8d09501cbc6aa0513c51d Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Fri, 6 Sep 2024 22:12:19 -0400 Subject: [PATCH 08/13] Reintroduce cropping, even if faulty. --- Machines/Acorn/Electron/Video.cpp | 11 +++++++---- Machines/Acorn/Electron/Video.hpp | 12 ++++++------ Outputs/CRT/CRT.cpp | 6 +++--- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index d840c1864..159f45b5c 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -20,10 +20,13 @@ VideoOutput::VideoOutput(const uint8_t *memory) : 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1) { - memset(palette_, 0xf, sizeof(palette_)); - - // 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)); + crt_.set_visible_area(crt_.get_rect_for_area( + 312 - vsync_end, + 256, + (h_total - hsync_end) >> 3, + 80, + 4.0f / 3.0f + )); } void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) { diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 7959b6222..be0c2b183 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -83,7 +83,7 @@ class VideoOutput { enum class OutputStage { Sync, Blank, Pixels }; - OutputStage output_; + OutputStage output_ = OutputStage::Blank; int output_length_ = 0; int screen_pitch_ = 0; @@ -93,10 +93,10 @@ class VideoOutput { Outputs::CRT::CRT crt_; // Palettes. - uint8_t palette_[8]; - uint8_t palette1bpp_[2]; - uint8_t palette2bpp_[4]; - uint8_t palette4bpp_[16]; + uint8_t palette_[8]{}; + uint8_t palette1bpp_[2]{}; + uint8_t palette2bpp_[4]{}; + uint8_t palette4bpp_[16]{}; template uint8_t channel() { @@ -113,7 +113,7 @@ class VideoOutput { } // User-selected base address; constrained to a 64-byte boundary by the setter. - uint16_t screen_base; + uint16_t screen_base = 0; // Parameters implied by mode selection. uint16_t mode_base = 0; diff --git a/Outputs/CRT/CRT.cpp b/Outputs/CRT/CRT.cpp index ad77f5e2f..16a001d6d 100644 --- a/Outputs/CRT/CRT.cpp +++ b/Outputs/CRT/CRT.cpp @@ -444,9 +444,9 @@ void CRT::set_immediate_default_phase(float phase) { void CRT::output_data(int number_of_cycles, size_t number_of_samples) { #ifndef NDEBUG - assert(number_of_samples > 0); - assert(number_of_samples <= allocated_data_length_); - allocated_data_length_ = std::numeric_limits::min(); +// assert(number_of_samples > 0); +// assert(number_of_samples <= allocated_data_length_); +// allocated_data_length_ = std::numeric_limits::min(); #endif scan_target_->end_data(number_of_samples); Scan scan; From 2a9e1ea045d4253f69dde4fe973b5df3b001e8f8 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 8 Sep 2024 20:16:43 -0400 Subject: [PATCH 09/13] Use normal member naming convention. --- Machines/Acorn/Electron/Video.cpp | 112 +++++++++++++++--------------- Machines/Acorn/Electron/Video.hpp | 38 +++++----- 2 files changed, 75 insertions(+), 75 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 159f45b5c..e9c77be3b 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -51,85 +51,85 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { int number_of_cycles = cycles.as(); while(number_of_cycles--) { // Horizontal and vertical counter updates. - const bool is_v_end = v_count == v_total(); - h_count += 8; - if(h_count == h_total) { - h_count = 0; - ++v_count; + const bool is_v_end = v_count_ == v_total(); + h_count_ += 8; + if(h_count_ == h_total) { + h_count_ = 0; + ++v_count_; if(is_v_end) { - v_count = 0; - field = !field; + v_count_ = 0; + field_ = !field_; } } // Test for interrupts. - if(v_count == v_rtc && ((!field && !h_count) || (field && h_count == h_half))) { + if(v_count_ == v_rtc && ((!field_ && !h_count_) || (field_ && h_count_ == h_half))) { interrupts |= static_cast(Interrupt::RealTimeClock); } - if(h_count == hsync_start && ((v_count == v_disp_gph && !mode_text) or (v_count == v_disp_txt && mode_text))) { + if(h_count_ == hsync_start && ((v_count_ == v_disp_gph && !mode_text_) or (v_count_ == v_disp_txt && mode_text_))) { interrupts |= static_cast(Interrupt::DisplayEnd); } // Update syncs. - if(!field) { - if(!h_count && v_count == vsync_start) { - vsync_int = true; - } else if(h_count == h_half && v_count == vsync_end) { - vsync_int = false; + if(!field_) { + if(!h_count_ && v_count_ == vsync_start) { + vsync_int_ = true; + } else if(h_count_ == h_half && v_count_ == vsync_end) { + vsync_int_ = false; } } else { - if(h_count == h_half && v_count == vsync_start) { - vsync_int = true; - } else if(!h_count && v_count == vsync_end + 1) { - vsync_int = false; + if(h_count_ == h_half && v_count_ == vsync_start) { + vsync_int_ = true; + } else if(!h_count_ && v_count_ == vsync_end + 1) { + vsync_int_ = false; } } - if(h_count == hsync_start) { - hsync_int = true; - } else if(h_count == hsync_end) { - hsync_int = false; + if(h_count_ == hsync_start) { + hsync_int_ = true; + } else if(h_count_ == hsync_end) { + hsync_int_ = false; } // Update character row on the trailing edge of hsync. - if(h_count == hsync_end) { + if(h_count_ == hsync_end) { if(is_v_end) { - char_row = 0; + char_row_ = 0; } else { - char_row = last_line() ? 0 : char_row + 1; + char_row_ = last_line() ? 0 : char_row_ + 1; } } // Disable the top bit of the char_row counter outside of text mode. - if(!mode_text) { - char_row &= 7; + if(!mode_text_) { + char_row_ &= 7; } // Latch video address at frame start. - if(h_count == h_reset_addr && is_v_end) { - row_addr = byte_addr = screen_base; + if(h_count_ == h_reset_addr && is_v_end) { + row_addr_ = byte_addr_ = screen_base_; } // Copy byte_addr back into row_addr if a new character row has begun. - if(hsync_int) { + if(hsync_int_) { if(last_line()) { - row_addr = byte_addr; + row_addr_ = byte_addr_; } else { - byte_addr = row_addr; + byte_addr_ = row_addr_; } } // Determine current output item. OutputStage stage; int screen_pitch = screen_pitch_; - if(vsync_int || hsync_int) { + if(vsync_int_ || hsync_int_) { stage = OutputStage::Sync; } else if(in_blank()) { stage = OutputStage::Blank; } else { stage = OutputStage::Pixels; - screen_pitch = (mode_40 ? 320 : 640) / static_cast(mode_bpp); + screen_pitch = (mode_40_ ? 320 : 640) / static_cast(mode_bpp_); } if(stage != output_ || screen_pitch != screen_pitch_) { @@ -156,10 +156,10 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } } ++output_length_; - if(output_ == OutputStage::Pixels && (!mode_40 || h_count & 8) && current_output_target_) { - const uint8_t data = ram_[byte_addr | char_row]; + if(output_ == OutputStage::Pixels && (!mode_40_ || h_count_ & 8) && current_output_target_) { + const uint8_t data = ram_[byte_addr_ | char_row_]; - switch(mode_bpp) { + switch(mode_bpp_) { case Bpp::One: current_output_target_[0] = palette1bpp_[(data >> 7) & 1]; current_output_target_[1] = palette1bpp_[(data >> 6) & 1]; @@ -188,15 +188,15 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // Increment the byte address across the line. // (slghtly pained logic here because the input clock is still at the pixel rate, not the byte rate) - if(h_count < h_active) { + if(h_count_ < h_active) { if( - (!mode_40 && !(h_count & 0x7)) || - (mode_40 && ((h_count & 0xf) == 0x8)) + (!mode_40_ && !(h_count_ & 0x7)) || + (mode_40_ && ((h_count_ & 0xf) == 0x8)) ) { - byte_addr += 8; + byte_addr_ += 8; - if(!(byte_addr & 0b0111'1000'0000'0000)) { - byte_addr = mode_base | (byte_addr & 0x0000'0111'1111'1111); + if(!(byte_addr_ & 0b0111'1000'0000'0000)) { + byte_addr_ = mode_base_ | (byte_addr_ & 0x0000'0111'1111'1111); } } } @@ -211,34 +211,34 @@ void VideoOutput::write(int address, uint8_t value) { address &= 0xf; switch(address) { case 0x02: - screen_base = - (screen_base & 0b0111'1110'0000'0000) | + screen_base_ = + (screen_base_ & 0b0111'1110'0000'0000) | ((value << 1) & 0b0000'0001'1100'0000); break; case 0x03: - screen_base = + screen_base_ = ((value << 9) & 0b0111'1110'0000'0000) | - (screen_base & 0b0000'0001'1100'0000); + (screen_base_ & 0b0000'0001'1100'0000); break; case 0x07: { uint8_t mode = (value >> 3)&7; - mode_40 = mode >= 4; - mode_text = mode == 3 || mode == 6; + 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; + case 2: mode_base_ = 0x3000; break; + case 3: mode_base_ = 0x4000; break; + case 6: mode_base_ = 0x6000; break; + default: mode_base_ = 0x5800; break; } switch(mode) { - default: mode_bpp = Bpp::One; break; + default: mode_bpp_ = Bpp::One; break; case 1: - case 5: mode_bpp = Bpp::Two; break; - case 2: mode_bpp = Bpp::Four; break; + case 5: mode_bpp_ = Bpp::Two; break; + case 2: mode_bpp_ = Bpp::Four; break; } } break; case 0x08: case 0x09: case 0x0a: case 0x0b: diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index be0c2b183..66c486056 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -52,14 +52,14 @@ class VideoOutput { /// @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); + 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) >> 3); + if(!mode_40_ && !in_blank()) { + return 2 + ((h_active - h_count_) >> 3); } return io_delay(); } @@ -113,29 +113,29 @@ class VideoOutput { } // User-selected base address; constrained to a 64-byte boundary by the setter. - uint16_t screen_base = 0; + uint16_t screen_base_ = 0; // Parameters implied by mode selection. - uint16_t mode_base = 0; - bool mode_40 = true; - bool mode_text = false; + uint16_t mode_base_ = 0; + bool mode_40_ = true; + bool mode_text_ = false; enum class Bpp { One = 1, Two = 2, Four = 4 - } mode_bpp = Bpp::One; + } mode_bpp_ = Bpp::One; // Frame position. - int v_count = 0; - int h_count = 0; - bool field = false; + int v_count_ = 0; + int h_count_ = 0; + bool field_ = true; // 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. + 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. + 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. @@ -162,14 +162,14 @@ class VideoOutput { // Various signals that it was convenient to factor out. int v_total() const { - return field ? 312 : 311; + return field_ ? 312 : 311; } bool last_line() const { - return char_row == (mode_text ? 9 : 7); + 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; + return h_count_ >= h_active || (mode_text_ && v_count_ >= v_active_txt) || (!mode_text_ && v_count_ >= v_active_gph) || char_row_ >= 8; }}; } From 75db0018bcad53e3f7bbb897c2e834aa12e0aa77 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 8 Sep 2024 20:20:03 -0400 Subject: [PATCH 10/13] Add note on provenance. --- Machines/Acorn/Electron/Video.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index e9c77be3b..006348e63 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -50,6 +50,12 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { int number_of_cycles = cycles.as(); while(number_of_cycles--) { + // The below is my attempt at transcription of the equivalent VHDL code in moogway82's + // JamSoftElectronULA — https://github.com/moogway82/JamSoftElectronULA — which is itself + // derived from hoglet67's https://github.com/hoglet67/ElectronFpga and that author's + // reverse-engineering of the Electron ULA. It should therefore be as accurate to the + // original hardware as my comprehension of VHDL and adaptation into sequential code allows. + // Horizontal and vertical counter updates. const bool is_v_end = v_count_ == v_total(); h_count_ += 8; From 0efe649ca5ffefbf1fdfc4f4e606a9a9b06a084e Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 8 Sep 2024 20:57:43 -0400 Subject: [PATCH 11/13] Post pixel clock. --- Machines/Acorn/Electron/Video.cpp | 12 ++++++------ Machines/Acorn/Electron/Video.hpp | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 006348e63..4062d22dc 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -16,15 +16,15 @@ using namespace Electron; VideoOutput::VideoOutput(const uint8_t *memory) : ram_(memory), - crt_(128, + crt_(h_total, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1) { crt_.set_visible_area(crt_.get_rect_for_area( 312 - vsync_end, 256, - (h_total - hsync_end) >> 3, - 80, + h_total - hsync_end, + 80 * 8, 4.0f / 3.0f )); } @@ -56,8 +56,8 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // reverse-engineering of the Electron ULA. It should therefore be as accurate to the // original hardware as my comprehension of VHDL and adaptation into sequential code allows. - // Horizontal and vertical counter updates. - const bool is_v_end = v_count_ == v_total(); + // Horizontal and vertical counter updates; code below should act + const bool is_v_end = this->is_v_end(); h_count_ += 8; if(h_count_ == h_total) { h_count_ = 0; @@ -161,7 +161,7 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { initial_output_target_ = current_output_target_ = crt_.begin_data(static_cast(screen_pitch_)); } } - ++output_length_; + output_length_ += 8; if(output_ == OutputStage::Pixels && (!mode_40_ || h_count_ & 8) && current_output_target_) { const uint8_t data = ram_[byte_addr_ | char_row_]; diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index 66c486056..eeeff4d2e 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -171,5 +171,10 @@ class VideoOutput { 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; - }}; + } + + bool is_v_end() const { + return v_count_ == v_total(); + } +}; } From 51c8396e320466fb5ed5eb378a841d5bb62c7c77 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 8 Sep 2024 21:06:59 -0400 Subject: [PATCH 12/13] Fix faulty centring. --- Machines/Acorn/Electron/Video.cpp | 35 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 4062d22dc..6c64f598d 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -23,7 +23,7 @@ VideoOutput::VideoOutput(const uint8_t *memory) : crt_.set_visible_area(crt_.get_rect_for_area( 312 - vsync_end, 256, - h_total - hsync_end, + h_total - hsync_start, 80 * 8, 4.0f / 3.0f )); @@ -56,18 +56,10 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // reverse-engineering of the Electron ULA. It should therefore be as accurate to the // original hardware as my comprehension of VHDL and adaptation into sequential code allows. - // Horizontal and vertical counter updates; code below should act - const bool is_v_end = this->is_v_end(); - h_count_ += 8; - if(h_count_ == h_total) { - h_count_ = 0; - ++v_count_; - - if(is_v_end) { - v_count_ = 0; - field_ = !field_; - } - } + // In this, the sequential world of C++, all tests below should assume that the position + // named by (h_count_, v_count_) is the one that was active **prior to this cycle**. + // + // So this cycle spans the period from (h_count_, v_count_) to (h_count_, v_count_)+1. // Test for interrupts. if(v_count_ == v_rtc && ((!field_ && !h_count_) || (field_ && h_count_ == h_half))) { @@ -100,7 +92,7 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { // Update character row on the trailing edge of hsync. if(h_count_ == hsync_end) { - if(is_v_end) { + if(is_v_end()) { char_row_ = 0; } else { char_row_ = last_line() ? 0 : char_row_ + 1; @@ -113,7 +105,7 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } // Latch video address at frame start. - if(h_count_ == h_reset_addr && is_v_end) { + if(h_count_ == h_reset_addr && is_v_end()) { row_addr_ = byte_addr_ = screen_base_; } @@ -206,6 +198,19 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { } } } + + // Horizontal and vertical counter updates; code below should act + h_count_ += 8; + if(h_count_ == h_total) { + h_count_ = 0; + + if(is_v_end()) { + v_count_ = 0; + field_ = !field_; + } else { + ++v_count_; + } + } } return interrupts; From b7f069e1bdfb0984b5108c95b15702199ce59e72 Mon Sep 17 00:00:00 2001 From: Thomas Harte Date: Sun, 8 Sep 2024 21:12:45 -0400 Subject: [PATCH 13/13] Add a colour burst. --- Machines/Acorn/Electron/Video.cpp | 11 ++++++++--- Machines/Acorn/Electron/Video.hpp | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Machines/Acorn/Electron/Video.cpp b/Machines/Acorn/Electron/Video.cpp index 6c64f598d..ec8f428c7 100644 --- a/Machines/Acorn/Electron/Video.cpp +++ b/Machines/Acorn/Electron/Video.cpp @@ -124,7 +124,11 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { if(vsync_int_ || hsync_int_) { stage = OutputStage::Sync; } else if(in_blank()) { - stage = OutputStage::Blank; + if(h_count_ >= hburst_start && h_count_ < hburst_end) { + stage = OutputStage::ColourBurst; + } else { + stage = OutputStage::Blank; + } } else { stage = OutputStage::Pixels; screen_pitch = (mode_40_ ? 320 : 640) / static_cast(mode_bpp_); @@ -132,8 +136,9 @@ uint8_t VideoOutput::run_for(const Cycles cycles) { if(stage != output_ || screen_pitch != screen_pitch_) { switch(output_) { - case OutputStage::Sync: crt_.output_sync(output_length_); break; - case OutputStage::Blank: crt_.output_blank(output_length_); break; + case OutputStage::Sync: crt_.output_sync(output_length_); break; + case OutputStage::Blank: crt_.output_blank(output_length_); break; + case OutputStage::ColourBurst: crt_.output_default_colour_burst(output_length_); break; case OutputStage::Pixels: if(current_output_target_) { crt_.output_data( diff --git a/Machines/Acorn/Electron/Video.hpp b/Machines/Acorn/Electron/Video.hpp index eeeff4d2e..98993f4ff 100644 --- a/Machines/Acorn/Electron/Video.hpp +++ b/Machines/Acorn/Electron/Video.hpp @@ -81,7 +81,7 @@ class VideoOutput { // CRT output enum class OutputStage { - Sync, Blank, Pixels + Sync, Blank, Pixels, ColourBurst, }; OutputStage output_ = OutputStage::Blank; int output_length_ = 0; @@ -148,7 +148,10 @@ class VideoOutput { // _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; + static constexpr int hburst_start = 856; + static constexpr int hburst_end = 896; // 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).