mirror of
https://github.com/TomHarte/CLK.git
synced 2024-09-07 12:54:34 +00:00
1457 lines
45 KiB
C++
1457 lines
45 KiB
C++
//
|
||
// Chipset.cpp
|
||
// Clock Signal
|
||
//
|
||
// Created by Thomas Harte on 22/07/2021.
|
||
// Copyright © 2021 Thomas Harte. All rights reserved.
|
||
//
|
||
|
||
#include "Chipset.hpp"
|
||
|
||
//#define NDEBUG
|
||
#define LOG_PREFIX "[Amiga chipset] "
|
||
#include "../../Outputs/Log.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cassert>
|
||
|
||
|
||
using namespace Amiga;
|
||
|
||
namespace {
|
||
|
||
template <typename EnumT, EnumT... T> struct Mask {
|
||
static constexpr uint16_t value = 0;
|
||
};
|
||
|
||
template <typename EnumT, EnumT F, EnumT... T> struct Mask<EnumT, F, T...> {
|
||
static constexpr uint16_t value = uint16_t(F) | Mask<EnumT, T...>::value;
|
||
};
|
||
|
||
template <InterruptFlag... Flags> struct InterruptMask: Mask<InterruptFlag, Flags...> {};
|
||
template <DMAFlag... Flags> struct DMAMask: Mask<DMAFlag, Flags...> {};
|
||
|
||
/// Expands @c source so that b7 is the least-significant bit of the most-significant byte of the result,
|
||
/// b6 is the least-significant bit of the next most significant byte, etc. b0 stays in place.
|
||
constexpr uint64_t expand_bitplane_byte(uint8_t source) {
|
||
uint64_t result = source; // 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 abcd efgh
|
||
result = (result | (result << 28)) & 0x0000'000f'0000'000f; // 0000 0000 0000 0000 0000 0000 0000 abcd 0000 0000 0000 0000 0000 0000 0000 efgh
|
||
result = (result | (result << 14)) & 0x0003'0003'0003'0003; // 0000 0000 0000 00ab 0000 0000 0000 00cd 0000 0000 0000 00ef 0000 0000 0000 00gh
|
||
result = (result | (result << 7)) & 0x0101'0101'0101'0101; // 0000 000a 0000 000b 0000 000c 0000 000d 0000 000e 0000 000f 0000 000g 0000 000h
|
||
return result;
|
||
}
|
||
|
||
/// Expands @c source from b15 ... b0 to 000b15 ... 000b0.
|
||
constexpr uint64_t expand_sprite_word(uint16_t source) {
|
||
uint64_t result = source;
|
||
result = (result | (result << 24)) & 0x0000'00ff'0000'00ff;
|
||
result = (result | (result << 12)) & 0x000f'000f'000f'000f;
|
||
result = (result | (result << 6)) & 0x0303'0303'0303'0303;
|
||
result = (result | (result << 3)) & 0x1111'1111'1111'1111;
|
||
return result;
|
||
}
|
||
|
||
// A very small selection of test cases.
|
||
static_assert(expand_bitplane_byte(0xff) == 0x01'01'01'01'01'01'01'01);
|
||
static_assert(expand_bitplane_byte(0x55) == 0x00'01'00'01'00'01'00'01);
|
||
static_assert(expand_bitplane_byte(0xaa) == 0x01'00'01'00'01'00'01'00);
|
||
static_assert(expand_bitplane_byte(0x00) == 0x00'00'00'00'00'00'00'00);
|
||
|
||
static_assert(expand_sprite_word(0xffff) == 0x11'11'11'11'11'11'11'11);
|
||
static_assert(expand_sprite_word(0x5555) == 0x01'01'01'01'01'01'01'01);
|
||
static_assert(expand_sprite_word(0xaaaa) == 0x10'10'10'10'10'10'10'10);
|
||
static_assert(expand_sprite_word(0x0000) == 0x00'00'00'00'00'00'00'00);
|
||
|
||
}
|
||
|
||
#define DMA_CONSTRUCT *this, reinterpret_cast<uint16_t *>(map.chip_ram.data()), map.chip_ram.size() >> 1
|
||
|
||
Chipset::Chipset(MemoryMap &map, int input_clock_rate) :
|
||
blitter_(DMA_CONSTRUCT),
|
||
sprites_{
|
||
{DMA_CONSTRUCT}, {DMA_CONSTRUCT}, {DMA_CONSTRUCT}, {DMA_CONSTRUCT},
|
||
{DMA_CONSTRUCT}, {DMA_CONSTRUCT}, {DMA_CONSTRUCT}, {DMA_CONSTRUCT}
|
||
},
|
||
bitplanes_(DMA_CONSTRUCT),
|
||
copper_(DMA_CONSTRUCT),
|
||
crt_(908, 4, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red4Green4Blue4),
|
||
cia_a_handler_(map, disk_controller_, mouse_),
|
||
cia_b_handler_(disk_controller_),
|
||
cia_a(cia_a_handler_),
|
||
cia_b(cia_b_handler_),
|
||
disk_(DMA_CONSTRUCT),
|
||
disk_controller_(Cycles(input_clock_rate), *this, disk_, cia_b) {
|
||
disk_controller_.set_clocking_hint_observer(this);
|
||
}
|
||
|
||
#undef DMA_CONSTRUCT
|
||
|
||
Chipset::Changes Chipset::run_for(HalfCycles length) {
|
||
return run<false>(length);
|
||
}
|
||
|
||
Chipset::Changes Chipset::run_until_cpu_slot() {
|
||
return run<true>();
|
||
}
|
||
|
||
void Chipset::set_cia_interrupts(bool cia_a_interrupt, bool cia_b_interrupt) {
|
||
// TODO: are these really latched, or are they active live?
|
||
// If latched, is it only on a leading edge?
|
||
// interrupt_requests_ &= ~InterruptMask<InterruptFlag::IOPortsAndTimers, InterruptFlag::External>::value;
|
||
interrupt_requests_ |=
|
||
(cia_a_interrupt ? InterruptMask<InterruptFlag::IOPortsAndTimers>::value : 0) |
|
||
(cia_b_interrupt ? InterruptMask<InterruptFlag::External>::value : 0);
|
||
update_interrupts();
|
||
}
|
||
|
||
void Chipset::posit_interrupt(InterruptFlag flag) {
|
||
interrupt_requests_ |= uint16_t(flag);
|
||
update_interrupts();
|
||
}
|
||
|
||
void DMADeviceBase::posit_interrupt(InterruptFlag flag) {
|
||
chipset_.posit_interrupt(flag);
|
||
}
|
||
|
||
template <int cycle> void Chipset::output() {
|
||
// Notes to self on guesses below:
|
||
//
|
||
// Hardware stop is at 0x18;
|
||
// 12/64 * 227 = 42.5625
|
||
//
|
||
// "However, horizontal blanking actually limits the displayable
|
||
// video to 368 low resolution pixel"
|
||
//
|
||
// => 184 windows out of 227 are visible, which concurs.
|
||
//
|
||
|
||
// A complete from-thin-air guess:
|
||
//
|
||
// 7 cycles blank;
|
||
// 17 cycles sync;
|
||
// 3 cycles blank;
|
||
// 9 cycles colour burst;
|
||
// 7 cycles blank.
|
||
constexpr int blank1 = 7;
|
||
constexpr int sync = 17 + blank1;
|
||
constexpr int blank2 = 3 + sync;
|
||
constexpr int burst = 9 + blank2;
|
||
constexpr int blank3 = 7 + burst;
|
||
static_assert(blank3 == 43);
|
||
|
||
// Trigger any sprite loads encountered.
|
||
constexpr auto dcycle = cycle << 1;
|
||
for(int c = 0; c < 8; c += 2) {
|
||
if( sprites_[c].active &&
|
||
dcycle <= sprites_[c].h_start &&
|
||
dcycle+2 > sprites_[c].h_start) {
|
||
sprite_shifters_[c >> 1].load<0>(
|
||
sprites_[c].data[1],
|
||
sprites_[c].data[0],
|
||
sprites_[c].h_start & 1);
|
||
}
|
||
|
||
if( sprites_[c+1].active &&
|
||
dcycle <= sprites_[c + 1].h_start &&
|
||
dcycle+2 > sprites_[c + 1].h_start) {
|
||
sprite_shifters_[c >> 1].load<1>(
|
||
sprites_[c + 1].data[1],
|
||
sprites_[c + 1].data[0],
|
||
sprites_[c + 1].h_start & 1);
|
||
}
|
||
}
|
||
|
||
#define LINK(location, action, length) \
|
||
if(cycle == (location)) { \
|
||
crt_.action((length) * 4); \
|
||
}
|
||
|
||
if(y_ < vertical_blank_height_) {
|
||
// Put three lines of sync at the centre of the vertical blank period.
|
||
// Offset by half a line if interlaced and on an odd frame.
|
||
|
||
const int midline = vertical_blank_height_ >> 1;
|
||
if(frame_height_ & 1) {
|
||
if(y_ < midline - 1 || y_ > midline + 2) {
|
||
LINK(blank1, output_blank, blank1);
|
||
LINK(sync, output_sync, sync - blank1);
|
||
LINK(line_length_ - 1, output_blank, line_length_ - 1 - sync);
|
||
} else if(y_ == midline - 1) {
|
||
LINK(113, output_blank, 113);
|
||
LINK(line_length_ - 1, output_sync, line_length_ - 1 - 113);
|
||
} else if(y_ == midline + 2) {
|
||
LINK(113, output_sync, 113);
|
||
LINK(line_length_ - 1, output_blank, line_length_ - 1 - 113);
|
||
} else {
|
||
LINK(blank1, output_sync, blank1);
|
||
LINK(sync, output_blank, sync - blank1);
|
||
LINK(line_length_ - 1, output_sync, line_length_ - 1 - sync);
|
||
}
|
||
} else {
|
||
if(y_ < midline - 1 || y_ > midline + 1) {
|
||
LINK(blank1, output_blank, blank1);
|
||
LINK(sync, output_sync, sync - blank1);
|
||
LINK(line_length_ - 1, output_blank, line_length_ - 1 - sync);
|
||
} else {
|
||
LINK(blank1, output_sync, blank1);
|
||
LINK(sync, output_blank, sync - blank1);
|
||
LINK(line_length_ - 1, output_sync, line_length_ - 1 - sync);
|
||
}
|
||
}
|
||
} else {
|
||
// Output the correct sequence of blanks, syncs and burst atomically.
|
||
LINK(blank1, output_blank, blank1);
|
||
LINK(sync, output_sync, sync - blank1);
|
||
LINK(blank2, output_blank, blank2 - sync);
|
||
LINK(burst, output_default_colour_burst, burst - blank2); // TODO: only if colour enabled.
|
||
LINK(blank3, output_blank, blank3 - burst);
|
||
|
||
// TODO: incorporate the lowest display window bits elsewhere.
|
||
display_horizontal_ |= cycle == (display_window_start_[0] >> 1);
|
||
display_horizontal_ &= cycle != (display_window_stop_[0] >> 1);
|
||
|
||
if constexpr (cycle > blank3) {
|
||
const bool is_pixel_display = display_horizontal_ && fetch_vertical_;
|
||
|
||
if(
|
||
(is_pixel_display == is_border_) ||
|
||
(is_border_ && border_colour_ != palette_[0])) {
|
||
flush_output();
|
||
|
||
is_border_ = !is_pixel_display;
|
||
border_colour_ = palette_[0];
|
||
}
|
||
|
||
if(is_pixel_display) {
|
||
if(!pixels_) {
|
||
uint16_t *const new_pixels = reinterpret_cast<uint16_t *>(crt_.begin_data(4 * size_t(line_length_ - cycle)));
|
||
if(new_pixels) {
|
||
flush_output();
|
||
}
|
||
pixels_ = new_pixels;
|
||
}
|
||
|
||
if(pixels_) {
|
||
// TODO: this doesn't support dual playfields; use an alternative
|
||
// palette table for that?
|
||
const uint32_t source = bitplane_pixels_.get(is_high_res_);
|
||
|
||
pixels_[0] = palette_[source >> 24];
|
||
pixels_[1] = palette_[(source >> 16) & 0xff];
|
||
pixels_[2] = palette_[(source >> 8) & 0xff];
|
||
pixels_[3] = palette_[source & 0xff];
|
||
|
||
for(int c = 3; c >= 0; --c) {
|
||
const auto data = sprite_shifters_[c].get();
|
||
if(!data) continue;
|
||
const int base = (c << 2) + 16;
|
||
|
||
// TODO: can do a better job of selection here —
|
||
// treat each 4-bit quantity as a single colour
|
||
// selection, much like dual playfield mode.
|
||
if(data >> 6) {
|
||
pixels_[0] = pixels_[1] = palette_[base + (data >> 6)];
|
||
}
|
||
if((data >> 4) & 3) {
|
||
pixels_[0] = pixels_[1] = palette_[base + ((data >> 4)&3)];
|
||
}
|
||
if((data >> 2) & 3) {
|
||
pixels_[2] = pixels_[3] = palette_[base + ((data >> 2)&3)];
|
||
}
|
||
if(data & 3) {
|
||
pixels_[2] = pixels_[3] = palette_[base + (data & 3)];
|
||
}
|
||
}
|
||
|
||
pixels_ += 4;
|
||
}
|
||
}
|
||
++zone_duration_;
|
||
|
||
// Output the rest of the line. TODO: optimise border area.
|
||
if(cycle == line_length_ - 1) {
|
||
flush_output();
|
||
}
|
||
|
||
// Update all active pixel shifters.
|
||
bitplane_pixels_.shift(is_high_res_);
|
||
sprite_shifters_[0].shift();
|
||
sprite_shifters_[1].shift();
|
||
sprite_shifters_[2].shift();
|
||
sprite_shifters_[3].shift();
|
||
}
|
||
}
|
||
|
||
#undef LINK
|
||
}
|
||
|
||
void Chipset::flush_output() {
|
||
if(!zone_duration_) return;
|
||
|
||
if(is_border_) {
|
||
uint16_t *const pixels = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
|
||
if(pixels) {
|
||
*pixels = border_colour_;
|
||
}
|
||
crt_.output_data(zone_duration_ * 4, 1);
|
||
} else {
|
||
crt_.output_data(zone_duration_ * 4);
|
||
}
|
||
zone_duration_ = 0;
|
||
pixels_ = nullptr;
|
||
}
|
||
|
||
/// @returns @c true if this was a CPU slot; @c false otherwise.
|
||
template <int cycle, bool stop_if_cpu> bool Chipset::perform_cycle() {
|
||
constexpr auto BlitterFlag = DMAMask<DMAFlag::Blitter, DMAFlag::AllBelow>::value;
|
||
constexpr auto BitplaneFlag = DMAMask<DMAFlag::Bitplane, DMAFlag::AllBelow>::value;
|
||
constexpr auto CopperFlag = DMAMask<DMAFlag::Copper, DMAFlag::AllBelow>::value;
|
||
constexpr auto DiskFlag = DMAMask<DMAFlag::Disk, DMAFlag::AllBelow>::value;
|
||
constexpr auto SpritesFlag = DMAMask<DMAFlag::Sprites, DMAFlag::AllBelow>::value;
|
||
|
||
// Update state as to whether bitplane fetching should happen now.
|
||
//
|
||
// TODO: figure out how the hard stops factor into this.
|
||
//
|
||
|
||
// Top priority: bitplane collection.
|
||
// TODO: mask off fetch_window_'s lower bits. (Dependant on high/low-res?)
|
||
// Also: fetch_stop_ and that + 12/8 is the best I can discern from the Hardware Reference,
|
||
// but very obviously isn't how the actual hardware works. Explore on that.
|
||
fetch_horizontal_ |= cycle == fetch_window_[0];
|
||
if(cycle == fetch_window_[1]) fetch_stop_ = cycle + (is_high_res_ ? 12 : 8);
|
||
fetch_horizontal_ &= cycle != fetch_stop_;
|
||
if((dma_control_ & BitplaneFlag) == BitplaneFlag) {
|
||
// TODO: offer a cycle for bitplane collection.
|
||
// Probably need to indicate odd or even?
|
||
if(fetch_vertical_ && fetch_horizontal_ && bitplanes_.advance(cycle)) {
|
||
did_fetch_ = true;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if constexpr (cycle & 1) {
|
||
// Odd slot use/priority:
|
||
//
|
||
// 1. Bitplane fetches [dealt with above].
|
||
// 2. Refresh, disk, audio, or sprites. Depending on region.
|
||
//
|
||
// Blitter and CPU priority is dealt with below.
|
||
if constexpr (cycle >= 0x07 && cycle < 0x0d) {
|
||
if((dma_control_ & DiskFlag) == DiskFlag) {
|
||
if(disk_.advance()) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
if constexpr(cycle >= 0x15 && cycle < 0x35) {
|
||
if((dma_control_ & SpritesFlag) == SpritesFlag) {
|
||
constexpr auto sprite_id = (cycle - 0x15) >> 2;
|
||
if(sprites_[sprite_id].advance(y_)) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// Bitplanes being dealt with, specific odd-cycle responsibility
|
||
// is just possibly to pass to the Copper.
|
||
//
|
||
// The Blitter and CPU are dealt with outside of the odd/even test.
|
||
if((dma_control_ & CopperFlag) == CopperFlag) {
|
||
if(copper_.advance(uint16_t(((y_ & 0xff) << 8) | (cycle & 0xfe)))) {
|
||
return false;
|
||
}
|
||
} else {
|
||
copper_.stop();
|
||
}
|
||
}
|
||
|
||
// Down here: give first refusal to the Blitter, otherwise
|
||
// pass on to the CPU.
|
||
return (dma_control_ & BlitterFlag) != BlitterFlag || !blitter_.advance();
|
||
}
|
||
|
||
/// Performs all slots starting with @c first_slot and ending just before @c last_slot.
|
||
/// If @c stop_on_cpu is true, stops upon discovery of a CPU slot.
|
||
///
|
||
/// @returns the number of slots completed if @c stop_on_cpu was true and a CPU slot was found.
|
||
/// @c -1 otherwise.
|
||
template <bool stop_on_cpu> int Chipset::advance_slots(int first_slot, int last_slot) {
|
||
if(first_slot == last_slot) {
|
||
return -1;
|
||
}
|
||
|
||
#define C(x) \
|
||
case x: \
|
||
if constexpr(stop_on_cpu) {\
|
||
if(perform_cycle<x, stop_on_cpu>()) {\
|
||
return x - first_slot;\
|
||
}\
|
||
} else {\
|
||
perform_cycle<x, stop_on_cpu>(); \
|
||
} \
|
||
output<x>(); \
|
||
if((x + 1) == last_slot) break; \
|
||
[[fallthrough]]
|
||
|
||
#define C10(x) C(x); C(x+1); C(x+2); C(x+3); C(x+4); C(x+5); C(x+6); C(x+7); C(x+8); C(x+9);
|
||
switch(first_slot) {
|
||
C10(0); C10(10); C10(20); C10(30); C10(40);
|
||
C10(50); C10(60); C10(70); C10(80); C10(90);
|
||
C10(100); C10(110); C10(120); C10(130); C10(140);
|
||
C10(150); C10(160); C10(170); C10(180); C10(190);
|
||
C10(200); C10(210);
|
||
C(220); C(221); C(222); C(223); C(224);
|
||
C(225); C(226); C(227); C(228);
|
||
|
||
default: assert(false);
|
||
}
|
||
#undef C
|
||
|
||
return -1;
|
||
}
|
||
|
||
template <bool stop_on_cpu> Chipset::Changes Chipset::run(HalfCycles length) {
|
||
Changes changes;
|
||
|
||
// This code uses 'pixels' as a measure, which is equivalent to one pixel clock time,
|
||
// or half a cycle.
|
||
auto pixels_remaining = length.as<int>();
|
||
int hsyncs = 0, vsyncs = 0;
|
||
|
||
// Update raster position, spooling out graphics.
|
||
while(pixels_remaining) {
|
||
// Determine number of pixels left on this line.
|
||
const int line_pixels = std::min(pixels_remaining, (line_length_ * 4) - line_cycle_);
|
||
|
||
const int start_slot = line_cycle_ >> 2;
|
||
const int end_slot = (line_cycle_ + line_pixels) >> 2;
|
||
const int actual_slots = advance_slots<stop_on_cpu>(start_slot, end_slot);
|
||
|
||
if(stop_on_cpu && actual_slots >= 0) {
|
||
// Run until the end of the named slot.
|
||
if(actual_slots) {
|
||
const int actual_line_pixels =
|
||
(4 - (line_cycle_ & 3)) + ((actual_slots - 1) << 2);
|
||
line_cycle_ += actual_line_pixels;
|
||
changes.duration += HalfCycles(actual_line_pixels);
|
||
}
|
||
|
||
// Just ensure an exit.
|
||
pixels_remaining = 0;
|
||
} else {
|
||
line_cycle_ += line_pixels;
|
||
changes.duration += HalfCycles(line_pixels);
|
||
pixels_remaining -= line_pixels;
|
||
}
|
||
|
||
// Advance intraline counter and possibly ripple upwards into
|
||
// lines and fields.
|
||
if(line_cycle_ == (line_length_ * 4)) {
|
||
++hsyncs;
|
||
|
||
line_cycle_ = 0;
|
||
++y_;
|
||
|
||
fetch_vertical_ |= y_ == display_window_start_[1];
|
||
fetch_vertical_ &= y_ != display_window_stop_[1];
|
||
|
||
if(did_fetch_) {
|
||
bitplanes_.do_end_of_line();
|
||
previous_bitplanes_.clear();
|
||
}
|
||
did_fetch_ = false;
|
||
fetch_horizontal_ = false;
|
||
fetch_stop_ = 0xffff;
|
||
|
||
if(y_ == frame_height_) {
|
||
++vsyncs;
|
||
interrupt_requests_ |= InterruptMask<InterruptFlag::VerticalBlank>::value;
|
||
update_interrupts();
|
||
|
||
y_ = 0;
|
||
|
||
// TODO: the manual is vague on when this happens. Try to find out.
|
||
copper_.reload<0>();
|
||
|
||
// TODO: is this really how sprite DMA proceeds?
|
||
for(int c = 0; c < 8; c++) {
|
||
sprites_[c].reset_dma();
|
||
}
|
||
}
|
||
}
|
||
assert(line_cycle_ < line_length_ * 4);
|
||
}
|
||
|
||
// The CIAs are on the E clock.
|
||
cia_divider_ += changes.duration;
|
||
const HalfCycles e_clocks = cia_divider_.divide(HalfCycles(20));
|
||
if(e_clocks > HalfCycles(0)) {
|
||
cia_a.run_for(e_clocks);
|
||
cia_b.run_for(e_clocks);
|
||
}
|
||
|
||
// Propagate TOD updates to the CIAs, and feed their new interrupt
|
||
// outputs back to here.
|
||
cia_a.advance_tod(vsyncs);
|
||
cia_b.advance_tod(hsyncs);
|
||
set_cia_interrupts(cia_a.get_interrupt_line(), cia_b.get_interrupt_line());
|
||
|
||
// Update the disk controller, if any drives are active.
|
||
if(!disk_controller_is_sleeping_) {
|
||
disk_controller_.run_for(changes.duration.cycles());
|
||
}
|
||
|
||
// Record the interrupt level.
|
||
// TODO: is this useful?
|
||
changes.interrupt_level = interrupt_level_;
|
||
return changes;
|
||
}
|
||
|
||
void Chipset::post_bitplanes(const BitplaneData &data) {
|
||
// Expand this
|
||
bitplane_pixels_.set(
|
||
previous_bitplanes_,
|
||
data,
|
||
odd_delay_,
|
||
even_delay_
|
||
);
|
||
previous_bitplanes_ = data;
|
||
}
|
||
|
||
void Chipset::BitplaneShifter::set(const BitplaneData &previous, const BitplaneData &next, int odd_delay, int even_delay) {
|
||
const uint16_t planes[6] = {
|
||
uint16_t(((previous[0] << 16) | next[0]) >> even_delay),
|
||
uint16_t(((previous[1] << 16) | next[1]) >> odd_delay),
|
||
uint16_t(((previous[2] << 16) | next[2]) >> even_delay),
|
||
uint16_t(((previous[3] << 16) | next[3]) >> odd_delay),
|
||
uint16_t(((previous[4] << 16) | next[4]) >> even_delay),
|
||
uint16_t(((previous[5] << 16) | next[5]) >> odd_delay),
|
||
};
|
||
|
||
data_[0] =
|
||
(expand_bitplane_byte(uint8_t(planes[0])) << 0) |
|
||
(expand_bitplane_byte(uint8_t(planes[1])) << 1) |
|
||
(expand_bitplane_byte(uint8_t(planes[2])) << 2) |
|
||
(expand_bitplane_byte(uint8_t(planes[3])) << 3) |
|
||
(expand_bitplane_byte(uint8_t(planes[4])) << 4) |
|
||
(expand_bitplane_byte(uint8_t(planes[5])) << 5);
|
||
|
||
data_[1] =
|
||
(expand_bitplane_byte(uint8_t(planes[0] >> 8)) << 0) |
|
||
(expand_bitplane_byte(uint8_t(planes[1] >> 8)) << 1) |
|
||
(expand_bitplane_byte(uint8_t(planes[2] >> 8)) << 2) |
|
||
(expand_bitplane_byte(uint8_t(planes[3] >> 8)) << 3) |
|
||
(expand_bitplane_byte(uint8_t(planes[4] >> 8)) << 4) |
|
||
(expand_bitplane_byte(uint8_t(planes[5] >> 8)) << 5);
|
||
}
|
||
|
||
void Chipset::update_interrupts() {
|
||
interrupt_level_ = 0;
|
||
|
||
const uint16_t enabled_requests = interrupt_enable_ & interrupt_requests_ & 0x3fff;
|
||
if(enabled_requests && (interrupt_enable_ & 0x4000)) {
|
||
if(enabled_requests & InterruptMask<InterruptFlag::External>::value) {
|
||
interrupt_level_ = 6;
|
||
} else if(enabled_requests & InterruptMask<InterruptFlag::SerialPortReceive, InterruptFlag::DiskSyncMatch>::value) {
|
||
interrupt_level_ = 5;
|
||
} else if(enabled_requests & InterruptMask<InterruptFlag::AudioChannel0, InterruptFlag::AudioChannel1, InterruptFlag::AudioChannel2, InterruptFlag::AudioChannel3>::value) {
|
||
interrupt_level_ = 4;
|
||
} else if(enabled_requests & InterruptMask<InterruptFlag::Copper, InterruptFlag::VerticalBlank, InterruptFlag::Blitter>::value) {
|
||
interrupt_level_ = 3;
|
||
} else if(enabled_requests & InterruptMask<InterruptFlag::IOPortsAndTimers>::value) {
|
||
interrupt_level_ = 2;
|
||
} else if(enabled_requests & InterruptMask<InterruptFlag::SerialPortTransmit, InterruptFlag::DiskBlock, InterruptFlag::Software>::value) {
|
||
interrupt_level_ = 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
void Chipset::perform(const CPU::MC68000::Microcycle &cycle) {
|
||
using Microcycle = CPU::MC68000::Microcycle;
|
||
|
||
#define RW(address) address | ((cycle.operation & Microcycle::Read) << 12)
|
||
#define Read(address) address | (Microcycle::Read << 12)
|
||
#define Write(address) address
|
||
|
||
#define ApplySetClear(target, mask) { \
|
||
const uint16_t value = cycle.value16(); \
|
||
if(value & 0x8000) { \
|
||
target |= (value & mask); \
|
||
} else { \
|
||
target &= ~(value & mask); \
|
||
} \
|
||
}
|
||
|
||
const uint32_t register_address = *cycle.address & 0x1fe;
|
||
switch(RW(register_address)) {
|
||
default:
|
||
LOG("Unimplemented chipset " << (cycle.operation & Microcycle::Read ? "read" : "write") << " " << PADHEX(6) << *cycle.address);
|
||
if(cycle.operation & Microcycle::Read) {
|
||
cycle.set_value16(0xffff);
|
||
}
|
||
break;
|
||
|
||
// Raster position.
|
||
case Read(0x004): { // VPOSR; b15 = LOF, b0 = b8 of y position.
|
||
const uint16_t position = uint16_t(y_ >> 8);
|
||
cycle.set_value16(position);
|
||
} break;
|
||
case Read(0x006): { // VHPOSR; b0–b7 = horizontal; b8–b15 = low bits of vertical position.
|
||
const uint16_t position = uint16_t(((line_cycle_ >> 1) & 0x00ff) | (y_ << 8));
|
||
cycle.set_value16(position);
|
||
} break;
|
||
|
||
case Write(0x02a):
|
||
LOG("TODO: write vertical position high " << PADHEX(4) << cycle.value16());
|
||
break;
|
||
case Write(0x02c):
|
||
LOG("TODO: write vertical position low " << PADHEX(4) << cycle.value16());
|
||
break;
|
||
|
||
// Joystick/mouse input.
|
||
case Read(0x00a): // JOY0DAT
|
||
cycle.set_value16(mouse_.get_position());
|
||
break;
|
||
case Read(0x00c): // JOY1DAT
|
||
cycle.set_value16(0x0202);
|
||
break;
|
||
|
||
case Write(0x034): // POTGO
|
||
// LOG("TODO: pot port start");
|
||
break;
|
||
case Read(0x016): // POTGOR / POTINP
|
||
// LOG("TODO: pot port read");
|
||
cycle.set_value16(0xff00);
|
||
break;
|
||
|
||
// Disk DMA and control.
|
||
case Write(0x020): disk_.set_pointer<0, 16>(cycle.value16()); break; // DSKPTH
|
||
case Write(0x022): disk_.set_pointer<0, 0>(cycle.value16()); break; // DSKPTL
|
||
case Write(0x024): disk_.set_length(cycle.value16()); break; // DSKLEN
|
||
|
||
case Write(0x026): // DSKDAT
|
||
LOG("TODO: disk DMA; " << PADHEX(4) << cycle.value16() << " to " << *cycle.address);
|
||
break;
|
||
|
||
case Write(0x09e): // ADKCON
|
||
LOG("Write disk control");
|
||
ApplySetClear(paula_disk_control_, 0x7fff);
|
||
|
||
disk_controller_.set_control(paula_disk_control_);
|
||
// TODO: should also post to Paula.
|
||
break;
|
||
case Read(0x010): // ADKCONR
|
||
LOG("Read disk control");
|
||
cycle.set_value16(paula_disk_control_);
|
||
break;
|
||
|
||
case Write(0x07e): // DSKSYNC
|
||
disk_controller_.set_sync_word(cycle.value16());
|
||
assert(false); // Not fully implemented.
|
||
break;
|
||
case Read(0x01a): // DSKBYTR
|
||
LOG("TODO: disk status");
|
||
assert(false); // Not yet implemented.
|
||
break;
|
||
|
||
// Refresh.
|
||
case Write(0x028): // REFPTR
|
||
LOG("TODO (maybe): refresh; " << PADHEX(4) << cycle.value16() << " to " << *cycle.address);
|
||
break;
|
||
|
||
// Serial port.
|
||
case Read(0x018): // SERDATR
|
||
LOG("TODO: serial data and status");
|
||
cycle.set_value16(0x3000); // i.e. transmit buffer empty.
|
||
break;
|
||
case Write(0x030): // SERDAT
|
||
LOG("TODO: serial data: " << PADHEX(4) << cycle.value16());
|
||
break;
|
||
case Write(0x032): // SERPER
|
||
LOG("TODO: serial control: " << PADHEX(4) << cycle.value16());
|
||
serial_.set_control(cycle.value16());
|
||
break;
|
||
|
||
// DMA management.
|
||
case Read(0x002): // DMACONR
|
||
cycle.set_value16(dma_control_ | blitter_.get_status());
|
||
break;
|
||
case Write(0x096): // DMACON
|
||
ApplySetClear(dma_control_, 0x1fff);
|
||
break;
|
||
|
||
// Interrupts.
|
||
case Write(0x09a): // INTENA
|
||
ApplySetClear(interrupt_enable_, 0x7fff);
|
||
update_interrupts();
|
||
break;
|
||
case Read(0x01c): // INTENAR
|
||
cycle.set_value16(interrupt_enable_);
|
||
break;
|
||
|
||
case Write(0x09c): // INTREQ
|
||
ApplySetClear(interrupt_requests_, 0x7fff);
|
||
update_interrupts();
|
||
break;
|
||
case Read(0x01e): // INTREQR
|
||
cycle.set_value16(interrupt_requests_);
|
||
break;
|
||
|
||
// Display management.
|
||
case Write(0x08e): { // DIWSTRT
|
||
const uint16_t value = cycle.value16();
|
||
display_window_start_[0] = value & 0xff;
|
||
display_window_start_[1] = value >> 8;
|
||
} break;
|
||
case Write(0x090): { // DIWSTOP
|
||
const uint16_t value = cycle.value16();
|
||
display_window_stop_[0] = 0x100 | (value & 0xff);
|
||
display_window_stop_[1] = value >> 8;
|
||
display_window_stop_[1] |= ((value >> 7) & 0x100) ^ 0x100;
|
||
} break;
|
||
case Write(0x092): // DDFSTRT
|
||
if(fetch_window_[0] != cycle.value16()) {
|
||
LOG("Fetch window start set to " << std::dec << cycle.value16());
|
||
}
|
||
fetch_window_[0] = cycle.value16();
|
||
break;
|
||
case Write(0x094): // DDFSTOP
|
||
// TODO: something in my interpretation of ddfstart and ddfstop
|
||
// means a + 8 is needed below for high-res displays. Investigate.
|
||
if(fetch_window_[1] != cycle.value16()) {
|
||
LOG("Fetch window stop set to " << std::dec << fetch_window_[1]);
|
||
}
|
||
fetch_window_[1] = cycle.value16();
|
||
break;
|
||
|
||
// Bitplanes.
|
||
case Write(0x0e0): bitplanes_.set_pointer<0, 16>(cycle.value16()); break; // BPL1PTH
|
||
case Write(0x0e2): bitplanes_.set_pointer<0, 0>(cycle.value16()); break; // BPL1PTL
|
||
case Write(0x0e4): bitplanes_.set_pointer<1, 16>(cycle.value16()); break; // BPL2PTH
|
||
case Write(0x0e6): bitplanes_.set_pointer<1, 0>(cycle.value16()); break; // BPL2PTL
|
||
case Write(0x0e8): bitplanes_.set_pointer<2, 16>(cycle.value16()); break; // BPL3PTH
|
||
case Write(0x0ea): bitplanes_.set_pointer<2, 0>(cycle.value16()); break; // BPL3PTL
|
||
case Write(0x0ec): bitplanes_.set_pointer<3, 16>(cycle.value16()); break; // BPL4PTH
|
||
case Write(0x0ee): bitplanes_.set_pointer<3, 0>(cycle.value16()); break; // BPL4PTL
|
||
case Write(0x0f0): bitplanes_.set_pointer<4, 16>(cycle.value16()); break; // BPL5PTH
|
||
case Write(0x0f2): bitplanes_.set_pointer<4, 0>(cycle.value16()); break; // BPL5PTL
|
||
case Write(0x0f4): bitplanes_.set_pointer<5, 16>(cycle.value16()); break; // BPL6PTH
|
||
case Write(0x0f6): bitplanes_.set_pointer<5, 0>(cycle.value16()); break; // BPL6PTL
|
||
|
||
case Write(0x100): // BPLCON0
|
||
bitplanes_.set_control(cycle.value16());
|
||
is_high_res_ = cycle.value16() & 0x8000;
|
||
break;
|
||
case Write(0x102): { // BPLCON1
|
||
const uint8_t delay = cycle.value8_low();
|
||
odd_delay_ = delay & 0x0f;
|
||
even_delay_ = delay >> 4;
|
||
} break;
|
||
|
||
|
||
case Write(0x104): // BPLCON2
|
||
case Write(0x106): // BPLCON3 (ECS)
|
||
LOG("TODO: Bitplane control; " << PADHEX(4) << cycle.value16() << " to " << *cycle.address);
|
||
break;
|
||
|
||
case Write(0x108): bitplanes_.set_modulo<0>(cycle.value16()); break; // BPL1MOD
|
||
case Write(0x10a): bitplanes_.set_modulo<1>(cycle.value16()); break; // BPL2MOD
|
||
|
||
case Write(0x110):
|
||
case Write(0x112):
|
||
case Write(0x114):
|
||
case Write(0x116):
|
||
case Write(0x118):
|
||
case Write(0x11a):
|
||
LOG("TODO: Bitplane data; " << PADHEX(4) << cycle.value16() << " to " << *cycle.address);
|
||
break;
|
||
|
||
case Read(0x110): case Read(0x112): case Read(0x114): case Read(0x116):
|
||
case Read(0x118): case Read(0x11a):
|
||
cycle.set_value16(0xffff);
|
||
LOG("Invalid read at " << PADHEX(6) << *cycle.address);
|
||
break;
|
||
|
||
// Blitter.
|
||
case Read(0x040): blitter_.set_control(0, 0xffff); break; // UGH. Have fallen into quite a hole here with my
|
||
case Read(0x042): blitter_.set_control(1, 0xffff); break; // Read/Write macros. TODO: some sort of canonical decode?
|
||
// Templatey to hit the usual Read/Write cases first?
|
||
case Write(0x040): blitter_.set_control(0, cycle.value16()); break;
|
||
case Write(0x042): blitter_.set_control(1, cycle.value16()); break;
|
||
case Write(0x044): blitter_.set_first_word_mask(cycle.value16()); break;
|
||
case Write(0x046): blitter_.set_last_word_mask(cycle.value16()); break;
|
||
|
||
case Write(0x048): blitter_.set_pointer<2, 16>(cycle.value16()); break;
|
||
case Write(0x04a): blitter_.set_pointer<2, 0>(cycle.value16()); break;
|
||
case Write(0x04c): blitter_.set_pointer<1, 16>(cycle.value16()); break;
|
||
case Write(0x04e): blitter_.set_pointer<1, 0>(cycle.value16()); break;
|
||
case Write(0x050): blitter_.set_pointer<0, 16>(cycle.value16()); break;
|
||
case Write(0x052): blitter_.set_pointer<0, 0>(cycle.value16()); break;
|
||
case Write(0x054): blitter_.set_pointer<3, 16>(cycle.value16()); break;
|
||
case Write(0x056): blitter_.set_pointer<3, 0>(cycle.value16()); break;
|
||
|
||
case Write(0x058): blitter_.set_size(cycle.value16()); break;
|
||
case Write(0x05a): blitter_.set_minterms(cycle.value16()); break;
|
||
case Write(0x05c): blitter_.set_vertical_size(cycle.value16()); break;
|
||
case Write(0x05e): blitter_.set_horizontal_size(cycle.value16()); break;
|
||
|
||
case Write(0x060): blitter_.set_modulo<2>(cycle.value16()); break;
|
||
case Write(0x062): blitter_.set_modulo<1>(cycle.value16()); break;
|
||
case Write(0x064): blitter_.set_modulo<0>(cycle.value16()); break;
|
||
case Write(0x066): blitter_.set_modulo<3>(cycle.value16()); break;
|
||
|
||
case Write(0x070): blitter_.set_data(2, cycle.value16()); break;
|
||
case Write(0x072): blitter_.set_data(1, cycle.value16()); break;
|
||
case Write(0x074): blitter_.set_data(0, cycle.value16()); break;
|
||
|
||
// Paula.
|
||
case Write(0x0a0): case Write(0x0a2): case Write(0x0a4): case Write(0x0a6):
|
||
case Write(0x0a8): case Write(0x0aa):
|
||
case Write(0x0b0): case Write(0x0b2): case Write(0x0b4): case Write(0x0b6):
|
||
case Write(0x0b8): case Write(0x0ba):
|
||
case Write(0x0c0): case Write(0x0c2): case Write(0x0c4): case Write(0x0c6):
|
||
case Write(0x0c8): case Write(0x0ca):
|
||
case Write(0x0d0): case Write(0x0d2): case Write(0x0d4): case Write(0x0d6):
|
||
case Write(0x0d8): case Write(0x0da):
|
||
LOG("TODO: Paula write " << PADHEX(2) << (*cycle.address & 0xff) << " " << PADHEX(4) << cycle.value16());
|
||
break;
|
||
|
||
// Copper.
|
||
case Write(0x02e): copper_.set_control(cycle.value16()); break; // COPCON
|
||
case Write(0x080): copper_.set_pointer<0, 16>(cycle.value16()); break; // COP1LCH
|
||
case Write(0x082): copper_.set_pointer<0, 0>(cycle.value16()); break; // COP1LCL
|
||
case Write(0x084): copper_.set_pointer<1, 16>(cycle.value16()); break; // COP2LCH
|
||
case Write(0x086): copper_.set_pointer<1, 0>(cycle.value16()); break; // COP2LCL
|
||
case Write(0x088): case Read(0x088):
|
||
copper_.reload<0>();
|
||
break;
|
||
case Write(0x08a): case Read(0x08a):
|
||
copper_.reload<1>();
|
||
break;
|
||
case Write(0x08c):
|
||
LOG("TODO: coprocessor instruction fetch identity " << PADHEX(4) << cycle.value16());
|
||
break;
|
||
|
||
// Sprites.
|
||
#define Sprite(index, pointer, position) \
|
||
case Write(pointer + 0): sprites_[index].set_pointer<0, 16>(cycle.value16()); break; \
|
||
case Write(pointer + 2): sprites_[index].set_pointer<0, 0>(cycle.value16()); break; \
|
||
case Write(position + 0): sprites_[index].set_start_position(cycle.value16()); break; \
|
||
case Write(position + 2): sprites_[index].set_stop_and_control(cycle.value16()); break; \
|
||
case Write(position + 4): sprites_[index].set_image_data(0, cycle.value16()); break; \
|
||
case Write(position + 6): sprites_[index].set_image_data(1, cycle.value16()); break;
|
||
|
||
Sprite(0, 0x120, 0x140);
|
||
Sprite(1, 0x124, 0x148);
|
||
Sprite(2, 0x128, 0x150);
|
||
Sprite(3, 0x12c, 0x158);
|
||
Sprite(4, 0x130, 0x160);
|
||
Sprite(5, 0x134, 0x168);
|
||
Sprite(6, 0x138, 0x170);
|
||
Sprite(7, 0x13c, 0x178);
|
||
|
||
#undef Sprite
|
||
|
||
// Colour palette.
|
||
case Write(0x180): case Write(0x182): case Write(0x184): case Write(0x186):
|
||
case Write(0x188): case Write(0x18a): case Write(0x18c): case Write(0x18e):
|
||
case Write(0x190): case Write(0x192): case Write(0x194): case Write(0x196):
|
||
case Write(0x198): case Write(0x19a): case Write(0x19c): case Write(0x19e):
|
||
case Write(0x1a0): case Write(0x1a2): case Write(0x1a4): case Write(0x1a6):
|
||
case Write(0x1a8): case Write(0x1aa): case Write(0x1ac): case Write(0x1ae):
|
||
case Write(0x1b0): case Write(0x1b2): case Write(0x1b4): case Write(0x1b6):
|
||
case Write(0x1b8): case Write(0x1ba): case Write(0x1bc): case Write(0x1be): {
|
||
// Store once in regular, linear order.
|
||
const auto entry_address = (register_address - 0x180) >> 1;
|
||
uint8_t *const entry = reinterpret_cast<uint8_t *>(&palette_[entry_address]);
|
||
entry[0] = cycle.value8_high();
|
||
entry[1] = cycle.value8_low();
|
||
|
||
// Also store in bit-swizzled order. In this array,
|
||
// instead of being indexed as [b4 b3 b2 b1 b0], index
|
||
// as [b3 b1 b4 b2 b0]. This is related to the dual/single-playfield
|
||
// decision being made relatively late in the planar -> chunky
|
||
// conversion performed by this implementation.
|
||
const auto swizzled_address =
|
||
(entry_address&0x01) |
|
||
((entry_address&0x02) << 2) |
|
||
((entry_address&0x04) >> 1) |
|
||
((entry_address&0x08) << 1) |
|
||
((entry_address&0x10) >> 2);
|
||
uint8_t *const swizzled_entry = reinterpret_cast<uint8_t *>(&swizzled_palette_[swizzled_address]);
|
||
swizzled_entry[0] = cycle.value8_high();
|
||
swizzled_entry[1] = cycle.value8_low();
|
||
} break;
|
||
}
|
||
|
||
#undef ApplySetClear
|
||
|
||
#undef Write
|
||
#undef Read
|
||
#undef RW
|
||
}
|
||
|
||
// MARK: - Bitplanes.
|
||
|
||
bool Chipset::Bitplanes::advance(int cycle) {
|
||
#define BIND_CYCLE(offset, plane) \
|
||
case offset: \
|
||
if(plane_count_ > plane) { \
|
||
next[plane] = ram_[pointer_[plane] & ram_mask_]; \
|
||
++pointer_[plane]; \
|
||
if constexpr (!plane) { \
|
||
chipset_.post_bitplanes(next); \
|
||
} \
|
||
return true; \
|
||
} \
|
||
return false;
|
||
|
||
if(is_high_res_) {
|
||
switch(cycle&3) {
|
||
default: return false;
|
||
BIND_CYCLE(0, 3);
|
||
BIND_CYCLE(1, 1);
|
||
BIND_CYCLE(2, 2);
|
||
BIND_CYCLE(3, 0);
|
||
}
|
||
} else {
|
||
switch(cycle&7) {
|
||
default: return false;
|
||
/* Omitted: 0. */
|
||
BIND_CYCLE(1, 3);
|
||
BIND_CYCLE(2, 5);
|
||
BIND_CYCLE(3, 1);
|
||
/* Omitted: 4. */
|
||
BIND_CYCLE(5, 2);
|
||
BIND_CYCLE(6, 4);
|
||
BIND_CYCLE(7, 0);
|
||
}
|
||
}
|
||
|
||
return false;
|
||
|
||
#undef BIND_CYCLE
|
||
}
|
||
|
||
void Chipset::Bitplanes::do_end_of_line() {
|
||
// Apply modulos here. Posssibly correct?
|
||
pointer_[0] += modulos_[1];
|
||
pointer_[2] += modulos_[1];
|
||
pointer_[4] += modulos_[1];
|
||
|
||
pointer_[1] += modulos_[0];
|
||
pointer_[3] += modulos_[0];
|
||
pointer_[5] += modulos_[0];
|
||
}
|
||
|
||
void Chipset::Bitplanes::set_control(uint16_t control) {
|
||
is_high_res_ = control & 0x8000;
|
||
plane_count_ = (control >> 12) & 7;
|
||
|
||
// TODO: who really has responsibility for clearing the other
|
||
// bit plane fields?
|
||
std::fill(next.begin() + plane_count_, next.end(), 0);
|
||
if(plane_count_ == 7) {
|
||
plane_count_ = 4;
|
||
}
|
||
}
|
||
|
||
// MARK: - Sprites.
|
||
|
||
void Chipset::Sprite::set_start_position(uint16_t value) {
|
||
v_start_ = (v_start_ & 0xff00) | (value >> 8);
|
||
h_start = uint16_t((h_start & 0x0001) | ((value & 0xff) << 1));
|
||
active = false;
|
||
}
|
||
|
||
void Chipset::Sprite::set_stop_and_control(uint16_t value) {
|
||
h_start = uint16_t((h_start & 0x01fe) | (value & 0x01));
|
||
v_stop_ = uint16_t((value >> 8) | ((value & 0x02) << 7));
|
||
v_start_ = uint16_t((v_start_ & 0x00ff) | ((value & 0x04) << 6));
|
||
attached = value & 0x80;
|
||
}
|
||
|
||
void Chipset::Sprite::set_image_data(int slot, uint16_t value) {
|
||
data[slot] = value;
|
||
active |= slot == 0;
|
||
}
|
||
|
||
bool Chipset::Sprite::advance(int y) {
|
||
switch(dma_state_) {
|
||
// i.e. stopped.
|
||
default: return false;
|
||
|
||
// FetchStart: fetch the first control word and proceed to the second.
|
||
case DMAState::FetchStart:
|
||
set_start_position(ram_[pointer_[0]]);
|
||
++pointer_[0];
|
||
dma_state_ = DMAState::FetchStopAndControl;
|
||
return true;
|
||
|
||
// FetchStopAndControl: fetch second control word and wait for V start.
|
||
case DMAState::FetchStopAndControl:
|
||
set_stop_and_control(ram_[pointer_[0]]);
|
||
++pointer_[0];
|
||
dma_state_ = DMAState::WaitingForStart;
|
||
return true;
|
||
|
||
// WaitingForStart: repeat until V start is found.
|
||
case DMAState::WaitingForStart:
|
||
if(y != v_start_) {
|
||
return false;
|
||
}
|
||
[[fallthrough]];
|
||
|
||
// FetchData1: if v end is reached, stop DMA. Otherwise fetch a word
|
||
// and proceed to FetchData0.
|
||
case DMAState::FetchData1:
|
||
if(y == v_stop_) {
|
||
dma_state_ = DMAState::Stopped;
|
||
return false;
|
||
}
|
||
set_image_data(1, ram_[pointer_[0]]);
|
||
++pointer_[0];
|
||
dma_state_ = DMAState::FetchData0;
|
||
return true;
|
||
|
||
// FetchData0: fetch a word and proceed back to FetchData1.
|
||
case DMAState::FetchData0:
|
||
set_image_data(0, ram_[pointer_[0]]);
|
||
++pointer_[0];
|
||
dma_state_ = DMAState::FetchData1;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
void Chipset::Sprite::reset_dma() {
|
||
dma_state_ = DMAState::FetchStart;
|
||
}
|
||
|
||
template <int sprite> void Chipset::TwoSpriteShifter::load(
|
||
uint16_t lsb,
|
||
uint16_t msb,
|
||
int delay) {
|
||
constexpr int sprite_shift = sprite << 1;
|
||
const int delay_shift = delay << 2;
|
||
|
||
// Clear out any current sprite pixels; this is a reload.
|
||
data_ &= 0xcccc'cccc'cccc'ccccull >> (sprite_shift + delay_shift);
|
||
|
||
// Map LSB and MSB up to 64-bits and load into the shifter.
|
||
const uint64_t new_data =
|
||
(
|
||
expand_sprite_word(lsb) |
|
||
(expand_sprite_word(msb) << 1)
|
||
) << sprite_shift;
|
||
|
||
data_ |= new_data >> delay_shift;
|
||
overflow_ |= uint8_t((new_data << 8) >> delay_shift);
|
||
}
|
||
|
||
// MARK: - Disk.
|
||
|
||
void Chipset::DiskDMA::enqueue(uint16_t value, bool matches_sync) {
|
||
if(matches_sync) {
|
||
// TODO: start buffering from the next word onwards if
|
||
// syncing is enabled.
|
||
}
|
||
|
||
// LOG("In: " << buffer_write_);
|
||
|
||
buffer_[buffer_write_ & 3] = value;
|
||
if(buffer_write_ == buffer_read_ + 4) {
|
||
++buffer_read_;
|
||
}
|
||
++buffer_write_;
|
||
}
|
||
|
||
void Chipset::DiskDMA::set_length(uint16_t value) {
|
||
if(value == last_set_length_) {
|
||
dma_enable_ = value & 0x8000;
|
||
write_ = value & 0x4000;
|
||
length_ = value & 0x3fff;
|
||
buffer_read_ = buffer_write_ = 0;
|
||
|
||
if(dma_enable_) {
|
||
LOG("Disk DMA " << (write_ ? "write" : "read") << " of " << length_ << " to " << PADHEX(8) << pointer_[0]);
|
||
}
|
||
}
|
||
|
||
last_set_length_ = value;
|
||
}
|
||
|
||
bool Chipset::DiskDMA::advance() {
|
||
if(!dma_enable_) return false;
|
||
|
||
if(!write_) {
|
||
if(length_ && buffer_read_ != buffer_write_) {
|
||
ram_[pointer_[0] & ram_mask_] = buffer_[buffer_read_ & 3];
|
||
++pointer_[0];
|
||
++buffer_read_;
|
||
--length_;
|
||
|
||
if(!length_) {
|
||
chipset_.posit_interrupt(InterruptFlag::DiskBlock);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
// MARK: - CRT connection.
|
||
|
||
void Chipset::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
||
crt_.set_scan_target(scan_target);
|
||
}
|
||
|
||
Outputs::Display::ScanStatus Chipset::get_scaled_scan_status() const {
|
||
return crt_.get_scaled_scan_status();
|
||
}
|
||
|
||
void Chipset::set_display_type(Outputs::Display::DisplayType type) {
|
||
crt_.set_display_type(type);
|
||
}
|
||
|
||
Outputs::Display::DisplayType Chipset::get_display_type() const {
|
||
return crt_.get_display_type();
|
||
}
|
||
|
||
// MARK: - CIA A.
|
||
|
||
Chipset::CIAAHandler::CIAAHandler(MemoryMap &map, DiskController &controller, Mouse &mouse) :
|
||
map_(map), controller_(controller), mouse_(mouse) {}
|
||
|
||
void Chipset::CIAAHandler::set_port_output(MOS::MOS6526::Port port, uint8_t value) {
|
||
if(port) {
|
||
// CIA A, Port B: Parallel port output.
|
||
LOG("TODO: parallel output " << PADHEX(2) << +value);
|
||
} else {
|
||
// CIA A, Port A:
|
||
//
|
||
// b7: /FIR1
|
||
// b6: /FIR0
|
||
// b5: /RDY
|
||
// b4: /TRK0
|
||
// b3: /WPRO
|
||
// b2: /CHNG
|
||
// b1: /LED [output]
|
||
// b0: OVL [output]
|
||
|
||
if(observer_) {
|
||
observer_->set_led_status(led_name, !(value & 2));
|
||
}
|
||
map_.set_overlay(value & 1);
|
||
}
|
||
}
|
||
|
||
uint8_t Chipset::CIAAHandler::get_port_input(MOS::MOS6526::Port port) {
|
||
if(port) {
|
||
LOG("TODO: parallel input?");
|
||
} else {
|
||
return
|
||
controller_.get_rdy_trk0_wpro_chng() &
|
||
mouse_.get_cia_button();
|
||
}
|
||
return 0xff;
|
||
}
|
||
|
||
void Chipset::CIAAHandler::set_activity_observer(Activity::Observer *observer) {
|
||
observer_ = observer;
|
||
if(observer) {
|
||
observer->register_led(led_name, Activity::Observer::LEDPresentation::Persistent);
|
||
}
|
||
}
|
||
|
||
// MARK: - CIA B.
|
||
|
||
Chipset::CIABHandler::CIABHandler(DiskController &controller) : controller_(controller) {}
|
||
|
||
void Chipset::CIABHandler::set_port_output(MOS::MOS6526::Port port, uint8_t value) {
|
||
if(port) {
|
||
// CIA B, Port B:
|
||
//
|
||
// Disk motor control, drive and head selection,
|
||
// and stepper control:
|
||
controller_.set_mtr_sel_side_dir_step(value);
|
||
} else {
|
||
// CIA B, Port A: Serial port control.
|
||
//
|
||
// b7: /DTR
|
||
// b6: /RTS
|
||
// b5: /CD
|
||
// b4: /CTS
|
||
// b3: /DSR
|
||
// b2: SEL
|
||
// b1: POUT
|
||
// b0: BUSY
|
||
LOG("TODO: DTR/RTS/etc: " << PADHEX(2) << +value);
|
||
}
|
||
}
|
||
|
||
uint8_t Chipset::CIABHandler::get_port_input(MOS::MOS6526::Port) {
|
||
LOG("Unexpected: input for CIA B");
|
||
return 0xff;
|
||
}
|
||
|
||
// MARK: - ClockingHintObserver.
|
||
|
||
void Chipset::set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference preference) {
|
||
disk_controller_is_sleeping_ = preference == ClockingHint::Preference::None;
|
||
}
|
||
|
||
// MARK: - Disk Controller.
|
||
|
||
Chipset::DiskController::DiskController(Cycles clock_rate, Chipset &chipset, DiskDMA &disk_dma, CIAB &cia) :
|
||
Storage::Disk::Controller(clock_rate),
|
||
chipset_(chipset),
|
||
disk_dma_(disk_dma),
|
||
cia_(cia) {
|
||
|
||
// Add four drives.
|
||
for(int c = 0; c < 4; c++) {
|
||
emplace_drive(clock_rate.as<int>(), 300, 2, Storage::Disk::Drive::ReadyType::IBMRDY);
|
||
}
|
||
}
|
||
|
||
void Chipset::DiskController::process_input_bit(int value) {
|
||
data_ = uint16_t((data_ << 1) | value);
|
||
++bit_count_;
|
||
|
||
const bool sync_matches = data_ == sync_word_;
|
||
if(sync_matches) {
|
||
chipset_.posit_interrupt(InterruptFlag::DiskSyncMatch);
|
||
|
||
if(sync_with_word_) {
|
||
bit_count_ = 0;
|
||
}
|
||
}
|
||
|
||
if(!(bit_count_ & 15)) {
|
||
disk_dma_.enqueue(data_, sync_matches);
|
||
}
|
||
}
|
||
|
||
void Chipset::DiskController::set_sync_word(uint16_t value) {
|
||
LOG("Set disk sync word to " << PADHEX(4) << value);
|
||
sync_word_ = value;
|
||
}
|
||
|
||
void Chipset::DiskController::set_control(uint16_t control) {
|
||
// b13 and b14: precompensation length specifier
|
||
// b12: 0 => GCR precompensation; 1 => MFM.
|
||
// b10: 1 => enable use of word sync; 0 => disable.
|
||
// b9: 1 => sync on MSB (Disk II style, presumably?); 0 => don't.
|
||
// b8: 1 => 2µs per bit; 0 => 4µs.
|
||
|
||
sync_with_word_ = control & 0x400;
|
||
|
||
Storage::Time bit_length;
|
||
bit_length.length = 1;
|
||
bit_length.clock_rate = (control & 0x100) ? 500000 : 250000;
|
||
set_expected_bit_length(bit_length);
|
||
|
||
LOG((sync_with_word_ ? "Will" : "Won't") << " sync with word; bit length is " << ((control & 0x100) ? "short" : "long"));
|
||
}
|
||
|
||
void Chipset::DiskController::process_index_hole() {
|
||
// Pulse the CIA flag input.
|
||
//
|
||
// TODO: rectify once drives do an actual index pulse, with length.
|
||
cia_.set_flag_input(true);
|
||
cia_.set_flag_input(false);
|
||
|
||
// Resync word output. Experimental!!
|
||
bit_count_ = 0;
|
||
}
|
||
|
||
void Chipset::DiskController::set_mtr_sel_side_dir_step(uint8_t value) {
|
||
// b7: /MTR
|
||
// b6: /SEL3
|
||
// b5: /SEL2
|
||
// b4: /SEL1
|
||
// b3: /SEL0
|
||
// b2: /SIDE
|
||
// b1: DIR
|
||
// b0: /STEP
|
||
|
||
// Select active drive.
|
||
set_drive(((value >> 3) & 0x0f) ^ 0x0f);
|
||
|
||
// "[The MTR] signal is nonstandard on the Amiga system.
|
||
// Each drive will latch the motor signal at the time its
|
||
// select signal turns on." — The Hardware Reference Manual.
|
||
const auto difference = int(previous_select_ ^ value);
|
||
previous_select_ = value;
|
||
|
||
// Check for changes in the SEL line per drive.
|
||
const bool motor_on = !(value & 0x80);
|
||
const int side = (value & 0x04) ? 0 : 1;
|
||
const bool did_step = difference & value & 0x01;
|
||
const auto direction = Storage::Disk::HeadPosition(
|
||
(value & 0x02) ? -1 : 1
|
||
);
|
||
|
||
for(int c = 0; c < 4; c++) {
|
||
auto &drive = get_drive(size_t(c));
|
||
const int select_mask = 0x08 << c;
|
||
const bool is_selected = !(value & select_mask);
|
||
|
||
// Both the motor state and the ID shifter are affected upon
|
||
// changes in drive selection only.
|
||
if(difference & select_mask) {
|
||
// If transitioning to inactive, shift the drive ID value;
|
||
// if transitioning to active, possibly reset the drive
|
||
// ID and definitely latch the new motor state.
|
||
if(!is_selected) {
|
||
drive_ids_[c] <<= 1;
|
||
LOG("Shifted drive ID shift register for drive " << +c << " to " << PADHEX(4) << std::bitset<16>{drive_ids_[c]});
|
||
} else {
|
||
// Motor transition on -> off => reload register.
|
||
if(!motor_on && drive.get_motor_on()) {
|
||
// NB:
|
||
// 0xffff'ffff = 3.5" drive;
|
||
// 0x5555'5555 = 5.25" drive;
|
||
// 0x0000'0000 = no drive.
|
||
drive_ids_[c] = 0xffff'ffff;
|
||
LOG("Reloaded drive ID shift register for drive " << +c);
|
||
}
|
||
|
||
// Also latch the new motor state.
|
||
drive.set_motor_on(motor_on);
|
||
}
|
||
}
|
||
|
||
// Set the new side.
|
||
drive.set_head(side);
|
||
|
||
// Possibly step.
|
||
if(did_step && is_selected) {
|
||
LOG("Stepped drive " << +c << " by " << std::dec << +direction.as_int());
|
||
drive.step(direction);
|
||
}
|
||
}
|
||
}
|
||
|
||
uint8_t Chipset::DiskController::get_rdy_trk0_wpro_chng() {
|
||
// b5: /RDY
|
||
// b4: /TRK0
|
||
// b3: /WPRO
|
||
// b2: /CHNG
|
||
|
||
// My interpretation:
|
||
//
|
||
// RDY isn't RDY, it's a shift value as described above, combined with the motor state.
|
||
// CHNG is what is normally RDY.
|
||
|
||
const uint32_t combined_id =
|
||
((previous_select_ & 0x40) ? 0 : drive_ids_[3]) |
|
||
((previous_select_ & 0x20) ? 0 : drive_ids_[2]) |
|
||
((previous_select_ & 0x10) ? 0 : drive_ids_[1]) |
|
||
((previous_select_ & 0x08) ? 0 : drive_ids_[0]);
|
||
|
||
auto &drive = get_drive();
|
||
const uint8_t active_high =
|
||
((combined_id & 0x8000) >> 10) |
|
||
(drive.get_motor_on() ? 0x20 : 0x00) |
|
||
(drive.get_is_ready() ? 0x00 : 0x04) |
|
||
(drive.get_is_track_zero() ? 0x10 : 0x00) |
|
||
(drive.get_is_read_only() ? 0x08 : 0x00);
|
||
|
||
return ~active_high;
|
||
}
|
||
|
||
void Chipset::DiskController::set_activity_observer(Activity::Observer *observer) {
|
||
for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
|
||
drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
|
||
});
|
||
}
|
||
|
||
bool Chipset::DiskController::insert(const std::shared_ptr<Storage::Disk::Disk> &disk, size_t drive) {
|
||
if(drive >= 4) return false;
|
||
get_drive(drive).set_disk(disk);
|
||
return true;
|
||
}
|
||
|
||
bool Chipset::insert(const std::vector<std::shared_ptr<Storage::Disk::Disk>> &disks) {
|
||
bool inserted = false;
|
||
|
||
size_t target = 0;
|
||
for(const auto &disk: disks) {
|
||
inserted |= disk_controller_.insert(disk, target);
|
||
++target;
|
||
}
|
||
|
||
return inserted;
|
||
}
|
||
|
||
// MARK: - Mouse.
|
||
|
||
int Chipset::Mouse::get_number_of_buttons() {
|
||
return 2;
|
||
}
|
||
|
||
void Chipset::Mouse::set_button_pressed(int button, bool is_set) {
|
||
switch(button) {
|
||
case 0:
|
||
cia_state_ = (cia_state_ &~ 0x40) | (is_set ? 0 : 0x40);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
uint8_t Chipset::Mouse::get_cia_button() {
|
||
return cia_state_;
|
||
}
|
||
|
||
void Chipset::Mouse::reset_all_buttons() {
|
||
cia_state_ = 0xff;
|
||
}
|
||
|
||
void Chipset::Mouse::move(int x, int y) {
|
||
position_[0] += x;
|
||
position_[1] += y;
|
||
}
|
||
|
||
Inputs::Mouse &Chipset::get_mouse() {
|
||
return mouse_;
|
||
}
|
||
|
||
uint16_t Chipset::Mouse::get_position() {
|
||
// The Amiga hardware retains only eight bits of position
|
||
// for the mouse; its software polls frequently and maps
|
||
// changes into a larger space.
|
||
//
|
||
// On modern computers with 5k+ displays and trackpads, it
|
||
// proved empirically possible to overflow the hardware
|
||
// counters more quickly than software would poll.
|
||
//
|
||
// Therefore the approach taken for mapping mouse motion
|
||
// into the Amiga is to do it in steps of no greater than
|
||
// [-128, +127], as per the below.
|
||
const int pending[] = {
|
||
position_[0], position_[1]
|
||
};
|
||
|
||
const int8_t change[] = {
|
||
int8_t(std::clamp(pending[0], -128, 127)),
|
||
int8_t(std::clamp(pending[1], -128, 127))
|
||
};
|
||
|
||
position_[0] -= change[0];
|
||
position_[1] -= change[1];
|
||
declared_position_[0] += change[0];
|
||
declared_position_[1] += change[1];
|
||
|
||
return uint16_t(
|
||
(declared_position_[1] << 8) |
|
||
declared_position_[0]
|
||
);
|
||
}
|