diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp index c8e156915..6052d48d0 100644 --- a/Machines/Electron/Electron.cpp +++ b/Machines/Electron/Electron.cpp @@ -7,46 +7,20 @@ // #include "Electron.hpp" -#include "TapeUEF.hpp" - -#include -#include using namespace Electron; -namespace { - static const unsigned int cycles_per_line = 128; - static const unsigned int lines_per_frame = 625; - static const unsigned int cycles_per_frame = lines_per_frame * cycles_per_line; - static const unsigned int crt_cycles_multiplier = 8; - static const unsigned int crt_cycles_per_line = crt_cycles_multiplier * cycles_per_line; - - static const unsigned 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 - static const unsigned int first_graphics_line = 31; - static const unsigned int first_graphics_cycle = 33; - - static const unsigned int display_end_interrupt_line = 256; - - static const unsigned int real_time_clock_interrupt_1 = 16704; - static const unsigned int real_time_clock_interrupt_2 = 56704; -} - -#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) +#pragma mark - Lifecycle Machine::Machine() : interrupt_control_(0), interrupt_status_(Interrupt::PowerOnReset | Interrupt::TransmitDataEmpty | 0x80), - frame_cycles_(0), - display_output_position_(0), - audio_output_position_(0), - current_pixel_line_(-1), - use_fast_tape_hack_(false) + cycles_since_display_update_(0), + cycles_since_audio_update_(0), + use_fast_tape_hack_(false), + cycles_until_display_interrupt_(0) { memset(key_states_, 0, sizeof(key_states_)); - memset(palette_, 0xf, sizeof(palette_)); for(int c = 0; c < 16; c++) memset(roms_[c], 0xff, 16384); @@ -54,32 +28,121 @@ Machine::Machine() : set_clock_rate(2000000); } +#pragma mark - Output + void Machine::setup_output(float aspect_ratio) { - speaker_.reset(new Speaker); - crt_.reset(new Outputs::CRT::CRT(crt_cycles_per_line, 8, Outputs::CRT::DisplayType::PAL50, 1)); - crt_->set_rgb_sampling_function( - "vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)" - "{" - "uint texValue = texture(sampler, coordinate).r;" - "texValue >>= 4 - (int(icoordinate.x * 8) & 4);" - "return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));" - "}"); - - // 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 - 3, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f)); + video_output_.reset(new VideoOutput(ram_)); // The maximum output frequency is 62500Hz and all other permitted output frequencies are integral divisions of that; // however setting the speaker on or off can happen on any 2Mhz cycle, and probably (?) takes effect immediately. So // run the speaker at a 2000000Hz input rate, at least for the time being. + speaker_.reset(new Speaker); speaker_->set_input_rate(2000000 / Speaker::clock_rate_divider); } void Machine::close_output() { - crt_ = nullptr; + video_output_.reset(); } +std::shared_ptr Machine::get_crt() +{ + return video_output_->get_crt(); +} + +std::shared_ptr Machine::get_speaker() +{ + return speaker_; +} + +#pragma mark - The keyboard + +void Machine::clear_all_keys() +{ + memset(key_states_, 0, sizeof(key_states_)); +} + +void Machine::set_key_state(uint16_t key, bool isPressed) +{ + if(key == KeyBreak) + { + set_reset_line(isPressed); + } + else + { + if(isPressed) + key_states_[key >> 4] |= key&0xf; + else + key_states_[key >> 4] &= ~(key&0xf); + } +} + +#pragma mark - Machine configuration + +void Machine::configure_as_target(const StaticAnalyser::Target &target) +{ + if(target.tapes.size()) + { + tape_.set_tape(target.tapes.front()); + } + + if(target.disks.size()) + { + plus3_.reset(new Plus3); + + if(target.acorn.has_dfs) + { + set_rom(ROMSlot0, dfs_, true); + } + if(target.acorn.has_adfs) + { + set_rom(ROMSlot4, adfs_, true); + set_rom(ROMSlot5, std::vector(adfs_.begin() + 16384, adfs_.end()), true); + } + + plus3_->set_disk(target.disks.front(), 0); + } + + ROMSlot slot = ROMSlot12; + for(std::shared_ptr cartridge : target.cartridges) + { + set_rom(slot, cartridge->get_segments().front().data, false); + slot = (ROMSlot)(((int)slot + 1)&15); + } + + if(target.loadingCommand.length()) // TODO: and automatic loading option enabled + { + set_typer_for_string(target.loadingCommand.c_str()); + } + + if(target.acorn.should_hold_shift) + { + set_key_state(KeyShift, true); + is_holding_shift_ = true; + } +} + +void Machine::set_rom(ROMSlot slot, std::vector data, bool is_writeable) +{ + uint8_t *target = nullptr; + switch(slot) + { + case ROMSlotDFS: dfs_ = data; return; + case ROMSlotADFS: adfs_ = data; return; + + case ROMSlotOS: target = os_; break; + default: + target = roms_[slot]; + rom_write_masks_[slot] = is_writeable; + break; + } + + memcpy(target, &data[0], std::min((size_t)16384, data.size())); +} + +#pragma mark - The bus + unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uint16_t address, uint8_t *value) { unsigned int cycles = 1; @@ -92,34 +155,16 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin } else { - if( - ( - ((frame_cycles_ >= first_graphics_line * cycles_per_line) && (frame_cycles_ < (first_graphics_line + 256) * cycles_per_line)) || - ((frame_cycles_ >= (first_graphics_line + field_divider_line) * cycles_per_line) && (frame_cycles_ < (first_graphics_line + 256 + field_divider_line) * cycles_per_line)) - ) - ) - update_display(); - + if(address >= video_access_range_.low_address && address <= video_access_range_.high_address) update_display(); 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 += 1 + (frame_cycles_&1); - if(screen_mode_ < 4) - { - const int current_line = graphics_line(frame_cycles_ + (frame_cycles_&1)); - const int current_column = graphics_column(frame_cycles_ + (frame_cycles_&1)); - if(current_line < 256 && current_column < 80 && !is_blank_line_) - cycles += (unsigned int)(80 - current_column); - } + cycles += video_output_->get_cycles_until_next_ram_availability((int)(cycles_since_display_update_ + 1)); } else { -// if((address >> 8) == 0xfc) -// { -// printf("d"); -// } switch(address & 0xff0f) { case 0xfe00: @@ -134,18 +179,35 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin evaluate_interrupts(); } break; - case 0xfe02: + case 0xfe07: if(!isReadOperation(operation)) { - start_screen_address_ = (start_screen_address_ & 0xfe00) | (uint16_t)(((*value) & 0xe0) << 1); - if(!start_screen_address_) start_screen_address_ |= 0x8000; + // update speaker mode + bool new_speaker_is_enabled = (*value & 6) == 2; + if(new_speaker_is_enabled != speaker_is_enabled_) + { + update_audio(); + speaker_->set_is_enabled(new_speaker_is_enabled); + speaker_is_enabled_ = new_speaker_is_enabled; + } + + tape_.set_is_enabled((*value & 6) != 6); + tape_.set_is_in_input_mode((*value & 6) == 0); + tape_.set_is_running(((*value)&0x40) ? true : false); + + // TODO: caps lock LED } - break; - case 0xfe03: + + // deliberate fallthrough + case 0xfe02: case 0xfe03: + case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b: + case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f: if(!isReadOperation(operation)) { - start_screen_address_ = (start_screen_address_ & 0x01ff) | (uint16_t)(((*value) & 0x3f) << 9); - if(!start_screen_address_) start_screen_address_ |= 0x8000; + update_display(); + video_output_->set_register(address, *value); + video_access_range_ = video_output_->get_memory_access_range(); + queue_next_display_interrupt(); } break; case 0xfe04: @@ -201,102 +263,6 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin tape_.set_counter(*value); } break; - case 0xfe07: - if(!isReadOperation(operation)) - { - // 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_) - { - update_display(); - screen_mode_ = new_screen_mode; - 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; - } - } - - // update speaker mode - bool new_speaker_is_enabled = (*value & 6) == 2; - if(new_speaker_is_enabled != speaker_is_enabled_) - { - update_audio(); - speaker_->set_is_enabled(new_speaker_is_enabled); - speaker_is_enabled_ = new_speaker_is_enabled; - } - - tape_.set_is_enabled((*value & 6) != 6); - tape_.set_is_in_input_mode((*value & 6) == 0); - tape_.set_is_running(((*value)&0x40) ? true : false); - - // TODO: caps lock LED - } - break; - case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b: case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f: - { - if(!isReadOperation(operation)) - { - update_display(); - - static const 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 -#define pack(a, b) (uint8_t)((a << 4) | (b)) - for(int byte = 0; byte < 256; byte++) - { - uint8_t *target = (uint8_t *)&palette_tables_.forty1bpp[byte]; - target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]); - target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]); - - target = (uint8_t *)&palette_tables_.eighty2bpp[byte]; - target[0] = pack(palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]); - target[1] = pack(palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)], palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]); - - target = (uint8_t *)&palette_tables_.eighty1bpp[byte]; - target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]); - target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]); - target[2] = pack(palette_[(byte&0x08) >> 0], palette_[(byte&0x04) << 1]); - target[3] = pack(palette_[(byte&0x02) << 2], palette_[(byte&0x01) << 3]); - - palette_tables_.forty2bpp[byte] = pack(palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]); - palette_tables_.eighty4bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)], - palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]); - } -#undef pack - } - } - break; case 0xfc04: case 0xfc05: case 0xfc06: case 0xfc07: if(plus3_ && (address&0x00f0) == 0x00c0) @@ -417,58 +383,19 @@ unsigned int Machine::perform_bus_operation(CPU6502::BusOperation operation, uin } } -// if(operation == CPU6502::BusOperation::ReadOpcode) -// { -// printf("%04x: %02x (%d)\n", address, *value, _fieldCycles); -// } - -// const int end_of_field = -// if(frame_cycles_ < (256 + first_graphics_line) << 7)) - - const unsigned int pixel_line_clock = frame_cycles_;// + 128 - first_graphics_cycle + 80; - const unsigned int line_before_cycle = graphics_line(pixel_line_clock); - const unsigned int line_after_cycle = graphics_line(pixel_line_clock + cycles); - - // implicit assumption here: the number of 2Mhz cycles this bus operation will take - // is never longer than a line. On the Electron, it's a safe one. - if(line_before_cycle != line_after_cycle) - { - switch(line_before_cycle) - { -// case real_time_clock_interrupt_line: signal_interrupt(Interrupt::RealTimeClock); break; -// case real_time_clock_interrupt_line+1: clear_interrupt(Interrupt::RealTimeClock); break; - case display_end_interrupt_line: signal_interrupt(Interrupt::DisplayEnd); break; -// case display_end_interrupt_line+1: clear_interrupt(Interrupt::DisplayEnd); break; - } - } - - if( - (pixel_line_clock < real_time_clock_interrupt_1 && pixel_line_clock + cycles >= real_time_clock_interrupt_1) || - (pixel_line_clock < real_time_clock_interrupt_2 && pixel_line_clock + cycles >= real_time_clock_interrupt_2)) - { - signal_interrupt(Interrupt::RealTimeClock); - } - - frame_cycles_ += cycles; - - // deal with frame wraparound by updating the two dependent subsystems - // as though the exact end of frame had been hit, then reset those - // and allow the frame cycle counter to assume its real value - if(frame_cycles_ >= cycles_per_frame) - { - unsigned int nextFrameCycles = frame_cycles_ - cycles_per_frame; - frame_cycles_ = cycles_per_frame; - update_display(); - update_audio(); - display_output_position_ = 0; - audio_output_position_ = 0; - frame_cycles_ = nextFrameCycles; - } - - if(!(frame_cycles_&16383)) - update_audio(); + cycles_since_display_update_ += cycles; + cycles_since_audio_update_ += cycles; + if(cycles_since_audio_update_ > 16384) update_audio(); tape_.run_for_cycles(cycles); + cycles_until_display_interrupt_ -= cycles; + if(cycles_until_display_interrupt_ < 0) + { + signal_interrupt(next_display_interrupt_); + update_display(); + queue_next_display_interrupt(); + } + if(typer_) typer_->update((int)cycles); if(plus3_) plus3_->run_for_cycles(4*cycles); @@ -482,66 +409,36 @@ void Machine::synchronise() speaker_->flush(); } -void Machine::configure_as_target(const StaticAnalyser::Target &target) +#pragma mark - Deferred scheduling + +inline void Machine::update_display() { - if(target.tapes.size()) + if(cycles_since_display_update_) { - tape_.set_tape(target.tapes.front()); - } - - if(target.disks.size()) - { - plus3_.reset(new Plus3); - - if(target.acorn.has_dfs) - { - set_rom(ROMSlot0, dfs_, true); - } - if(target.acorn.has_adfs) - { - set_rom(ROMSlot4, adfs_, true); - set_rom(ROMSlot5, std::vector(adfs_.begin() + 16384, adfs_.end()), true); - } - - plus3_->set_disk(target.disks.front(), 0); - } - - ROMSlot slot = ROMSlot12; - for(std::shared_ptr cartridge : target.cartridges) - { - set_rom(slot, cartridge->get_segments().front().data, false); - slot = (ROMSlot)(((int)slot + 1)&15); - } - - if(target.loadingCommand.length()) // TODO: and automatic loading option enabled - { - set_typer_for_string(target.loadingCommand.c_str()); - } - if(target.acorn.should_hold_shift) - { - set_key_state(KeyShift, true); - is_holding_shift_ = true; + video_output_->run_for_cycles((int)cycles_since_display_update_); + cycles_since_display_update_ = 0; } } -void Machine::set_rom(ROMSlot slot, std::vector data, bool is_writeable) +inline void Machine::queue_next_display_interrupt() { - uint8_t *target = nullptr; - switch(slot) - { - case ROMSlotDFS: dfs_ = data; return; - case ROMSlotADFS: adfs_ = data; return; - - case ROMSlotOS: target = os_; break; - default: - target = roms_[slot]; - rom_write_masks_[slot] = is_writeable; - break; - } - - memcpy(target, &data[0], std::min((size_t)16384, data.size())); + VideoOutput::Interrupt next_interrupt = video_output_->get_next_interrupt(); + cycles_until_display_interrupt_ = next_interrupt.cycles; + next_display_interrupt_ = next_interrupt.interrupt; } +inline void Machine::update_audio() +{ + if(cycles_since_audio_update_) + { + unsigned int difference = cycles_since_audio_update_ / Speaker::clock_rate_divider; + cycles_since_audio_update_ %= Speaker::clock_rate_divider; + speaker_->run_for_cycles(difference); + } +} + +#pragma mark - Interrupts + inline void Machine::signal_interrupt(Electron::Interrupt interrupt) { interrupt_status_ |= interrupt; @@ -554,12 +451,6 @@ inline void Machine::clear_interrupt(Electron::Interrupt interrupt) evaluate_interrupts(); } -void Machine::tape_did_change_interrupt_status(Tape *tape) -{ - interrupt_status_ = (interrupt_status_ & ~(Interrupt::TransmitDataEmpty | Interrupt::ReceiveDataFull | Interrupt::HighToneDetect)) | tape_.get_interrupt_status(); - evaluate_interrupts(); -} - inline void Machine::evaluate_interrupts() { if(interrupt_status_ & interrupt_control_) @@ -573,355 +464,10 @@ inline void Machine::evaluate_interrupts() set_irq_line(interrupt_status_ & 1); } -inline void Machine::update_audio() +#pragma mark - Tape::Delegate + +void Machine::tape_did_change_interrupt_status(Tape *tape) { - unsigned int difference = frame_cycles_ - audio_output_position_ + audio_output_position_error_; - audio_output_position_ = frame_cycles_; - speaker_->run_for_cycles(difference / Speaker::clock_rate_divider); - audio_output_position_error_ = difference % Speaker::clock_rate_divider; -} - -inline void Machine::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; -} - -inline void Machine::end_pixel_line() -{ - if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_); - current_character_row_++; -} - -inline void Machine::output_pixels(unsigned int number_of_cycles) -{ - if(!number_of_cycles) return; - - if(is_blank_line_) - { - crt_->output_blank(number_of_cycles * crt_cycles_multiplier); - } - else - { - unsigned int divider = 0; - switch(screen_mode_) - { - case 0: case 3: divider = 2; break; - case 1: case 4: case 6: divider = 4; break; - case 2: case 5: divider = 8; break; - } - - if(!initial_output_target_ || divider != current_output_divider_) - { - if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_); - current_output_divider_ = divider; - initial_output_target_ = current_output_target_ = crt_->allocate_write_area(640 / current_output_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(); - *(uint32_t *)current_output_target_ = palette_tables_.eighty1bpp[last_pixel_byte_]; - current_output_target_ += 4; - current_pixel_column_++; - } - } else current_output_target_ += 4*number_of_cycles; - break; - - case 1: - if(initial_output_target_) - { - while(number_of_cycles--) - { - get_pixel(); - *(uint16_t *)current_output_target_ = palette_tables_.eighty2bpp[last_pixel_byte_]; - current_output_target_ += 2; - current_pixel_column_++; - } - } else current_output_target_ += 2*number_of_cycles; - break; - - case 2: - if(initial_output_target_) - { - while(number_of_cycles--) - { - get_pixel(); - *current_output_target_ = palette_tables_.eighty4bpp[last_pixel_byte_]; - current_output_target_ += 1; - current_pixel_column_++; - } - } else current_output_target_ += number_of_cycles; - break; - - case 4: case 6: - if(initial_output_target_) - { - if(current_pixel_column_&1) - { - last_pixel_byte_ <<= 4; - *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 2; - - number_of_cycles--; - current_pixel_column_++; - } - while(number_of_cycles > 1) - { - get_pixel(); - *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 2; - - last_pixel_byte_ <<= 4; - *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 2; - - number_of_cycles -= 2; - current_pixel_column_+=2; - } - if(number_of_cycles) - { - get_pixel(); - *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; - current_output_target_ += 2; - current_pixel_column_++; - } - } else current_output_target_ += 2 * number_of_cycles; - break; - - case 5: - if(initial_output_target_) - { - if(current_pixel_column_&1) - { - last_pixel_byte_ <<= 2; - *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 1; - - number_of_cycles--; - current_pixel_column_++; - } - while(number_of_cycles > 1) - { - get_pixel(); - *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 1; - - last_pixel_byte_ <<= 2; - *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 1; - - number_of_cycles -= 2; - current_pixel_column_+=2; - } - if(number_of_cycles) - { - get_pixel(); - *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; - current_output_target_ += 1; - current_pixel_column_++; - } - } else current_output_target_ += number_of_cycles; - break; - } - -#undef get_pixel - } -} - -inline void Machine::update_display() -{ - /* - - Odd field: Even field: - - |--S--| -S-| - |--S--| |--S--| - |-S-B-| = 3 |--S--| = 2.5 - |--B--| |--B--| - |--P--| |--P--| - |--B--| = 312 |--B--| = 312.5 - |-B- - - */ - - int final_line = frame_cycles_ >> 7; - while(display_output_position_ < frame_cycles_) - { - int line = display_output_position_ >> 7; - - // Priority one: sync. - // =================== - - // full sync lines are 0, 1, field_divider_line+1 and field_divider_line+2 - if(line == 0 || line == 1 || line == field_divider_line+1 || line == field_divider_line+2) - { - // wait for the line to complete before signalling - if(final_line == line) return; - crt_->output_sync(128 * crt_cycles_multiplier); - display_output_position_ += 128; - continue; - } - - // line 2 is a left-sync line - if(line == 2) - { - // wait for the line to complete before signalling - if(final_line == line) return; - crt_->output_sync(64 * crt_cycles_multiplier); - crt_->output_blank(64 * crt_cycles_multiplier); - display_output_position_ += 128; - continue; - } - - // line field_divider_line is a right-sync line - if(line == field_divider_line) - { - // wait for the line to complete before signalling - if(final_line == line) return; - crt_->output_sync(9 * crt_cycles_multiplier); - crt_->output_blank(55 * crt_cycles_multiplier); - crt_->output_sync(64 * crt_cycles_multiplier); - display_output_position_ += 128; - continue; - } - - // Priority two: blank lines. - // ========================== - // - // Given that it is not a sync line, this is a blank line if it is less than first_graphics_line, or greater - // than first_graphics_line+255 and less than first_graphics_line+field_divider_line, or greater than - // first_graphics_line+field_divider_line+255 (TODO: or this is Mode 3 or 6 and this should be blank) - if( - line < first_graphics_line || - (line > first_graphics_line+255 && line < first_graphics_line+field_divider_line) || - line > first_graphics_line+field_divider_line+255) - { - if(final_line == line) return; - crt_->output_sync(9 * crt_cycles_multiplier); - crt_->output_blank(119 * crt_cycles_multiplier); - display_output_position_ += 128; - continue; - } - - // Final possibility: this is a pixel line. - // ======================================== - - // determine how far we're going from left to right - unsigned int this_cycle = display_output_position_&127; - unsigned int final_cycle = frame_cycles_&127; - if(final_line > line) - { - final_cycle = 128; - } - - // 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 - while(this_cycle < final_cycle) - { - if(this_cycle < 9) - { - if(final_cycle < 9) return; - crt_->output_sync(9 * crt_cycles_multiplier); - display_output_position_ += 9; - this_cycle = 9; - } - - if(this_cycle < 24) - { - if(final_cycle < 24) return; - crt_->output_default_colour_burst((24-9) * crt_cycles_multiplier); - display_output_position_ += 24-9; - this_cycle = 24; - // TODO: phase shouldn't be zero on every line - } - - if(this_cycle < first_graphics_cycle) - { - if(final_cycle < first_graphics_cycle) return; - crt_->output_blank((first_graphics_cycle - 24) * crt_cycles_multiplier); - display_output_position_ += first_graphics_cycle - 24; - this_cycle = first_graphics_cycle; - start_pixel_line(); - } - - if(this_cycle < first_graphics_cycle + 80) - { - unsigned int length_to_output = std::min(final_cycle, (first_graphics_cycle + 80)) - this_cycle; - output_pixels(length_to_output); - display_output_position_ += length_to_output; - this_cycle += length_to_output; - } - - if(this_cycle >= first_graphics_cycle + 80) - { - if(final_cycle < 128) return; - end_pixel_line(); - crt_->output_blank((128 - (first_graphics_cycle + 80)) * crt_cycles_multiplier); - display_output_position_ += 128 - (first_graphics_cycle + 80); - this_cycle = 128; - } - } - } -} - -void Machine::clear_all_keys() -{ - memset(key_states_, 0, sizeof(key_states_)); -} - -void Machine::set_key_state(uint16_t key, bool isPressed) -{ - if(key == KeyBreak) - { - set_reset_line(isPressed); - } - else - { - if(isPressed) - key_states_[key >> 4] |= key&0xf; - else - key_states_[key >> 4] &= ~(key&0xf); - } + interrupt_status_ = (interrupt_status_ & ~(Interrupt::TransmitDataEmpty | Interrupt::ReceiveDataFull | Interrupt::HighToneDetect)) | tape_.get_interrupt_status(); + evaluate_interrupts(); } diff --git a/Machines/Electron/Electron.hpp b/Machines/Electron/Electron.hpp index 980de1e7d..543d6abcc 100644 --- a/Machines/Electron/Electron.hpp +++ b/Machines/Electron/Electron.hpp @@ -15,10 +15,12 @@ #include "../ConfigurationTarget.hpp" #include "../CRTMachine.hpp" #include "../Typer.hpp" + +#include "Interrupts.hpp" #include "Plus3.hpp" #include "Speaker.hpp" #include "Tape.hpp" -#include "Interrupts.hpp" +#include "Video.hpp" #include #include @@ -92,8 +94,8 @@ class Machine: // to satisfy CRTMachine::Machine virtual void setup_output(float aspect_ratio); virtual void close_output(); - virtual std::shared_ptr get_crt() { return crt_; } - virtual std::shared_ptr get_speaker() { return speaker_; } + virtual std::shared_ptr get_crt(); + virtual std::shared_ptr get_speaker(); virtual void run_for_cycles(int number_of_cycles) { CPU6502::Processor::run_for_cycles(number_of_cycles); } // to satisfy Tape::Delegate @@ -105,13 +107,10 @@ class Machine: uint16_t *sequence_for_character(Utility::Typer *typer, char character); private: - inline void update_display(); - inline void start_pixel_line(); - inline void end_pixel_line(); - inline void output_pixels(unsigned int number_of_cycles); - + inline void queue_next_display_interrupt(); inline void update_audio(); + inline void signal_interrupt(Interrupt interrupt); inline void clear_interrupt(Interrupt interrupt); inline void evaluate_interrupts(); @@ -122,37 +121,20 @@ class Machine: uint8_t os_[16384], ram_[32768]; std::vector dfs_, adfs_; - // Things affected by registers, explicitly or otherwise. - uint8_t interrupt_status_, interrupt_control_; - uint8_t palette_[16]; - uint8_t key_states_[14]; + // Paging ROMSlot active_rom_; bool keyboard_is_active_, basic_is_active_; - uint8_t screen_mode_; - uint16_t screen_mode_base_address_; - uint16_t start_screen_address_; + + // Interrupt and keyboard state + uint8_t interrupt_status_, interrupt_control_; + uint8_t key_states_[14]; // Counters related to simultaneous subsystems - unsigned int frame_cycles_, display_output_position_; - unsigned int audio_output_position_, audio_output_position_error_; - - struct { - uint16_t forty1bpp[256]; - uint8_t forty2bpp[256]; - uint32_t eighty1bpp[256]; - uint16_t eighty2bpp[256]; - uint8_t eighty4bpp[256]; - } palette_tables_; - - // Display generation. - uint16_t start_line_address_, current_screen_address_; - int current_pixel_line_, current_pixel_column_, current_character_row_; - uint8_t last_pixel_byte_; - bool is_blank_line_; - - // CRT output - uint8_t *current_output_target_, *initial_output_target_; - unsigned int current_output_divider_; + unsigned int cycles_since_display_update_; + unsigned int cycles_since_audio_update_; + int cycles_until_display_interrupt_; + Interrupt next_display_interrupt_; + VideoOutput::Range video_access_range_; // Tape Tape tape_; @@ -164,7 +146,7 @@ class Machine: bool is_holding_shift_; // Outputs - std::shared_ptr crt_; + std::unique_ptr video_output_; std::shared_ptr speaker_; bool speaker_is_enabled_; }; diff --git a/Machines/Electron/Video.cpp b/Machines/Electron/Video.cpp new file mode 100644 index 000000000..b3465ef3b --- /dev/null +++ b/Machines/Electron/Video.cpp @@ -0,0 +1,511 @@ +// +// Video.cpp +// Clock Signal +// +// Created by Thomas Harte on 10/12/2016. +// Copyright © 2016 Thomas Harte. All rights reserved. +// + +#include "Video.hpp" + +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 { + static const int cycles_per_line = 128; + static const int lines_per_frame = 625; + static const int cycles_per_frame = lines_per_frame * cycles_per_line; + static const int crt_cycles_multiplier = 8; + static const int crt_cycles_per_line = crt_cycles_multiplier * cycles_per_line; + + static const 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 + static const int first_graphics_line = 31; + static const int first_graphics_cycle = 33; + + static const int display_end_interrupt_line = 256; + + static const int real_time_clock_interrupt_1 = 16704; + static const int real_time_clock_interrupt_2 = 56704; + static const int display_end_interrupt_1 = (first_graphics_line + display_end_interrupt_line)*cycles_per_line; + static const int display_end_interrupt_2 = (first_graphics_line + field_divider_line + display_end_interrupt_line)*cycles_per_line; +} + +#pragma mark - Lifecycle + +VideoOutput::VideoOutput(uint8_t *memory) : + ram_(memory), + current_pixel_line_(-1), + output_position_(0), + screen_mode_(6), + screen_map_pointer_(0), + cycles_into_draw_action_(0) +{ + memset(palette_, 0xf, sizeof(palette_)); + setup_screen_map(); + + crt_.reset(new Outputs::CRT::CRT(crt_cycles_per_line, 8, Outputs::CRT::DisplayType::PAL50, 1)); + crt_->set_rgb_sampling_function( + "vec3 rgb_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate)" + "{" + "uint texValue = texture(sampler, coordinate).r;" + "texValue >>= 4 - (int(icoordinate.x * 8) & 4);" + "return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));" + "}"); + // 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 - 3, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f)); +} + +#pragma mark - CRT getter + +std::shared_ptr VideoOutput::get_crt() +{ + return crt_; +} + +#pragma 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() +{ + if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_); + current_character_row_++; +} + +void VideoOutput::output_pixels(unsigned int number_of_cycles) +{ + if(!number_of_cycles) return; + + if(is_blank_line_) + { + crt_->output_blank(number_of_cycles * crt_cycles_multiplier); + } + else + { + unsigned int divider = 0; + switch(screen_mode_) + { + case 0: case 3: divider = 2; break; + case 1: case 4: case 6: divider = 4; break; + case 2: case 5: divider = 8; break; + } + + if(!initial_output_target_ || divider != current_output_divider_) + { + if(current_output_target_) crt_->output_data((unsigned int)((current_output_target_ - initial_output_target_) * current_output_divider_), current_output_divider_); + current_output_divider_ = divider; + initial_output_target_ = current_output_target_ = crt_->allocate_write_area(640 / current_output_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(); + *(uint32_t *)current_output_target_ = palette_tables_.eighty1bpp[last_pixel_byte_]; + current_output_target_ += 4; + current_pixel_column_++; + } + } else current_output_target_ += 4*number_of_cycles; + break; + + case 1: + if(initial_output_target_) + { + while(number_of_cycles--) + { + get_pixel(); + *(uint16_t *)current_output_target_ = palette_tables_.eighty2bpp[last_pixel_byte_]; + current_output_target_ += 2; + current_pixel_column_++; + } + } else current_output_target_ += 2*number_of_cycles; + break; + + case 2: + if(initial_output_target_) + { + while(number_of_cycles--) + { + get_pixel(); + *current_output_target_ = palette_tables_.eighty4bpp[last_pixel_byte_]; + current_output_target_ += 1; + current_pixel_column_++; + } + } else current_output_target_ += number_of_cycles; + break; + + case 4: case 6: + if(initial_output_target_) + { + if(current_pixel_column_&1) + { + last_pixel_byte_ <<= 4; + *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; + current_output_target_ += 2; + + number_of_cycles--; + current_pixel_column_++; + } + while(number_of_cycles > 1) + { + get_pixel(); + *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; + current_output_target_ += 2; + + last_pixel_byte_ <<= 4; + *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; + current_output_target_ += 2; + + number_of_cycles -= 2; + current_pixel_column_+=2; + } + if(number_of_cycles) + { + get_pixel(); + *(uint16_t *)current_output_target_ = palette_tables_.forty1bpp[last_pixel_byte_]; + current_output_target_ += 2; + current_pixel_column_++; + } + } else current_output_target_ += 2 * number_of_cycles; + break; + + case 5: + if(initial_output_target_) + { + if(current_pixel_column_&1) + { + last_pixel_byte_ <<= 2; + *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; + current_output_target_ += 1; + + number_of_cycles--; + current_pixel_column_++; + } + while(number_of_cycles > 1) + { + get_pixel(); + *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; + current_output_target_ += 1; + + last_pixel_byte_ <<= 2; + *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; + current_output_target_ += 1; + + number_of_cycles -= 2; + current_pixel_column_+=2; + } + if(number_of_cycles) + { + get_pixel(); + *current_output_target_ = palette_tables_.forty2bpp[last_pixel_byte_]; + current_output_target_ += 1; + current_pixel_column_++; + } + } else current_output_target_ += number_of_cycles; + break; + } + +#undef get_pixel + } +} + +void VideoOutput::run_for_cycles(int number_of_cycles) +{ + output_position_ = (output_position_ + number_of_cycles) % cycles_per_frame; + 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((unsigned int)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((unsigned int)(draw_action_length * crt_cycles_multiplier)); break; + case DrawAction::ColourBurst: crt_->output_default_colour_burst((unsigned int)(draw_action_length * crt_cycles_multiplier)); break; + case DrawAction::Blank: crt_->output_blank((unsigned int)(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(); + } + } +} + +#pragma mark - Register hub + +void VideoOutput::set_register(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; + break; + case 0x03: + start_screen_address_ = (start_screen_address_ & 0x01ff) | (uint16_t)((value & 0x3f) << 9); + if(!start_screen_address_) start_screen_address_ |= 0x8000; + 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; + 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; + } + } + } + break; + case 0x08: case 0x09: case 0x0a: case 0x0b: + case 0x0c: case 0x0d: case 0x0e: case 0x0f: + { + static const 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 +#define pack(a, b) (uint8_t)((a << 4) | (b)) + for(int byte = 0; byte < 256; byte++) + { + uint8_t *target = (uint8_t *)&palette_tables_.forty1bpp[byte]; + target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]); + target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]); + + target = (uint8_t *)&palette_tables_.eighty2bpp[byte]; + target[0] = pack(palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]); + target[1] = pack(palette_[((byte&0x20) >> 2) | ((byte&0x02) >> 0)], palette_[((byte&0x10) >> 1) | ((byte&0x01) << 1)]); + + target = (uint8_t *)&palette_tables_.eighty1bpp[byte]; + target[0] = pack(palette_[(byte&0x80) >> 4], palette_[(byte&0x40) >> 3]); + target[1] = pack(palette_[(byte&0x20) >> 2], palette_[(byte&0x10) >> 1]); + target[2] = pack(palette_[(byte&0x08) >> 0], palette_[(byte&0x04) << 1]); + target[3] = pack(palette_[(byte&0x02) << 2], palette_[(byte&0x01) << 3]); + + palette_tables_.forty2bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x08) >> 2)], palette_[((byte&0x40) >> 3) | ((byte&0x04) >> 1)]); + palette_tables_.eighty4bpp[byte] = pack( palette_[((byte&0x80) >> 4) | ((byte&0x20) >> 3) | ((byte&0x08) >> 2) | ((byte&0x02) >> 1)], + palette_[((byte&0x40) >> 3) | ((byte&0x10) >> 2) | ((byte&0x04) >> 1) | ((byte&0x01) >> 0)]); + } +#undef pack + } + break; + } +} + +#pragma mark - Interrupts + +VideoOutput::Interrupt VideoOutput::get_next_interrupt() +{ + VideoOutput::Interrupt interrupt; + + if(output_position_ < real_time_clock_interrupt_1) + { + interrupt.cycles = real_time_clock_interrupt_1 - output_position_; + interrupt.interrupt = RealTimeClock; + return interrupt; + } + + if(output_position_ < display_end_interrupt_1) + { + interrupt.cycles = display_end_interrupt_1 - output_position_; + interrupt.interrupt = DisplayEnd; + return interrupt; + } + + if(output_position_ < real_time_clock_interrupt_2) + { + interrupt.cycles = real_time_clock_interrupt_2 - output_position_; + interrupt.interrupt = RealTimeClock; + return interrupt; + } + + if(output_position_ < display_end_interrupt_2) + { + interrupt.cycles = display_end_interrupt_2 - output_position_; + interrupt.interrupt = DisplayEnd; + return interrupt; + } + + interrupt.cycles = real_time_clock_interrupt_1 + cycles_per_frame - output_position_; + interrupt.interrupt = RealTimeClock; + return interrupt; +} + +#pragma 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; + + result += 1 + (position&1); + 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) + { + if(screen_mode_ == 3) + { + int output_position_line = graphics_line(output_position_); + int implied_row = current_character_row_ + (current_line - output_position_line) % 10; + if(implied_row < 8) + result += (unsigned int)(80 - current_column); + } + else + result += (unsigned int)(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; +} + +#pragma 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 c = 0; c < first_graphics_line - 3; c++) emplace_blank_line(); + for(int c = 0; c < 256; c++) emplace_pixel_line(); + for(int c = 256 + first_graphics_line; c < 312; c++) 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/Electron/Video.hpp b/Machines/Electron/Video.hpp new file mode 100644 index 000000000..b53d67b51 --- /dev/null +++ b/Machines/Electron/Video.hpp @@ -0,0 +1,126 @@ +// +// Video.hpp +// Clock Signal +// +// Created by Thomas Harte on 10/12/2016. +// Copyright © 2016 Thomas Harte. All rights reserved. +// + +#ifndef Machines_Electron_Video_hpp +#define Machines_Electron_Video_hpp + +#include "../../Outputs/CRT/CRT.hpp" +#include "Interrupts.hpp" + +namespace Electron { + +/*! + Implements the Electron's video subsystem plus appropriate signalling. + + The Electron has an interlaced fully-bitmapped display with six different output modes, + running either at 40 or 80 columns. Memory is shared between video and CPU; when the video + is accessing it the CPU may not. +*/ +class VideoOutput { + public: + /*! + Instantiates a VideoOutput that will read its pixels from @c memory. The pointer supplied + should be to address 0 in the unexpanded Electron's memory map. + */ + VideoOutput(uint8_t *memory); + + /// @returns the CRT to which output is being painted. + std::shared_ptr get_crt(); + + /// Produces the next @c number_of_cycles cycles of video output. + void run_for_cycles(int number_of_cycles); + + /*! + 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 set_register(int address, uint8_t value); + + /*! + Describes an interrupt the video hardware will generate by its identity and scheduling time. + */ + struct Interrupt { + /// The interrupt that will be signalled. + Electron::Interrupt interrupt; + /// The number of cycles until it is signalled. + int cycles; + }; + /*! + @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_cycles. + + This result may be mutated by calls to @c set_register. + */ + Interrupt get_next_interrupt(); + + /*! + @returns the number of cycles after (final cycle of last run_for_cycles 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(unsigned int number_of_cycles); + + int output_position_, unused_cycles_; + + uint8_t palette_[16]; + uint8_t screen_mode_; + uint16_t screen_mode_base_address_; + uint16_t start_screen_address_; + + uint8_t *ram_; + struct { + uint16_t forty1bpp[256]; + uint8_t forty2bpp[256]; + uint32_t eighty1bpp[256]; + uint16_t eighty2bpp[256]; + uint8_t eighty4bpp[256]; + } palette_tables_; + + // Display generation. + uint16_t start_line_address_, current_screen_address_; + int current_pixel_line_, current_pixel_column_, current_character_row_; + uint8_t last_pixel_byte_; + bool is_blank_line_; + + // CRT output + uint8_t *current_output_target_, *initial_output_target_; + unsigned int current_output_divider_; + + std::shared_ptr 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(); + size_t screen_map_pointer_; + int cycles_into_draw_action_; +}; + +} + +#endif /* Video_hpp */ diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 86fbb6bba..ac97ad808 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -62,6 +62,7 @@ 4B69FB441C4D941400B5F0AA /* TapeUEF.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B69FB421C4D941400B5F0AA /* TapeUEF.cpp */; }; 4B69FB461C4D950F00B5F0AA /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B69FB451C4D950F00B5F0AA /* libz.tbd */; }; 4B6C73BD1D387AE500AFCFCA /* DiskController.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */; }; + 4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B7913CA1DFCD80E00175A82 /* Video.cpp */; }; 4B8805F01DCFC99C003085B1 /* Acorn.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805EE1DCFC99C003085B1 /* Acorn.cpp */; }; 4B8805F41DCFD22A003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F21DCFD22A003085B1 /* Commodore.cpp */; }; 4B8805F71DCFF6C9003085B1 /* Commodore.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8805F51DCFF6C9003085B1 /* Commodore.cpp */; }; @@ -522,6 +523,8 @@ 4B69FB451C4D950F00B5F0AA /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 4B6C73BB1D387AE500AFCFCA /* DiskController.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = DiskController.cpp; sourceTree = ""; }; 4B6C73BC1D387AE500AFCFCA /* DiskController.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = DiskController.hpp; sourceTree = ""; }; + 4B7913CA1DFCD80E00175A82 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = Electron/Video.cpp; sourceTree = ""; }; + 4B7913CB1DFCD80E00175A82 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = Electron/Video.hpp; sourceTree = ""; }; 4B8805EE1DCFC99C003085B1 /* Acorn.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Acorn.cpp; path = Parsers/Acorn.cpp; sourceTree = ""; }; 4B8805EF1DCFC99C003085B1 /* Acorn.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Acorn.hpp; path = Parsers/Acorn.hpp; sourceTree = ""; }; 4B8805F21DCFD22A003085B1 /* Commodore.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Commodore.cpp; path = Parsers/Commodore.cpp; sourceTree = ""; }; @@ -1070,11 +1073,13 @@ 4BEA52611DF339D7007E74F2 /* Speaker.cpp */, 4BEA525D1DF33323007E74F2 /* Tape.cpp */, 4BC8A62B1DCE60E000DAC693 /* Typer.cpp */, + 4B7913CA1DFCD80E00175A82 /* Video.cpp */, 4B2E2D9C1C3A070400138695 /* Electron.hpp */, 4BEA52601DF3343A007E74F2 /* Interrupts.hpp */, 4B30512F1D98ACC600B4FED8 /* Plus3.hpp */, 4BEA52621DF339D7007E74F2 /* Speaker.hpp */, 4BEA525F1DF333D8007E74F2 /* Tape.hpp */, + 4B7913CB1DFCD80E00175A82 /* Video.hpp */, ); name = Electron; sourceTree = ""; @@ -2353,6 +2358,7 @@ 4B2A332A1DB8544D002876E3 /* MemoryFuzzer.cpp in Sources */, 4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */, 4B2A332F1DB86869002876E3 /* OricOptionsPanel.swift in Sources */, + 4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */, 4B2A53A11D117D36003C6002 /* CSAtari2600.mm in Sources */, 4BF829661D8F732B001BAE39 /* Disk.cpp in Sources */, 4BEA52631DF339D7007E74F2 /* Speaker.cpp in Sources */,