mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-22 12:33:29 +00:00
1289 lines
42 KiB
C++
1289 lines
42 KiB
C++
//
|
||
// 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: HC18–HC35;
|
||
// Horizontal blank: HC15–HC53.
|
||
//
|
||
// 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);
|
||
|
||
// b8–b14 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; b0–b7 = horizontal; b8–b15 = 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;
|
||
}
|