diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp index 5322ea6f2..31141cc1d 100644 --- a/Components/9918/Implementation/9918.cpp +++ b/Components/9918/Implementation/9918.cpp @@ -15,77 +15,13 @@ using namespace TI::TMS; -// -// TODO notes. -// -// The TMS and Master System VDP run at 342 cycles/line. -// -// The Mega Drive VDP has 3420 master clocks per line, which it divides -// by 4 or 5 depending on pixel rate and which part of a line is active. -// (And, presumably, by 10 if operating in Master System mode?) -// Cf. https://gendev.spritesmind.net/forum/viewtopic.php?t=851 etc. -// -// The MSX2+ Yamaha VDPs have 1368 cycles/line. -// -// So if clock scaling were common, it would need to be to: -// -// 3420 = 2 * 2 * 3 * 3 * 5 * 19 -// 1368 = 2 * 2 * 2 * 3 * 3 * 19 -// -// => 2^3 * 3^2 * 5 * 19 = 6840 -// ... which would imply a multiply by 30 on the input clock if -// it were to remain 3.58Mhz. - -// Mega Drive notes, assorted: -// -// -// -// there are 60 EDCLK at MCLK/5 during a line ... like this: -// -// 15 @ /5; 2 @ /4; 15 @ /5; 2 @ /4; 15 @ /5; 2 @ /4; 15 @ /5 -// -// ... HSYNC signal is what triggers the changes in EDCLK frequency, with some latency though. -// -// The total is still 840 EDCLKS (420 pixels) with 780 @MCLK/4 and 60@MCLK/5 (total is 3420 MCLCKS). - namespace { -constexpr uint8_t StatusInterrupt = 0x80; -constexpr uint8_t StatusSpriteOverflow = 0x40; - -constexpr int StatusSpriteCollisionShift = 5; -constexpr uint8_t StatusSpriteCollision = 0x20; - // 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole // line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. constexpr unsigned int CRTCyclesPerLine = 1365; constexpr unsigned int CRTCyclesDivider = 4; -struct ReverseTable { - const std::array<uint8_t, 256> map; - constexpr ReverseTable() : map(reverse_table()) {} - - private: - static constexpr std::array<uint8_t, 256> reverse_table() { - std::array<uint8_t, 256> map{}; - for(size_t c = 0; c < 256; ++c) { - map[c] = uint8_t( - ((c & 0x80) >> 7) | - ((c & 0x40) >> 5) | - ((c & 0x20) >> 3) | - ((c & 0x10) >> 1) | - ((c & 0x08) << 1) | - ((c & 0x04) << 3) | - ((c & 0x02) << 5) | - ((c & 0x01) << 7) - ); - } - return map; - } -}; - -constexpr ReverseTable reverse_table; - template <Personality personality> constexpr int vram_access_delay() { // This seems to be correct for all currently-modelled VDPs; // it's the delay between an external device scheduling a @@ -720,22 +656,6 @@ uint8_t TMS9918<personality>::get_current_line() const { return uint8_t(source_row); } -template <Personality personality> -uint8_t TMS9918<personality>::get_latched_horizontal_counter() const { - // Translate from internal numbering, which puts pixel output - // in the final 256 pixels of 342, to the public numbering, - // which makes the 256 pixels the first 256 spots, but starts - // counting at -48, and returns only the top 8 bits of the number. - int public_counter = this->latched_column_ - 86; - if(public_counter < -46) public_counter += 342; - return uint8_t(public_counter >> 1); -} - -template <Personality personality> -void TMS9918<personality>::latch_horizontal_counter() { - this->latched_column_ = this->write_pointer_.column; -} - template <Personality personality> uint8_t TMS9918<personality>::read(int address) { this->write_phase_ = false; @@ -835,275 +755,21 @@ bool TMS9918<personality>::get_interrupt_line() const { (this->enable_line_interrupts_ && this->line_interrupt_pending_); } -// MARK: - - +// TODO: [potentially] remove Master System timing assumptions in latch and get_latched below. template <Personality personality> -void Base<personality>::draw_tms_character(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - - // 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 - ]; - } - } else { - const int shift = start & 7; - int byte_column = start >> 3; - - int length = std::min(pixels_left, 8 - shift); - - int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; - uint8_t colour = line_buffer.patterns[byte_column][1]; - uint32_t colours[2] = { - palette[(colour & 15) ? (colour & 15) : background_colour_], - palette[(colour >> 4) ? (colour >> 4) : background_colour_] - }; - - int background_pixels_left = pixels_left; - while(true) { - background_pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!background_pixels_left) break; - length = std::min(8, background_pixels_left); - byte_column++; - - pattern = reverse_table.map[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_]; - } - } - - // 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; - } +uint8_t TMS9918<personality>::get_latched_horizontal_counter() const { + // Translate from internal numbering, which puts pixel output + // in the final 256 pixels of 342, to the public numbering, + // which makes the 256 pixels the first 256 spots, but starts + // counting at -48, and returns only the top 8 bits of the number. + int public_counter = this->latched_column_ - 86; + if(public_counter < -46) public_counter += 342; + return uint8_t(public_counter >> 1); } template <Personality personality> -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_] }; - - const int shift = start % 6; - int byte_column = start / 6; - int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; - int pixels_left = end - start; - int length = std::min(pixels_left, 6 - shift); - while(true) { - pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!pixels_left) break; - length = std::min(6, pixels_left); - byte_column++; - pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; - } -} - -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]; - - /* - 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; - } - } - - 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; - } - } - - 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); - - // 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; - } - - // 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_]; - } - } +void TMS9918<personality>::latch_horizontal_counter() { + this->latched_column_ = this->write_pointer_.column; } template class TI::TMS::TMS9918<Personality::TMS9918A>; diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index 5ccaf25ad..83c00b001 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -122,6 +122,12 @@ struct LineBufferPointer { int row, column; }; +constexpr uint8_t StatusInterrupt = 0x80; +constexpr uint8_t StatusSpriteOverflow = 0x40; + +constexpr int StatusSpriteCollisionShift = 5; +constexpr uint8_t StatusSpriteCollision = 0x20; + template <Personality personality> struct Base { Base(); @@ -162,17 +168,20 @@ template <Personality personality> struct Base { Outputs::CRT::CRT crt_; TVStandard tv_standard_ = TVStandard::NTSC; - // Holds the contents of this VDP's connected DRAM. + // Personality-specific metrics and converters. + ClockConverter<personality> clock_converter_; + + // This VDP's DRAM. std::array<uint8_t, memory_size(personality)> ram_; - // Holds the state of the DRAM/CRAM-access mechanism. + // State of the DRAM/CRAM-access mechanism. uint16_t ram_pointer_ = 0; uint8_t read_ahead_buffer_ = 0; MemoryAccess queued_access_ = MemoryAccess::None; int cycles_until_access_ = 0; int minimum_access_column_ = 0; - // Holds the main status register. + // The main status register. uint8_t status_ = 0; // Current state of programmer input. @@ -189,24 +198,20 @@ template <Personality personality> struct Base { bool generate_interrupts_ = false; int 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. + // Default colours. uint8_t text_colour_ = 0; uint8_t background_colour_ = 0; - ClockConverter<personality> clock_converter_; - // Internal mechanisms for position tracking. int latched_column_ = 0; - // A helper function to output the current border colour for - // the number of cycles supplied. - void output_border(int cycles, uint32_t cram_dot); - // A struct to contain timing information for the current mode. struct { /* @@ -381,38 +386,7 @@ template <Personality personality> struct Base { queued_access_ = MemoryAccess::None; } -/* - Fetching routines follow below; they obey the following rules: - - 1) input is a start position and an end position; they should perform the proper - operations for the period: start <= time < end. - 2) times are measured relative to a 172-cycles-per-line clock (so: they directly - count access windows on the TMS and Master System). - 3) time 0 is the beginning of the access window immediately after the last pattern/data - block fetch that would contribute to this line, in a normal 32-column mode. So: - - * it's cycle 309 on Mattias' TMS diagram; - * it's cycle 1238 on his V9938 diagram; - * it's after the last background render block in Mask of Destiny's Master System timing diagram. - - That division point was selected, albeit arbitrarily, because it puts all the tile - fetches for a single line into the same [0, 171] period. - - 4) all of these functions are templated with a `use_end` parameter. That will be true if - end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks, - for the more usual path of execution. - - Provided for the benefit of the methods below: - - * the function external_slot(), which will perform any pending VRAM read/write. - * the macros slot(n) and external_slot(n) which can be used to schedule those things inside a - switch(start)-based implementation. - - All functions should just spool data to intermediary storage. This is because for most VDPs there is - a decoupling between fetch pattern and output pattern, and it's neater to keep the same division - for the exceptions. -*/ - + // 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); @@ -423,14 +397,22 @@ template <Personality personality> struct Base { template<bool use_end> void fetch_sms(int start, int end); + // A helper function to output the current border colour for + // the number of cycles supplied. + void output_border(int cycles, uint32_t cram_dot); + + // Output serialisation state. uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr; bool asked_for_write_area_ = false; + + // Output serialisers. void draw_tms_character(int start, int end); void draw_tms_text(int start, int end); void draw_sms(int start, int end, uint32_t cram_dot); }; #include "Fetch.hpp" +#include "Draw.hpp" } } diff --git a/Components/9918/Implementation/Draw.hpp b/Components/9918/Implementation/Draw.hpp new file mode 100644 index 000000000..304afeeef --- /dev/null +++ b/Components/9918/Implementation/Draw.hpp @@ -0,0 +1,295 @@ +// +// Draw.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef Draw_hpp +#define Draw_hpp + +#include "../../../Numeric/BitReverse.hpp" + +// MARK: - TMS9918 + +template <Personality personality> +void Base<personality>::draw_tms_character(int start, int end) { + LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; + + // 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 + ]; + } + } else { + const int shift = start & 7; + int byte_column = start >> 3; + + 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]; + uint32_t colours[2] = { + palette[(colour & 15) ? (colour & 15) : background_colour_], + palette[(colour >> 4) ? (colour >> 4) : background_colour_] + }; + + int background_pixels_left = pixels_left; + while(true) { + background_pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[pattern&0x01]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!background_pixels_left) break; + 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_]; + } + } + + // 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; + } +} + +template <Personality personality> +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_] }; + + const int shift = start % 6; + int byte_column = start / 6; + int pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]) >> shift; + int pixels_left = end - start; + int length = std::min(pixels_left, 6 - shift); + while(true) { + pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[pattern&0x01]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!pixels_left) break; + length = std::min(6, pixels_left); + byte_column++; + pattern = Numeric::bit_reverse(line_buffer.patterns[byte_column][0]); + } +} + +// MARK: - Master System + +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]; + + /* + 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; + } + } + + 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; + } + } + + 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); + + // 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; + } + + // 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_]; + } + } +} + +// MARK: - Yamaha + +// TODO. + +// MARK: - Mega Drive + +// TODO. + +#endif /* Draw_hpp */ diff --git a/Components/9918/Implementation/Fetch.hpp b/Components/9918/Implementation/Fetch.hpp index c38d51202..a6c2a2e48 100644 --- a/Components/9918/Implementation/Fetch.hpp +++ b/Components/9918/Implementation/Fetch.hpp @@ -9,6 +9,38 @@ #ifndef Fetch_hpp #define Fetch_hpp +/* + Fetching routines follow below; they obey the following rules: + + 1) input is a start position and an end position; they should perform the proper + operations for the period: start <= time < end. + 2) times are measured relative to a 172-cycles-per-line clock (so: they directly + count access windows on the TMS and Master System). + 3) time 0 is the beginning of the access window immediately after the last pattern/data + block fetch that would contribute to this line, in a normal 32-column mode. So: + + * it's cycle 309 on Mattias' TMS diagram; + * it's cycle 1238 on his V9938 diagram; + * it's after the last background render block in Mask of Destiny's Master System timing diagram. + + That division point was selected, albeit arbitrarily, because it puts all the tile + fetches for a single line into the same [0, 171] period. + + 4) all of these functions are templated with a `use_end` parameter. That will be true if + end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks, + for the more usual path of execution. + + Provided for the benefit of the methods below: + + * the function external_slot(), which will perform any pending VRAM read/write. + * the macros slot(n) and external_slot(n) which can be used to schedule those things inside a + switch(start)-based implementation. + + All functions should just spool data to intermediary storage. This is because for most VDPs there is + a decoupling between fetch pattern and output pattern, and it's neater to keep the same division + for the exceptions. +*/ + #define slot(n) \ if(use_end && end == n) return; \ [[fallthrough]]; \ diff --git a/Numeric/BitReverse.hpp b/Numeric/BitReverse.hpp new file mode 100644 index 000000000..440501f1b --- /dev/null +++ b/Numeric/BitReverse.hpp @@ -0,0 +1,62 @@ +// +// BitReverse.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#ifndef BitReverse_hpp +#define BitReverse_hpp + +#include <cstdint> + +namespace Numeric { + +/// @returns @c source with the order of its bits reversed. E.g. if @c IntT is @c uint8_t then +/// the reverse of bit pattern abcd efgh is hgfd dcba. +template <typename IntT> constexpr IntT bit_reverse(IntT source); + +// The single-byte specialisation uses a lookup table. +template<> constexpr uint8_t bit_reverse<uint8_t>(uint8_t source) { + struct ReverseTable { + static constexpr std::array<uint8_t, 256> reverse_table() { + std::array<uint8_t, 256> map{}; + for(std::size_t c = 0; c < 256; ++c) { + map[c] = uint8_t( + ((c & 0x80) >> 7) | + ((c & 0x40) >> 5) | + ((c & 0x20) >> 3) | + ((c & 0x10) >> 1) | + ((c & 0x08) << 1) | + ((c & 0x04) << 3) | + ((c & 0x02) << 5) | + ((c & 0x01) << 7) + ); + } + return map; + } + }; + + const std::array<uint8_t, 256> map = ReverseTable::reverse_table(); + return map[source]; +} + +// All other versions just call the byte-level reverse the appropriate number of times. +template <typename IntT> constexpr IntT bit_reverse(IntT source) { + IntT result; + + uint8_t *src = reinterpret_cast<uint8_t *>(&source); + uint8_t *dest = reinterpret_cast<uint8_t *>(&result) + sizeof(result) - 1; + for(size_t c = 0; c < sizeof(source); c++) { + *dest = bit_reverse(*src); + ++src; + --dest; + } + + return result; +} + +} + +#endif /* BitReverse_hpp */ diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index fc4ae9b88..1447f9c93 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -1319,6 +1319,8 @@ 4B43983829620FB1006B0BFC /* 9918.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = 9918.cpp; sourceTree = "<group>"; }; 4B43983C29621024006B0BFC /* ClockConverter.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ClockConverter.hpp; sourceTree = "<group>"; }; 4B43983E29628538006B0BFC /* Fetch.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Fetch.hpp; sourceTree = "<group>"; }; + 4B43983F2967459B006B0BFC /* Draw.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Draw.hpp; sourceTree = "<group>"; }; + 4B43984129674943006B0BFC /* BitReverse.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = BitReverse.hpp; sourceTree = "<group>"; }; 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = "<group>"; }; 4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = "<group>"; }; 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = "<group>"; }; @@ -3289,6 +3291,7 @@ 4B7BA03C23D55E7900B98D9E /* Numeric */ = { isa = PBXGroup; children = ( + 4B43984129674943006B0BFC /* BitReverse.hpp */, 4BD155312716362A00410C6E /* BitSpread.hpp */, 4B7BA03E23D55E7900B98D9E /* CRC.hpp */, 4B7BA03F23D55E7900B98D9E /* LFSR.hpp */, @@ -4738,6 +4741,7 @@ 4BD388411FE34E010042B588 /* 9918Base.hpp */, 4B43983C29621024006B0BFC /* ClockConverter.hpp */, 4B43983E29628538006B0BFC /* Fetch.hpp */, + 4B43983F2967459B006B0BFC /* Draw.hpp */, ); path = Implementation; sourceTree = "<group>";