mirror of
https://github.com/TomHarte/CLK.git
synced 2026-04-20 10:17:05 +00:00
Merge pull request #1574 from TomHarte/SAA5050
Add SAA5050 and hence BBC Mode 7.
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
//
|
||||
// SAA5050.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "SAA5050.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
// SAA5050 font, padded out to one byte per row. The least-significant five bits of each byte
|
||||
// are the meaningful pixels for that row, with the LSB being on the right.
|
||||
constexpr uint8_t font[][10] = {
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, // Character 32.
|
||||
{0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x0a, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x06, 0x09, 0x08, 0x1c, 0x08, 0x08, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x15, 0x14, 0x0e, 0x05, 0x15, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0d, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x15, 0x0e, 0x04, 0x0e, 0x15, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x06, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x02, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x13, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d, 0x00, 0x00, },
|
||||
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x0e, 0x11, 0x10, 0x0e, 0x01, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x08, 0x1f, 0x08, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x16, 0x01, 0x02, 0x04, 0x07, },
|
||||
{0x00, 0x00, 0x04, 0x02, 0x1f, 0x02, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x04, 0x0e, 0x15, 0x04, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x0a, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x01, 0x0f, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x10, 0x10, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x01, 0x01, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x1f, 0x10, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x02, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
|
||||
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, },
|
||||
{0x00, 0x08, 0x08, 0x09, 0x0a, 0x0c, 0x0a, 0x09, 0x00, 0x00, },
|
||||
{0x00, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1a, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x10, 0x10, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x01, },
|
||||
{0x00, 0x00, 0x00, 0x0b, 0x0c, 0x08, 0x08, 0x08, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x0e, 0x01, 0x1e, 0x00, 0x00, },
|
||||
{0x00, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x00, 0x00, },
|
||||
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
|
||||
{0x00, 0x00, 0x00, 0x1f, 0x02, 0x04, 0x08, 0x1f, 0x00, 0x00, },
|
||||
{0x00, 0x10, 0x10, 0x10, 0x10, 0x11, 0x03, 0x05, 0x07, 0x01, },
|
||||
{0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x00, 0x00, },
|
||||
{0x00, 0x18, 0x04, 0x18, 0x04, 0x19, 0x03, 0x05, 0x07, 0x01, },
|
||||
{0x00, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x04, 0x00, 0x00, 0x00, },
|
||||
{0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x00, },
|
||||
};
|
||||
|
||||
constexpr uint16_t scale(const uint8_t top, const uint8_t bottom) {
|
||||
// Adapted from old ElectrEm source; my original provenance for this being the correct logic is unknown.
|
||||
uint16_t wide =
|
||||
((top & 0b00001) ? 0b0000'0000'0011 : 0) |
|
||||
((top & 0b00010) ? 0b0000'0000'1100 : 0) |
|
||||
((top & 0b00100) ? 0b0000'0011'0000 : 0) |
|
||||
((top & 0b01000) ? 0b0000'1100'0000 : 0) |
|
||||
((top & 0b10000) ? 0b0011'0000'0000 : 0);
|
||||
|
||||
if((top & 0b10000) && (bottom & 0b11000) == 0b01000) wide |= 0b0000'1000'0000;
|
||||
if((top & 0b01000) && (bottom & 0b01100) == 0b00100) wide |= 0b0000'0010'0000;
|
||||
if((top & 0b00100) && (bottom & 0b00110) == 0b00010) wide |= 0b0000'0000'1000;
|
||||
if((top & 0b00010) && (bottom & 0b00011) == 0b00001) wide |= 0b0000'0000'0010;
|
||||
|
||||
if((top & 0b01000) && (bottom & 0b11000) == 0b10000) wide |= 0b0001'0000'0000;
|
||||
if((top & 0b00100) && (bottom & 0b01100) == 0b01000) wide |= 0b0000'0100'0000;
|
||||
if((top & 0b00010) && (bottom & 0b00110) == 0b00100) wide |= 0b0000'0001'0000;
|
||||
if((top & 0b00001) && (bottom & 0b00011) == 0b00010) wide |= 0b0000'0000'0100;
|
||||
|
||||
return wide;
|
||||
}
|
||||
|
||||
enum ControlCode: uint8_t {
|
||||
RedAlpha = 0x01,
|
||||
GreenAlpha = 0x02,
|
||||
YellowAlpha = 0x03,
|
||||
BlueAlpha = 0x04,
|
||||
MagentaAlpha = 0x05,
|
||||
CyanAlpha = 0x06,
|
||||
WhiteAlpha = 0x07,
|
||||
|
||||
Flash = 0x08,
|
||||
Steady = 0x09,
|
||||
|
||||
RedGraphics = 0x11,
|
||||
GreenGraphics = 0x12,
|
||||
YellowGraphics = 0x13,
|
||||
BlueGraphics = 0x14,
|
||||
MagentaGraphics = 0x15,
|
||||
CyanGraphics = 0x16,
|
||||
WhiteGraphics = 0x17,
|
||||
|
||||
Conceal = 0x18,
|
||||
|
||||
ContinuousGraphics = 0x19,
|
||||
SeparatedGraphics = 0x1a,
|
||||
|
||||
NormalHeight = 0xc,
|
||||
DoubleHeight = 0xd,
|
||||
|
||||
BlackBackground = 0x1c,
|
||||
NewBackground = 0x1d,
|
||||
|
||||
HoldGraphics = 0x1e,
|
||||
ReleaseGraphics = 0x1f,
|
||||
};}
|
||||
|
||||
using namespace Mullard;
|
||||
|
||||
void SAA5050Serialiser::begin_frame(const bool is_odd) {
|
||||
line_ = -2;
|
||||
row_ = 0;
|
||||
odd_frame_ = is_odd;
|
||||
|
||||
row_has_double_height_ = false;
|
||||
double_height_offset_ = 0;
|
||||
|
||||
++frame_counter_;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::begin_line() {
|
||||
line_ += 2;
|
||||
if(line_ == 20) {
|
||||
line_ = 0;
|
||||
++row_;
|
||||
|
||||
if(row_has_double_height_) {
|
||||
double_height_offset_ = (double_height_offset_ + 5) % 10;
|
||||
}
|
||||
row_has_double_height_ = false;
|
||||
}
|
||||
|
||||
output_.pixels = 0;
|
||||
has_output_ = false;
|
||||
|
||||
apply_control(ControlCode::WhiteAlpha);
|
||||
apply_control(ControlCode::Steady);
|
||||
apply_control(ControlCode::NormalHeight);
|
||||
apply_control(ControlCode::ContinuousGraphics);
|
||||
apply_control(ControlCode::BlackBackground);
|
||||
apply_control(ControlCode::ReleaseGraphics);
|
||||
}
|
||||
|
||||
bool SAA5050Serialiser::has_output() const {
|
||||
return has_output_;
|
||||
}
|
||||
|
||||
SAA5050Serialiser::Output SAA5050Serialiser::output() {
|
||||
has_output_ = false;
|
||||
return output_;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::apply_control(const uint8_t value) {
|
||||
const auto set_alpha = [&](const uint8_t colour) {
|
||||
alpha_mode_ = true;
|
||||
conceal_ = false;
|
||||
output_.alpha = colour;
|
||||
hold_graphics_ = false;
|
||||
};
|
||||
|
||||
const auto set_graphics = [&](const uint8_t colour) {
|
||||
alpha_mode_ = false;
|
||||
conceal_ = false;
|
||||
output_.alpha = colour;
|
||||
hold_graphics_ = false;
|
||||
};
|
||||
|
||||
switch(value) {
|
||||
default: break;
|
||||
|
||||
case RedAlpha: set_alpha(0b100); break;
|
||||
case GreenAlpha: set_alpha(0b010); break;
|
||||
case YellowAlpha: set_alpha(0b110); break;
|
||||
case BlueAlpha: set_alpha(0b001); break;
|
||||
case MagentaAlpha: set_alpha(0b101); break;
|
||||
case CyanAlpha: set_alpha(0b011); break;
|
||||
case WhiteAlpha: set_alpha(0b111); break;
|
||||
|
||||
case Flash: flash_ = true; break;
|
||||
case Steady: flash_ = false; break;
|
||||
|
||||
case RedGraphics: set_graphics(0b100); break;
|
||||
case GreenGraphics: set_graphics(0b010); break;
|
||||
case YellowGraphics: set_graphics(0b110); break;
|
||||
case BlueGraphics: set_graphics(0b001); break;
|
||||
case MagentaGraphics: set_graphics(0b101); break;
|
||||
case CyanGraphics: set_graphics(0b011); break;
|
||||
case WhiteGraphics: set_graphics(0b111); break;
|
||||
|
||||
case Conceal: conceal_ = true; break;
|
||||
|
||||
case ContinuousGraphics: separated_graphics_ = false; break;
|
||||
case SeparatedGraphics: separated_graphics_ = true; break;
|
||||
|
||||
case NormalHeight: double_height_ = false; break;
|
||||
case DoubleHeight: double_height_ = row_has_double_height_ = true; break;
|
||||
|
||||
case BlackBackground: output_.background = 0; break;
|
||||
case NewBackground: output_.background = output_.alpha; break;
|
||||
|
||||
case HoldGraphics: hold_graphics_ = true; break;
|
||||
case ReleaseGraphics: hold_graphics_ = false; last_graphic_ = 32; break;
|
||||
}
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::set_reveal(const bool reveal) {
|
||||
reveal_ = reveal;
|
||||
}
|
||||
|
||||
void SAA5050Serialiser::add(const Numeric::SizedCounter<7> c) {
|
||||
has_output_ = true;
|
||||
if(c.get() < 32) {
|
||||
output_.pixels = hold_graphics_ ? pixels(last_graphic_) : 0;
|
||||
apply_control(c.get());
|
||||
return;
|
||||
}
|
||||
output_.pixels = pixels(c.get());
|
||||
}
|
||||
|
||||
uint16_t SAA5050Serialiser:: pixels(const uint8_t c) {
|
||||
if(flash_ && frame_counter_&16) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(conceal_ && !reveal_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Divert into graphics only if both the mode and the character code allows it.
|
||||
if(!alpha_mode_ && (c & (1 << 5))) {
|
||||
last_graphic_ = c;
|
||||
|
||||
// Graphics layout:
|
||||
//
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b0 | b1 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b2 | b3 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
// | | |
|
||||
// | b4 | b6 |
|
||||
// | | |
|
||||
// |----|----|
|
||||
|
||||
if(separated_graphics_ && (line_ == 6 || line_ == 12 || line_ == 18)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t pixels;
|
||||
if(line_ < 6) {
|
||||
pixels =
|
||||
((c & 1) ? 0b1111'1100'0000 : 0) |
|
||||
((c & 2) ? 0b0000'0011'1111 : 0);
|
||||
} else if(line_ < 12) {
|
||||
pixels =
|
||||
((c & 4) ? 0b1111'1100'0000 : 0) |
|
||||
((c & 8) ? 0b0000'0011'1111 : 0);
|
||||
} else {
|
||||
pixels =
|
||||
((c & 16) ? 0b1111'1100'0000 : 0) |
|
||||
((c & 64) ? 0b0000'0011'1111 : 0);
|
||||
}
|
||||
|
||||
if(separated_graphics_) {
|
||||
pixels &= 0b0111'1101'1111;
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
if(double_height_) {
|
||||
const auto top_address = (line_ >> 2) + double_height_offset_;
|
||||
const uint8_t top = font[c - 32][top_address];
|
||||
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
|
||||
|
||||
if(line_ & 2) {
|
||||
return scale(bottom, top);
|
||||
} else {
|
||||
return scale(top, bottom);
|
||||
}
|
||||
} else {
|
||||
if(double_height_offset_) {
|
||||
return 0;
|
||||
} else {
|
||||
const auto top_address = line_ >> 1;
|
||||
const uint8_t top = font[c - 32][top_address];
|
||||
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
|
||||
|
||||
if(odd_frame_) {
|
||||
return scale(bottom, top);
|
||||
} else {
|
||||
return scale(top, bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// SAA5050.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 24/09/2025.
|
||||
// Copyright © 2025 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "Numeric/SizedCounter.hpp"
|
||||
|
||||
namespace Mullard {
|
||||
|
||||
struct SAA5050Serialiser {
|
||||
public:
|
||||
void begin_frame(bool is_odd);
|
||||
void begin_line();
|
||||
|
||||
void add(Numeric::SizedCounter<7>);
|
||||
|
||||
struct Output {
|
||||
// The low twelve bits of this word provide 1bpp pixels.
|
||||
uint16_t pixels;
|
||||
|
||||
// Colours for set and background pixels.
|
||||
uint8_t alpha;
|
||||
uint8_t background;
|
||||
};
|
||||
bool has_output() const;
|
||||
Output output();
|
||||
|
||||
void set_reveal(bool);
|
||||
|
||||
private:
|
||||
Output output_;
|
||||
bool has_output_ = false;
|
||||
|
||||
int row_, line_;
|
||||
bool odd_frame_;
|
||||
|
||||
bool flash_ = false;
|
||||
int frame_counter_ = 0;
|
||||
|
||||
bool reveal_ = false;
|
||||
bool conceal_ = false;
|
||||
|
||||
bool alpha_mode_ = true;
|
||||
bool separated_graphics_ = false;
|
||||
|
||||
bool double_height_ = false;
|
||||
bool row_has_double_height_ = false;
|
||||
int double_height_offset_ = 0;
|
||||
|
||||
bool hold_graphics_ = false;
|
||||
uint8_t last_graphic_ = 0;
|
||||
|
||||
uint16_t pixels(const uint8_t);
|
||||
void apply_control(const uint8_t);
|
||||
};
|
||||
|
||||
}
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
#include "Components/6522/6522.hpp"
|
||||
#include "Components/6845/CRTC6845.hpp"
|
||||
#include "Components/SN76489/SN76489.hpp"
|
||||
#include "Components/6850/6850.hpp"
|
||||
#include "Components/SAA5050/SAA5050.hpp"
|
||||
#include "Components/SN76489/SN76489.hpp"
|
||||
#include "Components/uPD7002/uPD7002.hpp"
|
||||
|
||||
// TODO: factor this more appropriately.
|
||||
@@ -306,10 +307,6 @@ public:
|
||||
|
||||
active_collation_.pixels_per_clock = 1 << ((value >> 2) & 0x03);
|
||||
active_collation_.is_teletext = value & 0x02;
|
||||
if(active_collation_.is_teletext) {
|
||||
Logger::error().append("TODO: video control => teletext %d", bool(value & 0x02));
|
||||
}
|
||||
|
||||
flash_mask_ = value & 0x01 ? 7 : 0;
|
||||
cursor_mask_ = value & 0b1110'0000;
|
||||
}
|
||||
@@ -327,15 +324,13 @@ public:
|
||||
system_via_.set_control_line_input<MOS::MOS6522::Port::A, MOS::MOS6522::Line::One>(state.vsync);
|
||||
|
||||
// Count cycles since horizontal sync to insert a colour burst.
|
||||
if(state.hsync) {
|
||||
++cycles_into_hsync_;
|
||||
} else {
|
||||
cycles_into_hsync_ = 0;
|
||||
}
|
||||
const bool is_colour_burst = cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9;
|
||||
|
||||
// Sync is taken to override pixels, and is combined as a simple OR.
|
||||
const bool is_sync = state.hsync || state.vsync;
|
||||
// TODO: this is copy/pasted from the CPC. How does the BBC do it?
|
||||
// if(state.hsync) {
|
||||
// ++cycles_into_hsync_;
|
||||
// } else {
|
||||
// cycles_into_hsync_ = 0;
|
||||
// }
|
||||
// const bool is_colour_burst = cycles_into_hsync_ >= 5 && cycles_into_hsync_ < 9;
|
||||
|
||||
// Check for a cursor leading edge.
|
||||
cursor_shifter_ >>= 4;
|
||||
@@ -349,17 +344,64 @@ public:
|
||||
previous_cursor_enabled_ = state.cursor;
|
||||
}
|
||||
|
||||
OutputMode output_mode;
|
||||
const bool should_fetch = state.display_enable && (active_collation_.is_teletext || !(state.line.get() & 8));
|
||||
if(is_sync) {
|
||||
output_mode = OutputMode::Sync;
|
||||
} else if(is_colour_burst) {
|
||||
output_mode = OutputMode::ColourBurst;
|
||||
} else if(should_fetch || cursor_shifter_) {
|
||||
output_mode = OutputMode::Pixels;
|
||||
} else {
|
||||
output_mode = OutputMode::Blank;
|
||||
// Consider some SAA5050 signalling.
|
||||
if(!state.vsync && previous_vsync_) {
|
||||
// Complete fiction here; the SAA5050 field flag is set by peeking inside CRTC state.
|
||||
// TODO: what really sets CRS for the SAA5050? Time since hsync maybe?
|
||||
saa5050_serialiser_.begin_frame(state.field_count.bit<0>());
|
||||
}
|
||||
previous_vsync_ = state.vsync;
|
||||
|
||||
if(state.display_enable && !previous_display_enabled_) {
|
||||
saa5050_serialiser_.begin_line();
|
||||
}
|
||||
previous_display_enabled_ = state.display_enable;
|
||||
|
||||
// Grab 5050 output, if any.
|
||||
bool has_5050_output_ = saa5050_serialiser_.has_output();
|
||||
const auto saa_50505_output_ = saa5050_serialiser_.output();
|
||||
|
||||
// Fetch, possibly.
|
||||
const bool should_fetch = state.display_enable && (active_collation_.is_teletext || !(state.line.get() & 8));
|
||||
if(should_fetch) {
|
||||
const uint16_t address = [&] {
|
||||
// Teletext address generation.
|
||||
if(state.refresh.get() & (1 << 13)) {
|
||||
return uint16_t(
|
||||
0x3c00 |
|
||||
((state.refresh.get() & 0x800) << 3) |
|
||||
(state.refresh.get() & 0x3ff)
|
||||
);
|
||||
}
|
||||
|
||||
uint16_t address = uint16_t((state.refresh.get() << 3) | (state.line.get() & 7));
|
||||
if(address & 0x8000) {
|
||||
address = (address + video_base_) & 0x7fff;
|
||||
}
|
||||
return address;
|
||||
} ();
|
||||
const uint8_t fetched = ram_[address];
|
||||
pixel_shifter_ = fetched;
|
||||
saa5050_serialiser_.add(fetched);
|
||||
}
|
||||
|
||||
// Pick new output mode.
|
||||
const OutputMode output_mode = [&] {
|
||||
if(state.hsync || state.vsync) {
|
||||
return OutputMode::Sync;
|
||||
}
|
||||
// if(is_colour_burst) {
|
||||
// return OutputMode::ColourBurst;
|
||||
// }
|
||||
if(
|
||||
(should_fetch && !active_collation_.is_teletext) ||
|
||||
(has_5050_output_ && active_collation_.is_teletext) ||
|
||||
cursor_shifter_
|
||||
) {
|
||||
return OutputMode::Pixels;
|
||||
}
|
||||
return OutputMode::Blank;
|
||||
} ();
|
||||
|
||||
// If a transition between sync/border/pixels just occurred, flush whatever was
|
||||
// in progress to the CRT and reset counting. Also flush if this mode has just been effective
|
||||
@@ -379,11 +421,8 @@ public:
|
||||
previous_output_mode_ = output_mode;
|
||||
}
|
||||
|
||||
// Increment cycles since state changed.
|
||||
cycles_ += crtc_clock_multiplier_ << 3;
|
||||
|
||||
// Collect some more pixels if output is ongoing.
|
||||
if(previous_output_mode_ == OutputMode::Pixels) {
|
||||
if(output_mode == OutputMode::Pixels) {
|
||||
// Flush the current buffer pixel if full; the CRTC allows many different display
|
||||
// widths so it's not necessarily possible to predict the correct number in advance
|
||||
// and using the upper bound could lead to inefficient behaviour.
|
||||
@@ -396,30 +435,20 @@ public:
|
||||
if(!pixel_data_) {
|
||||
pixel_pointer_ = pixel_data_ = crt_.begin_data(PixelAllocationUnit, 8);
|
||||
}
|
||||
if(pixel_pointer_) {
|
||||
uint16_t address;
|
||||
|
||||
if(state.refresh.get() & (1 << 13)) {
|
||||
// Teletext address generation mode.
|
||||
address = uint16_t(
|
||||
0x3c00 |
|
||||
((state.refresh.get() & 0x800) << 3) |
|
||||
(state.refresh.get() & 0x3ff)
|
||||
);
|
||||
// TODO: wraparound? Does that happen on Mode 7?
|
||||
} else {
|
||||
address = uint16_t((state.refresh.get() << 3) | (state.line.get() & 7));
|
||||
if(address & 0x8000) {
|
||||
address = (address + video_base_) & 0x7fff;
|
||||
}
|
||||
}
|
||||
|
||||
pixel_shifter_ = should_fetch ? ram_[address] : 0;
|
||||
if(pixel_data_) {
|
||||
if(active_collation_.is_teletext) {
|
||||
// TODO: output meaningful teletext pixels.
|
||||
for(int c = 0; c < 12; c++) {
|
||||
*pixel_pointer_++ = (pixel_shifter_ & 0x80 ? 0xff : 0x00) ^ cursor_mask_;
|
||||
pixel_shifter_ <<= 1;
|
||||
if(has_5050_output_) {
|
||||
uint16_t pixels = saa_50505_output_.pixels;
|
||||
for(int c = 0; c < 12; c++) {
|
||||
*pixel_pointer_++ =
|
||||
((pixels & 0b1000'0000'0000) ? saa_50505_output_.alpha : saa_50505_output_.background)
|
||||
^ uint8_t(cursor_shifter_);
|
||||
pixels <<= 1;
|
||||
}
|
||||
} else {
|
||||
std::fill(pixel_pointer_, pixel_pointer_ + 12, 0);
|
||||
pixel_pointer_ += 12;
|
||||
}
|
||||
} else {
|
||||
switch(crtc_clock_multiplier_ * active_collation_.pixels_per_clock) {
|
||||
@@ -433,6 +462,9 @@ public:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment cycles since state changed.
|
||||
cycles_ += crtc_clock_multiplier_ << 3;
|
||||
}
|
||||
|
||||
/// Sets the destination for output.
|
||||
@@ -499,6 +531,9 @@ private:
|
||||
uint32_t cursor_shifter_ = 0;
|
||||
bool previous_cursor_enabled_ = false;
|
||||
|
||||
bool previous_display_enabled_ = false;
|
||||
bool previous_vsync_ = false;
|
||||
|
||||
template <int count> void shift_pixels(const uint8_t cursor_mask) {
|
||||
for(int c = 0; c < count; c++) {
|
||||
const uint8_t colour =
|
||||
@@ -513,6 +548,8 @@ private:
|
||||
|
||||
const uint8_t *const ram_ = nullptr;
|
||||
SystemVIA &system_via_;
|
||||
|
||||
Mullard::SAA5050Serialiser saa5050_serialiser_;
|
||||
};
|
||||
using CRTC = Motorola::CRTC::CRTC6845<
|
||||
CRTCBusHandler,
|
||||
|
||||
@@ -641,6 +641,9 @@
|
||||
4B882F5B2C2F9C7700D84031 /* Shaker in Resources */ = {isa = PBXBuildFile; fileRef = 4B882F5A2C2F9C7700D84031 /* Shaker */; };
|
||||
4B882F5C2C32199400D84031 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
|
||||
4B882F5D2C3219A400D84031 /* AmstradCPC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */; };
|
||||
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8855A32E84D51B00E251DD /* SAA5050.cpp */; };
|
||||
4B89449520194CB3007DE474 /* MachineForTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055ABE1FAE98000060FFFF /* MachineForTarget.cpp */; };
|
||||
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
|
||||
4B894519201967B4007DE474 /* ConfidenceCounter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8944E6201967B4007DE474 /* ConfidenceCounter.cpp */; };
|
||||
@@ -1795,6 +1798,8 @@
|
||||
4B882F582C2F9C6900D84031 /* CPCShakerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = CPCShakerTests.mm; sourceTree = "<group>"; };
|
||||
4B882F5A2C2F9C7700D84031 /* Shaker */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Shaker; sourceTree = "<group>"; };
|
||||
4B88559C2E8185BF00E251DD /* SizedCounter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SizedCounter.hpp; sourceTree = "<group>"; };
|
||||
4B8855A22E84D51B00E251DD /* SAA5050.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SAA5050.hpp; sourceTree = "<group>"; };
|
||||
4B8855A32E84D51B00E251DD /* SAA5050.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SAA5050.cpp; sourceTree = "<group>"; };
|
||||
4B89449220194A47007DE474 /* CSStaticAnalyser+TargetVector.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "CSStaticAnalyser+TargetVector.h"; path = "StaticAnalyser/CSStaticAnalyser+TargetVector.h"; sourceTree = "<group>"; };
|
||||
4B8944E4201967B4007DE474 /* ConfidenceSummary.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSummary.hpp; sourceTree = "<group>"; };
|
||||
4B8944E5201967B4007DE474 /* ConfidenceSource.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ConfidenceSource.hpp; sourceTree = "<group>"; };
|
||||
@@ -3847,6 +3852,15 @@
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B8855A42E84D51B00E251DD /* SAA5050 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4B8855A22E84D51B00E251DD /* SAA5050.hpp */,
|
||||
4B8855A32E84D51B00E251DD /* SAA5050.cpp */,
|
||||
);
|
||||
path = SAA5050;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4B8944E2201967B4007DE474 /* Analyser */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -5089,6 +5103,7 @@
|
||||
4B4B1A39200198C900A0F866 /* KonamiSCC */,
|
||||
4BC23A212467600E001A6030 /* OPx */,
|
||||
4BF0BC6E2973318E00CCA2B5 /* RP5C01 */,
|
||||
4B8855A42E84D51B00E251DD /* SAA5050 */,
|
||||
4B0ACBFF237756EC008902D0 /* Serial */,
|
||||
4BB0A6582044FD3000FB3688 /* SN76489 */,
|
||||
4B47F3B42E7B9A14005D4DEC /* uPD7002 */,
|
||||
@@ -6301,6 +6316,7 @@
|
||||
4B302185208A550100773308 /* DiskII.cpp in Sources */,
|
||||
42EB81292B23AAC300429AF4 /* IMD.cpp in Sources */,
|
||||
4B051CB1267C1CA200CA44E8 /* Keyboard.cpp in Sources */,
|
||||
4B8855A72E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4B0F1BB32602645900B85C66 /* StaticAnalyser.cpp in Sources */,
|
||||
4B055A931FAE85B50060FFFF /* BinaryDump.cpp in Sources */,
|
||||
4B89452D201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
@@ -6397,6 +6413,7 @@
|
||||
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
|
||||
4BE8EB6625C750B50040BC40 /* DAT.cpp in Sources */,
|
||||
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
|
||||
4B8855A52E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */,
|
||||
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */,
|
||||
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
|
||||
@@ -6628,6 +6645,7 @@
|
||||
4B778F3123A5F0CB0000D260 /* Keyboard.cpp in Sources */,
|
||||
4B778F4A23A5F1FB0000D260 /* StaticAnalyser.cpp in Sources */,
|
||||
4B7752AB28217E560073E2C5 /* SZX.cpp in Sources */,
|
||||
4B8855A62E84D51B00E251DD /* SAA5050.cpp in Sources */,
|
||||
4BD91D772401C2B8007BDC91 /* PatrikRakTests.swift in Sources */,
|
||||
4B06AAE22C645F970034D014 /* PCBooter.cpp in Sources */,
|
||||
4B1082C32C1A87CA00B07C5D /* CSL.cpp in Sources */,
|
||||
|
||||
@@ -76,6 +76,7 @@ SOURCES += \
|
||||
$$SRC/Components/KonamiSCC/*.cpp \
|
||||
$$SRC/Components/OPx/*.cpp \
|
||||
$$SRC/Components/RP5C01/*.cpp \
|
||||
$$SRC/Components/SAA5050/*.cpp \
|
||||
$$SRC/Components/Serial/*.cpp \
|
||||
$$SRC/Components/SN76489/*.cpp \
|
||||
$$SRC/Components/uPD7002/*.cpp \
|
||||
@@ -212,6 +213,7 @@ HEADERS += \
|
||||
$$SRC/Components/OPx/*.hpp \
|
||||
$$SRC/Components/OPx/Implementation/*.hpp \
|
||||
$$SRC/Components/RP5C01/*.hpp \
|
||||
$$SRC/Components/SAA5050/*.hpp \
|
||||
$$SRC/Components/Serial/*.hpp \
|
||||
$$SRC/Components/SN76489/*.hpp \
|
||||
$$SRC/Components/uPD7002/*.hpp \
|
||||
|
||||
@@ -60,6 +60,7 @@ SOURCES += glob.glob('../../Components/I2C/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/KonamiSCC/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/OPx/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/RP5C01/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/SAA5050/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/SN76489/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/Serial/*.cpp')
|
||||
SOURCES += glob.glob('../../Components/uPD7002/*.cpp')
|
||||
|
||||
@@ -58,6 +58,7 @@ set(CLK_SOURCES
|
||||
Components/KonamiSCC/KonamiSCC.cpp
|
||||
Components/OPx/OPLL.cpp
|
||||
Components/RP5C01/RP5C01.cpp
|
||||
Components/SAA5050/SAA5050.cpp
|
||||
Components/SN76489/SN76489.cpp
|
||||
Components/Serial/Line.cpp
|
||||
Components/uPD7002/uPD7002.cpp
|
||||
|
||||
Reference in New Issue
Block a user