Files
2026-05-26 13:52:41 -04:00

371 lines
11 KiB
C++

//
// 6847.hpp
// Clock Signal
//
// Created by Thomas Harte on 04/05/2026.
// Copyright © 2026 Thomas Harte. All rights reserved.
//
#pragma once
#include "ClockReceiver/ClockReceiver.hpp"
#include "Numeric/SegmentCounter.hpp"
#include "Outputs/CRT/CRT.hpp"
#include "Numeric/SizedInt.hpp"
namespace Motorola::MC6847 {
enum class FrameTiming {
/// Official frame timing, as implemented by the chip.
NTSC,
/// Standard PAL adaptation of adding 25 lines before vsync and 25 aftter, as used independently by
/// the Acorn Atom and Dragon 32.
PALPaddedVsync,
};
constexpr bool is_ntsc(const FrameTiming timing) {
return timing == FrameTiming::NTSC;
}
template <FrameTiming timing> struct FrameLayout {
static constexpr int BorderLength = timing == FrameTiming::NTSC ? 27 : 52;
static constexpr int EndOfPixels = 192;
static constexpr int EndOfBottomBorder = EndOfPixels + BorderLength;
static constexpr int EndOfFrontPorch = EndOfBottomBorder + 2;
static constexpr int EndOfSync = EndOfFrontPorch + 2;
static constexpr int EndOfBackPorch = EndOfSync + 12;
static constexpr int EndOfField = EndOfBackPorch + BorderLength;
};
static_assert(FrameLayout<FrameTiming::NTSC>::EndOfField == 262, "NTSC frames should be 262 lines long");
static_assert(FrameLayout<FrameTiming::PALPaddedVsync>::EndOfField == 312, "PAL-padded frames should be 312 lines long");
namespace Mode {
using IntT = uint8_t;
constexpr IntT Semigraphics = 0b1000'0000;
constexpr IntT Graphics = 0b0100'0000;
constexpr IntT ExternalROM = 0b0010'0000;
constexpr IntT Invert = 0b0001'0000;
constexpr IntT ColourSelect = 0b0000'1000;
// ... 0b0000'0100 unused ...
constexpr IntT BPP2 = 0b0000'0010;
constexpr IntT Columns16 = 0b0000'0001;
/// Maps between [GM2, GM1, GM0] and named mode.
enum GraphicsMode: IntT {
ColourGraphics1 = 0b000, // 64x64 @ 2bpp; 1kb required.
ResolutionGraphics1 = 0b001, // 128x64 @ 1bpp; 1kb required.
ColourGraphics2 = 0b010, // 128x64 @ 2bpp: 2kb required.
ResolutionGraphics2 = 0b011, // 128x96 @ 1bpp; 2kb required.
ColourGraphics3 = 0b100, // 128x96 @ 2bpp; 3kb required.
ResolutionGraphics3 = 0b101, // 128x192 @ 1bpp; 3kb required.
ColourGraphics6 = 0b110, // 128x192 @ 1bpp; 6kb required.
ResolutionGraphics6 = 0b111, // 256x192 @ 1bpp; 6kb required.
};
constexpr bool is_16column(const GraphicsMode mode) {
switch(mode) {
using enum Mode::GraphicsMode;
case ResolutionGraphics1:
case ResolutionGraphics2:
case ResolutionGraphics3:
case ColourGraphics1:
return true;
default: return false;
}
}
constexpr uint8_t is_2bpp(const GraphicsMode mode) {
switch(mode) {
using enum Mode::GraphicsMode;
case ColourGraphics1:
case ColourGraphics2:
case ColourGraphics3:
case ColourGraphics6:
return true;
default: return false;
}
}
constexpr uint8_t repeated_rows(const GraphicsMode mode) {
switch(mode) {
using enum Mode::GraphicsMode;
case ResolutionGraphics1:
case ColourGraphics1:
case ColourGraphics2:
return 3;
case ResolutionGraphics2:
case ColourGraphics3:
return 2;
default: return 1;
}
}
}
struct MC6847Base {
MC6847Base(Outputs::Display::Type);
void sync_and_burst(int begin, int end);
void pixel_line(int begin, int end);
void border_line(int begin, int end);
void porch_line(int begin, int end);
void sync_line(int begin, int end);
void reset();
bool hsync(int column) const;
Cycles next_sequence_point(int column) const;
void toggle_colour_phase();
struct LineLayout {
// Start of line: sync is active.
static const int EndOfSync = 28;
static const int EndOfColourBurst = 55;
static const int EndOfBackPorch = 72;
static const int EndOfLeftBorder = 130;
static const int EndOfPixels = 386;
static const int EndOfRightBorder = 442;
static const int EndOfLine = 456;
};
Outputs::CRT::CRT crt_;
uint32_t *pixels_ = nullptr;
uint32_t border_colour_ = 0;
void set_mode(
bool graphics,
bool semigraphics,
bool external_rom,
bool invert,
uint8_t graphics_mode,
const bool colour_select
);
Mode::IntT mode_;
struct LineCapture {
struct Column {
uint8_t data;
Mode::IntT mode;
} columns[32];
} line_;
struct Address {
public:
uint16_t address() const {
return address_;
}
int row() const {
return row_.get();
}
void advance(int column);
void apply_vertical_preload();
void apply_hsync();
void reset_row();
uint16_t increment_mask_ = 1;
uint16_t line_mask_ = uint16_t(~31);
uint8_t target_row_ = 12;
private:
uint16_t address_ = 0;
Numeric::SizedInt<4> row_;
} address_;
Outputs::Display::Type display_type_;
static constexpr uint8_t DefaultColourPhase = 215;
uint8_t colour_burst_phase_ = DefaultColourPhase;
};
struct NullMapper {
Mode::IntT operator()(const uint8_t, const Mode::IntT mode) {
return mode;
}
};
struct NullDelegate {
template <bool> void set_hsync() {}
template <bool> void set_field_sync() {}
template <bool> void set_row_preset() {}
};
template <
FrameTiming timing,
typename MemoryAccessT,
typename DelegateT = NullDelegate,
typename ModeMapperT = NullMapper
>
class MC6847: private MC6847Base {
public:
MC6847(MemoryAccessT &, DelegateT &delegate);
void run_for(const Cycles);
Cycles next_sequence_point() const;
bool hsync() const;
bool fsync() const;
using MC6847Base::set_mode;
using MC6847Base::toggle_colour_phase;
// MARK: - ScanTarget entrypoints.
void set_scan_target(Outputs::Display::ScanTarget *);
Outputs::Display::ScanStatus get_scaled_scan_status() const;
void set_display_type(const Outputs::Display::DisplayType);
Outputs::Display::DisplayType get_display_type() const;
private:
MemoryAccessT &memory_;
Numeric::DividingAccumulator<LineLayout::EndOfLine, FrameLayout<timing>::EndOfField> position_{2};
DelegateT &delegate_;
};
// MARK: - Implementation.
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::MC6847(MemoryAccessT &memory, DelegateT &delegate) :
MC6847Base(is_ntsc(timing) ? Outputs::Display::Type::NTSC60 : Outputs::Display::Type::PAL50),
memory_(memory),
delegate_(delegate) {
crt_.set_fixed_framing([&] {
run_for(Cycles(10'000));
});
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
void MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::run_for(const Cycles cycles) {
// This template binds the drawing logic to whatever was supplied
// as MemoryAccessT; a consequence of that is that frame layout logic is here.
position_.advance(
cycles.as<int>(),
[&] (const int line, const int begin, const int end) {
if(!begin) delegate_.template set_hsync<true>();
if(line < FrameLayout<timing>::EndOfPixels) {
if(!begin && !address_.row()) delegate_.template set_row_preset<true>();
// Perform meaningful fetches.
Numeric::clamp<LineLayout::EndOfLeftBorder, LineLayout::EndOfPixels>(
begin,
end,
[&](const int fetch_begin, const int fetch_end) {
const int column_begin = (fetch_begin - LineLayout::EndOfLeftBorder) >> 3;
const int column_end = (fetch_end - LineLayout::EndOfLeftBorder) >> 3;
for(int c = column_begin; c < column_end; c++) {
const auto source = address_.address();
line_.columns[c].data = memory_[source];
line_.columns[c].mode = ModeMapperT()(line_.columns[c].data, mode_);
address_.advance(c);
}
}
);
// Fetches actually continue until end of line, but they're not used.
Numeric::clamp<LineLayout::EndOfPixels, LineLayout::EndOfLine>(
begin,
end,
[&](const int fetch_begin, const int fetch_end) {
const int column_begin = (fetch_begin - LineLayout::EndOfLeftBorder) >> 3;
const int column_end = (fetch_end - LineLayout::EndOfLeftBorder) >> 3;
for(int c = column_begin; c < column_end; c++) {
const auto source = uint16_t(address_.address() + c);
(void)memory_[source];
}
}
);
pixel_line(begin, end);
// Extra accesses required: ten more in 32-byte video modes, and six more in 16-byte modes.
// That's 80 to 96 more cycles of fetches?
//
// If fetching continued until end of sync then that'd be 354 cycles of fetching = 44.25 columns.
// I'm unclear how to make a meaningful case for 10 vs 6 though. Might need to hack this?
//
// static const int EndOfSync = 28;
// static const int EndOfColourBurst = 55;
// static const int EndOfBackPorch = 72;
// static const int EndOfLeftBorder = 130;
// static const int EndOfPixels = 386;
// static const int EndOfRightBorder = 442;
// static const int EndOfLine = 456;
if(end >= LineLayout::EndOfSync && !address_.row()) delegate_.template set_row_preset<false>();
} else if(line < FrameLayout<timing>::EndOfBottomBorder) {
if(!begin && line == FrameLayout<timing>::EndOfPixels) {
delegate_.template set_field_sync<true>();
}
border_line(begin, end);
} else if(line < FrameLayout<timing>::EndOfFrontPorch) {
porch_line(begin, end);
} else if(line < FrameLayout<timing>::EndOfSync) {
sync_line(begin, end);
} else if(line < FrameLayout<timing>::EndOfBackPorch) {
if(!begin && line == FrameLayout<timing>::EndOfSync) {
delegate_.template set_field_sync<false>();
}
porch_line(begin, end);
} else {
border_line(begin, end);
}
if(end >= LineLayout::EndOfSync) delegate_.template set_hsync<false>();
},
[&] {
reset();
}
);
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
Cycles MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::next_sequence_point() const {
return MC6847Base::next_sequence_point(position_.subsegment());
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
bool MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::hsync() const {
return MC6847Base::hsync(position_.subsegment());
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
bool MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::fsync() const {
const auto segment = position_.segment();
return segment >= FrameLayout<timing>::EndOfPixels && segment < FrameLayout<timing>::EndOfSync;
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
void MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::set_scan_target(Outputs::Display::ScanTarget *const target) {
crt_.set_scan_target(target);
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
Outputs::Display::ScanStatus MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::get_scaled_scan_status() const {
return crt_.get_scaled_scan_status();
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
void MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::set_display_type(const Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
template <FrameTiming timing, typename MemoryAccessT, typename DelegateT, typename ModeMapperT>
Outputs::Display::DisplayType MC6847<timing, MemoryAccessT, DelegateT, ModeMapperT>::get_display_type() const {
return crt_.get_display_type();
}
}