1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-01-28 13:30:55 +00:00

Merge pull request #1117 from TomHarte/MSX2

Flesh out the MSX 2.
This commit is contained in:
Thomas Harte 2023-04-27 10:00:16 -04:00 committed by GitHub
commit 19d03dd4fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 3741 additions and 1190 deletions

View File

@ -37,6 +37,11 @@ static std::unique_ptr<Analyser::Static::Target> CartridgeTarget(
auto target = std::make_unique<Analyser::Static::MSX::Target>();
target->confidence = confidence;
// Observation: all ROMs of 48kb or less are from the MSX 1 era.
if(segment.data.size() < 48*1024) {
target->model = Analyser::Static::MSX::Target::Model::MSX1;
}
if(type == Analyser::Static::MSX::Cartridge::Type::None) {
target->media.cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments));
} else {
@ -100,6 +105,7 @@ static Analyser::Static::TargetList CartridgeTargetsFrom(
// TODO: check for a rational init address?
// If this ROM is less than 48kb in size then it's an ordinary ROM. Just emplace it and move on.
// Bonus observation: all such ROMs are from the MSX 1 era.
if(data_size <= 0xc000) {
targets.emplace_back(CartridgeTarget(segment, start_address, Analyser::Static::MSX::Cartridge::Type::None, 1.0));
continue;

View File

@ -26,7 +26,7 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl<
MSX1,
MSX2
);
Model model = Model::MSX1;
Model model = Model::MSX2;
ReflectableEnum(Region,
Japan,

View File

@ -14,6 +14,8 @@
#include "ClockingHintSource.hpp"
#include "ForceInline.hpp"
#include <atomic>
/*!
A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount
of time since run_for was last called.
@ -121,7 +123,13 @@ template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int di
/// If this object provides sequence points, checks for changes to the next
/// sequence point upon deletion of the pointer.
[[nodiscard]] forceinline auto operator->() {
#ifndef NDEBUG
assert(!flush_concurrency_check_.test_and_set());
#endif
flush();
#ifndef NDEBUG
flush_concurrency_check_.clear();
#endif
return std::unique_ptr<T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(this));
}
@ -130,7 +138,13 @@ template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int di
/// Despite being const, this will flush the object and, if relevant, update the next sequence point.
[[nodiscard]] forceinline auto operator -> () const {
auto non_const_this = const_cast<JustInTimeActor<T, LocalTimeScale, multiplier, divider> *>(this);
#ifndef NDEBUG
assert(!non_const_this->flush_concurrency_check_.test_and_set());
#endif
non_const_this->flush();
#ifndef NDEBUG
non_const_this->flush_concurrency_check_.clear();
#endif
return std::unique_ptr<const T, SequencePointAwareDeleter>(&object_, SequencePointAwareDeleter(non_const_this));
}
@ -264,6 +278,10 @@ template <class T, class LocalTimeScale = HalfCycles, int multiplier = 1, int di
void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) {
clocking_preference_ = clocking;
}
#ifndef NDEBUG
std::atomic_flag flush_concurrency_check_{};
#endif
};
/*!

View File

@ -14,8 +14,7 @@
#include <cstdint>
namespace TI {
namespace TMS {
namespace TI::TMS {
enum Personality {
TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired.
@ -36,13 +35,11 @@ enum class TVStandard {
NTSC
};
}
}
#include "Implementation/9918Base.hpp"
namespace TI {
namespace TMS {
namespace TI::TMS {
/*!
Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the
@ -127,7 +124,6 @@ template <Personality personality> class TMS9918: private Base<personality> {
bool get_interrupt_line() const;
};
}
}
#endif /* TMS9918_hpp */

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,11 @@
#include "../../../Numeric/BitReverse.hpp"
#include "../../../Outputs/CRT/CRT.hpp"
#include "AccessEnums.hpp"
#include "LineBuffer.hpp"
#include "PersonalityTraits.hpp"
#include "Storage.hpp"
#include "YamahaCommands.hpp"
#include <array>
#include <cassert>
@ -24,93 +28,7 @@
#include <memory>
#include <vector>
namespace TI {
namespace TMS {
// The screen mode is a necessary predecessor to picking the line mode,
// which is the thing latched per line.
enum class ScreenMode {
Blank,
Text,
MultiColour,
ColouredText,
Graphics,
SMSMode4
};
enum class LineMode {
Text,
Character,
Refresh,
SMS
};
enum class MemoryAccess {
Read, Write, None
};
// Temporary buffers collect a representation of each line prior to pixel serialisation.
struct LineBuffer {
// The line mode describes the proper timing diagram for this line.
LineMode line_mode = LineMode::Text;
// Holds the horizontal scroll position to apply to this line;
// of those VDPs currently implemented, affects the Master System only.
uint8_t latched_horizontal_scroll = 0;
// The names array holds pattern names, as an offset into memory, and
// potentially flags also.
struct {
size_t offset = 0;
uint8_t flags = 0;
} names[40];
// The patterns array holds tile patterns, corresponding 1:1 with names.
// Four bytes per pattern is the maximum required by any
// currently-implemented VDP.
uint8_t patterns[40][4];
/*
Horizontal layout (on a 342-cycle clock):
15 cycles right border
58 cycles blanking & sync
13 cycles left border
... i.e. to cycle 86, then:
border up to first_pixel_output_column;
pixels up to next_border_column;
border up to the end.
e.g. standard 256-pixel modes will want to set
first_pixel_output_column = 86, next_border_column = 342.
*/
int first_pixel_output_column = 94;
int next_border_column = 334;
size_t pixel_count = 256;
// An active sprite is one that has been selected for composition onto
// this line.
struct ActiveSprite {
int index = 0; // The original in-table index of this sprite.
int row = 0; // The row of the sprite that should be drawn.
int x = 0; // The sprite's x position on screen.
uint8_t image[4]; // Up to four bytes of image information.
int shift_position = 0; // An offset representing how much of the image information has already been drawn.
} active_sprites[8];
int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required.
bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites
// being evaluated for display. This flag determines whether the sentinel has yet been reached.
void reset_sprite_collection();
};
struct LineBufferPointer {
int row, column;
};
namespace TI::TMS {
constexpr uint8_t StatusInterrupt = 0x80;
constexpr uint8_t StatusSpriteOverflow = 0x40;
@ -118,7 +36,7 @@ constexpr uint8_t StatusSpriteOverflow = 0x40;
constexpr int StatusSpriteCollisionShift = 5;
constexpr uint8_t StatusSpriteCollision = 0x20;
template <Personality personality> struct Base {
template <Personality personality> struct Base: public Storage<personality> {
Base();
static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles
@ -133,7 +51,7 @@ template <Personality personality> struct Base {
}
// The default TMS palette.
static constexpr std::array<uint32_t, 16> palette {
static constexpr std::array<uint32_t, 16> default_palette {
palette_pack(0, 0, 0),
palette_pack(0, 0, 0),
palette_pack(33, 200, 66),
@ -154,9 +72,29 @@ template <Personality personality> struct Base {
palette_pack(204, 204, 204),
palette_pack(255, 255, 255)
};
const std::array<uint32_t, 16> &palette() {
if constexpr (is_yamaha_vdp(personality)) {
return Storage<personality>::solid_background_ ? Storage<personality>::palette_ : Storage<personality>::background_palette_;
}
return default_palette;
}
Outputs::CRT::CRT crt_;
TVStandard tv_standard_ = TVStandard::NTSC;
using AddressT = typename Storage<personality>::AddressT;
/// Mutates @c target such that @c source replaces the @c length bits that currently start
/// at bit @c shift . Subsequently ensures @c target is constrained by the
/// applicable @c memory_mask.
template <int shift, int length = 8> void install_field(AddressT &target, uint8_t source) {
static_assert(length > 0 && length <= 8);
constexpr auto source_mask = (1 << length) - 1;
constexpr auto mask = AddressT(~(source_mask << shift));
target = (
(target & mask) |
AddressT((source & source_mask) << shift)
) & memory_mask(personality);
}
// Personality-specific metrics and converters.
ClockConverter<personality> clock_converter_;
@ -165,10 +103,9 @@ template <Personality personality> struct Base {
std::array<uint8_t, memory_size(personality)> ram_;
// State of the DRAM/CRAM-access mechanism.
uint16_t ram_pointer_ = 0;
AddressT ram_pointer_ = 0;
uint8_t read_ahead_buffer_ = 0;
MemoryAccess queued_access_ = MemoryAccess::None;
int cycles_until_access_ = 0;
int minimum_access_column_ = 0;
// The main status register.
@ -186,14 +123,25 @@ template <Personality personality> struct Base {
bool sprites_16x16_ = false;
bool sprites_magnified_ = false;
bool generate_interrupts_ = false;
int sprite_height_ = 8;
uint8_t sprite_height_ = 8;
// Programmer-specified addresses.
size_t pattern_name_address_ = 0; // i.e. address of the tile map.
size_t colour_table_address_ = 0; // address of the colour map (if applicable).
size_t pattern_generator_table_address_ = 0; // address of the tile contents.
size_t sprite_attribute_table_address_ = 0; // address of the sprite list.
size_t sprite_generator_table_address_ = 0; // address of the sprite contents.
//
// The TMS and descendants combine various parts of the address with AND operations,
// e.g. the fourth byte in the pattern name table will be at `pattern_name_address_ & 4`;
// ordinarily the difference between that and plain substitution is invisible because
// the programmer mostly can't set low-enough-order bits. That's not universally true
// though, so this implementation uses AND throughout.
//
// ... therefore, all programmer-specified addresses are seeded as all '1's. As and when
// actual addresses are specified, the relevant bits will be substituted in.
//
// Cf. install_field.
AddressT pattern_name_address_ = memory_mask(personality); // Address of the tile map.
AddressT colour_table_address_ = memory_mask(personality); // Address of the colour map (if applicable).
AddressT pattern_generator_table_address_ = memory_mask(personality); // Address of the tile contents.
AddressT sprite_attribute_table_address_ = memory_mask(personality); // Address of the sprite list.
AddressT sprite_generator_table_address_ = memory_mask(personality); // Address of the sprite contents.
// Default colours.
uint8_t text_colour_ = 0;
@ -224,79 +172,117 @@ template <Personality personality> struct Base {
// Set the position, in cycles, of the two interrupts,
// within a line.
//
// TODO: redetermine where this number came from.
struct {
int column = 4;
int row = 193;
int column = 313;
int row = 192;
} end_of_frame_interrupt_position;
int line_interrupt_position = -1;
// Enables or disabled the recognition of the sprite
// list terminator, and sets the terminator value.
bool allow_sprite_terminator = true;
uint8_t sprite_terminator = 0xd0;
uint8_t sprite_terminator(ScreenMode mode) {
switch(mode) {
default: return 0xd0;
case ScreenMode::YamahaGraphics3:
case ScreenMode::YamahaGraphics4:
case ScreenMode::YamahaGraphics5:
case ScreenMode::YamahaGraphics6:
case ScreenMode::YamahaGraphics7:
return 0xd8;
}
}
} mode_timing_;
uint8_t line_interrupt_target = 0xff;
uint8_t line_interrupt_counter = 0;
uint8_t line_interrupt_target_ = 0xff;
uint8_t line_interrupt_counter_ = 0;
bool enable_line_interrupts_ = false;
bool line_interrupt_pending_ = false;
bool vertical_active_ = false;
ScreenMode screen_mode_;
LineBuffer line_buffers_[313];
void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row);
ScreenMode screen_mode_, underlying_mode_;
using LineBufferArray = std::array<LineBuffer, 313>;
LineBufferArray line_buffers_;
LineBufferArray::iterator fetch_line_buffer_;
LineBufferArray::iterator draw_line_buffer_;
void advance(LineBufferArray::iterator &iterator) {
++iterator;
if(iterator == line_buffers_.end()) {
iterator = line_buffers_.begin();
}
}
using SpriteBufferArray = std::array<SpriteBuffer, 313>;
SpriteBufferArray sprite_buffers_;
SpriteBufferArray::iterator fetch_sprite_buffer_;
SpriteBuffer *fetched_sprites_ = nullptr;
void advance(SpriteBufferArray::iterator &iterator) {
++iterator;
if(iterator == sprite_buffers_.end()) {
iterator = sprite_buffers_.begin();
}
}
void regress(SpriteBufferArray::iterator &iterator) {
if(iterator == sprite_buffers_.begin()) {
iterator = sprite_buffers_.end();
}
--iterator;
}
AddressT tile_offset_ = 0;
uint8_t name_[4]{};
void posit_sprite(int sprite_number, int sprite_y, uint8_t screen_row);
// There is a delay between reading into the line buffer and outputting from there to the screen. That delay
// is observeable because reading time affects availability of memory accesses and therefore time in which
// to update sprites and tiles, but writing time affects when the palette is used and when the collision flag
// may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap
// with the beginning of writing the next, hence the two separate line buffers.
LineBufferPointer read_pointer_, write_pointer_;
LineBufferPointer output_pointer_, fetch_pointer_;
// The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly
// fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there
// isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps
// track of pending collisions, for visual reproduction.
struct CRAMDot {
LineBufferPointer location;
uint32_t value;
};
std::vector<CRAMDot> upcoming_cram_dots_;
int fetch_line() const;
bool is_horizontal_blank() const;
VerticalState vertical_state() const;
// Extra information that affects the Master System output mode.
struct {
// Programmer-set flags.
bool vertical_scroll_lock = false;
bool horizontal_scroll_lock = false;
bool hide_left_column = false;
bool shift_sprites_8px_left = false;
bool mode4_enable = false;
uint8_t horizontal_scroll = 0;
uint8_t vertical_scroll = 0;
int masked_address(int address) const;
void write_vram(uint8_t);
void write_register(uint8_t);
void write_palette(uint8_t);
void write_register_indirect(uint8_t);
uint8_t read_vram();
uint8_t read_register();
// The Master System's additional colour RAM.
uint32_t colour_ram[32];
bool cram_is_selected = false;
void commit_register(int reg, uint8_t value);
// Holds the vertical scroll position for this frame; this is latched
// once and cannot dynamically be changed until the next frame.
uint8_t latched_vertical_scroll = 0;
size_t pattern_name_address;
size_t sprite_attribute_table_address;
size_t sprite_generator_table_address;
} master_system_;
ScreenMode current_screen_mode() const {
if(blank_display_) {
template <bool check_blank> ScreenMode current_screen_mode() const {
if(check_blank && blank_display_) {
return ScreenMode::Blank;
}
if constexpr (is_sega_vdp(personality)) {
if(master_system_.mode4_enable) {
if(Storage<personality>::mode4_enable_) {
return ScreenMode::SMSMode4;
}
}
if constexpr (is_yamaha_vdp(personality)) {
switch(Storage<personality>::mode_) {
case 0b00001: return ScreenMode::Text;
case 0b01001: return ScreenMode::YamahaText80;
case 0b00010: return ScreenMode::MultiColour;
case 0b00000: return ScreenMode::YamahaGraphics1;
case 0b00100: return ScreenMode::YamahaGraphics2;
case 0b01000: return ScreenMode::YamahaGraphics3;
case 0b01100: return ScreenMode::YamahaGraphics4;
case 0b10000: return ScreenMode::YamahaGraphics5;
case 0b10100: return ScreenMode::YamahaGraphics6;
case 0b11100: return ScreenMode::YamahaGraphics7;
}
}
if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
return ScreenMode::ColouredText;
}
@ -317,24 +303,252 @@ template <Personality personality> struct Base {
return ScreenMode::Blank;
}
static AddressT rotate(AddressT address) {
return AddressT((address >> 1) | (address << 16)) & memory_mask(personality);
}
AddressT command_address(Vector location, bool expansion) const {
if constexpr (is_yamaha_vdp(personality)) {
switch(this->underlying_mode_) {
default:
case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp
return AddressT(
((location.v[0] >> 1) & 127) +
(location.v[1] << 7)
);
case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp
return AddressT(
((location.v[0] >> 2) & 127) +
(location.v[1] << 7)
);
case ScreenMode::YamahaGraphics6: { // 512 pixels @ 4bpp
const auto linear_address =
AddressT(
((location.v[0] >> 1) & 255) +
(location.v[1] << 8)
);
return expansion ? linear_address : rotate(linear_address);
}
case ScreenMode::YamahaGraphics7: { // 256 pixels @ 8bpp
const auto linear_address =
AddressT(
((location.v[0] >> 0) & 255) +
(location.v[1] << 8)
);
return expansion ? linear_address : rotate(linear_address);
}
}
} else {
return 0;
}
}
uint8_t extract_colour(uint8_t byte, Vector location) const {
switch(this->screen_mode_) {
default:
case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp
case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp
return (byte >> (((location.v[0] & 1) ^ 1) << 2)) & 0xf;
case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp
return (byte >> (((location.v[0] & 3) ^ 3) << 1)) & 0x3;
case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp
return byte;
}
}
std::pair<uint8_t, uint8_t> command_colour_mask(Vector location) const {
if constexpr (is_yamaha_vdp(personality)) {
auto &context = Storage<personality>::command_context_;
auto colour = context.latched_colour.has_value() ? context.latched_colour : context.colour;
switch(this->screen_mode_) {
default:
case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp
case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp
return
std::make_pair(
0xf0 >> ((location.v[0] & 1) << 2),
colour.colour4bpp
);
case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp
return
std::make_pair(
0xc0 >> ((location.v[0] & 3) << 1),
colour.colour2bpp
);
case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp
return
std::make_pair(
0xff,
colour.colour
);
}
} else {
return std::make_pair(0, 0);
}
}
void do_external_slot(int access_column) {
// Don't do anything if the required time for the access to become executable
// has yet to pass.
if(access_column < minimum_access_column_) {
if(queued_access_ == MemoryAccess::None || access_column < minimum_access_column_) {
if constexpr (is_yamaha_vdp(personality)) {
using CommandStep = typename Storage<personality>::CommandStep;
if(
Storage<personality>::next_command_step_ == CommandStep::None ||
access_column < Storage<personality>::minimum_command_column_
) {
return;
}
auto &context = Storage<personality>::command_context_;
const uint8_t *const source = (context.arguments & 0x10) ? Storage<personality>::expansion_ram_.data() : ram_.data();
const AddressT source_mask = (context.arguments & 0x10) ? 0xfff : 0x1ffff;
uint8_t *const destination = (context.arguments & 0x20) ? Storage<personality>::expansion_ram_.data() : ram_.data();
const AddressT destination_mask = (context.arguments & 0x20) ? 0xfff : 0x1ffff;
switch(Storage<personality>::next_command_step_) {
// Duplicative, but keeps the compiler happy.
case CommandStep::None:
break;
case CommandStep::CopySourcePixelToStatus:
Storage<personality>::colour_status_ =
extract_colour(
source[command_address(context.source, context.arguments & 0x10) & source_mask],
context.source
);
Storage<personality>::command_->advance();
Storage<personality>::update_command_step(access_column);
break;
case CommandStep::ReadSourcePixel:
context.latched_colour.set(
extract_colour(
source[command_address(context.source, context.arguments & 0x10)] & source_mask,
context.source)
);
Storage<personality>::minimum_command_column_ = access_column + 32;
Storage<personality>::next_command_step_ = CommandStep::ReadDestinationPixel;
break;
case CommandStep::ReadDestinationPixel:
Storage<personality>::command_latch_ =
source[command_address(context.destination, context.arguments & 0x20) & source_mask];
Storage<personality>::minimum_command_column_ = access_column + 24;
Storage<personality>::next_command_step_ = CommandStep::WritePixel;
break;
case CommandStep::WritePixel: {
const auto [mask, unmasked_colour] = command_colour_mask(context.destination);
const auto address = command_address(context.destination, context.arguments & 0x20) & destination_mask;
const uint8_t colour = unmasked_colour & mask;
context.latched_colour.reset();
using LogicalOperation = CommandContext::LogicalOperation;
if(!context.test_source || colour) {
switch(context.pixel_operation) {
default:
case LogicalOperation::Copy:
Storage<personality>::command_latch_ &= ~mask;
Storage<personality>::command_latch_ |= colour;
break;
case LogicalOperation::And:
Storage<personality>::command_latch_ &= ~mask | colour;
break;
case LogicalOperation::Or:
Storage<personality>::command_latch_ |= colour;
break;
case LogicalOperation::Xor:
Storage<personality>::command_latch_ ^= colour;
break;
case LogicalOperation::Not:
Storage<personality>::command_latch_ &= ~mask;
Storage<personality>::command_latch_ |= colour ^ mask;
break;
}
}
destination[address] = Storage<personality>::command_latch_;
Storage<personality>::command_->advance();
Storage<personality>::update_command_step(access_column);
} break;
case CommandStep::ReadSourceByte: {
Vector source_vector = context.source;
if(Storage<personality>::command_->y_only) {
source_vector.v[0] = context.destination.v[0];
}
context.latched_colour.set(source[command_address(source_vector, context.arguments & 0x10) & source_mask]);
Storage<personality>::minimum_command_column_ = access_column + 24;
Storage<personality>::next_command_step_ = CommandStep::WriteByte;
} break;
case CommandStep::WriteByte:
destination[command_address(context.destination, context.arguments & 0x20) & destination_mask]
= context.latched_colour.has_value() ? context.latched_colour.colour : context.colour.colour;
context.latched_colour.reset();
Storage<personality>::command_->advance();
Storage<personality>::update_command_step(access_column);
break;
}
}
return;
}
// Copy and mutate the RAM pointer.
AddressT address = ram_pointer_;
++ram_pointer_;
// Determine the relevant RAM and its mask.
uint8_t *ram = ram_.data();
AddressT mask = memory_mask(personality);
if constexpr (is_yamaha_vdp(personality)) {
// The Yamaha increments only 14 bits of the address in TMS-compatible modes.
if(this->underlying_mode_ < ScreenMode::YamahaText80) {
ram_pointer_ = (ram_pointer_ & 0x3fff) | (address & AddressT(~0x3fff));
}
if(this->underlying_mode_ == ScreenMode::YamahaGraphics6 || this->underlying_mode_ == ScreenMode::YamahaGraphics7) {
// Rotate address one to the right as the hardware accesses
// the underlying banks of memory alternately but presents
// them as if linear.
address = rotate(address);
}
// Also check whether expansion RAM is the true target here.
if(Storage<personality>::command_context_.arguments & 0x40) {
ram = Storage<personality>::expansion_ram_.data();
mask = AddressT(Storage<personality>::expansion_ram_.size() - 1);
}
}
switch(queued_access_) {
default: return;
default: break;
case MemoryAccess::Write:
if constexpr (is_sega_vdp(personality)) {
if(master_system_.cram_is_selected) {
if(Storage<personality>::cram_is_selected_) {
// Adjust the palette. In a Master System blue has a slightly different
// scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html
constexpr uint8_t rg_scale[] = {0, 85, 170, 255};
constexpr uint8_t b_scale[] = {0, 104, 170, 255};
master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack(
Storage<personality>::colour_ram_[address & 0x1f] = palette_pack(
rg_scale[(read_ahead_buffer_ >> 0) & 3],
rg_scale[(read_ahead_buffer_ >> 2) & 3],
b_scale[(read_ahead_buffer_ >> 4) & 3]
@ -343,9 +557,9 @@ template <Personality personality> struct Base {
// Schedule a CRAM dot; this is scheduled for wherever it should appear
// on screen. So it's wherever the output stream would be now. Which
// is output_lag cycles ago from the point of view of the input stream.
CRAMDot &dot = upcoming_cram_dots_.emplace_back();
dot.location.column = write_pointer_.column - output_lag;
dot.location.row = write_pointer_.row;
auto &dot = Storage<personality>::upcoming_cram_dots_.emplace_back();
dot.location.column = fetch_pointer_.column - output_lag;
dot.location.row = fetch_pointer_.row;
// Handle before this row conditionally; then handle after (or, more realistically,
// exactly at the end of) naturally.
@ -356,30 +570,36 @@ template <Personality personality> struct Base {
dot.location.row += dot.location.column / 342;
dot.location.column %= 342;
dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f];
dot.value = Storage<personality>::colour_ram_[address & 0x1f];
break;
}
}
ram_[ram_pointer_ & memory_mask(personality)] = read_ahead_buffer_;
ram[address & mask] = read_ahead_buffer_;
break;
case MemoryAccess::Read:
read_ahead_buffer_ = ram_[ram_pointer_ & memory_mask(personality)];
read_ahead_buffer_ = ram[address & mask];
break;
}
++ram_pointer_;
queued_access_ = MemoryAccess::None;
}
/// Helper for TMS dispatches; contains a switch statement with cases 0 to 170, each of the form:
///
/// if constexpr (use_end && end == n) return; [[fallthrough]]; case n: fetcher.fetch<n>();
///
/// i.e. it provides standard glue to enter a fetch sequence at any point, while the fetches themselves are templated on the cycle
/// at which they appear for neater expression.
template<bool use_end, typename Fetcher> void dispatch(Fetcher &fetcher, int start, int end);
// Various fetchers.
template<bool use_end> void fetch_tms_refresh(int start, int end);
template<bool use_end> void fetch_tms_text(int start, int end);
template<bool use_end> void fetch_tms_character(int start, int end);
template<bool use_end> void fetch_tms_refresh(uint8_t y, int start, int end);
template<bool use_end> void fetch_tms_text(uint8_t y, int start, int end);
template<bool use_end> void fetch_tms_character(uint8_t y, int start, int end);
template<bool use_end> void fetch_yamaha_refresh(int start, int end);
template<bool use_end> void fetch_yamaha_no_sprites(int start, int end);
template<bool use_end> void fetch_yamaha_sprites(int start, int end);
template<bool use_end> void fetch_yamaha(uint8_t y, int start, int end);
template<ScreenMode> void fetch_yamaha(uint8_t y, int end);
template<bool use_end> void fetch_sms(int start, int end);
template<bool use_end> void fetch_sms(uint8_t y, int start, int end);
// A helper function to output the current border colour for
// the number of cycles supplied.
@ -390,15 +610,19 @@ template <Personality personality> struct Base {
bool asked_for_write_area_ = false;
// Output serialisers.
void draw_tms_character(int start, int end);
void draw_tms_text(int start, int end);
template <SpriteMode mode = SpriteMode::Mode1> void draw_tms_character(int start, int end);
template <bool apply_blink> void draw_tms_text(int start, int end);
void draw_sms(int start, int end, uint32_t cram_dot);
template<ScreenMode mode> void draw_yamaha(uint8_t y, int start, int end);
void draw_yamaha(uint8_t y, int start, int end);
template <SpriteMode mode, bool double_width> void draw_sprites(uint8_t y, int start, int end, const std::array<uint32_t, 16> &palette, int *colour_buffer = nullptr);
};
#include "Fetch.hpp"
#include "Draw.hpp"
}
}
#endif /* TMS9918Base_hpp */

View File

@ -0,0 +1,112 @@
//
// AccessEnums.hpp
// Clock Signal
//
// Created by Thomas Harte on 26/01/2023.
// Copyright © 2023 Thomas Harte. All rights reserved.
//
#ifndef AccessEnums_hpp
#define AccessEnums_hpp
namespace TI::TMS {
// The screen mode is a necessary predecessor to picking the line mode,
// which is the thing latched per line.
enum class ScreenMode {
// Original TMS modes.
Blank,
Text,
MultiColour,
ColouredText,
Graphics,
// 8-bit Sega modes.
SMSMode4,
// New Yamaha V9938 modes.
YamahaText80,
YamahaGraphics3,
YamahaGraphics4,
YamahaGraphics5,
YamahaGraphics6,
YamahaGraphics7,
// Rebranded Yamaha V9938 modes.
YamahaGraphics1 = ColouredText,
YamahaGraphics2 = Graphics,
};
constexpr int pixels_per_byte(ScreenMode mode) {
switch(mode) {
default:
case ScreenMode::Blank: return 1;
case ScreenMode::Text: return 6;
case ScreenMode::MultiColour: return 2;
case ScreenMode::ColouredText: return 8;
case ScreenMode::Graphics: return 8;
case ScreenMode::SMSMode4: return 2;
case ScreenMode::YamahaText80: return 6;
case ScreenMode::YamahaGraphics3: return 8;
case ScreenMode::YamahaGraphics4: return 2;
case ScreenMode::YamahaGraphics5: return 4;
case ScreenMode::YamahaGraphics6: return 2;
case ScreenMode::YamahaGraphics7: return 1;
}
}
constexpr int width(ScreenMode mode) {
switch(mode) {
default:
case ScreenMode::Blank: return 0;
case ScreenMode::Text: return 240;
case ScreenMode::MultiColour: return 256;
case ScreenMode::ColouredText: return 256;
case ScreenMode::Graphics: return 256;
case ScreenMode::SMSMode4: return 256;
case ScreenMode::YamahaText80: return 480;
case ScreenMode::YamahaGraphics3: return 256;
case ScreenMode::YamahaGraphics4: return 256;
case ScreenMode::YamahaGraphics5: return 512;
case ScreenMode::YamahaGraphics6: return 512;
case ScreenMode::YamahaGraphics7: return 256;
}
}
constexpr bool interleaves_banks(ScreenMode mode) {
return mode == ScreenMode::YamahaGraphics6 || mode == ScreenMode::YamahaGraphics7;
}
enum class FetchMode {
Text,
Character,
Refresh,
SMS,
Yamaha,
};
enum class MemoryAccess {
Read, Write, None
};
enum class VerticalState {
/// Describes any line on which pixels do not appear and no fetching occurs, including
/// the border, blanking and sync.
Blank,
/// A line on which pixels do not appear but fetching occurs.
Prefetch,
/// A line on which pixels appear and fetching occurs.
Pixels,
};
enum class SpriteMode {
Mode1,
Mode2,
MasterSystem,
};
}
#endif /* AccessEnums_hpp */

View File

@ -12,8 +12,7 @@
#include "../9918.hpp"
#include "PersonalityTraits.hpp"
namespace TI {
namespace TMS {
namespace TI::TMS {
enum class Clock {
Internal,
@ -61,35 +60,6 @@ template <Personality personality> struct StandardTiming {
/// The number of internal cycles that must elapse between a request to read or write and
/// it becoming a candidate for action.
constexpr static int VRAMAccessDelay = 6;
/// The first internal cycle at which pixels will be output in any mode other than text.
/// Pixels implicitly run from here to the end of the line.
constexpr static int FirstPixelCycle = 86 * CyclesPerLine / 342;
/// The first internal cycle at which pixels will be output text mode.
constexpr static int FirstTextCycle = 94 * CyclesPerLine / 342;
/// The final internal cycle at which pixels will be output text mode.
constexpr static int LastTextCycle = 334 * CyclesPerLine / 342;
// For the below, the fixed portion of line layout is:
//
// [0, EndOfRightBorder): right border colour
// [EndOfRightBorder, StartOfSync): blank
// [StartOfSync, EndOfSync): sync
// [EndOfSync, StartOfColourBurst): blank
// [StartOfColourBurst, EndOfColourBurst): the colour burst
// [EndOfColourBurst, StartOfLeftBorder): blank
//
// The region from StartOfLeftBorder until the end is then filled with
// some combination of pixels and more border, depending on the vertical
// position of this line and the current screen mode.
constexpr static int EndOfRightBorder = 15 * CyclesPerLine / 342;
constexpr static int StartOfSync = 23 * CyclesPerLine / 342;
constexpr static int EndOfSync = 49 * CyclesPerLine / 342;
constexpr static int StartOfColourBurst = 51 * CyclesPerLine / 342;
constexpr static int EndOfColourBurst = 65 * CyclesPerLine / 342;
constexpr static int StartOfLeftBorder = 73 * CyclesPerLine / 342;
};
/// Provides concrete, specific timing for the nominated personality.
@ -174,7 +144,54 @@ template <Personality personality> class ClockConverter {
int cycles_error_ = 0;
};
}
//
//
//
template <Personality personality, typename Enable = void> struct LineLayout;
// Line layout is:
//
// [0, EndOfSync] sync
// (EndOfSync, StartOfColourBurst] blank
// (StartOfColourBurst, EndOfColourBurst] colour burst
// (EndOfColourBurst, EndOfLeftErase] blank
// (EndOfLeftErase, EndOfLeftBorder] border colour
// (EndOfLeftBorder, EndOfPixels] pixel content
// (EndOfPixels, EndOfRightBorder] border colour
// [EndOfRightBorder, <end of line>] blank
//
// ... with minor caveats:
// * horizontal adjust on the Yamaha VDPs is applied to EndOfLeftBorder and EndOfPixels;
// * the Sega VDPs may programatically extend the left border; and
// * text mode on all VDPs adjusts border width.
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_classic_vdp(personality)>> {
constexpr static int EndOfSync = 26;
constexpr static int StartOfColourBurst = 29;
constexpr static int EndOfColourBurst = 43;
constexpr static int EndOfLeftErase = 50;
constexpr static int EndOfLeftBorder = 63;
constexpr static int EndOfPixels = 319;
constexpr static int EndOfRightBorder = 334;
constexpr static int TextModeEndOfLeftBorder = 69;
constexpr static int TextModeEndOfPixels = 309;
};
template <Personality personality> struct LineLayout<personality, std::enable_if_t<is_yamaha_vdp(personality)>> {
constexpr static int EndOfSync = 100;
constexpr static int StartOfColourBurst = 113;
constexpr static int EndOfColourBurst = 167;
constexpr static int EndOfLeftErase = 202;
constexpr static int EndOfLeftBorder = 258;
constexpr static int EndOfPixels = 1282;
constexpr static int EndOfRightBorder = 1341;
constexpr static int TextModeEndOfLeftBorder = 294;
constexpr static int TextModeEndOfPixels = 1254;
};
}
#endif /* ClockConverter_hpp */

View File

@ -9,18 +9,260 @@
#ifndef Draw_hpp
#define Draw_hpp
// MARK: - Sprites, as generalised.
template <Personality personality>
template <SpriteMode mode, bool double_width>
void Base<personality>::draw_sprites(uint8_t y, int start, int end, const std::array<uint32_t, 16> &palette, int *colour_buffer) {
if(!draw_line_buffer_->sprites) {
return;
}
auto &buffer = *draw_line_buffer_->sprites;
if(!buffer.active_sprite_slot) {
return;
}
const int shift_advance = sprites_magnified_ ? 1 : 2;
// If this is the start of the line clip any part of any sprites that is off to the left.
if(!start) {
for(int index = 0; index < buffer.active_sprite_slot; ++index) {
auto &sprite = buffer.active_sprites[index];
if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x;
}
}
int sprite_buffer[256];
int sprite_collision = 0;
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0]));
if constexpr (mode == SpriteMode::MasterSystem) {
// Draw all sprites into the sprite buffer.
for(int index = buffer.active_sprite_slot - 1; index >= 0; --index) {
auto &sprite = buffer.active_sprites[index];
if(sprite.shift_position >= 16) {
continue;
}
const int pixel_start = std::max(start, sprite.x);
// TODO: it feels like the work below should be simplifiable;
// the double shift in particular, and hopefully the variable shift.
for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) {
const int shift = (sprite.shift_position >> 1);
const int sprite_colour =
(((sprite.image[3] << shift) & 0x80) >> 4) |
(((sprite.image[2] << shift) & 0x80) >> 5) |
(((sprite.image[1] << shift) & 0x80) >> 6) |
(((sprite.image[0] << shift) & 0x80) >> 7);
if(sprite_colour) {
sprite_collision |= sprite_buffer[c];
sprite_buffer[c] = sprite_colour | 0x10;
}
sprite.shift_position += shift_advance;
}
}
// Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have
// priority (or is transparent).
for(int c = start; c < end; ++c) {
if(
sprite_buffer[c] &&
(!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf))
) colour_buffer[c] = sprite_buffer[c];
}
if(sprite_collision) {
status_ |= StatusSpriteCollision;
}
return;
}
if constexpr (SpriteBuffer::test_is_filling) {
assert(!buffer.is_filling);
}
constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
const int sprite_width = sprites_16x16_ ? 16 : 8;
const int shifter_target = sprite_width << 1;
const int pixel_width = sprites_magnified_ ? sprite_width << 1 : sprite_width;
int min_sprite = 0;
//
// Approach taken for Mode 2 sprites:
//
// (1) precompute full sprite images, at up to 32 pixels wide;
// (2) for each sprite that is marked as CC, walk backwards until the
// first sprite that is not marked CC, ORing it into the precomputed
// image at each step;
// (3) subsequently, just draw each sprite image independently.
//
if constexpr (mode == SpriteMode::Mode2) {
// Determine the lowest visible sprite; exit early if that leaves no sprites visible.
for(; min_sprite < buffer.active_sprite_slot; min_sprite++) {
auto &sprite = buffer.active_sprites[min_sprite];
if(sprite.opaque()) {
break;
}
}
if(min_sprite == buffer.active_sprite_slot) {
return;
}
if(!start) {
// Pre-rasterise the sprites one-by-one.
if(sprites_magnified_) {
for(int index = min_sprite; index < buffer.active_sprite_slot; index++) {
auto &sprite = buffer.active_sprites[index];
for(int c = 0; c < 32; c+= 2) {
const int shift = (c >> 1) ^ 7;
const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7));
Storage<personality>::sprite_cache_[index][c] =
Storage<personality>::sprite_cache_[index][c + 1] =
(sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) |
uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit());
}
}
} else {
for(int index = min_sprite; index < buffer.active_sprite_slot; index++) {
auto &sprite = buffer.active_sprites[index];
for(int c = 0; c < 16; c++) {
const int shift = c ^ 7;
const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7));
Storage<personality>::sprite_cache_[index][c] =
(sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) |
uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit());
}
}
}
// Go backwards compositing any sprites that are set as OR masks onto their parents.
for(int index = buffer.active_sprite_slot - 1; index >= min_sprite + 1; --index) {
auto &sprite = buffer.active_sprites[index];
if(sprite.opaque()) {
continue;
}
// Sprite may affect all previous up to and cindlugin the next one that is opaque.
for(int previous_index = index - 1; previous_index >= min_sprite; --previous_index) {
// Determine region of overlap (if any).
auto &previous = buffer.active_sprites[previous_index];
const int origin = sprite.x - previous.x;
const int x1 = std::max(0, -origin);
const int x2 = std::min(pixel_width - origin, pixel_width);
// Composite sprites.
for(int x = x1; x < x2; x++) {
Storage<personality>::sprite_cache_[previous_index][x + origin]
|= Storage<personality>::sprite_cache_[index][x];
}
// If a previous opaque sprite has been found, stop.
if(previous.opaque()) {
break;
}
}
}
}
// Draw.
for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) {
auto &sprite = buffer.active_sprites[index];
const int x1 = std::max(0, start - sprite.x);
const int x2 = std::min(end - sprite.x, pixel_width);
for(int x = x1; x < x2; x++) {
const uint8_t colour = Storage<personality>::sprite_cache_[index][x];
// Plot colour, if visible.
if(colour) {
pixel_origin_[sprite.x + x] = palette[colour & 0xf];
}
// TODO: is collision location recorded in mode 1?
// Check for a new collision.
if(!(status_ & StatusSpriteCollision)) {
sprite_collision |= sprite_buffer[sprite.x + x];
sprite_buffer[sprite.x + x] |= colour;
status_ |= sprite_collision & StatusSpriteCollision;
if(status_ & StatusSpriteCollision) {
Storage<personality>::collision_location_[0] = uint16_t(x);
Storage<personality>::collision_location_[1] = uint16_t(y);
}
}
}
}
return;
}
if constexpr (mode == SpriteMode::Mode1) {
for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) {
auto &sprite = buffer.active_sprites[index];
if(sprite.shift_position >= shifter_target) {
continue;
}
const int pixel_start = std::max(start, sprite.x);
for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) {
const int shift = (sprite.shift_position >> 1) ^ 7;
int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1;
// A colision is detected regardless of sprite colour ...
sprite_collision |= sprite_buffer[c] & sprite_colour;
sprite_buffer[c] |= sprite_colour;
// ... but a sprite with the transparent colour won't actually be visible.
sprite_colour &= colour_masks[sprite.image[2] & 0xf];
pixel_origin_[c] =
(pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) |
(palette[sprite.image[2] & 0xf] & sprite_colour_selection_masks[sprite_colour]);
sprite.shift_position += shift_advance;
}
}
status_ |= sprite_collision << StatusSpriteCollisionShift;
return;
}
}
// Mode 2 logic, as I currently understand it, as a note for my future self:
//
// If a sprite is marked as 'CC' then it doesn't collide, but its colour value is
// ORd with those of all lower-numbered sprites down to the next one that is visible on
// that line and not marked CC.
//
// If no previous sprite meets that criteria, no pixels are displayed. But if one does
// then pixels are displayed even where they don't overlap with the earlier sprites.
//
// ... so in terms of my loop above, I guess I need temporary storage to accumulate
// an OR mask up until I hit a non-CC sprite, at which point I composite everything out?
// I'm not immediately sure whether I can appropriately reuse sprite_buffer, but possibly?
// MARK: - TMS9918
template <Personality personality>
template <SpriteMode sprite_mode>
void Base<personality>::draw_tms_character(int start, int end) {
LineBuffer &line_buffer = line_buffers_[read_pointer_.row];
auto &line_buffer = *draw_line_buffer_;
// Paint the background tiles.
const int pixels_left = end - start;
if(this->screen_mode_ == ScreenMode::MultiColour) {
for(int c = start; c < end; ++c) {
pixel_target_[c] = palette[
(line_buffer.patterns[c >> 3][0] >> (((c & 4)^4))) & 15
pixel_target_[c] = palette()[
(line_buffer.tiles.patterns[c >> 3][0] >> (((c & 4)^4))) & 15
];
}
} else {
@ -29,11 +271,11 @@ void Base<personality>::draw_tms_character(int start, int end) {
int length = std::min(pixels_left, 8 - shift);
int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift;
uint8_t colour = line_buffer.patterns[byte_column][1];
int pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]) >> shift;
uint8_t colour = line_buffer.tiles.patterns[byte_column][1];
uint32_t colours[2] = {
palette[(colour & 15) ? (colour & 15) : background_colour_],
palette[(colour >> 4) ? (colour >> 4) : background_colour_]
palette()[(colour & 15) ? (colour & 15) : background_colour_],
palette()[(colour >> 4) ? (colour >> 4) : background_colour_]
};
int background_pixels_left = pixels_left;
@ -49,75 +291,42 @@ void Base<personality>::draw_tms_character(int start, int end) {
length = std::min(8, background_pixels_left);
byte_column++;
pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]);
colour = line_buffer.patterns[byte_column][1];
colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_];
colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_];
pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]);
colour = line_buffer.tiles.patterns[byte_column][1];
colours[0] = palette()[(colour & 15) ? (colour & 15) : background_colour_];
colours[1] = palette()[(colour >> 4) ? (colour >> 4) : background_colour_];
}
}
// Paint sprites and check for collisions, but only if at least one sprite is active
// on this line.
if(line_buffer.active_sprite_slot) {
const int shift_advance = sprites_magnified_ ? 1 : 2;
// If this is the start of the line clip any part of any sprites that is off to the left.
if(!start) {
for(int index = 0; index < line_buffer.active_sprite_slot; ++index) {
LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index];
if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x;
}
}
int sprite_buffer[256];
int sprite_collision = 0;
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0]));
constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
// Draw all sprites into the sprite buffer.
const int shifter_target = sprites_16x16_ ? 32 : 16;
for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) {
LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index];
if(sprite.shift_position < shifter_target) {
const int pixel_start = std::max(start, sprite.x);
for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) {
const int shift = (sprite.shift_position >> 1) ^ 7;
int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1;
// A colision is detected regardless of sprite colour ...
sprite_collision |= sprite_buffer[c] & sprite_colour;
sprite_buffer[c] |= sprite_colour;
// ... but a sprite with the transparent colour won't actually be visible.
sprite_colour &= colour_masks[sprite.image[2]&15];
pixel_origin_[c] =
(pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) |
(palette[sprite.image[2]&15] & sprite_colour_selection_masks[sprite_colour]);
sprite.shift_position += shift_advance;
}
}
}
status_ |= sprite_collision << StatusSpriteCollisionShift;
}
draw_sprites<sprite_mode, false>(0, start, end, palette()); // TODO: propagate a real 'y' into here.
}
template <Personality personality>
template <bool apply_blink>
void Base<personality>::draw_tms_text(int start, int end) {
LineBuffer &line_buffer = line_buffers_[read_pointer_.row];
const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] };
auto &line_buffer = *draw_line_buffer_;
uint32_t colours[2][2] = {
{palette()[background_colour_], palette()[text_colour_]},
{0, 0}
};
if constexpr (apply_blink) {
colours[1][0] = palette()[Storage<personality>::blink_background_colour_];
colours[1][1] = palette()[Storage<personality>::blink_text_colour_];
}
const int shift = start % 6;
int byte_column = start / 6;
int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift;
int pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]) >> shift;
int pixels_left = end - start;
int length = std::min(pixels_left, 6 - shift);
int flag = 0;
if constexpr (apply_blink) {
flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage<personality>::in_blink_;
}
while(true) {
pixels_left -= length;
for(int c = 0; c < length; ++c) {
pixel_target_[c] = colours[pattern&0x01];
pixel_target_[c] = colours[flag][(pattern&0x01)];
pattern >>= 1;
}
pixel_target_ += length;
@ -125,7 +334,10 @@ void Base<personality>::draw_tms_text(int start, int end) {
if(!pixels_left) break;
length = std::min(6, pixels_left);
byte_column++;
pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]);
pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]);
if constexpr (apply_blink) {
flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage<personality>::in_blink_;
}
}
}
@ -133,158 +345,221 @@ void Base<personality>::draw_tms_text(int start, int end) {
template <Personality personality>
void Base<personality>::draw_sms(int start, int end, uint32_t cram_dot) {
LineBuffer &line_buffer = line_buffers_[read_pointer_.row];
int colour_buffer[256];
if constexpr (is_sega_vdp(personality)) {
int colour_buffer[256];
auto &line_buffer = *draw_line_buffer_;
/*
Add extra border for any pixels that fall before the fine scroll.
*/
int tile_start = start, tile_end = end;
int tile_offset = start;
if(read_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) {
for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) {
colour_buffer[c] = 16 + background_colour_;
++tile_offset;
}
// Remove the border area from that to which tiles will be drawn.
tile_start = std::max(start - (line_buffer.latched_horizontal_scroll & 7), 0);
tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0);
}
uint32_t pattern;
uint8_t *const pattern_index = reinterpret_cast<uint8_t *>(&pattern);
/*
Add background tiles; these will fill the colour_buffer with values in which
the low five bits are a palette index, and bit six is set if this tile has
priority over sprites.
*/
if(tile_start < end) {
const int shift = tile_start & 7;
int byte_column = tile_start >> 3;
int pixels_left = tile_end - tile_start;
int length = std::min(pixels_left, 8 - shift);
pattern = *reinterpret_cast<const uint32_t *>(line_buffer.patterns[byte_column]);
if(line_buffer.names[byte_column].flags&2)
pattern >>= shift;
else
pattern <<= shift;
while(true) {
const int palette_offset = (line_buffer.names[byte_column].flags&0x18) << 1;
if(line_buffer.names[byte_column].flags&2) {
for(int c = 0; c < length; ++c) {
colour_buffer[tile_offset] =
((pattern_index[3] & 0x01) << 3) |
((pattern_index[2] & 0x01) << 2) |
((pattern_index[1] & 0x01) << 1) |
((pattern_index[0] & 0x01) << 0) |
palette_offset;
++tile_offset;
pattern >>= 1;
}
} else {
for(int c = 0; c < length; ++c) {
colour_buffer[tile_offset] =
((pattern_index[3] & 0x80) >> 4) |
((pattern_index[2] & 0x80) >> 5) |
((pattern_index[1] & 0x80) >> 6) |
((pattern_index[0] & 0x80) >> 7) |
palette_offset;
++tile_offset;
pattern <<= 1;
}
/*
Add extra border for any pixels that fall before the fine scroll.
*/
int tile_start = start, tile_end = end;
int tile_offset = start;
if(output_pointer_.row >= 16 || !Storage<personality>::horizontal_scroll_lock_) {
for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) {
colour_buffer[c] = 16 + background_colour_;
++tile_offset;
}
pixels_left -= length;
if(!pixels_left) break;
length = std::min(8, pixels_left);
byte_column++;
pattern = *reinterpret_cast<const uint32_t *>(line_buffer.patterns[byte_column]);
}
}
/*
Apply sprites (if any).
*/
if(line_buffer.active_sprite_slot) {
const int shift_advance = sprites_magnified_ ? 1 : 2;
// If this is the start of the line clip any part of any sprites that is off to the left.
if(!start) {
for(int index = 0; index < line_buffer.active_sprite_slot; ++index) {
LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index];
if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x;
}
// Remove the border area from that to which tiles will be drawn.
tile_start = std::max(start - (line_buffer.latched_horizontal_scroll & 7), 0);
tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0);
}
int sprite_buffer[256];
int sprite_collision = 0;
memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0]));
// Draw all sprites into the sprite buffer.
for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) {
LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index];
if(sprite.shift_position < 16) {
const int pixel_start = std::max(start, sprite.x);
uint32_t pattern;
uint8_t *const pattern_index = reinterpret_cast<uint8_t *>(&pattern);
// TODO: it feels like the work below should be simplifiable;
// the double shift in particular, and hopefully the variable shift.
for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) {
const int shift = (sprite.shift_position >> 1);
const int sprite_colour =
(((sprite.image[3] << shift) & 0x80) >> 4) |
(((sprite.image[2] << shift) & 0x80) >> 5) |
(((sprite.image[1] << shift) & 0x80) >> 6) |
(((sprite.image[0] << shift) & 0x80) >> 7);
/*
Add background tiles; these will fill the colour_buffer with values in which
the low five bits are a palette index, and bit six is set if this tile has
priority over sprites.
*/
if(tile_start < end) {
const int shift = tile_start & 7;
int byte_column = tile_start >> 3;
int pixels_left = tile_end - tile_start;
int length = std::min(pixels_left, 8 - shift);
if(sprite_colour) {
sprite_collision |= sprite_buffer[c];
sprite_buffer[c] = sprite_colour | 0x10;
pattern = *reinterpret_cast<const uint32_t *>(line_buffer.tiles.patterns[byte_column]);
if(line_buffer.tiles.flags[byte_column]&2)
pattern >>= shift;
else
pattern <<= shift;
while(true) {
const int palette_offset = (line_buffer.tiles.flags[byte_column]&0x18) << 1;
if(line_buffer.tiles.flags[byte_column]&2) {
for(int c = 0; c < length; ++c) {
colour_buffer[tile_offset] =
((pattern_index[3] & 0x01) << 3) |
((pattern_index[2] & 0x01) << 2) |
((pattern_index[1] & 0x01) << 1) |
((pattern_index[0] & 0x01) << 0) |
palette_offset;
++tile_offset;
pattern >>= 1;
}
} else {
for(int c = 0; c < length; ++c) {
colour_buffer[tile_offset] =
((pattern_index[3] & 0x80) >> 4) |
((pattern_index[2] & 0x80) >> 5) |
((pattern_index[1] & 0x80) >> 6) |
((pattern_index[0] & 0x80) >> 7) |
palette_offset;
++tile_offset;
pattern <<= 1;
}
sprite.shift_position += shift_advance;
}
pixels_left -= length;
if(!pixels_left) break;
length = std::min(8, pixels_left);
byte_column++;
pattern = *reinterpret_cast<const uint32_t *>(line_buffer.tiles.patterns[byte_column]);
}
}
// Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have
// priority (or is transparent).
for(int c = start; c < end; ++c) {
if(
sprite_buffer[c] &&
(!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf))
) colour_buffer[c] = sprite_buffer[c];
/*
Apply sprites (if any).
*/
draw_sprites<SpriteMode::MasterSystem, false>(0, start, end, palette(), colour_buffer); // TODO provide good y, as per elsewhere.
// Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any.
pixel_target_[start] = Storage<personality>::colour_ram_[colour_buffer[start] & 0x1f] | cram_dot;
for(int c = start+1; c < end; ++c) {
pixel_target_[c] = Storage<personality>::colour_ram_[colour_buffer[c] & 0x1f];
}
if(sprite_collision)
status_ |= StatusSpriteCollision;
}
// Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any.
pixel_target_[start] = master_system_.colour_ram[colour_buffer[start] & 0x1f] | cram_dot;
for(int c = start+1; c < end; ++c) {
pixel_target_[c] = master_system_.colour_ram[colour_buffer[c] & 0x1f];
}
// If the VDP is set to hide the left column and this is the final call that'll come
// this line, hide it.
if(end == 256) {
if(master_system_.hide_left_column) {
pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] =
pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] =
master_system_.colour_ram[16 + background_colour_];
// If the VDP is set to hide the left column and this is the final call that'll come
// this line, hide it.
if(end == 256) {
if(Storage<personality>::hide_left_column_) {
pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] =
pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] =
Storage<personality>::colour_ram_[16 + background_colour_];
}
}
}
}
// MARK: - Yamaha
// TODO.
template <Personality personality>
template <ScreenMode mode>
void Base<personality>::draw_yamaha(uint8_t y, int start, int end) {
const auto active_palette = palette();
const int sprite_start = start >> 2;
const int sprite_end = end >> 2;
auto &line_buffer = *draw_line_buffer_;
// Observation justifying Duff's device below: it's acceptable to paint too many pixels — to paint
// beyond `end` — provided that the overpainting is within normal bitmap bounds, because any
// mispainted pixels will be replaced before becoming visible to the user.
if constexpr (mode == ScreenMode::YamahaGraphics4 || mode == ScreenMode::YamahaGraphics6) {
start >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1;
end >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1;
int column = start & ~1;
const int offset = start & 1;
start >>= 1;
end = (end + 1) >> 1;
switch(offset) {
case 0:
do {
pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 4];
case 1: pixel_target_[column+1] = active_palette[line_buffer.bitmap[start] & 0xf];
++start;
column += 2;
} while(start < end);
}
}
if constexpr (mode == ScreenMode::YamahaGraphics5) {
start >>= 1;
end >>= 1;
int column = start & ~3;
const int offset = start & 3;
start >>= 2;
end = (end + 3) >> 2;
switch(offset) {
case 0:
do {
pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 6];
case 1: pixel_target_[column+1] = active_palette[(line_buffer.bitmap[start] >> 4) & 3];
case 2: pixel_target_[column+2] = active_palette[(line_buffer.bitmap[start] >> 2) & 3];
case 3: pixel_target_[column+3] = active_palette[line_buffer.bitmap[start] & 3];
++start;
column += 4;
} while(start < end);
}
}
if constexpr (mode == ScreenMode::YamahaGraphics7) {
start >>= 2;
end >>= 2;
while(start < end) {
pixel_target_[start] =
palette_pack(
uint8_t((line_buffer.bitmap[start] & 0x1c) + ((line_buffer.bitmap[start] & 0x1c) << 3) + ((line_buffer.bitmap[start] & 0x1c) >> 3)),
uint8_t((line_buffer.bitmap[start] & 0xe0) + ((line_buffer.bitmap[start] & 0xe0) >> 3) + ((line_buffer.bitmap[start] & 0xe0) >> 6)),
uint8_t((line_buffer.bitmap[start] & 0x03) + ((line_buffer.bitmap[start] & 0x03) << 2) + ((line_buffer.bitmap[start] & 0x03) << 4) + ((line_buffer.bitmap[start] & 0x03) << 6))
);
++start;
}
}
constexpr std::array<uint32_t, 16> graphics7_sprite_palette = {
palette_pack(0b00000000, 0b00000000, 0b00000000), palette_pack(0b00000000, 0b00000000, 0b01001001),
palette_pack(0b00000000, 0b01101101, 0b00000000), palette_pack(0b00000000, 0b01101101, 0b01001001),
palette_pack(0b01101101, 0b00000000, 0b00000000), palette_pack(0b01101101, 0b00000000, 0b01001001),
palette_pack(0b01101101, 0b01101101, 0b00000000), palette_pack(0b01101101, 0b01101101, 0b01001001),
palette_pack(0b10010010, 0b11111111, 0b01001001), palette_pack(0b00000000, 0b00000000, 0b11111111),
palette_pack(0b00000000, 0b11111111, 0b00000000), palette_pack(0b00000000, 0b11111111, 0b11111111),
palette_pack(0b11111111, 0b00000000, 0b00000000), palette_pack(0b11111111, 0b00000000, 0b11111111),
palette_pack(0b11111111, 0b11111111, 0b00000000), palette_pack(0b11111111, 0b11111111, 0b11111111),
};
// Possibly TODO: is the data-sheet trying to allege some sort of colour mixing for sprites in Mode 6?
draw_sprites<
SpriteMode::Mode2,
mode == ScreenMode::YamahaGraphics5 || mode == ScreenMode::YamahaGraphics6
>(y, sprite_start, sprite_end, mode == ScreenMode::YamahaGraphics7 ? graphics7_sprite_palette : palette());
}
template <Personality personality>
void Base<personality>::draw_yamaha(uint8_t y, int start, int end) {
if constexpr (is_yamaha_vdp(personality)) {
switch(draw_line_buffer_->screen_mode) {
// Modes that are the same (or close enough) to those on the TMS.
case ScreenMode::Text: draw_tms_text<false>(start >> 2, end >> 2); break;
case ScreenMode::YamahaText80: draw_tms_text<true>(start >> 1, end >> 1); break;
case ScreenMode::MultiColour:
case ScreenMode::ColouredText:
case ScreenMode::Graphics: draw_tms_character(start >> 2, end >> 2); break;
case ScreenMode::YamahaGraphics3:
draw_tms_character<SpriteMode::Mode2>(start >> 2, end >> 2);
break;
#define Dispatch(x) case ScreenMode::x: draw_yamaha<ScreenMode::x>(y, start, end); break;
Dispatch(YamahaGraphics4);
Dispatch(YamahaGraphics5);
Dispatch(YamahaGraphics6);
Dispatch(YamahaGraphics7);
#undef Dispatch
default: break;
}
}
}
// MARK: - Mega Drive

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,132 @@
//
// LineBuffer.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2023.
// Copyright © 2023 Thomas Harte. All rights reserved.
//
#ifndef LineBuffer_hpp
#define LineBuffer_hpp
#include "AccessEnums.hpp"
namespace TI::TMS {
// Temporary buffers collect a representation of each line prior to pixel serialisation.
struct SpriteBuffer {
// An active sprite is one that has been selected for composition onto
// _this_ line.
struct ActiveSprite {
int index = 0; // The original in-table index of this sprite.
int row = 0; // The row of the sprite that should be drawn.
int x = 0; // The sprite's x position on screen.
uint8_t image[4]; // Up to four bytes of image information.
//
// In practice:
//
// Master System mode: the four bytes of this 8x8 sprite;
// TMS and Yamaha: [0] = the left half of this sprite; [1] = the right side (if 16x16 sprites enabled); [2] = colour, early-clock bit, etc.
int shift_position = 0; // An offset representing how much of the image information has already been drawn.
// Yamaha helpers.
bool opaque() const {
return !(image[2] & 0x40);
}
/// @returns @c 0x20 if this sprite should generate collisions; @c 0x00 otherwise.
int collision_bit() const {
return ((image[2] & 0x20) | ((image[2] & 0x40) >> 1)) ^ 0x20;
}
// Yamaha and TMS helpers.
int early_clock() const {
return (image[2] & 0x80) >> 2;
}
} active_sprites[8];
int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required.
bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites
// being evaluated for display. This flag determines whether the sentinel has yet been reached.
uint8_t sprite_terminator = 0;
#ifndef NDEBUG
static constexpr bool test_is_filling = true;
#else
static constexpr bool test_is_filling = false;
#endif
bool is_filling = false;
void reset_sprite_collection();
};
struct LineBuffer {
LineBuffer() {}
// The fetch mode describes the proper timing diagram for this line;
// screen mode captures proper output mode.
FetchMode fetch_mode = FetchMode::Text;
ScreenMode screen_mode = ScreenMode::Text;
VerticalState vertical_state = VerticalState::Blank;
SpriteBuffer *sprites = nullptr;
// Holds the horizontal scroll position to apply to this line;
// of those VDPs currently implemented, affects the Master System only.
uint8_t latched_horizontal_scroll = 0;
// The names array holds pattern names, as an offset into memory, and
// potentially flags also.
union {
// This struct captures maximal potential detail across the TMS9918
// and Sega VDP for tiled modes (plus multicolour).
struct {
uint8_t flags[32]{};
// The patterns array holds tile patterns, corresponding 1:1 with names.
// Four bytes per pattern is the maximum required by any
// currently-implemented VDP.
uint8_t patterns[32][4]{};
} tiles;
// The Yamaha and TMS both have text modes, with the former going up to
// 80 columns plus 10 bytes of colour-esque flags.
struct {
uint8_t shapes[80];
uint8_t flags[10];
} characters;
// The Yamaha VDP also has a variety of bitmap modes,
// the widest of which is 512px @ 4bpp.
uint8_t bitmap[256];
};
/*
Horizontal layout (on a 342-cycle clock):
15 cycles right border
58 cycles blanking & sync
13 cycles left border
... i.e. to cycle 86, then:
border up to first_pixel_output_column;
pixels up to next_border_column;
border up to the end.
e.g. standard 256-pixel modes will want to set
first_pixel_output_column = 86, next_border_column = 342.
*/
int first_pixel_output_column = 94;
int next_border_column = 334;
int pixel_count = 256;
};
struct LineBufferPointer {
int row = 0, column = 0;
};
}
#endif /* LineBuffer_hpp */

View File

@ -9,8 +9,7 @@
#ifndef PersonalityTraits_hpp
#define PersonalityTraits_hpp
namespace TI {
namespace TMS {
namespace TI::TMS {
// Genus determinants for the various personalityes.
constexpr bool is_sega_vdp(Personality p) {
@ -42,12 +41,10 @@ constexpr size_t memory_size(Personality p) {
}
}
constexpr uint16_t memory_mask(Personality p) {
return (memory_size(p) >= 65536) ? 0xffff : uint16_t(memory_size(p) - 1);
constexpr size_t memory_mask(Personality p) {
return memory_size(p) - 1;
}
}
}
#endif /* PersonalityTraits_hpp */

View File

@ -0,0 +1,489 @@
//
//
// Storage.hpp
// Clock Signal
//
// Created by Thomas Harte on 12/02/2023.
// Copyright © 2023 Thomas Harte. All rights reserved.
//
#ifndef Storage_h
#define Storage_h
#include "LineBuffer.hpp"
#include "YamahaCommands.hpp"
#include <optional>
namespace TI::TMS {
/// A container for personality-specific storage; see specific instances below.
template <Personality personality, typename Enable = void> struct Storage {
};
template <> struct Storage<Personality::TMS9918A> {
using AddressT = uint16_t;
void begin_line(ScreenMode, bool) {}
};
// Yamaha-specific storage.
template <Personality personality> struct Storage<personality, std::enable_if_t<is_yamaha_vdp(personality)>> {
using AddressT = uint32_t;
std::array<uint8_t, 65536> expansion_ram_;
int selected_status_ = 0;
int indirect_register_ = 0;
bool increment_indirect_register_ = false;
int adjustment_[2]{};
std::array<uint32_t, 16> palette_{};
std::array<uint32_t, 16> background_palette_{};
bool solid_background_ = true;
uint8_t new_colour_ = 0;
uint8_t palette_entry_ = 0;
bool palette_write_phase_ = false;
uint8_t mode_ = 0;
uint8_t vertical_offset_ = 0;
uint8_t sprite_cache_[8][32]{};
/// Describes an _observable_ memory access event. i.e. anything that it is safe
/// (and convenient) to treat as atomic in between external slots.
struct Event {
/// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when
/// external data must be ready by in order to take part in those slots.
uint16_t offset = 1368;
enum class Type: uint8_t {
/// A slot for reading or writing data on behalf of the CPU or the command engine.
External,
//
// Sprites.
//
SpriteY,
SpriteLocation,
SpritePattern,
//
// Backgrounds.
//
Name,
Colour,
Pattern,
} type = Type::External;
uint8_t id = 0;
constexpr Event(Type type, uint8_t id = 0) noexcept :
type(type),
id(id) {}
constexpr Event() noexcept {}
};
// State that tracks fetching position within a line.
const Event *next_event_ = nullptr;
// Text blink colours.
uint8_t blink_text_colour_ = 0;
uint8_t blink_background_colour_ = 0;
// Blink state (which is also affects even/odd page display in applicable modes).
int in_blink_ = 1;
uint8_t blink_periods_ = 0;
uint8_t blink_counter_ = 0;
// Sprite collection state.
bool sprites_enabled_ = true;
// Additional status.
uint8_t colour_status_ = 0;
uint16_t colour_location_ = 0;
uint16_t collision_location_[2]{};
/// Resets line-ephemeral state for a new line.
void begin_line(ScreenMode mode, bool is_refresh) {
if(is_refresh) {
next_event_ = refresh_events.data();
return;
}
switch(mode) {
case ScreenMode::YamahaText80:
case ScreenMode::Text:
next_event_ = text_events.data();
break;
case ScreenMode::MultiColour:
case ScreenMode::YamahaGraphics1:
case ScreenMode::YamahaGraphics2:
next_event_ = character_events.data();
break;
case ScreenMode::YamahaGraphics3: // TODO: verify; my guess is that G3 is timed like a bitmap mode
// in order to fit the pattern for sprite mode 2. Just a guess.
default:
next_event_ = sprites_enabled_ ? sprites_events.data() : no_sprites_events.data();
break;
}
}
// Command engine state.
CommandContext command_context_;
ModeDescription mode_description_;
std::unique_ptr<Command> command_ = nullptr;
enum class CommandStep {
None,
CopySourcePixelToStatus,
ReadSourcePixel,
ReadDestinationPixel,
WritePixel,
ReadSourceByte,
WriteByte,
};
CommandStep next_command_step_ = CommandStep::None;
int minimum_command_column_ = 0;
uint8_t command_latch_ = 0;
void update_command_step(int current_column) {
if(!command_) {
next_command_step_ = CommandStep::None;
return;
}
if(command_->done()) {
command_ = nullptr;
next_command_step_ = CommandStep::None;
return;
}
minimum_command_column_ = current_column + command_->cycles;
switch(command_->access) {
case Command::AccessType::ReadPoint:
next_command_step_ = CommandStep::CopySourcePixelToStatus;
break;
case Command::AccessType::CopyPoint:
next_command_step_ = CommandStep::ReadSourcePixel;
break;
case Command::AccessType::PlotPoint:
next_command_step_ = CommandStep::ReadDestinationPixel;
break;
case Command::AccessType::WaitForColourReceipt:
// i.e. nothing to do until a colour is received.
next_command_step_ = CommandStep::None;
break;
case Command::AccessType::CopyByte:
next_command_step_ = CommandStep::ReadSourceByte;
break;
case Command::AccessType::WriteByte:
next_command_step_ = CommandStep::WriteByte;
break;
}
}
Storage() noexcept {
// Perform sanity checks on the event lists.
#ifndef NDEBUG
const Event *lists[] = { no_sprites_events.data(), sprites_events.data(), text_events.data(), character_events.data(), refresh_events.data(), nullptr };
const Event **list = lists;
while(*list) {
const Event *cursor = *list;
++list;
while(cursor[1].offset != 1368) {
assert(cursor[1].offset > cursor[0].offset);
++cursor;
}
}
#endif
// Seed to _something_ meaningful.
//
// TODO: this is a workaround [/hack], in effect, for the main TMS' habit of starting
// in a randomised position, which means that start-of-line isn't announced.
//
// Do I really want that behaviour?
next_event_ = refresh_events.data();
}
private:
template <typename GeneratorT> static constexpr size_t events_size() {
size_t size = 0;
for(int c = 0; c < 1368; c++) {
const auto event_type = GeneratorT::event(c);
size += event_type.has_value();
}
return size + 1;
}
template <typename GeneratorT, size_t size = events_size<GeneratorT>()>
static constexpr std::array<Event, size> events() {
std::array<Event, size> result{};
size_t index = 0;
for(int c = 0; c < 1368; c++) {
const auto event = GeneratorT::event(c);
if(!event) {
continue;
}
result[index] = *event;
result[index].offset = uint16_t(c);
++index;
}
result[index] = Event();
return result;
}
struct StandardGenerators {
static constexpr std::optional<Event> external_every_eight(int index) {
if(index & 7) return std::nullopt;
return Event::Type::External;
}
};
struct RefreshGenerator {
static constexpr std::optional<Event> event(int grauw_index) {
// From 0 to 126: CPU/CMD slots at every cycle divisible by 8.
if(grauw_index < 126) {
return StandardGenerators::external_every_eight(grauw_index - 0);
}
// From 164 to 1234: eight-cycle windows, the first 15 of each 16 being
// CPU/CMD and the final being refresh.
if(grauw_index >= 164 && grauw_index < 1234) {
const int offset = grauw_index - 164;
if(offset & 7) return std::nullopt;
if(((offset >> 3) & 15) == 15) return std::nullopt;
return Event::Type::External;
}
// From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8.
if(grauw_index >= 1268 && grauw_index < 1330) {
return StandardGenerators::external_every_eight(grauw_index - 1268);
}
// A CPU/CMD at 1334.
if(grauw_index == 1334) {
return Event::Type::External;
}
// From 1344 to 1366: CPU/CMD slots every cycle divisible by 8.
if(grauw_index >= 1344 && grauw_index < 1366) {
return StandardGenerators::external_every_eight(grauw_index - 1344);
}
// Otherwise: nothing.
return std::nullopt;
}
};
static constexpr auto refresh_events = events<RefreshGenerator>();
template <bool include_sprites> struct BitmapGenerator {
static constexpr std::optional<Event> event(int grauw_index) {
if(!include_sprites) {
// Various standard zones of one-every-eight external slots.
if(grauw_index < 124) {
return StandardGenerators::external_every_eight(grauw_index + 2);
}
if(grauw_index > 1266) {
return StandardGenerators::external_every_eight(grauw_index - 1266);
}
} else {
// This records collection points for all data for selected sprites.
// There's only four of them (each site covering two sprites),
// so it's clearer just to be explicit.
//
// There's also a corresponding number of extra external slots to spell out.
switch(grauw_index) {
default: break;
case 1238: return Event(Event::Type::SpriteLocation, 0);
case 1302: return Event(Event::Type::SpriteLocation, 2);
case 2: return Event(Event::Type::SpriteLocation, 4);
case 66: return Event(Event::Type::SpriteLocation, 6);
case 1270: return Event(Event::Type::SpritePattern, 0);
case 1338: return Event(Event::Type::SpritePattern, 2);
case 34: return Event(Event::Type::SpritePattern, 4);
case 98: return Event(Event::Type::SpritePattern, 6);
case 1264: case 1330: case 28: case 92:
return Event::Type::External;
}
}
if(grauw_index >= 162 && grauw_index < 176) {
return StandardGenerators::external_every_eight(grauw_index - 162);
}
// Everywhere else the pattern is:
//
// external or sprite y, external, data block
//
// Subject to caveats:
//
// 1) the first data block is just a dummy fetch with no side effects,
// so this emulator declines to record it; and
// 2) every fourth block, the second external is actually a refresh.
//
if(grauw_index >= 182 && grauw_index < 1238) {
const int offset = grauw_index - 182;
const int block = offset / 32;
const int sub_block = offset & 31;
switch(sub_block) {
default: return std::nullopt;
case 0:
if(include_sprites) {
// Don't include the sprite post-amble (i.e. a spurious read with no side effects).
if(block < 32) {
return Event(Event::Type::SpriteY, uint8_t(block));
}
} else {
return Event::Type::External;
}
case 6:
if((block & 3) != 3) {
return Event::Type::External;
}
break;
case 12:
if(block) {
return Event(Event::Type::Pattern, uint8_t(block - 1));
}
break;
}
}
return std::nullopt;
}
};
static constexpr auto no_sprites_events = events<BitmapGenerator<false>>();
static constexpr auto sprites_events = events<BitmapGenerator<true>>();
struct TextGenerator {
static constexpr std::optional<Event> event(int grauw_index) {
// Capture various one-in-eight zones.
if(grauw_index < 72) {
return StandardGenerators::external_every_eight(grauw_index - 2);
}
if(grauw_index >= 166 && grauw_index < 228) {
return StandardGenerators::external_every_eight(grauw_index - 166);
}
if(grauw_index >= 1206 && grauw_index < 1332) {
return StandardGenerators::external_every_eight(grauw_index - 1206);
}
if(grauw_index == 1336) {
return Event::Type::External;
}
if(grauw_index >= 1346) {
return StandardGenerators::external_every_eight(grauw_index - 1346);
}
// Elsewhere...
if(grauw_index >= 246) {
const int offset = grauw_index - 246;
const int block = offset / 48;
const int sub_block = offset % 48;
switch(sub_block) {
default: break;
case 0: return Event(Event::Type::Name, uint8_t(block));
case 18: return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1));
case 24: return Event(Event::Type::Pattern, uint8_t(block));
}
}
return std::nullopt;
}
};
static constexpr auto text_events = events<TextGenerator>();
struct CharacterGenerator {
static constexpr std::optional<Event> event(int grauw_index) {
// Grab sprite events.
switch(grauw_index) {
default: break;
case 1242: return Event(Event::Type::SpriteLocation, 0);
case 1306: return Event(Event::Type::SpriteLocation, 1);
case 6: return Event(Event::Type::SpriteLocation, 2);
case 70: return Event(Event::Type::SpriteLocation, 3);
case 1274: return Event(Event::Type::SpritePattern, 0);
case 1342: return Event(Event::Type::SpritePattern, 1);
case 38: return Event(Event::Type::SpritePattern, 2);
case 102: return Event(Event::Type::SpritePattern, 3);
case 1268: case 1334: case 32: case 96: return Event::Type::External;
}
if(grauw_index >= 166 && grauw_index < 180) {
return StandardGenerators::external_every_eight(grauw_index - 166);
}
if(grauw_index >= 182 && grauw_index < 1238) {
const int offset = grauw_index - 182;
const int block = offset / 32;
const int sub_block = offset & 31;
switch(sub_block) {
case 0: if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1));
case 6: if((sub_block & 3) != 3) return Event::Type::External;
case 12: if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block));
case 18: if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1));
case 24: if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1));
}
}
return std::nullopt;
}
};
static constexpr auto character_events = events<CharacterGenerator>();
};
// Master System-specific storage.
template <Personality personality> struct Storage<personality, std::enable_if_t<is_sega_vdp(personality)>> {
using AddressT = uint16_t;
// The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly
// fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there
// isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps
// track of pending collisions, for visual reproduction.
struct CRAMDot {
LineBufferPointer location;
uint32_t value;
};
std::vector<CRAMDot> upcoming_cram_dots_;
// The Master System's additional colour RAM.
uint32_t colour_ram_[32];
bool cram_is_selected_ = false;
// Programmer-set flags.
bool vertical_scroll_lock_ = false;
bool horizontal_scroll_lock_ = false;
bool hide_left_column_ = false;
bool shift_sprites_8px_left_ = false;
bool mode4_enable_ = false;
uint8_t horizontal_scroll_ = 0;
uint8_t vertical_scroll_ = 0;
// Holds the vertical scroll position for this frame; this is latched
// once and cannot dynamically be changed until the next frame.
uint8_t latched_vertical_scroll_ = 0;
// Various resource addresses with VDP-version-specific modifications
// built in.
AddressT pattern_name_address_;
AddressT sprite_attribute_table_address_;
AddressT sprite_generator_table_address_;
void begin_line(ScreenMode, bool) {}
};
}
#endif /* Storage_h */

View File

@ -0,0 +1,382 @@
//
// YamahaCommands.hpp
// Clock Signal
//
// Created by Thomas Harte on 26/01/2023.
// Copyright © 2023 Thomas Harte. All rights reserved.
//
#ifndef YamahaCommands_hpp
#define YamahaCommands_hpp
#include "AccessEnums.hpp"
namespace TI::TMS {
// MARK: - Generics.
struct Vector {
int v[2]{};
template <int offset, bool high> void set(uint8_t value) {
constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff;
constexpr int shift = high ? 8 : 0;
v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift);
}
template <int offset> void add(int amount) {
v[offset] += amount;
if constexpr (offset == 1) {
v[offset] &= 0x3ff;
} else {
v[offset] &= 0x1ff;
}
}
Vector & operator += (const Vector &rhs) {
add<0>(rhs.v[0]);
add<1>(rhs.v[1]);
return *this;
}
};
struct Colour {
void set(uint8_t value) {
colour = value;
colour4bpp = uint8_t((value & 0xf) | (value << 4));
colour2bpp = uint8_t((colour4bpp & 0x33) | ((colour4bpp & 0x33) << 2));
}
void reset() {
colour = 0x00;
colour4bpp = 0xff;
}
bool has_value() const {
return (colour & 0xf) == (colour4bpp & 0xf);
}
/// Colour as written by the CPU.
uint8_t colour = 0x00;
/// The low four bits of the CPU-written colour, repeated twice.
uint8_t colour4bpp = 0xff;
/// The low two bits of the CPU-written colour, repeated four times.
uint8_t colour2bpp = 0xff;
};
struct CommandContext {
Vector source;
Vector destination;
Vector size;
uint8_t arguments = 0;
Colour colour;
Colour latched_colour;
enum class LogicalOperation {
Copy = 0b0000,
And = 0b0001,
Or = 0b0010,
Xor = 0b0011,
Not = 0b0100,
};
LogicalOperation pixel_operation;
bool test_source;
};
struct ModeDescription {
int width = 256;
int pixels_per_byte = 4;
bool rotate_address = false;
};
struct Command {
// In net:
//
// This command is blocked until @c access has been performed, reading
// from or writing to @c value. It should not be performed until at least
// @c cycles have passed.
enum class AccessType {
/// Plots a single pixel of the current contextual colour at @c destination,
/// which occurs as a read, then a 24-cycle gap, then a write.
PlotPoint,
/// Blocks until the next CPU write to the colour register.
WaitForColourReceipt,
/// Writes an entire byte to the address containing the current @c destination.
WriteByte,
/// Copies a single pixel from @c source location to @c destination,
/// being a read, a 32-cycle gap, then a PlotPoint.
CopyPoint,
/// Copies a complete byte from @c source location to @c destination,
/// being a read, a 24-cycle gap, then a write.
CopyByte,
/// Copies a single pixel from @c source to the colour status register.
ReadPoint,
// ReadByte,
// WaitForColourSend,
};
AccessType access = AccessType::PlotPoint;
int cycles = 0;
bool is_cpu_transfer = false;
bool y_only = false;
/// Current command parameters.
CommandContext &context;
ModeDescription &mode_description;
Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {}
virtual ~Command() {}
/// @returns @c true if all output from this command is done; @c false otherwise.
virtual bool done() = 0;
/// Repopulates the fields above with the next action to take, being provided with the
/// number of pixels per byte in the current screen mode.
virtual void advance() = 0;
protected:
template <int axis, bool include_source> void advance_axis(int offset = 1) {
context.destination.add<axis>(context.arguments & (0x4 << axis) ? -offset : offset);
if constexpr (include_source) {
context.source.add<axis>(context.arguments & (0x4 << axis) ? -offset : offset);
}
}
};
namespace Commands {
// MARK: - Line drawing.
/// Implements the LINE command, which is plain-old Bresenham.
///
/// Per Grauw timing is:
///
/// * 88 cycles between every pixel plot;
/// * plus an additional 32 cycles if a step along the minor axis is taken.
struct Line: public Command {
public:
Line(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
// context.destination = start position;
// context.size.v[0] = long side dots;
// context.size.v[1] = short side dots;
// context.arguments => direction
position_ = context.size.v[1];
numerator_ = position_ << 1;
denominator_ = context.size.v[0] << 1;
cycles = 32;
access = AccessType::PlotPoint;
}
bool done() final {
return !context.size.v[0];
}
void advance() final {
--context.size.v[0];
cycles = 88;
// b0: 1 => long direction is y;
// 0 => long direction is x.
//
// b2: 1 => x direction is left;
// 0 => x direction is right.
//
// b3: 1 => y direction is up;
// 0 => y direction is down.
if(context.arguments & 0x1) {
advance_axis<1, false>();
} else {
advance_axis<0, false>();
}
position_ -= numerator_;
if(position_ < 0) {
position_ += denominator_;
cycles += 32;
if(context.arguments & 0x1) {
advance_axis<0, false>();
} else {
advance_axis<1, false>();
}
}
}
private:
int position_, numerator_, denominator_, duration_;
};
// MARK: - Single pixel manipulation.
/// Implements the PSET command, which plots a single pixel and POINT, which reads one.
///
/// No timings are documented, so this'll output or input as quickly as possible.
template <bool is_read> struct Point: public Command {
public:
Point(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
cycles = 0; // TODO.
access = is_read ? AccessType::ReadPoint : AccessType::PlotPoint;
}
bool done() final {
return done_;
}
void advance() final {
done_ = true;
}
private:
bool done_ = false;
};
// MARK: - Rectangular base.
/// Useful base class for anything that does logical work in a rectangle.
template <bool logical, bool include_source> struct Rectangle: public Command {
public:
Rectangle(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) {
if constexpr (include_source) {
start_x_[0] = context.source.v[0];
}
start_x_[1] = context.destination.v[0];
width_ = context.size.v[0];
if(!width_) {
// Width = 0 => maximal width for this mode.
// (aside: it's still unclear to me whether commands are
// automatically clipped to the display; I think so but
// don't want to spend any time on it until I'm certain)
// context.size.v[0] = width_ = mode_description.width;
}
}
/// Advances the current destination and, if @c include_source is @c true also the source;
/// @returns @c true if a new row was started; @c false otherwise.
bool advance_pixel() {
if constexpr (logical) {
advance_axis<0, include_source>();
--context.size.v[0];
if(context.size.v[0]) {
return false;
}
} else {
advance_axis<0, include_source>(mode_description.pixels_per_byte);
context.size.v[0] -= mode_description.pixels_per_byte;
if(context.size.v[0] & ~(mode_description.pixels_per_byte - 1)) {
return false;
}
}
context.size.v[0] = width_;
if constexpr (include_source) {
context.source.v[0] = start_x_[0];
}
context.destination.v[0] = start_x_[1];
advance_axis<1, include_source>();
--context.size.v[1];
return true;
}
bool done() final {
return !context.size.v[1] || !width_;
}
private:
int start_x_[2]{}, width_ = 0;
};
// MARK: - Rectangular moves to/from CPU.
template <bool logical> struct MoveFromCPU: public Rectangle<logical, false> {
MoveFromCPU(CommandContext &context, ModeDescription &mode_description) : Rectangle<logical, false>(context, mode_description) {
Command::is_cpu_transfer = true;
// This command is started with the first colour ready to transfer.
Command::cycles = 32;
Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte;
}
void advance() final {
switch(Command::access) {
default: break;
case Command::AccessType::WaitForColourReceipt:
Command::cycles = 32;
Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte;
break;
case Command::AccessType::WriteByte:
case Command::AccessType::PlotPoint:
Command::cycles = 0;
Command::access = Command::AccessType::WaitForColourReceipt;
if(Rectangle<logical, false>::advance_pixel()) {
Command::cycles = 64;
// TODO: I'm not sure this will be honoured per the outer wrapping.
}
break;
}
}
};
// MARK: - Rectangular moves within VRAM.
enum class MoveType {
Logical,
HighSpeed,
YOnly,
};
template <MoveType type> struct Move: public Rectangle<type == MoveType::Logical, true> {
static constexpr bool is_logical = type == MoveType::Logical;
static constexpr bool is_y_only = type == MoveType::YOnly;
using RectangleBase = Rectangle<is_logical, true>;
Move(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) {
Command::access = is_logical ? Command::AccessType::CopyPoint : Command::AccessType::CopyByte;
Command::cycles = is_y_only ? 0 : 64;
Command::y_only = is_y_only;
}
void advance() final {
Command::cycles = is_y_only ? 40 : 64;
if(RectangleBase::advance_pixel()) {
Command::cycles += is_y_only ? 0 : 64;
}
}
};
// MARK: - Rectangular fills.
template <bool logical> struct Fill: public Rectangle<logical, false> {
using RectangleBase = Rectangle<logical, false>;
Fill(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) {
Command::cycles = logical ? 64 : 56;
Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte;
}
void advance() final {
Command::cycles = logical ? 72 : 48;
if(RectangleBase::advance_pixel()) {
Command::cycles += logical ? 64 : 56;
}
}
};
}
}
#endif /* YamahaCommands_hpp */

View File

@ -27,7 +27,7 @@ RP5C01::RP5C01(HalfCycles clock_rate) : clock_rate_(clock_rate) {
day_of_the_week_ = time_date->tm_wday;
day_ = time_date->tm_mday;
month_ = time_date->tm_mon;
year_ = time_date->tm_year % 100;
year_ = (time_date->tm_year + 20) % 100; // This is probably MSX specific; rethink if/when other machines use this chip.
leap_year_ = time_date->tm_year % 4;
}
@ -171,19 +171,19 @@ void RP5C01::write(int address, uint8_t value) {
} break;
// Day of the week.
case Reg(0, 0x06): day_of_the_week_ = value % 7; break;
case Reg(0, 0x06): day_of_the_week_ = value % 7; break;
// Day.
case Reg(0, 0x07): TwoDigitEncoder::encode<0>(day_, value); break;
case Reg(0, 0x08): TwoDigitEncoder::encode<1>(day_, value & 3); break;
case Reg(0, 0x07): TwoDigitEncoder::encode<0>(day_, value); break;
case Reg(0, 0x08): TwoDigitEncoder::encode<1>(day_, value & 3); break;
// Month.
case Reg(0, 0x09): TwoDigitEncoder::encode<0>(month_, value); break;
case Reg(0, 0x0a): TwoDigitEncoder::encode<1>(month_, value & 1); break;
case Reg(0, 0x09): TwoDigitEncoder::encode<0>(month_, (value - 1)); break;
case Reg(0, 0x0a): TwoDigitEncoder::encode<1>(month_, (value - 1) & 1); break;
// Year.
case Reg(0, 0x0b): TwoDigitEncoder::encode<0>(year_, value); break;
case Reg(0, 0x0c): TwoDigitEncoder::encode<1>(year_, value); break;
case Reg(0, 0x0b): TwoDigitEncoder::encode<0>(year_, value); break;
case Reg(0, 0x0c): TwoDigitEncoder::encode<1>(year_, value); break;
// TODO: alarm minutes.
case Reg(1, 0x02):
@ -271,10 +271,10 @@ uint8_t RP5C01::read(int address) {
case Reg(0, 0x08): value = TwoDigitEncoder::decode<1>(day_); break;
// Month.
case Reg(0, 0x09): value = TwoDigitEncoder::decode<0>(month_); break;
case Reg(0, 0x0a): value = TwoDigitEncoder::decode<1>(month_); break;
case Reg(0, 0x09): value = TwoDigitEncoder::decode<0>(month_ + 1); break;
case Reg(0, 0x0a): value = TwoDigitEncoder::decode<1>(month_ + 1); break;
// Year;
// Year.
case Reg(0, 0x0b): value = TwoDigitEncoder::decode<0>(year_); break;
case Reg(0, 0x0c): value = TwoDigitEncoder::decode<1>(year_); break;

View File

@ -175,19 +175,19 @@ class ConcreteMachine:
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
vdp_->set_scan_target(scan_target);
vdp_.last_valid()->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return vdp_->get_scaled_scan_status();
return vdp_.last_valid()->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
vdp_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return vdp_->get_display_type();
return vdp_.last_valid()->get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() final {

View File

@ -319,11 +319,11 @@ class ConcreteMachine:
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
vdp_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return vdp_->get_display_type();
return vdp_.last_valid()->get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() final {
@ -568,6 +568,11 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Input:
switch(address & 0xff) {
case 0x9a: case 0x9b:
if constexpr (vdp_model() == TI::TMS::TMS9918A) {
break;
}
[[fallthrough]];
case 0x98: case 0x99:
*cycle.value = vdp_->read(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
@ -591,7 +596,7 @@ class ConcreteMachine:
break;
default:
printf("Unhandled read %02x\n", address & 0xff);
// printf("Unhandled read %02x\n", address & 0xff);
*cycle.value = 0xff;
break;
}
@ -600,6 +605,11 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Output: {
const int port = address & 0xff;
switch(port) {
case 0x9a: case 0x9b:
if constexpr (vdp_model() == TI::TMS::TMS9918A) {
break;
}
[[fallthrough]];
case 0x98: case 0x99:
vdp_->write(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());

View File

@ -181,24 +181,27 @@ template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
}
void set_scan_target(Outputs::Display::ScanTarget *scan_target) final {
vdp_->set_tv_standard(
vdp_.last_valid()->set_tv_standard(
(region_ == Target::Region::Europe) ?
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
time_until_debounce_ = vdp_->get_time_until_line(-1);
vdp_->set_scan_target(scan_target);
// Doing the following would be technically correct, but isn't
// especially thread-safe and won't make a substantial difference.
// time_until_debounce_ = vdp_->get_time_until_line(-1);
vdp_.last_valid()->set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const final {
return vdp_->get_scaled_scan_status();
return vdp_.last_valid()->get_scaled_scan_status();
}
void set_display_type(Outputs::Display::DisplayType display_type) final {
vdp_->set_display_type(display_type);
vdp_.last_valid()->set_display_type(display_type);
}
Outputs::Display::DisplayType get_display_type() const final {
return vdp_->get_display_type();
return vdp_.last_valid()->get_display_type();
}
Outputs::Speaker::Speaker *get_speaker() final {
@ -312,7 +315,7 @@ template <Analyser::Static::Sega::Target::Model model> class ConcreteMachine:
case 0x00: // i.e. even ports less than 0x40.
if constexpr (is_master_system(model)) {
// TODO: Obey the RAM enable.
LOG("Memory control: " << PADHEX(2) << memory_control_);
LOG("Memory control: " << PADHEX(2) << +memory_control_);
memory_control_ = *cycle.value;
page_cartridge();
}

View File

@ -19,7 +19,7 @@
namespace CPU {
/// Provides access to all intermediate parts of a larger int.
template <typename Full, typename Half> union alignas(Full) RegisterPair {
template <typename Full, typename Half> union alignas(Full) alignas(Half) RegisterPair {
RegisterPair(Full v) : full(v) {}
RegisterPair() {}

View File

@ -1253,6 +1253,8 @@
4B2530F3244E6773007980BF /* fm.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = fm.json; sourceTree = "<group>"; };
4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = PersonalityTraits.hpp; sourceTree = "<group>"; };
4B2A332C1DB86821002876E3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/OricOptions.xib"; sourceTree = SOURCE_ROOT; };
4B2A3B5A29993DFA007CE366 /* Storage.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Storage.hpp; sourceTree = "<group>"; };
4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LineBuffer.hpp; sourceTree = "<group>"; };
4B2A53901D117D36003C6002 /* CSAudioQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSAudioQueue.h; sourceTree = "<group>"; };
4B2A53911D117D36003C6002 /* CSAudioQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSAudioQueue.m; sourceTree = "<group>"; };
4B2A53951D117D36003C6002 /* CSMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CSMachine.h; sourceTree = "<group>"; };
@ -2220,6 +2222,8 @@
4BF0BC67297108D100CCA2B5 /* MemorySlotHandler.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = MemorySlotHandler.cpp; sourceTree = "<group>"; };
4BF0BC6F2973318E00CCA2B5 /* RP5C01.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = RP5C01.cpp; sourceTree = "<group>"; };
4BF0BC702973318E00CCA2B5 /* RP5C01.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = RP5C01.hpp; sourceTree = "<group>"; };
4BF0BC732982E54700CCA2B5 /* YamahaCommands.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = YamahaCommands.hpp; sourceTree = "<group>"; };
4BF0BC742982E6D300CCA2B5 /* AccessEnums.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AccessEnums.hpp; sourceTree = "<group>"; };
4BF40A5525424C770033EA39 /* LanguageCardSwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = LanguageCardSwitches.hpp; sourceTree = "<group>"; };
4BF40A5A254263140033EA39 /* AuxiliaryMemorySwitches.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AuxiliaryMemorySwitches.hpp; sourceTree = "<group>"; };
4BF437EC209D0F7E008CBD6B /* SegmentParser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = SegmentParser.cpp; sourceTree = "<group>"; };
@ -4757,10 +4761,14 @@
children = (
4B43983829620FB1006B0BFC /* 9918.cpp */,
4BD388411FE34E010042B588 /* 9918Base.hpp */,
4BF0BC742982E6D300CCA2B5 /* AccessEnums.hpp */,
4B43983C29621024006B0BFC /* ClockConverter.hpp */,
4B43983F2967459B006B0BFC /* Draw.hpp */,
4B43983E29628538006B0BFC /* Fetch.hpp */,
4B2A3B5B29995FF6007CE366 /* LineBuffer.hpp */,
4B262BFF29691F55002EC0F7 /* PersonalityTraits.hpp */,
4B2A3B5A29993DFA007CE366 /* Storage.hpp */,
4BF0BC732982E54700CCA2B5 /* YamahaCommands.hpp */,
);
path = Implementation;
sourceTree = "<group>";

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.8">
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">

View File

@ -63,11 +63,6 @@ class MachineDocument:
return "MachineDocument"
}
convenience init(type typeName: String) throws {
self.init()
self.fileType = typeName
}
override func read(from url: URL, ofType typeName: String) throws {
if let analyser = CSStaticAnalyser(fileAt: url) {
self.displayName = analyser.displayName

View File

@ -112,6 +112,11 @@ typedef NS_ENUM(NSInteger, CSMachineVic20Region) {
CSMachineVic20RegionJapanese,
};
typedef NS_ENUM(NSInteger, CSMachineMSXModel) {
CSMachineMSXModelMSX1,
CSMachineMSXModelMSX2,
};
typedef NS_ENUM(NSInteger, CSMachineMSXRegion) {
CSMachineMSXRegionAmerican,
CSMachineMSXRegionEuropean,
@ -132,7 +137,7 @@ typedef int Kilobytes;
- (instancetype)initWithElectronDFS:(BOOL)dfs adfs:(BOOL)adfs ap6:(BOOL)ap6 sidewaysRAM:(BOOL)sidewaysRAM;
- (instancetype)initWithEnterpriseModel:(CSMachineEnterpriseModel)model speed:(CSMachineEnterpriseSpeed)speed exosVersion:(CSMachineEnterpriseEXOS)exosVersion basicVersion:(CSMachineEnterpriseBASIC)basicVersion dos:(CSMachineEnterpriseDOS)dos;
- (instancetype)initWithMacintoshModel:(CSMachineMacintoshModel)model;
- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive;
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive;
- (instancetype)initWithOricModel:(CSMachineOricModel)model diskInterface:(CSMachineOricDiskInterface)diskInterface;
- (instancetype)initWithSpectrumModel:(CSMachineSpectrumModel)model;
- (instancetype)initWithVic20Region:(CSMachineVic20Region)region memorySize:(Kilobytes)memorySize hasC1540:(BOOL)hasC1540;

View File

@ -229,7 +229,7 @@
return self;
}
- (instancetype)initWithMSXRegion:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive {
- (instancetype)initWithMSXModel:(CSMachineMSXModel)model region:(CSMachineMSXRegion)region hasDiskDrive:(BOOL)hasDiskDrive {
self = [super init];
if(self) {
using Target = Analyser::Static::MSX::Target;
@ -240,6 +240,10 @@
case CSMachineMSXRegionEuropean: target->region = Target::Region::Europe; break;
case CSMachineMSXRegionJapanese: target->region = Target::Region::Japan; break;
}
switch(model) {
case CSMachineMSXModelMSX1: target->model = Target::Model::MSX1; break;
case CSMachineMSXModelMSX2: target->model = Target::Model::MSX2; break;
}
_targets.push_back(std::move(target));
}
return self;

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -313,13 +313,13 @@ Gw
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QD0-qk-qCa">
<rect key="frame" x="79" y="178" width="80" height="25"/>
<popUpButtonCell key="cell" type="push" title="512 kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" borderStyle="borderAndBezel" tag="512" imageScaling="axesIndependently" inset="2" selectedItem="LVX-CI-lo9" id="dSS-yv-CDV">
<popUpButtonCell key="cell" type="push" title="512 kb" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="512" imageScaling="axesIndependently" inset="2" selectedItem="LVX-CI-lo9" id="dSS-yv-CDV">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="jsD-9U-bwN">
<items>
<menuItem title="512 kb" tag="512" id="LVX-CI-lo9"/>
<menuItem title="1 mb" state="on" tag="1024" id="jMF-5n-E33"/>
<menuItem title="512 kb" state="on" tag="512" id="LVX-CI-lo9"/>
<menuItem title="1 mb" tag="1024" id="jMF-5n-E33"/>
<menuItem title="4 mb" tag="4096" id="Z77-x8-Hzh"/>
</items>
</menu>
@ -575,18 +575,18 @@ Gw
</tabViewItem>
<tabViewItem label="MSX" identifier="msx" id="6SR-DY-zdI">
<view key="view" id="mWD-An-tR7">
<rect key="frame" x="10" y="7" width="400" height="223"/>
<rect key="frame" x="10" y="7" width="400" height="222"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="8xT-Pr-8SE">
<rect key="frame" x="18" y="158" width="128" height="18"/>
<rect key="frame" x="18" y="127" width="128" height="18"/>
<buttonCell key="cell" type="check" title="Attach disk drive" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="CB3-nA-VTM">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
</button>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="LG6-mP-SeG">
<rect key="frame" x="71" y="179" width="146" height="25"/>
<rect key="frame" x="71" y="148" width="146" height="25"/>
<popUpButtonCell key="cell" type="push" title="European (PAL)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="axesIndependently" inset="2" selectedItem="xAh-Ch-tby" id="yR4-yv-Lvu">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -600,22 +600,48 @@ Gw
</popUpButtonCell>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ZaD-7v-rMS">
<rect key="frame" x="18" y="185" width="50" height="16"/>
<rect key="frame" x="18" y="154" width="50" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Region:" id="x4m-eh-Nif">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="gFV-RB-7dB">
<rect key="frame" x="18" y="184" width="46" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="r7C-zE-gyj">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="44m-4z-PVv">
<rect key="frame" x="67" y="178" width="79" height="25"/>
<popUpButtonCell key="cell" type="push" title="MSX 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="axesIndependently" inset="2" selectedItem="G9U-vd-Cjk" id="2TO-f6-Gmp">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="v3T-Jv-VLK">
<items>
<menuItem title="MSX 1" state="on" tag="1" id="G9U-vd-Cjk"/>
<menuItem title="MSX 2" tag="2" id="8fG-eg-UOc"/>
</items>
</menu>
</popUpButtonCell>
</popUpButton>
</subviews>
<constraints>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="LG6-mP-SeG" secondAttribute="trailing" constant="20" symbolic="YES" id="0Oc-n7-gaM"/>
<constraint firstItem="LG6-mP-SeG" firstAttribute="top" secondItem="44m-4z-PVv" secondAttribute="bottom" constant="10" symbolic="YES" id="6vI-gy-nYE"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="44m-4z-PVv" secondAttribute="trailing" constant="20" symbolic="YES" id="D4D-Z0-Vit"/>
<constraint firstItem="44m-4z-PVv" firstAttribute="leading" secondItem="gFV-RB-7dB" secondAttribute="trailing" constant="8" symbolic="YES" id="JG1-Kp-dYk"/>
<constraint firstItem="8xT-Pr-8SE" firstAttribute="top" secondItem="LG6-mP-SeG" secondAttribute="bottom" constant="8" symbolic="YES" id="LBt-4m-GDc"/>
<constraint firstItem="LG6-mP-SeG" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="20" symbolic="YES" id="bcb-ZZ-VpQ"/>
<constraint firstItem="gFV-RB-7dB" firstAttribute="centerY" secondItem="44m-4z-PVv" secondAttribute="centerY" id="Rf8-1i-ttI"/>
<constraint firstItem="gFV-RB-7dB" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="aaL-4g-iW8"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="trailing" constant="20" symbolic="YES" id="l8P-UW-8ig"/>
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="8xT-Pr-8SE" secondAttribute="bottom" constant="20" symbolic="YES" id="mga-YX-Bek"/>
<constraint firstItem="8xT-Pr-8SE" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="q8Q-kh-Opj"/>
<constraint firstItem="LG6-mP-SeG" firstAttribute="leading" secondItem="ZaD-7v-rMS" secondAttribute="trailing" constant="8" symbolic="YES" id="svb-nH-GlP"/>
<constraint firstItem="44m-4z-PVv" firstAttribute="top" secondItem="mWD-An-tR7" secondAttribute="top" constant="20" symbolic="YES" id="zJk-w1-PWX"/>
<constraint firstItem="ZaD-7v-rMS" firstAttribute="leading" secondItem="mWD-An-tR7" secondAttribute="leading" constant="20" symbolic="YES" id="zgh-a5-FNF"/>
<constraint firstItem="ZaD-7v-rMS" firstAttribute="centerY" secondItem="LG6-mP-SeG" secondAttribute="centerY" id="zkw-kD-lRV"/>
</constraints>
@ -966,7 +992,7 @@ Gw
<constraint firstAttribute="bottom" secondItem="hKn-1l-OSN" secondAttribute="bottom" constant="20" symbolic="YES" id="rG2-Ea-klR"/>
</constraints>
</view>
<point key="canvasLocation" x="-320" y="103"/>
<point key="canvasLocation" x="-320" y="102.5"/>
</window>
<customObject id="192-Eb-Rpg" customClass="MachinePicker" customModule="Clock_Signal" customModuleProvider="target">
<connections>
@ -991,6 +1017,7 @@ Gw
<outlet property="machineSelector" destination="VUb-QG-x7c" id="crR-hB-jGd"/>
<outlet property="macintoshModelTypeButton" destination="xa6-NA-JY5" id="2jX-PY-v2z"/>
<outlet property="msxHasDiskDriveButton" destination="8xT-Pr-8SE" id="zGH-GA-9QF"/>
<outlet property="msxModelButton" destination="44m-4z-PVv" id="qRk-Hf-aBL"/>
<outlet property="msxRegionButton" destination="LG6-mP-SeG" id="3a9-VG-6Wf"/>
<outlet property="oricDiskInterfaceButton" destination="fYL-p6-wyn" id="aAt-wM-hRZ"/>
<outlet property="oricModelTypeButton" destination="ENP-hI-BVZ" id="n9i-Ym-miE"/>

View File

@ -54,6 +54,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
@IBOutlet var macintoshModelTypeButton: NSPopUpButton!
// MARK: - MSX properties
@IBOutlet var msxModelButton: NSPopUpButton!
@IBOutlet var msxRegionButton: NSPopUpButton!
@IBOutlet var msxHasDiskDriveButton: NSButton!
@ -134,6 +135,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
macintoshModelTypeButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.macintoshModel"))
// MSX settings
msxModelButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxModel"))
msxRegionButton.selectItem(withTag: standardUserDefaults.integer(forKey: "new.msxRegion"))
msxHasDiskDriveButton.state = standardUserDefaults.bool(forKey: "new.msxDiskDrive") ? .on : .off
@ -198,6 +200,7 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
standardUserDefaults.set(macintoshModelTypeButton.selectedTag(), forKey: "new.macintoshModel")
// MSX settings
standardUserDefaults.set(msxModelButton.selectedTag(), forKey: "new.msxModel")
standardUserDefaults.set(msxRegionButton.selectedTag(), forKey: "new.msxRegion")
standardUserDefaults.set(msxHasDiskDriveButton.state == .on, forKey: "new.msxDiskDrive")
@ -355,15 +358,20 @@ class MachinePicker: NSObject, NSTableViewDataSource, NSTableViewDelegate {
case "msx":
let hasDiskDrive = msxHasDiskDriveButton.state == .on
var region: CSMachineMSXRegion
switch msxRegionButton.selectedTag() {
case 2:
return CSStaticAnalyser(msxRegion: .japanese, hasDiskDrive: hasDiskDrive)
case 1:
return CSStaticAnalyser(msxRegion: .american, hasDiskDrive: hasDiskDrive)
case 0: fallthrough
default:
return CSStaticAnalyser(msxRegion: .european, hasDiskDrive: hasDiskDrive)
case 2: region = .japanese
case 1: region = .american
case 0: fallthrough
default: region = .european
}
var model: CSMachineMSXModel
switch msxModelButton.selectedTag() {
case 2: model = .MSX2
case 1: fallthrough
default: model = .MSX1
}
return CSStaticAnalyser(msxModel: model, region: region, hasDiskDrive: hasDiskDrive)
case "oric":
var diskInterface: CSMachineOricDiskInterface = .none