// // // Storage.hpp // Clock Signal // // Created by Thomas Harte on 12/02/2023. // Copyright © 2023 Thomas Harte. All rights reserved. // #pragma once #include "LineBuffer.hpp" #include "YamahaCommands.hpp" #include #include namespace TI::TMS { /// A container for personality-specific storage; see specific instances below. template struct Storage { }; template <> struct Storage { using AddressT = uint16_t; void begin_line(ScreenMode, bool) {} }; struct YamahaFetcher { public: /// 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 = default; }; // State that tracks fetching position within a line. const Event *next_event_ = nullptr; // Sprite collection state. bool sprites_enabled_ = true; protected: /// @return 1 + the number of times within a line that @c GeneratorT produces an event. template 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; } /// @return An array of all events generated by @c GeneratorT in line order. template ()> static constexpr std::array events() { std::array result{}; size_t index = 0; for(int c = 0; c < 1368; c++) { // Specific personality doesn't matter here; both Yamahas use the same internal timing. const auto event = GeneratorT::event(from_internal(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 external_every_eight(int index) { if(index & 7) return std::nullopt; return Event::Type::External; } }; struct RefreshGenerator { static constexpr std::optional 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; } }; template struct BitmapGenerator { static constexpr std::optional 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; } }; struct TextGenerator { static constexpr std::optional 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; } }; struct CharacterGenerator { static constexpr std::optional 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; } }; }; struct YamahaCommandState { CommandContext command_context_; ModeDescription mode_description_; std::unique_ptr 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; } } }; // Yamaha-specific storage. template struct Storage>: public YamahaFetcher, public YamahaCommandState { using AddressT = uint32_t; // The Yamaha's (optional in real hardware) additional 64kb of expansion RAM. // This is a valid target and source for the command engine, but can't be used as a source for current video data. std::array expansion_ram_; // Register indirections. int selected_status_ = 0; int indirect_register_ = 0; bool increment_indirect_register_ = false; // Output horizontal and vertical adjustment, plus the selected vertical offset (i.e. hardware scroll). int adjustment_[2]{}; uint8_t vertical_offset_ = 0; // The palette, plus a shadow copy in which colour 0 is not the current palette colour 0, // but is rather the current global background colour. This simplifies flow when colour 0 // is set as transparent. std::array palette_{}; std::array background_palette_{}; bool solid_background_ = true; // Transient state for palette setting. uint8_t new_colour_ = 0; uint8_t palette_entry_ = 0; bool palette_write_phase_ = false; // Recepticle for all five bits of the current screen mode. uint8_t mode_ = 0; // Used ephemerally during drawing to compound sprites with the 'CC' // (compound colour?) bit set. uint8_t sprite_cache_[8][32]{}; // 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; // Additional things exposed by status registers. uint8_t colour_status_ = 0; uint16_t colour_location_ = 0; uint16_t collision_location_[2]{}; bool line_matches_ = false; Storage() noexcept { // Seed to something valid. next_event_ = refresh_events.data(); } /// 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; } } private: static constexpr auto refresh_events = events(); static constexpr auto no_sprites_events = events>(); static constexpr auto sprites_events = events>(); static constexpr auto text_events = events(); static constexpr auto character_events = events(); }; // Master System-specific storage. template struct Storage> { 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 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) {} }; }