1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-06-09 17:29:36 +00:00
CLK/Machines/Amiga/Chipset.cpp
2024-01-19 22:17:35 -05:00

1289 lines
42 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

//
// Chipset.cpp
// Clock Signal
//
// Created by Thomas Harte on 22/07/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Chipset.hpp"
#include "../../Outputs/Log.hpp"
#include <algorithm>
#include <cassert>
#include <tuple>
namespace {
Log::Logger<Log::Source::AmigaChipset> logger;
}
using namespace Amiga;
#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_{
Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT},
Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT}, Sprite{DMA_CONSTRUCT}
},
bitplanes_(DMA_CONSTRUCT),
copper_(DMA_CONSTRUCT),
audio_(DMA_CONSTRUCT, float(input_clock_rate / 2.0)),
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),
keyboard_(cia_a.serial_input) {
disk_controller_.set_clocking_hint_observer(this);
joysticks_.emplace_back(new Joystick());
cia_a_handler_.set_joystick(&joystick(0));
// Very conservatively crop, to roughly the centre 88% of a frame.
// This rectange was specifically calibrated around the default Workbench display.
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.055f, 0.88f, 0.88f));
}
#undef DMA_CONSTRUCT
Chipset::Changes Chipset::run_for(HalfCycles length) {
return run<false>(length);
}
Chipset::Changes Chipset::run_until_after_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 ? InterruptFlag::IOPortsAndTimers : 0) |
(cia_b_interrupt ? InterruptFlag::External : 0);
update_interrupts();
}
void Chipset::posit_interrupt(InterruptFlag::FlagT flag) {
interrupt_requests_ |= flag;
update_interrupts();
}
void DMADeviceBase::posit_interrupt(InterruptFlag::FlagT flag) {
chipset_.posit_interrupt(flag);
}
void Chipset::apply_ham(uint8_t modification) {
uint8_t *const colour = reinterpret_cast<uint8_t *>(&last_colour_);
// Allow for swizzled storage.
switch(modification & 0x24) {
case 0x00: // Direct palette lookup.
last_colour_ = swizzled_palette_[modification & 0x1b];
break;
case 0x04: // Replace blue.
colour[1] = uint8_t(
(colour[1] & 0xf0) |
((modification & 0x10) >> 1) | // bit 3.
((modification & 0x02) << 1) | // bit 2.
((modification & 0x08) >> 2) | // bit 1.
(modification & 0x01) // bit 0.
);
break;
case 0x20: // Replace red.
colour[0] = uint8_t(
((modification & 0x10) >> 1) | // bit 3.
((modification & 0x02) << 1) | // bit 2.
((modification & 0x08) >> 2) | // bit 1.
(modification & 0x01) // bit 0.
);
break;
case 0x24: // Replace green.
colour[1] = uint8_t(
(colour[1] & 0x0f) |
((modification & 0x10) << 3) | // bit 3.
((modification & 0x02) << 5) | // bit 2.
((modification & 0x08) << 2) | // bit 1.
((modification & 0x01) << 4) // bit 0.
);
break;
}
}
void Chipset::output_pixels(int cycles_until_sync) {
// Try to get a new buffer if none is currently allocated.
if(!pixels_) {
uint16_t *const new_pixels = reinterpret_cast<uint16_t *>(crt_.begin_data(size_t(4 * cycles_until_sync)));
if(new_pixels) {
flush_output();
}
pixels_ = new_pixels;
}
// Get the next four playfield pixels (which, in low resolution mode, will
// be repetitious — the playfield has been expanded as if in high res).
const uint32_t playfield = bitplane_pixels_.get(is_high_res_);
// Output playfield pixels, if a buffer was allocated.
if(pixels_) {
if(hold_and_modify_) {
apply_ham(uint8_t(playfield >> 16));
pixels_[0] = pixels_[1] = last_colour_;
apply_ham(uint8_t(playfield));
pixels_[2] = pixels_[3] = last_colour_;
} else if(dual_playfields_) {
// Dense: just write both.
// TODO: this could easily be just a table lookup, exactly as per swizzled_palette_.
if(even_over_odd_) {
pixels_[0] = palette_[8 + ((playfield >> 27) & 7)];
pixels_[1] = palette_[8 + ((playfield >> 19) & 7)];
pixels_[2] = palette_[8 + ((playfield >> 11) & 7)];
pixels_[3] = palette_[8 + ((playfield >> 3) & 7)];
if((playfield >> 24) & 7) pixels_[0] = palette_[(playfield >> 24) & 7];
if((playfield >> 16) & 7) pixels_[1] = palette_[(playfield >> 16) & 7];
if((playfield >> 8) & 7) pixels_[2] = palette_[(playfield >> 8) & 7];
if(playfield & 7) pixels_[3] = palette_[playfield & 7];
} else {
pixels_[0] = palette_[(playfield >> 24) & 7];
pixels_[1] = palette_[(playfield >> 16) & 7];
pixels_[2] = palette_[(playfield >> 8) & 7];
pixels_[3] = palette_[playfield & 7];
if((playfield >> 27) & 7) pixels_[0] = palette_[8 + ((playfield >> 27) & 7)];
if((playfield >> 19) & 7) pixels_[1] = palette_[8 + ((playfield >> 19) & 7)];
if((playfield >> 11) & 7) pixels_[2] = palette_[8 + ((playfield >> 11) & 7)];
if((playfield >> 3) & 7) pixels_[3] = palette_[8 + ((playfield >> 3) & 7)];
}
} else {
pixels_[0] = swizzled_palette_[playfield >> 24];
pixels_[1] = swizzled_palette_[(playfield >> 16) & 0xff];
pixels_[2] = swizzled_palette_[(playfield >> 8) & 0xff];
pixels_[3] = swizzled_palette_[playfield & 0xff];
}
}
// This will store flags to indicate presence or absence of sprite pixels for the four shifters.
int collision_masks[4] = {0, 0, 0, 0};
// If there are sprites visible, bother to figure out the playfield masks here.
if(sprite_shifters_[0].get() | sprite_shifters_[1].get() | sprite_shifters_[2].get() | sprite_shifters_[3].get()) {
// The playfield value is arranged as:
//
// pixel = [0 0 b5 b3 b1 b4 b2 b0]
// full value = [pixel] [pixel] [pixel] [pixel]
//
// i.e. the odd pixel mask is:
// b0 = bits 3, 4, 5;
// b1 = bits 11, 12, 13;
// b2 = bits 19, 20, 21;
// b3 = bits 27, 28, 29.
//
// ... and the even pixel mask is the other set.
// Ensure that b0, b8, b16, b24 are the complete mask state of the even playfields,
// and b3, b11, b19, b27 are the complete mask state of the odd playfields.
const uint32_t merged_playfield = playfield | (playfield >> 1) | (playfield >> 2);
// Collect b0, b8, b16 and b24 as b0, b1, b2, b3 (and give no regard to the other bits).
uint32_t playfield_even_pixel_mask = merged_playfield & 0x01010101;
playfield_even_pixel_mask |= playfield_even_pixel_mask >> 7;
playfield_even_pixel_mask |= playfield_even_pixel_mask >> 14;
// Collect b3, b11, b19 and b27 as b0, b1, b2, b3 (and give no regard to the other bits).
uint32_t playfield_odd_pixel_mask = (merged_playfield >> 3) & 0x01010101;
playfield_odd_pixel_mask |= playfield_odd_pixel_mask >> 7;
playfield_odd_pixel_mask |= playfield_odd_pixel_mask >> 14;
// If only a single playfield is in use, treat the mask as playing
// into the priority selected for the even bitfields.
if(!dual_playfields_) {
playfield_even_pixel_mask |= playfield_odd_pixel_mask;
playfield_odd_pixel_mask = 0;
}
// Draw sprites.
int index = int(sprite_shifters_.size());
for(auto shifter = sprite_shifters_.rbegin(); shifter != sprite_shifters_.rend(); ++shifter) {
// Update the index, and skip this shifter entirely if it's empty.
--index;
const uint8_t data = shifter->get();
if(!data) continue;
// Determine the collision mask.
collision_masks[index] = data | (data >> 1);
if(collisions_flags_ & (0x1000 << index)) {
collision_masks[index] |= (data >> 2) | (data >> 3);
}
collision_masks[index] = (collision_masks[index] & 0x01) | ((collision_masks[index] & 0x10) >> 3);
// Get the specific pixel mask;
//
// Playfield priority meanings:
//
// 4: behind all sprites;
// 3: in front of sprites 6 & 7, behind all others;
// 2: in front of 4, 5, 6 & 7; behind all others;
// 1: in front of 2, 3, 4, 5, 6, & 7; behind 0 & 1;
// 0: in front of all sprites.
//
// i.e. the playfield is in front of the two sprites in shifter n
// if and only if it has a priority of n or less.
const auto pixel_mask =
(
((odd_priority_ <= index) ? playfield_odd_pixel_mask : 0) |
((even_priority_ <= index) ? playfield_even_pixel_mask : 0)
);
// Output pixels, if a buffer exists and only where the pixel
// mask allows. TODO: try to find a less branchy version of the below.
const auto base = (index << 2) + 16;
if(pixels_) {
if(sprites_[size_t((index << 1) + 1)].attached) {
// Left pixel.
if(data >> 4) {
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[16 + (data >> 4)];
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[16 + (data >> 4)];
}
// Right pixel.
if(data & 15) {
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[16 + (data & 15)];
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[16 + (data & 15)];
}
} else {
// Left pixel.
if((data >> 4) & 3) {
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + ((data >> 4)&3)];
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + ((data >> 4)&3)];
}
if(data >> 6) {
if(!(pixel_mask & 0x8)) pixels_[0] = palette_[base + (data >> 6)];
if(!(pixel_mask & 0x4)) pixels_[1] = palette_[base + (data >> 6)];
}
// Right pixel.
if(data & 3) {
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + (data & 3)];
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + (data & 3)];
}
if((data >> 2) & 3) {
if(!(pixel_mask & 0x2)) pixels_[2] = palette_[base + ((data >> 2)&3)];
if(!(pixel_mask & 0x1)) pixels_[3] = palette_[base + ((data >> 2)&3)];
}
}
}
}
}
// Compute playfield collision mask and populate collisions register.
const uint32_t playfield_collisions = (playfield & playfield_collision_mask_) ^ playfield_collision_complement_;
int playfield_collisions_mask =
(playfield_collisions | (playfield_collisions >> 1) | (playfield_collisions >> 2)) & 0x09090909;
playfield_collisions_mask =
playfield_collisions_mask | (playfield_collisions_mask >> 8) | (playfield_collisions_mask >> 15) | (playfield_collisions_mask >> 22);
const int playfield_collision_masks[2] = {
playfield_collisions_mask,
playfield_collisions_mask >> 3
};
// TODO: as below, but without conditionals...
collisions_ |=
((collision_masks[2] & collision_masks[3]) ? 0x4000 : 0x0000) |
((collision_masks[1] & collision_masks[3]) ? 0x2000 : 0x0000) |
((collision_masks[1] & collision_masks[2]) ? 0x1000 : 0x0000) |
((collision_masks[0] & collision_masks[3]) ? 0x0800 : 0x0000) |
((collision_masks[0] & collision_masks[2]) ? 0x0400 : 0x0000) |
((collision_masks[0] & collision_masks[1]) ? 0x0200 : 0x0000) |
((playfield_collision_masks[1] & collision_masks[3]) ? 0x0100 : 0x0000) |
((playfield_collision_masks[1] & collision_masks[2]) ? 0x0080 : 0x0000) |
((playfield_collision_masks[1] & collision_masks[1]) ? 0x0040 : 0x0000) |
((playfield_collision_masks[1] & collision_masks[0]) ? 0x0020 : 0x0000) |
((playfield_collision_masks[0] & collision_masks[3]) ? 0x0010 : 0x0000) |
((playfield_collision_masks[0] & collision_masks[2]) ? 0x0008 : 0x0000) |
((playfield_collision_masks[0] & collision_masks[1]) ? 0x0004 : 0x0000) |
((playfield_collision_masks[0] & collision_masks[0]) ? 0x0002 : 0x0000) |
((playfield_collision_masks[0] & playfield_collision_masks[1]) ? 0x0001 : 0x0000);
// Advance pixel pointer (if applicable).
if(pixels_) {
pixels_ += 4;
}
}
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.
// TODO: Reload bitplanes if anything is pending.
// if(has_next_bitplanes_) {
// has_next_bitplanes_ = false;
// bitplane_pixels_.set(
// previous_bitplanes_,
// next_bitplanes_,
// odd_delay_,
// even_delay_
// );
// previous_bitplanes_ = next_bitplanes_;
// }
// Advance audio.
audio_.output();
// Trigger any sprite loads encountered.
constexpr auto dcycle = cycle << 1;
static_assert(std::tuple_size<decltype(sprites_)>::value % 2 == 0);
for(size_t c = 0; c < sprites_.size(); c += 2) {
if( sprites_[c].visible &&
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].visible &&
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);
}
}
//
// Horizontal sync: HC18HC35;
// Horizontal blank: HC15HC53.
//
// Beyond that: guesswork.
//
// So, from cycle 0:
//
// 15 cycles border/pixels;
// 3 cycles blank;
// 17 cycles sync;
// 3 cycles blank;
// 9 cycles colour burst;
// 6 cycles blank;
// then more border/pixels to end of line.
//
// (???)
constexpr int end_of_pixels = 15;
constexpr int blank1 = 3 + end_of_pixels;
constexpr int sync = 17 + blank1;
constexpr int blank2 = 3 + sync;
constexpr int burst = 9 + blank2;
constexpr int blank3 = 6 + burst;
static_assert(blank3 == 53);
#define LINK(location, action, length) \
if(cycle == (location)) { \
crt_.action((length) * 4); \
}
if(y_ < vertical_blank_height_) {
if(!cycle) {
flush_output();
}
// 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(is_long_field_) {
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 {
// 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(cycle == end_of_pixels) {
flush_output();
}
// Output the correct sequence of blanks, syncs and burst atomically.
LINK(blank1, output_blank, blank1 - end_of_pixels);
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);
if constexpr (cycle < end_of_pixels || 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) {
// This is factored out because it is fairly convoluted and is not a function of
// the template parameter; I doubt I'm somehow being smarter than the optimising
// compiler, but this makes my debugging life a lot easier and I don't imagine
// the compiler will do a worse job.
output_pixels(line_length_ + end_of_pixels - cycle);
}
++zone_duration_;
}
}
#undef LINK
// 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();
}
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);
last_colour_ = border_colour_;
} 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() {
// Update state as to whether bitplane fetching should happen now.
//
// TODO: figure out how the hard stops factor into this.
//
// Top priority: bitplane collection.
if(cycle == fetch_window_[0]) {
horizontal_fetch_ = HorizontalFetch::Started;
horizontal_offset_ = cycle;
}
if(cycle == fetch_window_[1]) {
horizontal_fetch_ = HorizontalFetch::WillRequestStop;
}
if(horizontal_fetch_ != HorizontalFetch::Stopped) {
if(!((cycle - horizontal_offset_) & 7)) {
switch(horizontal_fetch_) {
case HorizontalFetch::WillRequestStop: horizontal_fetch_ = HorizontalFetch::StopRequested; break;
case HorizontalFetch::StopRequested: horizontal_fetch_ = HorizontalFetch::Stopped; break;
default: break;
}
}
constexpr auto BitplaneEnabled = DMAFlag::AllBelow | DMAFlag::Bitplane;
if(
horizontal_fetch_ != HorizontalFetch::Stopped &&
(dma_control_ & BitplaneEnabled) == BitplaneEnabled &&
fetch_vertical_ &&
bitplanes_.advance_dma(cycle - horizontal_offset_)
) {
did_fetch_ = true;
return false;
}
}
// Contradictory snippets from the Hardware Reference manual:
//
// 1)
// The Copper is a two-cycle processor that requests the bus only during
// odd-numbered memory cycles. This prevents collision with audio, disk,
// refresh, sprites, and most low resolution display DMA access, all of which
// use only the even-numbered memory cycles.
//
// 2)
// |<- - - - - - - - average 68000 cycle - - - - - - - - ->|
// | |
// |<- - - - internal - - - ->|<- - - - - memory - - - ->|
// | operation | access |
// | portion | portion |
// | | |
// | odd cycle, | even cycle, |
// | assigned to | available to |
// | other devices | the 68000 |
//
// Figure 6-10: Normal 68000 Cycle
// There's also Figure 6-9, which in theory nails down slot usage, but
// numbers the boundaries between slots rather than the slots themselves...
// and has nine slots depicted between positions $20 and $21. So
// whether the boundary numbers assign to the slots on their left or on
// their right is entirely opaque.
// I therefore take the word of Toni Wilen via https://eab.abime.net/showpost.php?p=938307&postcount=2
// as definitive: "CPU ... generally ... uses even [cycles] only".
//
// So probably the Copper requests the bus only on _even_ cycles?
// General rule:
//
// Chipset work on odd cycles, 68000 access on even.
//
// Exceptions:
//
// Bitplanes, the Blitter if a flag is set.
if constexpr (cycle & 1) {
// Odd slot use/priority:
//
// 1. Bitplane fetches [dealt with above].
// 2. Refresh, disk, audio, sprites or Copper. Depending on region.
//
// Blitter and CPU priority is dealt with below.
if constexpr (cycle >= 0x00 && cycle < 0x08) {
// Memory refresh, four slots per line.
return false;
}
if constexpr (cycle >= 0x08 && cycle < 0x0e) {
constexpr auto DiskEnabled = DMAFlag::AllBelow | DMAFlag::Disk;
if((dma_control_ & DiskEnabled) == DiskEnabled) {
if(disk_.advance_dma()) {
return false;
}
}
}
if constexpr (cycle >= 0xe && cycle < 0x16) {
constexpr auto channel = (cycle - 0xe) >> 1;
static_assert(channel >= 0 && channel < 4);
static_assert(cycle != 0x15 || channel == 3);
constexpr DMAFlag::FlagT AudioFlags[] = {
DMAFlag::AllBelow | DMAFlag::AudioChannel0,
DMAFlag::AllBelow | DMAFlag::AudioChannel1,
DMAFlag::AllBelow | DMAFlag::AudioChannel2,
DMAFlag::AllBelow | DMAFlag::AudioChannel3,
};
if((dma_control_ & AudioFlags[channel]) == AudioFlags[channel]) {
if(audio_.advance_dma(channel)) {
return false;
}
}
}
if constexpr (cycle >= 0x16 && cycle < 0x36) {
constexpr auto SpritesEnabled = DMAFlag::AllBelow | DMAFlag::Sprites;
if(y_ >= vertical_blank_height_ && (dma_control_ & SpritesEnabled) == SpritesEnabled) {
constexpr auto sprite_id = (cycle - 0x16) >> 2;
static_assert(sprite_id >= 0 && sprite_id < std::tuple_size<decltype(sprites_)>::value);
if(sprites_[sprite_id].advance_dma((~cycle&2) >> 1, y_, y_ == vertical_blank_height_)) {
return false;
}
}
}
} else {
// Bitplanes having been dealt with, specific even-cycle responsibility
// is just possibly to pass to the Copper.
constexpr auto CopperEnabled = DMAFlag::AllBelow | DMAFlag::Copper;
if((dma_control_ & CopperEnabled) == CopperEnabled) {
if(copper_.advance_dma(uint16_t(((y_ & 0xff) << 8) | cycle), blitter_.get_status())) {
return false;
}
} else {
copper_.stop();
}
// Picking between the Blitter and CPU occurs below, if applicable.
// But if the Blitter priority bit isn't set then don't even give it
// a look-in — nothing else having claimed this slot, leave it vacant
// for the CPU.
if(!(dma_control_ & DMAFlag::BlitterPriority)) {
return true;
}
}
// Give first refusal to the Blitter (if enabled), otherwise pass on to the CPU.
//
// TODO: determine why I see Blitter issues if I don't allow it to complete immediately.
// All tests pass without immediate completion, and immediate completion just runs the
// non-immediate version until the busy flag is disabled. So probably a scheduling or
// signalling issue out here.
constexpr auto BlitterEnabled = DMAFlag::AllBelow | DMAFlag::Blitter;
return (dma_control_ & BlitterEnabled) != BlitterEnabled || !blitter_.advance_dma<true>();
}
/// 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;
}
assert(last_slot > first_slot);
#define C(x) \
case x: \
output<x>(); \
\
if constexpr (stop_on_cpu) { \
if(perform_cycle<x, stop_on_cpu>()) { \
return 1 + x - first_slot; \
} \
} else { \
perform_cycle<x, stop_on_cpu>(); \
} \
\
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_;
if(did_fetch_) {
bitplanes_.do_end_of_line();
previous_bitplanes_.clear();
}
did_fetch_ = false;
horizontal_fetch_ = HorizontalFetch::Stopped;
if(y_ == short_field_height_ + is_long_field_) {
++vsyncs;
interrupt_requests_ |= InterruptFlag::VerticalBlank;
update_interrupts();
y_ = 0;
// TODO: the manual is vague on when this happens. Try to find out.
copper_.reload<0>();
// Toggle next field length if interlaced.
is_long_field_ ^= interlace_;
}
fetch_vertical_ |= y_ == display_window_start_[1];
fetch_vertical_ &= y_ != display_window_stop_[1];
}
assert(line_cycle_ < line_length_ * 4);
}
// Advance the keyboard's serial output, at
// close enough to 1,000,000 ticks/second.
keyboard_divider_ += changes.duration;
keyboard_.run_for(keyboard_divider_.divide(HalfCycles(14)));
// 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) {
// For now this retains the storage that'll be used when I switch to
// deferred loading, but continues to act as if the Amiga were barrel
// shifting bitplane data.
next_bitplanes_ = data;
bitplane_pixels_.set(
previous_bitplanes_,
next_bitplanes_,
odd_delay_,
even_delay_
);
previous_bitplanes_ = next_bitplanes_;
}
void Chipset::update_interrupts() {
audio_.set_interrupt_requests(interrupt_requests_);
interrupt_level_ = 0;
const uint16_t enabled_requests = interrupt_enable_ & interrupt_requests_ & 0x3fff;
if(enabled_requests && (interrupt_enable_ & 0x4000)) {
if(enabled_requests & InterruptFlag::External) {
interrupt_level_ = 6;
} else if(enabled_requests & (InterruptFlag::SerialPortReceive | InterruptFlag::DiskSyncMatch)) {
interrupt_level_ = 5;
} else if(enabled_requests & (InterruptFlag::AudioChannel0 | InterruptFlag::AudioChannel1 | InterruptFlag::AudioChannel2 | InterruptFlag::AudioChannel3)) {
interrupt_level_ = 4;
} else if(enabled_requests & (InterruptFlag::Copper | InterruptFlag::VerticalBlank | InterruptFlag::Blitter)) {
interrupt_level_ = 3;
} else if(enabled_requests & InterruptFlag::IOPortsAndTimers) {
interrupt_level_ = 2;
} else if(enabled_requests & (InterruptFlag::SerialPortTransmit | InterruptFlag::DiskBlock | InterruptFlag::Software)) {
interrupt_level_ = 1;
}
}
}
void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) {
#define ApplySetClear(target, mask) { \
if(value & 0x8000) { \
target |= (value & mask); \
} else { \
target &= ~(value & mask); \
} \
}
switch(address & ChipsetAddressMask) {
default:
// If there was nothing to write, perform a throwaway read.
if(allow_conversion) read(address, false);
break;
// Raster position.
case 0x098: // CLXCON
collisions_flags_ = value;
// Produce appropriate bitfield manipulation values, including shuffling the bits.
playfield_collision_mask_ = bitplane_swizzle(uint32_t((collisions_flags_ & 0xfc0) >> 6));
playfield_collision_complement_ = bitplane_swizzle(uint32_t((collisions_flags_ & 0x3f) ^ 0x3f));
playfield_collision_mask_ |= (playfield_collision_mask_ << 8) | (playfield_collision_mask_ << 16) | (playfield_collision_mask_ << 24);
playfield_collision_complement_ |= (playfield_collision_complement_ << 8) | (playfield_collision_complement_ << 16) | (playfield_collision_complement_ << 24);
break;
case 0x02a: // VPOSW
logger.error().append("TODO: write vertical position high %04x", value);
break;
case 0x02c: // VHPOSW
logger.error().append("TODO: write vertical position low %04x", value);
is_long_field_ = value & 0x8000;
break;
// Joystick/mouse input.
case 0x034: // POTGO
// logger.error().append("TODO: pot port start");
break;
// Disk DMA and control.
case 0x020: disk_.set_pointer<0, 16>(value); break; // DSKPTH
case 0x022: disk_.set_pointer<0, 0>(value); break; // DSKPTL
case 0x024: disk_.set_length(value); break; // DSKLEN
case 0x026: // DSKDAT
logger.error().append("TODO: disk DMA; %04x to %04x", value, address);
break;
case 0x09e: // ADKCON
logger.info().append("Write disk control");
ApplySetClear(paula_disk_control_, 0x7fff);
disk_controller_.set_control(paula_disk_control_);
disk_.set_control(paula_disk_control_);
audio_.set_modulation_flags(paula_disk_control_);
break;
case 0x07e: // DSKSYNC
disk_controller_.set_sync_word(value);
break;
// Refresh.
case 0x028: // REFPTR
logger.info().append("TODO (maybe): refresh; %04x to %08x", value, address);
break;
// Serial port.
case 0x030: // SERDAT
serial_.set_data(value);
break;
case 0x032: // SERPER
serial_.set_control(value);
break;
// DMA management.
case 0x096: // DMACON
ApplySetClear(dma_control_, 0x1fff);
audio_.set_channel_enables(dma_control_);
break;
// Interrupts.
case 0x09a: // INTENA
ApplySetClear(interrupt_enable_, 0x7fff);
update_interrupts();
break;
case 0x09c: // INTREQ
ApplySetClear(interrupt_requests_, 0x7fff);
update_interrupts();
break;
// Display management.
case 0x08e: // DIWSTRT
display_window_start_[0] = value & 0xff;
display_window_start_[1] = value >> 8;
break;
case 0x090: // DIWSTOP
display_window_stop_[0] = 0x100 | (value & 0xff);
display_window_stop_[1] = value >> 8;
display_window_stop_[1] |= ((value >> 7) & 0x100) ^ 0x100;
break;
case 0x092: // DDFSTRT
if(fetch_window_[0] != value) {
logger.info().append("Fetch window start set to %d", value);
}
fetch_window_[0] = value & 0xfe;
break;
case 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] != value) {
logger.info().append("Fetch window stop set to %d", fetch_window_[1]);
}
fetch_window_[1] = value & 0xfe;
break;
// Bitplanes.
case 0x0e0: bitplanes_.set_pointer<0, 16>(value); break; // BPL1PTH
case 0x0e2: bitplanes_.set_pointer<0, 0>(value); break; // BPL1PTL
case 0x0e4: bitplanes_.set_pointer<1, 16>(value); break; // BPL2PTH
case 0x0e6: bitplanes_.set_pointer<1, 0>(value); break; // BPL2PTL
case 0x0e8: bitplanes_.set_pointer<2, 16>(value); break; // BPL3PTH
case 0x0ea: bitplanes_.set_pointer<2, 0>(value); break; // BPL3PTL
case 0x0ec: bitplanes_.set_pointer<3, 16>(value); break; // BPL4PTH
case 0x0ee: bitplanes_.set_pointer<3, 0>(value); break; // BPL4PTL
case 0x0f0: bitplanes_.set_pointer<4, 16>(value); break; // BPL5PTH
case 0x0f2: bitplanes_.set_pointer<4, 0>(value); break; // BPL5PTL
case 0x0f4: bitplanes_.set_pointer<5, 16>(value); break; // BPL6PTH
case 0x0f6: bitplanes_.set_pointer<5, 0>(value); break; // BPL6PTL
case 0x100: // BPLCON0
bitplanes_.set_control(value);
is_high_res_ = value & 0x8000;
hold_and_modify_ = value & 0x0800;
dual_playfields_ = value & 0x0400;
interlace_ = value & 0x0004;
break;
case 0x102: // BPLCON1
odd_delay_ = value & 0x0f;
even_delay_ = (value >> 4) & 0x0f;
break;
case 0x104: // BPLCON2
odd_priority_ = value & 7; // i.e. "Playfield 1"; planes 1, 3 and 5.
even_priority_ = (value >> 3) & 7; // i.e. "Playfield 2"; planes 2, 4 and 6.
even_over_odd_ = value & 0x40;
break;
case 0x106: // BPLCON3 (ECS)
logger.error().append("TODO: Bitplane control; %04x to %08x", value, address);
break;
case 0x108: bitplanes_.set_modulo<0>(value); break; // BPL1MOD
case 0x10a: bitplanes_.set_modulo<1>(value); break; // BPL2MOD
case 0x110:
case 0x112:
case 0x114:
case 0x116:
case 0x118:
case 0x11a:
logger.error().append("TODO: Bitplane data; %04x to %08x", value, address);
break;
// Blitter.
case 0x040: blitter_.set_control(0, value); break;
case 0x042: blitter_.set_control(1, value); break;
case 0x044: blitter_.set_first_word_mask(value); break;
case 0x046: blitter_.set_last_word_mask(value); break;
case 0x048: blitter_.set_pointer<2, 16>(value); break;
case 0x04a: blitter_.set_pointer<2, 0>(value); break;
case 0x04c: blitter_.set_pointer<1, 16>(value); break;
case 0x04e: blitter_.set_pointer<1, 0>(value); break;
case 0x050: blitter_.set_pointer<0, 16>(value); break;
case 0x052: blitter_.set_pointer<0, 0>(value); break;
case 0x054: blitter_.set_pointer<3, 16>(value); break;
case 0x056: blitter_.set_pointer<3, 0>(value); break;
case 0x058: blitter_.set_size(value); break;
case 0x05a: blitter_.set_minterms(value); break;
case 0x060: blitter_.set_modulo<2>(value); break;
case 0x062: blitter_.set_modulo<1>(value); break;
case 0x064: blitter_.set_modulo<0>(value); break;
case 0x066: blitter_.set_modulo<3>(value); break;
case 0x070: blitter_.set_data(2, value); break;
case 0x072: blitter_.set_data(1, value); break;
case 0x074: blitter_.set_data(0, value); break;
// Audio.
#define Audio(index, pointer) \
case pointer + 0: audio_.set_pointer<index, 16>(value); break; \
case pointer + 2: audio_.set_pointer<index, 0>(value); break; \
case pointer + 4: audio_.set_length(index, value); break; \
case pointer + 6: audio_.set_period(index, value); break; \
case pointer + 8: audio_.set_volume(index, value); break; \
case pointer + 10: audio_.set_data(index, value); break; \
Audio(0, 0x0a0);
Audio(1, 0x0b0);
Audio(2, 0x0c0);
Audio(3, 0x0d0);
#undef Audio
// Copper.
case 0x02e: copper_.set_control(value); break; // COPCON
case 0x080: copper_.set_pointer<0, 16>(value); break; // COP1LCH
case 0x082: copper_.set_pointer<0, 0>(value); break; // COP1LCL
case 0x084: copper_.set_pointer<1, 16>(value); break; // COP2LCH
case 0x086: copper_.set_pointer<1, 0>(value); break; // COP2LCL
case 0x088: copper_.reload<0>(); break;
case 0x08a: copper_.reload<1>(); break;
case 0x08c:
logger.error().append("TODO: coprocessor instruction fetch identity %04x", value);
break;
// Sprites.
#define Sprite(index, pointer, position) \
case pointer + 0: sprites_[index].set_pointer<0, 16>(value); break; \
case pointer + 2: sprites_[index].set_pointer<0, 0>(value); break; \
case position + 0: sprites_[index].set_start_position(value); break; \
case position + 2: sprites_[index].set_stop_and_control(value); break; \
case position + 4: sprites_[index].set_image_data(0, value); break; \
case position + 6: sprites_[index].set_image_data(1, value); 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 0x180: case 0x182: case 0x184: case 0x186: case 0x188: case 0x18a: case 0x18c: case 0x18e:
case 0x190: case 0x192: case 0x194: case 0x196: case 0x198: case 0x19a: case 0x19c: case 0x19e:
case 0x1a0: case 0x1a2: case 0x1a4: case 0x1a6: case 0x1a8: case 0x1aa: case 0x1ac: case 0x1ae:
case 0x1b0: case 0x1b2: case 0x1b4: case 0x1b6: case 0x1b8: case 0x1ba: case 0x1bc: case 0x1be: {
// Store once in regular, linear order.
const auto entry_address = (address - 0x180) >> 1;
uint8_t *const entry = reinterpret_cast<uint8_t *>(&palette_[entry_address]);
entry[0] = value >> 8;
entry[1] = value & 0xff;
// 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], and include a second set of the
// 32 colours, stored as half-bright.
const auto swizzled_address = bitplane_swizzle(entry_address & 0x1f);
uint8_t *const swizzled_entry = reinterpret_cast<uint8_t *>(&swizzled_palette_[swizzled_address]);
swizzled_entry[0] = value >> 8;
swizzled_entry[1] = value & 0xff;
swizzled_entry[64] = (swizzled_entry[0] >> 1) & 0x77;
swizzled_entry[65] = (swizzled_entry[1] >> 1) & 0x77;
} break;
}
#undef ApplySetClear
}
uint16_t Chipset::read(uint32_t address, bool allow_conversion) {
switch(address & ChipsetAddressMask) {
default:
// If there was nothing to read, perform a write.
// TODO: Rather than 0xffff, should be whatever is left on the bus, vapour-lock style.
if(allow_conversion) write(address, 0xffff, false);
return 0xffff;
// Raster position.
case 0x004: { // VPOSR; b15 = LOF, b0 = b8 of y position.
const uint16_t position = uint16_t(y_ >> 8);
return
position |
(is_long_field_ ? 0x8000 : 0x0000);
// b8b14 should be:
// 00 for PAL Agnus or fat Agnus
// 10 for NTSC Agnus or fat Agnus
// 20 for PAL high-res
// 30 for NTSC high-res
}
case 0x006: { // VHPOSR; b0b7 = horizontal; b8b15 = low bits of vertical position.
const uint16_t position = uint16_t(((line_cycle_ >> 1) & 0x00ff) | (y_ << 8));
return position;
}
case 0x00e: { // CLXDAT
const uint16_t result = collisions_;
collisions_ = 0;
return result;
};
// Joystick/mouse input.
case 0x00a: return mouse_.get_position(); // JOY0DAT
case 0x00c: return joystick(0).get_position(); // JOY1DAT
case 0x016: // POTGOR / POTINP
// logger.error().append("TODO: pot port read");
return 0xff00;
// Disk DMA and control.
case 0x010: // ADKCONR
logger.info().append("Read disk control");
return paula_disk_control_;
case 0x01a: // DSKBYTR
logger.error().append("TODO: disk status");
assert(false); // Not yet implemented.
return 0xffff;
// Serial port.
case 0x018: return serial_.get_status();
// DMA management.
case 0x002: return dma_control_ | blitter_.get_status(); // DMACONR
// Interrupts.
case 0x01c: return interrupt_enable_; // INTENAR
case 0x01e: return interrupt_requests_; // INTREQR
}
}
// 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.
logger.info().append("TODO: parallel output %02x", 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) {
logger.info().append("TODO: parallel input?");
} else {
// Use the mouse as FIR0, the joystick as FIR1.
return
controller_.get_rdy_trk0_wpro_chng() &
mouse_.get_cia_button() &
(1 | (joystick_->get_cia_button() << 1));
}
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
logger.error().append("TODO: DTR/RTS/etc: %02x", value);
}
}
uint8_t Chipset::CIABHandler::get_port_input(MOS::MOS6526::Port) {
logger.error().append("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: - Synchronisation.
void Chipset::flush() {
}
// MARK: - Serial port.
void Chipset::SerialPort::set_control(uint16_t) {
}
void Chipset::SerialPort::set_data(uint16_t) {
}
uint16_t Chipset::SerialPort::get_status() {
return 0x3000;
}