1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-27 16:31:31 +00:00
CLK/Machines/Amiga/Blitter.cpp

583 lines
20 KiB
C++
Raw 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.

//
// Blitter.cpp
// Clock Signal
//
// Created by Thomas Harte on 22/07/2021.
// Copyright © 2021 Thomas Harte. All rights reserved.
//
#include "Blitter.hpp"
#include "Minterms.hpp"
#include <cassert>
#ifndef NDEBUG
#define NDEBUG
#endif
#define LOG_PREFIX "[Blitter] "
#include "../../Outputs/Log.hpp"
using namespace Amiga;
namespace {
/// @returns Either the final carry flag or the output nibble when using fill mode given that it either @c is_exclusive fill mode, or isn't;
/// and the specified initial @c carry and input @c nibble.
template <bool wants_carry> constexpr uint32_t fill_nibble(bool is_exclusive, uint8_t carry, uint8_t nibble) {
uint8_t fill_output = 0;
uint8_t bit = 0x01;
while(bit < 0x10) {
auto pre_toggle = nibble & bit, post_toggle = pre_toggle;
if(!is_exclusive) {
pre_toggle &= ~carry; // Accept bits that would transition to set immediately.
post_toggle &= carry; // Accept bits that would transition to clear after the fact.
} else {
post_toggle = 0; // Just do the pre-toggle.
}
carry ^= pre_toggle;
fill_output |= carry;
carry ^= post_toggle;
bit <<= 1;
carry <<= 1;
}
if constexpr (wants_carry) {
return carry >> 4;
} else {
return fill_output;
}
}
// Lookup key for these tables is:
//
// b0b3: input nibble
// b4: carry
// b5: is_exclusive
//
// i.e. it's in the range [0, 63].
//
// Tables below are indexed such that the higher-order bits select a table entry, lower-order bits select
// a bit or nibble from within the indexed item.
constexpr uint32_t fill_carries[] = {
(fill_nibble<true>(false, 0, 0x0) << 0x0) | (fill_nibble<true>(false, 0, 0x1) << 0x1) | (fill_nibble<true>(false, 0, 0x2) << 0x2) | (fill_nibble<true>(false, 0, 0x3) << 0x3) |
(fill_nibble<true>(false, 0, 0x4) << 0x4) | (fill_nibble<true>(false, 0, 0x5) << 0x5) | (fill_nibble<true>(false, 0, 0x6) << 0x6) | (fill_nibble<true>(false, 0, 0x7) << 0x7) |
(fill_nibble<true>(false, 0, 0x8) << 0x8) | (fill_nibble<true>(false, 0, 0x9) << 0x9) | (fill_nibble<true>(false, 0, 0xa) << 0xa) | (fill_nibble<true>(false, 0, 0xb) << 0xb) |
(fill_nibble<true>(false, 0, 0xc) << 0xc) | (fill_nibble<true>(false, 0, 0xd) << 0xd) | (fill_nibble<true>(false, 0, 0xe) << 0xe) | (fill_nibble<true>(false, 0, 0xf) << 0xf) |
(fill_nibble<true>(false, 1, 0x0) << 0x10) | (fill_nibble<true>(false, 1, 0x1) << 0x11) | (fill_nibble<true>(false, 1, 0x2) << 0x12) | (fill_nibble<true>(false, 1, 0x3) << 0x13) |
(fill_nibble<true>(false, 1, 0x4) << 0x14) | (fill_nibble<true>(false, 1, 0x5) << 0x15) | (fill_nibble<true>(false, 1, 0x6) << 0x16) | (fill_nibble<true>(false, 1, 0x7) << 0x17) |
(fill_nibble<true>(false, 1, 0x8) << 0x18) | (fill_nibble<true>(false, 1, 0x9) << 0x19) | (fill_nibble<true>(false, 1, 0xa) << 0x1a) | (fill_nibble<true>(false, 1, 0xb) << 0x1b) |
(fill_nibble<true>(false, 1, 0xc) << 0x1c) | (fill_nibble<true>(false, 1, 0xd) << 0x1d) | (fill_nibble<true>(false, 1, 0xe) << 0x1e) | (fill_nibble<true>(false, 1, 0xf) << 0x1f),
(fill_nibble<true>(true, 0, 0x0) << 0x0) | (fill_nibble<true>(true, 0, 0x1) << 0x1) | (fill_nibble<true>(true, 0, 0x2) << 0x2) | (fill_nibble<true>(true, 0, 0x3) << 0x3) |
(fill_nibble<true>(true, 0, 0x4) << 0x4) | (fill_nibble<true>(true, 0, 0x5) << 0x5) | (fill_nibble<true>(true, 0, 0x6) << 0x6) | (fill_nibble<true>(true, 0, 0x7) << 0x7) |
(fill_nibble<true>(true, 0, 0x8) << 0x8) | (fill_nibble<true>(true, 0, 0x9) << 0x9) | (fill_nibble<true>(true, 0, 0xa) << 0xa) | (fill_nibble<true>(true, 0, 0xb) << 0xb) |
(fill_nibble<true>(true, 0, 0xc) << 0xc) | (fill_nibble<true>(true, 0, 0xd) << 0xd) | (fill_nibble<true>(true, 0, 0xe) << 0xe) | (fill_nibble<true>(true, 0, 0xf) << 0xf) |
(fill_nibble<true>(true, 1, 0x0) << 0x10) | (fill_nibble<true>(true, 1, 0x1) << 0x11) | (fill_nibble<true>(true, 1, 0x2) << 0x12) | (fill_nibble<true>(true, 1, 0x3) << 0x13) |
(fill_nibble<true>(true, 1, 0x4) << 0x14) | (fill_nibble<true>(true, 1, 0x5) << 0x15) | (fill_nibble<true>(true, 1, 0x6) << 0x16) | (fill_nibble<true>(true, 1, 0x7) << 0x17) |
(fill_nibble<true>(true, 1, 0x8) << 0x18) | (fill_nibble<true>(true, 1, 0x9) << 0x19) | (fill_nibble<true>(true, 1, 0xa) << 0x1a) | (fill_nibble<true>(true, 1, 0xb) << 0x1b) |
(fill_nibble<true>(true, 1, 0xc) << 0x1c) | (fill_nibble<true>(true, 1, 0xd) << 0x1d) | (fill_nibble<true>(true, 1, 0xe) << 0x1e) | (fill_nibble<true>(true, 1, 0xf) << 0x1f),
};
constexpr uint32_t fill_values[] = {
(fill_nibble<false>(false, 0, 0x0) << 0) | (fill_nibble<false>(false, 0, 0x1) << 4) | (fill_nibble<false>(false, 0, 0x2) << 8) | (fill_nibble<false>(false, 0, 0x3) << 12) |
(fill_nibble<false>(false, 0, 0x4) << 16) | (fill_nibble<false>(false, 0, 0x5) << 20) | (fill_nibble<false>(false, 0, 0x6) << 24) | (fill_nibble<false>(false, 0, 0x7) << 28),
(fill_nibble<false>(false, 0, 0x8) << 0) | (fill_nibble<false>(false, 0, 0x9) << 4) | (fill_nibble<false>(false, 0, 0xa) << 8) | (fill_nibble<false>(false, 0, 0xb) << 12) |
(fill_nibble<false>(false, 0, 0xc) << 16) | (fill_nibble<false>(false, 0, 0xd) << 20) | (fill_nibble<false>(false, 0, 0xe) << 24) | (fill_nibble<false>(false, 0, 0xf) << 28),
(fill_nibble<false>(false, 1, 0x0) << 0) | (fill_nibble<false>(false, 1, 0x1) << 4) | (fill_nibble<false>(false, 1, 0x2) << 8) | (fill_nibble<false>(false, 1, 0x3) << 12) |
(fill_nibble<false>(false, 1, 0x4) << 16) | (fill_nibble<false>(false, 1, 0x5) << 20) | (fill_nibble<false>(false, 1, 0x6) << 24) | (fill_nibble<false>(false, 1, 0x7) << 28),
(fill_nibble<false>(false, 1, 0x8) << 0) | (fill_nibble<false>(false, 1, 0x9) << 4) | (fill_nibble<false>(false, 1, 0xa) << 8) | (fill_nibble<false>(false, 1, 0xb) << 12) |
(fill_nibble<false>(false, 1, 0xc) << 16) | (fill_nibble<false>(false, 1, 0xd) << 20) | (fill_nibble<false>(false, 1, 0xe) << 24) | (fill_nibble<false>(false, 1, 0xf) << 28),
(fill_nibble<false>(true, 0, 0x0) << 0) | (fill_nibble<false>(true, 0, 0x1) << 4) | (fill_nibble<false>(true, 0, 0x2) << 8) | (fill_nibble<false>(true, 0, 0x3) << 12) |
(fill_nibble<false>(true, 0, 0x4) << 16) | (fill_nibble<false>(true, 0, 0x5) << 20) | (fill_nibble<false>(true, 0, 0x6) << 24) | (fill_nibble<false>(true, 0, 0x7) << 28),
(fill_nibble<false>(true, 0, 0x8) << 0) | (fill_nibble<false>(true, 0, 0x9) << 4) | (fill_nibble<false>(true, 0, 0xa) << 8) | (fill_nibble<false>(true, 0, 0xb) << 12) |
(fill_nibble<false>(true, 0, 0xc) << 16) | (fill_nibble<false>(true, 0, 0xd) << 20) | (fill_nibble<false>(true, 0, 0xe) << 24) | (fill_nibble<false>(true, 0, 0xf) << 28),
(fill_nibble<false>(true, 1, 0x0) << 0) | (fill_nibble<false>(true, 1, 0x1) << 4) | (fill_nibble<false>(true, 1, 0x2) << 8) | (fill_nibble<false>(true, 1, 0x3) << 12) |
(fill_nibble<false>(true, 1, 0x4) << 16) | (fill_nibble<false>(true, 1, 0x5) << 20) | (fill_nibble<false>(true, 1, 0x6) << 24) | (fill_nibble<false>(true, 1, 0x7) << 28),
(fill_nibble<false>(true, 1, 0x8) << 0) | (fill_nibble<false>(true, 1, 0x9) << 4) | (fill_nibble<false>(true, 1, 0xa) << 8) | (fill_nibble<false>(true, 1, 0xb) << 12) |
(fill_nibble<false>(true, 1, 0xc) << 16) | (fill_nibble<false>(true, 1, 0xd) << 20) | (fill_nibble<false>(true, 1, 0xe) << 24) | (fill_nibble<false>(true, 1, 0xf) << 28),
};
}
template <bool record_bus>
void Blitter<record_bus>::set_control(int index, uint16_t value) {
if(index) {
line_mode_ = (value & 0x0001);
one_dot_ = value & 0x0002;
line_direction_ = (value >> 2) & 7;
line_sign_ = (value & 0x0040) ? -1 : 1;
direction_ = one_dot_ ? uint32_t(-1) : uint32_t(1);
exclusive_fill_ = (value & 0x0010);
inclusive_fill_ = !exclusive_fill_ && (value & 0x0008); // Exclusive fill takes precedence. Probably? TODO: verify.
fill_carry_ = (value & 0x0004);
} else {
minterms_ = value & 0xff;
sequencer_.set_control(value >> 8);
}
shifts_[index] = value >> 12;
LOG("Set control " << index << " to " << PADHEX(4) << value);
}
template <bool record_bus>
void Blitter<record_bus>::set_first_word_mask(uint16_t value) {
LOG("Set first word mask: " << PADHEX(4) << value);
a_mask_[0] = value;
}
template <bool record_bus>
void Blitter<record_bus>::set_last_word_mask(uint16_t value) {
LOG("Set last word mask: " << PADHEX(4) << value);
a_mask_[1] = value;
}
template <bool record_bus>
void Blitter<record_bus>::set_size(uint16_t value) {
// width_ = (width_ & ~0x3f) | (value & 0x3f);
// height_ = (height_ & ~0x3ff) | (value >> 6);
width_ = value & 0x3f;
if(!width_) width_ = 0x40;
height_ = value >> 6;
if(!height_) height_ = 1024;
LOG("Set size to " << std::dec << width_ << ", " << height_);
// Current assumption: writing this register informs the
// blitter that it should treat itself as about to start a new line.
}
template <bool record_bus>
void Blitter<record_bus>::set_minterms(uint16_t value) {
LOG("Set minterms " << PADHEX(4) << value);
minterms_ = value & 0xff;
}
//template <bool record_bus>
//void Blitter<record_bus>::set_vertical_size([[maybe_unused]] uint16_t value) {
// LOG("Set vertical size " << PADHEX(4) << value);
// // TODO. This is ECS only, I think. Ditto set_horizontal_size.
//}
//
//template <bool record_bus>
//void Blitter<record_bus>::set_horizontal_size([[maybe_unused]] uint16_t value) {
// LOG("Set horizontal size " << PADHEX(4) << value);
//}
template <bool record_bus>
void Blitter<record_bus>::set_data(int channel, uint16_t value) {
LOG("Set data " << channel << " to " << PADHEX(4) << value);
// Ugh, backed myself into a corner. TODO: clean.
switch(channel) {
case 0: a_data_ = value; break;
case 1: b_data_ = value; break;
case 2: c_data_ = value; break;
default: break;
}
}
template <bool record_bus>
uint16_t Blitter<record_bus>::get_status() {
const uint16_t result =
(not_zero_flag_ ? 0x0000 : 0x2000) | (height_ ? 0x4000 : 0x0000);
LOG("Returned status of " << result);
return result;
}
// Due to the pipeline, writes are delayed by one slot — the first write will occur
// after the second set of inputs has been fetched, and every sequence with writes enabled
// will end with an additional write.
//
// USE Code
// in Active
// BLTCON0 Channels Cycle Sequence
// --------- -------- --------------
// F A B C D A0 B0 C0 - A1 B1 C1 D0 A2 B2 C2 D1 D2
// E A B C A0 B0 C0 A1 B1 C1 A2 B2 C2
// D A B D A0 B0 - A1 B1 D0 A2 B2 D1 - D2
// C A B A0 B0 - A1 B1 - A2 B2
// B A C D A0 C0 - A1 C1 D0 A2 C2 D1 - D2
// A A C A0 C0 A1 C1 A2 C2
// 9 A D A0 - A1 D0 A2 D1 - D2
// 8 A A0 - A1 - A2
// 7 B C D B0 C0 - - B1 C1 D0 - B2 C2 D1 - D2
// 6 B C B0 C0 - B1 C1 - B2 C2
// 5 B D B0 - - B1 D0 - B2 D1 - D2
// 4 B B0 - - B1 - - B2
// 3 C D C0 - - C1 D0 - C2 D1 - D2
// 2 C C0 - C1 - C2
// 1 D D0 - D1 - D2
// 0 none - - - -
//
//
// Table 6-2: Typical Blitter Cycle Sequence
template <bool record_bus>
void Blitter<record_bus>::add_modulos() {
pointer_[0] += modulos_[0] * sequencer_.channel_enabled<0>()* direction_;
pointer_[1] += modulos_[1] * sequencer_.channel_enabled<1>() * direction_;
pointer_[2] += modulos_[2] * sequencer_.channel_enabled<2>() * direction_;
pointer_[3] += modulos_[3] * sequencer_.channel_enabled<3>() * direction_;
}
template <bool record_bus>
template <bool complete_immediately>
bool Blitter<record_bus>::advance_dma() {
if(!height_) return false;
// TODO: eliminate @c complete_immediately and this workaround.
// See commentary in Chipset.cpp.
if constexpr (complete_immediately) {
// HACK! HACK!! HACK!!!
//
// This resolves an issue with loading the particular copy of Spindizzy Worlds
// I am testing against.
//
// TODO: DO NOT PUBLISH THIS.
//
// This is committed solely so that I can continue researching the real, underlying
// issue across machines. It would not be acceptable to me to ship this.
// (and the printf is another reminder-to-self)
if(width_ == 8 && height_ == 32) {
printf("Accelerating %d x %d\n", width_, height_);
while(get_status() & 0x4000) {
advance_dma<false>();
}
return true;
}
}
if(line_mode_) {
not_zero_flag_ = false;
// As-yet unimplemented:
assert(b_data_ == 0xffff);
//
// Line mode.
//
// Bluffer's guide to line mode:
//
// In Bresenham terms, the following registers have been set up:
//
// [A modulo] = 4 * (dy - dx)
// [B modulo] = 4 * dy
// [A pointer] = 4 * dy - 2 * dx, with the sign flag in BLTCON1 indicating sign.
//
// [A data] = 0x8000
// [Both masks] = 0xffff
// [A shift] = x1 & 15
//
// [B data] = texture
// [B shift] = bit at which to start the line texture (0 = LSB)
//
// [C and D pointers] = word containing the first pixel of the line
// [C and D modulo] = width of the bitplane in bytes
//
// height = number of pixels
//
// If ONEDOT of BLTCON1 is set, plot only a single bit per horizontal row.
//
// BLTCON1 quadrants are (bits 24):
//
// 110 -> step in x, x positive, y negative
// 111 -> step in x, x negative, y negative
// 101 -> step in x, x negative, y positive
// 100 -> step in x, x positive, y positive
//
// 001 -> step in y, x positive, y negative
// 011 -> step in y, x negative, y negative
// 010 -> step in y, x negative, y positive
// 000 -> step in y, x positive, y positive
//
// So that's:
//
// * bit 4 = x [=1] or y [=0] major;
// * bit 3 = 1 => major variable negative; otherwise positive;
// * bit 2 = 1 => minor variable negative; otherwise positive.
//
// Implementation below is heavily based on the documentation found
// at https://github.com/niklasekstrom/blitter-subpixel-line/blob/master/Drawing%20lines%20using%20the%20Amiga%20blitter.pdf
//
//
// Caveat: I've no idea how the DMA access slots should be laid out for
// line drawing.
//
if(!busy_) {
error_ = int16_t(pointer_[0] << 1) >> 1; // TODO: what happens if line_sign_ doesn't agree with this?
draw_ = true;
busy_ = true;
has_c_data_ = false;
}
bool did_output = false;
if(draw_) {
// TODO: patterned lines. Unclear what to do with the bit that comes out of b.
// Probably extend it to a full word?
if(!has_c_data_) {
has_c_data_ = true;
c_data_ = ram_[pointer_[3] & ram_mask_];
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::ReadC, pointer_[3], c_data_);
}
return true;
}
const uint16_t output =
apply_minterm<uint16_t>(a_data_ >> shifts_[0], b_data_, c_data_, minterms_);
ram_[pointer_[3] & ram_mask_] = output;
not_zero_flag_ |= output;
draw_ &= !one_dot_;
has_c_data_ = false;
did_output = true;
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::WriteFromPipeline, pointer_[3], output);
}
}
constexpr int LEFT = 1 << 0;
constexpr int RIGHT = 1 << 1;
constexpr int UP = 1 << 2;
constexpr int DOWN = 1 << 3;
int step = (line_direction_ & 4) ?
((line_direction_ & 1) ? LEFT : RIGHT) :
((line_direction_ & 1) ? UP : DOWN);
if(error_ < 0) {
error_ += modulos_[1];
} else {
step |=
(line_direction_ & 4) ?
((line_direction_ & 2) ? UP : DOWN) :
((line_direction_ & 2) ? LEFT : RIGHT);
error_ += modulos_[0];
}
if(step & LEFT) {
--shifts_[0];
if(shifts_[0] == -1) {
--pointer_[3];
}
} else if(step & RIGHT) {
++shifts_[0];
if(shifts_[0] == 16) {
++pointer_[3];
}
}
shifts_[0] &= 15;
if(step & UP) {
pointer_[3] -= modulos_[2];
draw_ = true;
} else if(step & DOWN) {
pointer_[3] += modulos_[2];
draw_ = true;
}
--height_;
if(!height_) {
busy_ = false;
posit_interrupt(InterruptFlag::Blitter);
}
return did_output;
} else {
// Copy mode.
if(!busy_) {
sequencer_.begin();
a32_ = 0;
b32_ = 0;
y_ = 0;
x_ = 0;
loop_index_ = -1;
write_phase_ = WritePhase::Starting;
not_zero_flag_ = false;
busy_ = true;
}
const auto next = sequencer_.next();
// If this is the start of a new iteration, check for end of line,
// or of blit, and pick an appropriate mask for A based on location.
if(next.second != loop_index_) {
transient_a_mask_ = x_ ? 0xffff : a_mask_[0];
// Check whether an entire row was completed in the previous iteration.
// If so then add modulos. Though this won't capture the move off the
// final line, so that's handled elsewhere.
if(!x_ && y_) {
add_modulos();
}
++x_;
if(x_ == width_) {
transient_a_mask_ &= a_mask_[1];
x_ = 0;
++y_;
if(y_ == height_) {
sequencer_.complete();
}
}
++loop_index_;
}
using Channel = BlitterSequencer::Channel;
switch(next.first) {
case Channel::A:
a_data_ = ram_[pointer_[0] & ram_mask_];
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::ReadA, pointer_[0], a_data_);
}
pointer_[0] += direction_;
return true;
case Channel::B:
b_data_ = ram_[pointer_[1] & ram_mask_];
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::ReadB, pointer_[1], b_data_);
}
pointer_[1] += direction_;
return true;
case Channel::C:
c_data_ = ram_[pointer_[2] & ram_mask_];
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::ReadC, pointer_[2], c_data_);
}
pointer_[2] += direction_;
return true;
case Channel::FlushPipeline:
add_modulos();
posit_interrupt(InterruptFlag::Blitter);
height_ = 0;
busy_ = false;
if(write_phase_ == WritePhase::Full) {
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::WriteFromPipeline, write_address_, write_value_);
}
ram_[write_address_ & ram_mask_] = write_value_;
write_phase_ = WritePhase::Starting;
}
return true;
case Channel::None:
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::SkippedSlot);
}
return false;
case Channel::Write: break;
}
a32_ = (a32_ << 16) | (a_data_ & transient_a_mask_);
b32_ = (b32_ << 16) | b_data_;
uint16_t a, b;
// The barrel shifter shifts to the right in ascending address mode,
// but to the left otherwise.
if(!one_dot_) {
a = uint16_t(a32_ >> shifts_[0]);
b = uint16_t(b32_ >> shifts_[1]);
} else {
// TODO: there must be a neater solution than this.
a = uint16_t(
(a32_ << shifts_[0]) |
(a32_ >> (32 - shifts_[0]))
);
b = uint16_t(
(b32_ << shifts_[1]) |
(b32_ >> (32 - shifts_[1]))
);
}
uint16_t output =
apply_minterm<uint16_t>(
a,
b,
c_data_,
minterms_);
if(exclusive_fill_ || inclusive_fill_) {
// Use the fill tables nibble-by-nibble to figure out the filled word.
uint16_t fill_output = 0;
int ongoing_carry = fill_carry_;
const int type_mask = exclusive_fill_ ? (1 << 5) : 0;
for(int c = 0; c < 16; c += 4) {
const int total_index = (output & 0xf) | (ongoing_carry << 4) | type_mask;
fill_output |= ((fill_values[total_index >> 3] >> ((total_index & 7) * 4)) & 0xf) << c;
ongoing_carry = (fill_carries[total_index >> 5] >> (total_index & 31)) & 1;
output >>= 4;
}
output = fill_output;
fill_carry_ = ongoing_carry;
}
not_zero_flag_ |= output;
switch(write_phase_) {
case WritePhase::Full:
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::WriteFromPipeline, write_address_, write_value_);
}
ram_[write_address_ & ram_mask_] = write_value_;
[[fallthrough]];
case WritePhase::Starting:
write_phase_ = WritePhase::Full;
write_address_ = pointer_[3];
write_value_ = output;
if constexpr (record_bus) {
transactions_.emplace_back(Transaction::Type::AddToPipeline, write_address_, write_value_);
}
pointer_[3] += direction_;
return true;
default: assert(false);
}
}
return true;
}
template <bool record_bus>
std::vector<typename Blitter<record_bus>::Transaction> Blitter<record_bus>::get_and_reset_transactions() {
std::vector<Transaction> result;
std::swap(result, transactions_);
return result;
}
template class Amiga::Blitter<false>;
template class Amiga::Blitter<true>;
template bool Amiga::Blitter<true>::advance_dma<true>();
template bool Amiga::Blitter<true>::advance_dma<false>();
template bool Amiga::Blitter<false>::advance_dma<true>();
template bool Amiga::Blitter<false>::advance_dma<false>();