mirror of
https://github.com/TomHarte/CLK.git
synced 2025-01-28 13:30:55 +00:00
commit
19d03dd4fd
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -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
@ -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 */
|
||||
|
112
Components/9918/Implementation/AccessEnums.hpp
Normal file
112
Components/9918/Implementation/AccessEnums.hpp
Normal 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 */
|
@ -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 */
|
||||
|
@ -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
132
Components/9918/Implementation/LineBuffer.hpp
Normal file
132
Components/9918/Implementation/LineBuffer.hpp
Normal 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 */
|
@ -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 */
|
||||
|
489
Components/9918/Implementation/Storage.hpp
Normal file
489
Components/9918/Implementation/Storage.hpp
Normal 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 */
|
382
Components/9918/Implementation/YamahaCommands.hpp
Normal file
382
Components/9918/Implementation/YamahaCommands.hpp
Normal 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 */
|
@ -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;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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() {}
|
||||
|
||||
|
@ -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>";
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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"/>
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user