1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-08-09 05:25:01 +00:00
Files
CLK/Machines/Commodore/Plus4/Video.hpp
2024-12-16 22:11:06 -05:00

516 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// Video.hpp
// Clock Signal
//
// Created by Thomas Harte on 10/12/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#pragma once
#include "Interrupts.hpp"
#include "Pager.hpp"
#include "../../../Numeric/UpperBound.hpp"
#include "../../../Outputs/CRT/CRT.hpp"
#include <array>
namespace Commodore::Plus4 {
struct Video {
public:
Video(const Commodore::Plus4::Pager &pager, Interrupts &interrupts) :
crt_(465, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Luminance8Phase8),
pager_(pager),
interrupts_(interrupts)
{
// TODO: perfect crop.
crt_.set_visible_area(Outputs::Display::Rect(0.075f, 0.065f, 0.85f, 0.85f));
}
template <uint16_t address>
uint8_t read() const {
switch(address) {
case 0xff06: return ff06_;
case 0xff07: return ff07_;
case 0xff0b: return uint8_t(raster_interrupt_);
case 0xff1c: return uint8_t(vertical_counter_ >> 8);
case 0xff1d: return uint8_t(vertical_counter_);
case 0xff14: return uint8_t((screen_memory_address_ >> 8) & 0xf8);
case 0xff15: case 0xff16: case 0xff17: case 0xff18: case 0xff19:
return raw_background_[size_t(address - 0xff15)];
}
return 0xff;
}
template <uint16_t address>
void write(const uint8_t value) {
const auto load_high10 = [&](uint16_t &target) {
target = uint16_t(
(target & 0x00ff) | ((value & 0x3) << 8)
);
};
const auto load_low8 = [&](uint16_t &target) {
target = uint16_t(
(target & 0xff00) | value
);
};
switch(address) {
case 0xff06:
ff06_ = value;
extended_colour_mode_ = value & 0x40;
bitmap_mode_ = value & 0x20;
display_enable_ = value & 0x10;
rows_25_ = value & 8;
y_scroll_ = value & 7;
break;
case 0xff07:
ff07_ = value;
characters_256_ = value & 0x80;
is_ntsc_ = value & 0x40;
ted_off_ = value & 0x20;
multicolour_mode_ = value & 0x10;
columns_40_ = value & 8;
x_scroll_ = value & 7;
break;
case 0xff12:
// bitmap_base_ = uint16_t((value & 0x3c) << 10);
break;
case 0xff13:
character_generator_address_ = uint16_t((value & 0xfc) << 8);
single_clock_ = value & 0x02;
break;
case 0xff14:
screen_memory_address_ = uint16_t((value & 0xf8) << 8);
break;
case 0xff0a:
raster_interrupt_ = (raster_interrupt_ & 0x00ff) | ((value & 1) << 8);
break;
case 0xff0b:
raster_interrupt_ = (raster_interrupt_ & 0xff00) | value;
break;
case 0xff0c: load_high10(cursor_address_); break;
case 0xff0d: load_low8(cursor_address_); break;
case 0xff1a: load_high10(character_row_address_); break;
case 0xff1b: load_low8(character_row_address_); break;
case 0xff15: case 0xff16: case 0xff17: case 0xff18: case 0xff19: {
raw_background_[size_t(address - 0xff15)] = value;
const uint8_t luminance = (value & 0x0f) ? uint8_t(
((value & 0x70) << 1) | ((value & 0x70) >> 2) | ((value & 0x70) >> 5)
) : 0;
background_[size_t(address - 0xff15)] = uint16_t(
luminance | (chrominances[value & 0x0f] << 8)
);
} break;
}
}
Cycles cycle_length([[maybe_unused]] bool is_ready) const {
if(is_ready) {
// return
// Cycles(EndCharacterFetchWindow - horizontal_counter_ + EndOfLine) * is_ntsc_ ? Cycles(4) : Cycles(5) / 2;
}
const bool is_long_cycle = single_clock_ || refresh_ || external_fetch_;
if(is_ntsc_) {
return is_long_cycle ? Cycles(16) : Cycles(8);
} else {
return is_long_cycle ? Cycles(20) : Cycles(10);
}
}
Cycles timer_cycle_length() const {
return is_ntsc_ ? Cycles(16) : Cycles(20);
}
// Outer clock is [NTSC or PAL] colour subcarrier * 2.
//
// 65 cycles = 64µs?
// 65*262*60 = 1021800
//
// In an NTSC television system. 262 raster lines are produced (0 to 261), 312 for PAL (0311).
//
// An interrupt is generated 8 cycles before the character window. For a 25 row display, the visible
// raster lines are from 4 to 203.
//
// The horizontal position register counts 456 dots, 0 to 455.
void run_for(Cycles cycles) {
// Timing:
//
// Input clock is at 17.7Mhz PAL or 14.38Mhz NTSC. i.e. each is four times the colour subcarrier.
//
// In PAL mode, divide by 5 and multiply by 2 to get the internal pixel clock.
//
// In NTSC mode just dividing by 2 would do to get the pixel clock but in practice that's implemented as
// a divide by 4 and a multiply by 2 to keep it similar to the PAL code.
//
// That gives close enough to 456 pixel clocks per line in both systems so the TED just rolls with that.
// See page 34 of plus4_tech.pdf for event times.
subcycles_ += cycles * 2;
auto ticks_remaining = subcycles_.divide(is_ntsc_ ? Cycles(4) : Cycles(5)).as<int>();
while(ticks_remaining) {
//
// Test vertical first; this will catch both any programmed change that has occurred outside
// of the loop and any change to the vertical counter that occurs during the horizontal runs.
//
const auto attribute_fetch_start = [&] {
character_address_ = 0;
};
switch(vertical_counter_) {
case 261: // End of screen NTSC. [and hence 0: Attribute fetch start].
if(is_ntsc_) {
vertical_counter_ = 0;
attribute_fetch_start();
}
break;
case 311: // End of screen PAL. [and hence 0: Attribute fetch start].
if(!is_ntsc_) {
vertical_counter_ = 0;
attribute_fetch_start();
}
break;
case 203: // Attribute fetch end. But I think this might be fairly nominal, assuming attribute fetches
// are triggered by testing against y scroll.
break;
case 4: if(rows_25_) vertical_window_ = true; break; // Vertical screen window start (25 lines).
case 204: if(rows_25_) vertical_window_ = false; break; // Vertical screen window stop (25 lines).
case 8: if(!rows_25_) vertical_window_ = true; break; // Vertical screen window start (24 lines).
case 200: if(!rows_25_) vertical_window_ = false; break; // Vertical screen window stop (24 lines).
case 226: if(is_ntsc_) vertical_blank_ = true; break; // NTSC vertical blank start.
case 229: if(is_ntsc_) vertical_sync_ = true; break; // NTSC vsync start.
case 232: if(is_ntsc_) vertical_sync_ = false; break; // NTSC vsync end.
case 244: if(is_ntsc_) vertical_blank_ = false; break; // NTSC vertical blank end.
case 251: if(!is_ntsc_) vertical_blank_ = true; break; // PAL vertical blank start.
case 254: if(!is_ntsc_) vertical_sync_ = true; break; // PAL vsync start.
case 257: if(!is_ntsc_) vertical_sync_ = false; break; // PAL vsync end.
case 269: if(!is_ntsc_) vertical_blank_ = false; break; // PAL vertical blank end.
}
if(raster_interrupt_ == vertical_counter_) {
interrupts_.apply(Interrupts::Flag::Raster);
}
const auto next = Numeric::upper_bound<
0, Begin38Columns,
EndExternalFetchWindow, LatchCharacterPosition,
EndCharacterFetchWindow, EndVideoShiftRegister,
End38Columns, End40Columns,
EndRefresh, IncrementBlink,
BeginBlank, BeginSync,
TestRasterInterrupt, IncrementVerticalLine,
BeginBurst, EndSync,
BeginExternalFetchWindow, //EndBurst,
EndBlank, IncrementCharacterPositionReload,
BeginCharacterFetchWindow, BeginVideoShiftRegister,
Begin40Columns, EndOfLine
>(horizontal_counter_);
const auto period = std::min(next - horizontal_counter_, ticks_remaining);
//
// Fetch as appropriate.
//
if(fetch_characters_) {
const int start = fetch_count_ >> 3;
const int end = (fetch_count_ + period) >> 3;
fetch_count_ += period;
auto &line = lines_[line_pointer_];
// uint8_t attributes[40];
// uint8_t characters[40];
// } lines_[2];
// int line_pointer_ = 0;
for(int x = start; x < end; x++) {
// line.attributes[x] = pager_.read(character_generator_address_ + x * 8 + (row & 7));
// line.characters[x] = pager_.read(uint16_t(line_character_address_ + x + screen_memory_address_ + 0x400));
}
// switch(fetch_phase_) {
// case FetchPh
// }
}
//
// Output.
//
OutputState state;
if(vertical_sync_ || horizontal_sync_) {
state = OutputState::Sync;
} else if(vertical_blank_ || horizontal_blank_) {
state = horizontal_burst_ ? OutputState::Burst : OutputState::Blank;
} else {
state = vertical_window_ && output_pixels_ ? OutputState::Pixels : OutputState::Border;
}
if(state != output_state_) {
switch(output_state_) {
case OutputState::Blank: crt_.output_blank(time_in_state_); break;
case OutputState::Sync: crt_.output_sync(time_in_state_); break;
case OutputState::Burst: crt_.output_default_colour_burst(time_in_state_); break;
case OutputState::Border: crt_.output_level<uint16_t>(time_in_state_, background_[4]); break;
case OutputState::Pixels: crt_.output_data(time_in_state_, size_t(time_in_state_)); break;
}
time_in_state_ = 0;
output_state_ = state;
if(output_state_ == OutputState::Pixels) {
pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(384, 2));
}
}
// Output pixels.
// TODO: properly. THIS HACKS IN TEXT OUTPUT. IT IS NOT CORRECT. NOT AS TO TIMING, NOT AS TO CONTENT.
if(pixels_) {
for(int c = 0; c < period; c++) {
const auto pixel = time_in_state_ + c;
const auto row = vertical_counter_ - 4;
const auto index = (row >> 3) * 40 + (pixel >> 3);
const uint8_t character = pager_.read(uint16_t(index + screen_memory_address_ + 0x400));
const uint8_t glyph = pager_.read(character_generator_address_ + character * 8 + (row & 7));
pixels_[c] = glyph & (0x80 >> (pixel & 7)) ? 0xff00 : 0xffff;
}
pixels_ += period;
}
time_in_state_ += period;
//
// Advance for current period.
//
horizontal_counter_ += period;
ticks_remaining -= period;
switch(horizontal_counter_) {
case EndExternalFetchWindow:
// TODO: release RDY if it was held.
// TODO: increment character position end.
refresh_ = true;
break;
case BeginExternalFetchWindow:
external_fetch_ = true;
switch(fetch_phase_) {
case FetchPhase::Waiting:
++character_line_;
// TODO: the < 200 is obviously phoney baloney. Figure out what the actual condition is here.
if(vertical_counter_ < 200 && (vertical_counter_&7) == y_scroll_ && vertical_window_) {
fetch_phase_ = FetchPhase::FetchingCharacters;
}
break;
case FetchPhase::FetchingCharacters:
fetch_phase_ = FetchPhase::FetchingAttributes;
character_line_ = 0;
break;
case FetchPhase::FetchingAttributes:
fetch_phase_ = FetchPhase::Waiting;
break;
}
interrupts_.bus().set_ready_line(fetch_phase_ != FetchPhase::Waiting);
horizontal_burst_ = false;
break;
case LatchCharacterPosition:
line_character_address_ = character_address_;
break;
case EndCharacterFetchWindow:
fetch_characters_ = false;
external_fetch_ = false;
interrupts_.bus().set_ready_line(false);
break;
case BeginCharacterFetchWindow:
fetch_characters_ = true;
fetch_count_ = 0;
break;
case Begin38Columns: if(!columns_40_) output_pixels_ = true; break;
case End38Columns: if(!columns_40_) output_pixels_ = false; break;
case Begin40Columns: if(columns_40_) output_pixels_ = true; break;
case End40Columns: if(columns_40_) output_pixels_ = false; break;
case EndRefresh:
refresh_ = false;
break;
case IncrementBlink:
break;
case IncrementVerticalLine:
vertical_counter_ = (vertical_counter_ + 1) & 0x1ff;
break;
case BeginBurst:
horizontal_burst_ = true;
// TODO: rest.
break;
case BeginBlank: horizontal_blank_ = true; break;
case BeginSync: horizontal_sync_ = true; break;
case EndSync: horizontal_sync_ = false; break;
// case EndBurst: horizontal_burst_ = false; break;
case EndBlank: horizontal_blank_ = false; break;
case TestRasterInterrupt:
if(raster_interrupt_ == vertical_counter_) {
interrupts_.apply(Interrupts::Flag::Raster);
}
break;
case IncrementCharacterPositionReload:
break;
case BeginVideoShiftRegister:
break;
case EndOfLine:
horizontal_counter_ = 0;
break;
}
}
}
void set_scan_target(Outputs::Display::ScanTarget *const target) {
crt_.set_scan_target(target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const {
return crt_.get_scaled_scan_status();
}
private:
Outputs::CRT::CRT crt_;
Cycles subcycles_;
// Programmable values.
bool extended_colour_mode_ = false;
bool bitmap_mode_ = false;
bool display_enable_ = false;
bool rows_25_ = false;
int y_scroll_ = 0;
bool characters_256_ = false;
bool is_ntsc_ = false;
bool ted_off_ = false;
bool multicolour_mode_ = false;
bool columns_40_ = false;
int x_scroll_ = 0;
uint16_t cursor_address_ = 0;
uint16_t character_row_address_ = 0;
uint16_t character_generator_address_ = 0;
uint16_t screen_memory_address_ = 0;
int raster_interrupt_ = 0x1ff;
// Field position.
int horizontal_counter_ = 0;
int vertical_counter_ = 0;
// Running state.
bool vertical_blank_ = false;
bool vertical_sync_ = false;
bool vertical_window_ = false;
bool horizontal_blank_ = false;
bool horizontal_sync_ = false;
bool horizontal_burst_ = false;
uint8_t ff06_;
uint16_t character_address_ = 0;
uint16_t line_character_address_ = 0;
bool fetch_characters_ = false;
bool external_fetch_ = false;
bool output_pixels_ = false;
bool refresh_ = false;
bool single_clock_ = false;
uint8_t ff07_;
enum class OutputState {
Blank,
Sync,
Burst,
Border,
Pixels,
} output_state_ = OutputState::Blank;
int time_in_state_ = 0;
uint16_t *pixels_ = nullptr;
std::array<uint16_t, 5> background_{};
std::array<uint8_t, 5> raw_background_{};
const Commodore::Plus4::Pager &pager_;
Interrupts &interrupts_;
// The following aren't accurate; they're eyeballed to be close enough for now in PAL.
static constexpr uint8_t chrominances[] = {
0xff, 0xff,
90, 23, 105, 59,
14, 69, 83, 78,
50, 96, 32, 9,
5, 41,
};
enum class FetchPhase {
Waiting,
FetchingCharacters,
FetchingAttributes,
} fetch_phase_ = FetchPhase::Waiting;
int character_line_ = 0;
int fetch_count_ = 0;
struct LineBuffer {
uint8_t attributes[40];
uint8_t characters[40];
} lines_[2];
uint8_t bitmap_[40];
int line_pointer_ = 0;
// List of events.
enum HorizontalEvent {
Begin38Columns = 3,
EndExternalFetchWindow = 288,
LatchCharacterPosition = 290,
EndCharacterFetchWindow = 300, // 296
EndVideoShiftRegister = 304,
End38Columns = 307,
End40Columns = 315,
EndRefresh = 328,
IncrementBlink = 336,
BeginBlank = 344,
BeginSync = 358,
TestRasterInterrupt = 368,
IncrementVerticalLine = 376,
BeginBurst = 384,
EndSync = 390,
BeginExternalFetchWindow = 408, // 400,
// EndBurst = 408,
EndBlank = 416,
IncrementCharacterPositionReload = 424,
BeginCharacterFetchWindow = 436, // 432,
BeginVideoShiftRegister = 440,
Begin40Columns = 451,
EndOfLine = 456,
};
};
}