/* SPDX-License-Identifier: Unlicense */ #include "vdp.hpp" #include "musashi-m68k/m68k.h" #include "utils.hpp" #include #include #define REG_VAL_GET(reg, mask, shift) (((reg) & ((mask) << (shift))) >> (shift)) #define REG_VAL_SET(reg, mask, shift, v) \ ((reg) = ((reg) & ~((mask) << (shift))) | ((v & (mask)) << (shift))) /// DISP: Enable (1) / Disable (0) Display #define MODESET2_DISP_MASK (1) #define MODESET2_DISP_SHIFT (6) #define MODESET2_DISP_GET(reg) REG_VAL_GET(reg, MODESET2_DISP_MASK, MODESET2_DISP_SHIFT) #define MODESET2_DISP_SET(reg, v) REG_VAL_SET(reg, MODESET2_DISP_MASK, MODESET2_DISP_SHIFT, v) /// IE0: Enable (1) / Disable (0) V interrupt (IRQ 6) #define MODESET2_IE0_MASK (1) #define MODESET2_IE0_SHIFT (5) #define MODESET2_IE0_GET(reg) REG_VAL_GET(reg, MODESET2_IE0_MASK, MODESET2_IE0_SHIFT) #define MODESET2_IE0_SET(reg, v) REG_VAL_SET(reg, MODESET2_IE0_MASK, MODESET2_IE0_SHIFT, v) /// M1: Enable (1) / Disable (0) DMA #define MODESET2_M1_MASK (1) #define MODESET2_M1_SHIFT (4) #define MODESET2_M1_GET(reg) REG_VAL_GET(reg, MODESET2_M1_MASK, MODESET2_M1_SHIFT) #define MODESET2_M1_SET(reg, v) REG_VAL_SET(reg, MODESET2_M1_MASK, MODESET2_M1_SHIFT, v) /// M2: V 30 (1) / V 28 (0) cell mode (PAL mode, always 0 in NTSC mdoe) #define MODESET2_M2_MASK (1) #define MODESET2_M2_SHIFT (3) #define MODESET2_M2_GET(reg) REG_VAL_GET(reg, MODESET2_M2_MASK, MODESET2_M2_SHIFT) #define MODESET2_M2_SET(reg, v) REG_VAL_SET(reg, MODESET2_M2_MASK, MODESET2_M2_SHIFT, v) #define SCROLLA_ADDR_MASK (0x38) #define SCROLLA_ADDR_SHIFT (10) #define SCROLLA_ADDR_GET(reg) \ ((((uint16_t)(reg)) & (SCROLLA_ADDR_MASK)) << SCROLLA_ADDR_SHIFT) #define SCROLLA_ADDR_SET(reg, v) \ ((reg) = ((reg) & ~SCROLLA_ADDR_MASK) \ | (((uint16_t)(v) >> SCROLLA_ADDR_SHIFT) & SCROLLA_ADDR_MASK)) #define SCROLLB_ADDR_MASK (7) #define SCROLLB_ADDR_SHIFT (13) #define SCROLLB_ADDR_GET(reg) \ ((((uint16_t)(reg)) & (SCROLLB_ADDR_MASK)) << SCROLLB_ADDR_SHIFT) #define SCROLLB_ADDR_SET(reg, v) \ ((reg) = ((reg) & ~SCROLLB_ADDR_MASK) \ | (((uint16_t)(v) >> SCROLLB_ADDR_SHIFT) & SCROLLB_ADDR_MASK)) #define SPRITESTABLE_ADDR_MASK (0x7f) #define SPRITESTABLE_ADDR_SHIFT (9) #define SPRITESTABLE_ADDR_GET(reg) \ ((((uint16_t)(reg)) & (SPRITESTABLE_ADDR_MASK)) << SPRITESTABLE_ADDR_SHIFT) #define SPRITESTABLE_ADDR_SET(reg, v) \ ((reg) = ((reg) & ~SPRITESTABLE_ADDR_MASK) \ | (((uint16_t)(v) >> SPRITESTABLE_ADDR_SHIFT) & SPRITESTABLE_ADDR_MASK)) #define HSCROLL_TABLE_ADDR_MASK (0x3f) #define HSCROLL_TABLE_ADDR_SHIFT (10) #define HSCROLL_TABLE_ADDR_GET(reg) \ ((((uint16_t)(reg)) & (HSCROLL_TABLE_ADDR_MASK)) << HSCROLL_TABLE_ADDR_SHIFT) #define HSCROLL_TABLE_ADDR_SET(reg, v) \ ((reg) = ((reg) & ~HSCROLL_TABLE_ADDR_MASK) \ | (((uint16_t)(v) >> HSCROLL_TABLE_ADDR_SHIFT) & HSCROLL_TABLE_ADDR_MASK)) // 0: V32 CELL, 1: V64 CELL, 2: Prohibited, 3: V128 CELL #define SCROLLSIZE_VCELL_MASK (3) #define SCROLLSIZE_VCELL_SHIFT (4) #define SCROLLSIZE_VCELL_GET(reg) REG_VAL_GET(reg, SCROLLSIZE_VCELL_MASK, SCROLLSIZE_VCELL_SHIFT) #define SCROLLSIZE_VCELL_SET(reg, v) REG_VAL_SET(reg, SCROLLSIZE_VCELL_MASK, SCROLLSIZE_VCELL_SHIFT, v) // 0: H32 CELL, 1: H64 CELL, 2: Prohibited, 3: H128 CELL #define SCROLLSIZE_HCELL_MASK (3) #define SCROLLSIZE_HCELL_SHIFT (0) #define SCROLLSIZE_HCELL_GET(reg) REG_VAL_GET(reg, SCROLLSIZE_HCELL_MASK, SCROLLSIZE_HCELL_SHIFT) #define SCROLLSIZE_HCELL_SET(reg, v) REG_VAL_SET(reg, SCROLLSIZE_HCELL_MASK, SCROLLSIZE_HCELL_SHIFT, v) #define BGCOLOR_COLOR_MASK (0xf) #define BGCOLOR_COLOR_SHIFT (0) #define BGCOLOR_COLOR_GET(reg) REG_VAL_GET(reg, BGCOLOR_COLOR_MASK, BGCOLOR_COLOR_SHIFT) #define BGCOLOR_COLOR_SET(reg, v) REG_VAL_SET(reg, BGCOLOR_COLOR_MASK, BGCOLOR_COLOR_SHIFT, v) #define BGCOLOR_PALETTE_MASK (2) #define BGCOLOR_PALETTE_SHIFT (4) #define BGCOLOR_PALETTE_GET(reg) REG_VAL_GET(reg, BGCOLOR_PALETTE_MASK, BGCOLOR_PALETTE_SHIFT) #define BGCOLOR_PALETTE_SET(reg, v) REG_VAL_SET(reg, BGCOLOR_PALETTE_MASK, BGCOLOR_PALETTE_SHIFT, v) #define BGCOLOR_ALL_MASK (0x3f) #define BGCOLOR_ALL_SHIFT (0) #define BGCOLOR_ALL_GET(reg) REG_VAL_GET(reg, BGCOLOR_ALL_MASK, BGCOLOR_ALL_SHIFT) #define BGCOLOR_ALL_SET(reg, v) REG_VAL_SET(reg, BGCOLOR_ALL_MASK, BGCOLOR_ALL_SHIFT, v) #define DMASRCADRHIGH_DMD_MASK (3) #define DMASRCADRHIGH_DMD_SHIFT (6) #define DMASRCADRHIGH_DMD_GET(reg) REG_VAL_GET(reg, DMASRCADRHIGH_DMD_MASK, DMASRCADRHIGH_DMD_SHIFT) #define DMASRCADRHIGH_DMD_SET(reg, v) REG_VAL_SET(reg, DMASRCADRHIGH_DMD_MASK, DMASRCADRHIGH_DMD_SHIFT, v) /// RS0: Horizontal 40 (1) / Horizontal 32 (0) cell mode (should be same as RS1) #define MODESET4_RS0_MASK (1) #define MODESET4_RS0_SHIFT (7) #define MODESET4_RS0_GET(reg) REG_VAL_GET(reg, MODESET4_RS0_MASK, MODESET4_RS0_SHIFT) #define MODESET4_RS0_SET(reg, v) REG_VAL_SET(reg, MODESET4_RS0_MASK, MODESET4_RS0_SHIFT, v) /// RS1: Horizontal 40 (1) / Horizontal 32 (0) cell mode (should be same as RS0) #define MODESET4_RS1_MASK (1) #define MODESET4_RS1_SHIFT (0) #define MODESET4_RS1_GET(reg) REG_VAL_GET(reg, MODESET4_RS1_MASK, MODESET4_RS1_SHIFT) #define MODESET4_RS1_SET(reg, v) REG_VAL_SET(reg, MODESET4_RS1_MASK, MODESET4_RS1_SHIFT, v) static constexpr size_t BitsCountToMask(const size_t bits) { return bits ? ((1 << (bits - 1)) | BitsCountToMask(bits - 1)) : 0; } static constexpr size_t kSpriteHBorder = 128; static constexpr size_t kSpriteVBorder = 128; static constexpr size_t kSpriteHField = 512; static constexpr size_t kSpriteVField = 1024; static constexpr size_t kTileWidthPixels = 8; static constexpr size_t kTileHeightPixels = 8; static constexpr size_t kPixelsPerByte = 2; static constexpr size_t kBitsPerPixel = 4; // Only 10 bits are effective in scroll speed value static constexpr uint16_t kScrollSpeedMask = BitsCountToMask(10); // Each pixel has 4 bit depth color static constexpr uint16_t kPixelColorMask = BitsCountToMask(kBitsPerPixel); static const char* reg_mode_set_1_bits_desc[16] = { "", "", "", "", "", "", "", "", "Unused(7)", "Unused(6)", "Unused(5)", "HInt(4)", "Unused(3)", "AllBitsFromColor(2)", "HVCounter(1)", "Unused(0)", }; static const char* reg_mode_set_2_bits_desc[16] = { "", "", "", "", "", "", "", "", "Unused(7)", "Display(6)", "VInt(5)", "DMA(4)", "V30Cell(3)", "Unused(2)", "Unused(1)", "Unused(0)", }; static const char* reg_mode_set_3_bits_desc[16] = { "", "", "", "", "", "", "", "", "Unused(7)", "Unused(6)", "Unused(5)", "Unused(4)", "ExtInt(3)", "2CellVScroll(2)", "HScrollHi(1)", "HScrollLo(0)", }; static const char* reg_mode_set_4_bits_desc[16] = { "", "", "", "", "", "", "", "", "H40CellHi(7)", "Unused(6)", "Unused(5)", "Unused(4)", "Shadow(3)", "InterlaceHi(2)", "InterlaceLo(1)", "H40CellLo(0)", }; static const char* reg_status_bits_desc[16] = { "", "", "", "", "", "", "FIFOEmpty(9)", "FIFOFull(8)", "VInt(7)", "SpriteOverflow(6)", "SpriteCollision(5)", "OddFrame(4)", "InVBlank(3)", "InHBlank(2)", "DMABusy(1)", "PAL(0)", }; static void TraceRegBits(FILE* s, const char* bits_desc[16], const uint16_t val) { fprintf(s, " ("); bool separator{}; for (size_t i = 0; i < 16; i++) { if (val & (1 << (15 - i))) { if (separator) { fprintf(s, " | "); separator = false; } fprintf(s, bits_desc[i]); separator = true; } } fprintf(s, ")"); } uint32_t VDP::Read(const uint32_t offset, const enum bitness bitness) { uint32_t ret{}; if (DEBUG_TRACE_VDP_ACCESS) { printf( "VDP r%d%s @0x%08x", bitness * 8, (bitness <= 1 ? " " : ""), base_address + offset); } if (bitness == BITNESS_32) { if (offset == 0) { ret = readData(_address_mode, _address & 0xfffe) << 16; _address += _reg[static_cast(RegID::kAutoIncrement)]; ret |= readData(_address_mode, _address & 0xfffe); _address += _reg[static_cast(RegID::kAutoIncrement)]; } else if (offset == 4) { ret = readStatusRegister() | (readStatusRegister() << 16); } } else if (bitness == BITNESS_16) { if (offset == 0 || offset == 2) { ret = readData(_address_mode, _address & 0xfffe); _address += _reg[static_cast(RegID::kAutoIncrement)]; } else if (offset == 4 || offset == 6) { ret = readStatusRegister(); } } // XXX: Ignore 8-bit reads for now if (DEBUG_TRACE_VDP_ACCESS) { printf("\n"); } return ret; } void VDP::Write(const uint32_t offset, const enum bitness bitness, const uint32_t value) { if (DEBUG_TRACE_VDP_ACCESS) { printf( "VDP w%d%s @0x%08x 0x%0*x", bitness * 8, (bitness <= 1 ? " " : ""), base_address + offset, bitness * 2, value); } if (_dma_ready) { _dma_ready = false; if (DEBUG_TRACE_VDP_ACCESS) { printf(": Running DMA VRAM Fill"); } const uint16_t destination_address = _address; const uint16_t transfer_size = _reg[static_cast(RegID::kDMALengthCounterLow)] | (_reg[static_cast(RegID::kDMALengthCounterHigh)] << 8); const uint8_t increment = _reg[static_cast(RegID::kAutoIncrement)]; runDMAVRAMFill( baseFromAddressMode(_address_mode), destination_address, transfer_size, increment, value); _address += transfer_size * increment / 2; } else if (bitness == BITNESS_32) { if (offset == 0) { writeData(_address_mode, _address & 0xfffe, (value >> 16) & 0xffff); _address += _reg[static_cast(RegID::kAutoIncrement)]; writeData(_address_mode, _address & 0xfffe, value & 0xffff); _address += _reg[static_cast(RegID::kAutoIncrement)]; } else if (offset == 4) { writeControl((value >> 16) & 0xffff); writeControl(value & 0xffff); } } else if (bitness == BITNESS_16) { if (offset == 0 || offset == 2) { writeData(_address_mode, _address & 0xfffe, value & 0xffff); _address += _reg[static_cast(RegID::kAutoIncrement)]; } else if (offset == 4 || offset == 6) { writeControl(value & 0xffff); } } // XXX: Ignore 8-bit writes for now if (DEBUG_TRACE_VDP_ACCESS) { printf("\n"); } } static inline size_t scrollsize_cells_count(uint8_t xcell_value) { assert(xcell_value <= 3); // It actually returns 96 when xcell_value == 2, but it is prohibited and we // just assume that this is not the case. return (xcell_value + 1) * 32; } static constexpr inline uint32_t RenderColorFromCRAM(uint16_t color) { const uint32_t blue = ((color >> 9) & 7) << 5; const uint32_t green = ((color >> 5) & 7) << 5; const uint32_t red = ((color >> 1) & 7) << 5; return 0xff000000 | (red << 16) | (green << 8) | blue; } static constexpr inline uint32_t GetU32BE(const void *buf) { return (static_cast(static_cast(buf)[0]) << 24) | (static_cast(static_cast(buf)[1]) << 16) | (static_cast(static_cast(buf)[2]) << 8) | (static_cast(static_cast(buf)[3]) << 0); } static constexpr inline uint16_t GetU16BE(const void *buf) { return (static_cast(static_cast(buf)[0]) << 8) | (static_cast(static_cast(buf)[1]) << 0); } static constexpr size_t AddWrapping(size_t a, size_t b, const size_t range) { a = a % range; b = b % range; if (a >= b) { if (range - a < b) { return b - (range - a); } } else if (range - b < a) { return a - (range - b); } return a + b; } void VDP::renderScrollLine( const size_t line_index, const uint16_t plane_addr, const uint16_t hscroll_table_addr, const size_t plane_index, const size_t hcell_count, const size_t vcell_count) { // TODO impl cell scroll and line scroll depending on kModeSet3 regester value const size_t render_offset_y = line_index * kRenderWidth; if (render_offset_y >= kRenderWidth * kRenderHeight) { return; } const size_t vscroll = GetU16BE(_vsram + sizeof(uint16_t) * plane_index) & kScrollSpeedMask; const size_t hscroll = GetU16BE(_vram + hscroll_table_addr + sizeof(uint16_t) * plane_index) & kScrollSpeedMask; const size_t cy = AddWrapping(line_index, vscroll, vcell_count * kTileHeightPixels) / kTileHeightPixels; const size_t y = AddWrapping(line_index, vscroll, vcell_count * kTileHeightPixels) % kTileHeightPixels; for (size_t cx = 0; cx < hcell_count; cx++) { const uint16_t sprite_id = GetU16BE( _vram + plane_addr + (cy * hcell_count + cx) * sizeof(uint16_t)); // Fetch whole 8 pixel (32 bit) line of sprite const uint32_t val = GetU32BE( _vram + (sprite_id * kTileHeightPixels + y) * (kTileWidthPixels / kPixelsPerByte)); for (size_t x = 0; x < kTileWidthPixels; x++) { const size_t render_offset_x = (cx * kTileWidthPixels + x + hscroll) % (hcell_count * kTileWidthPixels); if (render_offset_x >= kRenderWidth) { continue; } const size_t color_index = (val >> ((kTileWidthPixels - 1 - x) * kBitsPerPixel)) & kPixelColorMask; const uint16_t bgcolor_reg = _reg[static_cast(RegID::kBackgroundColor)]; const uint16_t bgcolor_color = BGCOLOR_COLOR_GET(bgcolor_reg); const uint16_t bgcolor_palette = BGCOLOR_PALETTE_GET(bgcolor_reg); // TODO compare current palette too (void) bgcolor_palette; if (color_index == bgcolor_color) { // Do not render anything - background color means transparency continue; } const uint16_t cram_color = GetU16BE(_cram + color_index * sizeof(uint16_t)); const uint32_t color = RenderColorFromCRAM(cram_color); _rendered_buffer[render_offset_y + render_offset_x] = color; } } } void VDP::renderScrollBLine( const size_t line_index, const size_t hcell_count, const size_t vcell_count) { const uint16_t plane_addr = SCROLLB_ADDR_GET(_reg[static_cast(RegID::kScrollBAddress)]); const size_t hscroll_table_addr = HSCROLL_TABLE_ADDR_GET(_reg[static_cast(RegID::kHScrollTableAddress)]); renderScrollLine(line_index, plane_addr, hscroll_table_addr, 1, hcell_count, vcell_count); } void VDP::renderScrollALine( const size_t line_index, const size_t hcell_count, const size_t vcell_count) { const uint16_t plane_addr = SCROLLA_ADDR_GET(_reg[static_cast(RegID::kScrollAAddress)]); const size_t hscroll_table_addr = HSCROLL_TABLE_ADDR_GET(_reg[static_cast(RegID::kHScrollTableAddress)]); renderScrollLine(line_index, plane_addr, hscroll_table_addr, 0, hcell_count, vcell_count); } static inline SpriteAttributeEntry FetchSAE( const uint8_t* vram, const uint16_t satable_addr, const uint8_t link) { const uint16_t r0 = GetU16BE(vram + satable_addr + link * 8 + 0); const uint16_t r1 = GetU16BE(vram + satable_addr + link * 8 + 2); const uint16_t r2 = GetU16BE(vram + satable_addr + link * 8 + 4); const uint16_t r3 = GetU16BE(vram + satable_addr + link * 8 + 6); return SpriteAttributeEntry{ /* .vpos = */ static_cast(r0 & BitsCountToMask(10)), /* .hpos = */ static_cast(r3 & BitsCountToMask(9)), /* .tile_id = */ static_cast(r2 & BitsCountToMask(11)), /* .link = */ static_cast(r1 & BitsCountToMask(7)), /* .palette_id = */ static_cast((r2 >> 13) & BitsCountToMask(2)), /* .priority = */ static_cast((r2 >> 15) & BitsCountToMask(1)), /* .vreverse = */ static_cast((r2 >> 12) & BitsCountToMask(1)), /* .hreverse = */ static_cast((r2 >> 11) & BitsCountToMask(1)), /* .vsize = */ static_cast(((r1 >> 8) & BitsCountToMask(2))), /* .hsize = */ static_cast(((r1 >> 10) & BitsCountToMask(2))), }; } void VDP::renderSpriteOnTheLine( const SpriteAttributeEntry sae, const size_t line_index) { const size_t render_offset_y = line_index * kRenderWidth; if (render_offset_y >= kRenderWidth * kRenderHeight) { return; } const size_t top = sae.vpos; const size_t bottom = top + (sae.vsize + 1) * kTileHeightPixels; const size_t left = sae.hpos; const size_t right = sae.hpos + (sae.hsize + 1) * kTileWidthPixels; // Test if sprite is outside of rendering range. { // We can test without thinking about wrapping since there is no sprite // larger than 64 pixel by any dimension, but we have 128 pixels border // on the left, right, bottom and top, which is twice as big as the // biggest possible sprite. if (top > line_index + kSpriteVBorder) { return; } if (bottom <= line_index + kSpriteVBorder) { return; } if (left > kRenderWidth + kSpriteHBorder) { return; } if (right <= kSpriteHBorder) { return; } } const size_t tfirst = (line_index + kSpriteVBorder - top) / kTileHeightPixels; const size_t tstep = sae.vsize + 1u; const size_t y = (line_index + kSpriteVBorder - top) % kTileHeightPixels; for (size_t tx = 0; tx < (sae.hsize + 1u); tx++) { const uint16_t sprite_id = sae.tile_id + tfirst + tx * tstep; // Fetch whole 8 pixel (32 bit) line of cell (tile) const uint32_t val = GetU32BE( _vram + (sprite_id * kTileHeightPixels + y) * (kTileWidthPixels / kPixelsPerByte)); for (size_t x = 0; x < kTileWidthPixels; x++) { const size_t render_offset_x = tx * kTileWidthPixels + x + sae.hpos - kSpriteHBorder; if (render_offset_x >= kRenderWidth) { continue; } const size_t color_index = ((val >> ((kTileWidthPixels - 1 - x) * kBitsPerPixel)) & kPixelColorMask) | (sae.palette_id << BGCOLOR_PALETTE_SHIFT); const uint8_t bgcolor = BGCOLOR_ALL_GET(_reg[static_cast(RegID::kBackgroundColor)]); if (color_index == bgcolor) { // Do not render anything - background color means transparency continue; } const uint16_t cram_color = GetU16BE(_cram + color_index * sizeof(uint16_t)); const uint32_t color = RenderColorFromCRAM(cram_color); _rendered_buffer[render_offset_y + render_offset_x] = color; } } } void VDP::renderSpritesLine(const size_t line_index) { const uint16_t sprites_table_addr = SPRITESTABLE_ADDR_GET( _reg[static_cast(RegID::kSpritesTableAddress)]); const uint8_t modeset4_reg = _reg[static_cast(RegID::kModeSet4)]; const uint8_t rs0 = MODESET4_RS0_GET(modeset4_reg); const uint8_t rs1 = MODESET4_RS1_GET(modeset4_reg); if (rs0 != rs1) { // It is unknown what should happen, so nothing will. return; } const size_t sprites_max_count = rs0 ? 40 : 32; SpriteAttributeEntry sae{}; for (size_t i = 0; i < sprites_max_count; i++) { const uint8_t link = sae.link; sae = FetchSAE(_vram, sprites_table_addr, link); renderSpriteOnTheLine(sae, line_index); if (sae.link == 0) { break; } } if (sae.link != 0 && !_unclosed_sprites_list) { _unclosed_sprites_list = true; // TODO warn of unclosed sprites list } else if (sae.link == 0 && _unclosed_sprites_list) { _unclosed_sprites_list = false; // TODO info about sprites list become closed again } } bool VDP::Scanline() { const uint16_t mode_set_2 = _reg[static_cast(RegID::kModeSet2)]; if (!MODESET2_DISP_GET(mode_set_2)) { return true; } const uint16_t lines_per_screen = _status.pal_mode ? kLinesPerScreenPAL : kLinesPerScreenNTSC; memset( _rendered_buffer + kRenderWidth * _lines_counter, 0, kRenderWidth * sizeof(*_rendered_buffer)); if (_lines_counter >= (int)kRenderHeight) { // just render palette for (size_t i = 0; i < kRenderWidth; i++) { const size_t color_index = (i * 2) % kCRAMSize; const uint16_t cram_color = GetU16BE(_cram +color_index * sizeof(uint16_t)); const uint32_t color = RenderColorFromCRAM(cram_color); _rendered_buffer[kRenderWidth * _lines_counter + i] = color; } } else { // Render sprites const uint8_t scrollsize_reg = _reg[static_cast(RegID::kScrollSize)]; const uint8_t vcell = SCROLLSIZE_VCELL_GET(scrollsize_reg); const uint8_t hcell = SCROLLSIZE_HCELL_GET(scrollsize_reg); // 2 is prohibited value, so we just won't render if it is set if (vcell != 2 && hcell != 2) { const size_t hcell_count = scrollsize_cells_count(hcell); const size_t vcell_count = scrollsize_cells_count(vcell); renderScrollBLine(_lines_counter, hcell_count, vcell_count); renderScrollALine(_lines_counter, hcell_count, vcell_count); renderSpritesLine(_lines_counter); } } _lines_counter++; if (_lines_counter >= lines_per_screen) { _lines_counter = 0; if (MODESET2_IE0_GET(mode_set_2)) { m68k_set_irq(M68K_IRQ_6); _status.vblank = true; } return true; } _status.vblank = false; return false; } void VDP::Reset() { _status = {}; _control_write_second_word = {}; _dma_ready = {}; _lines_counter = {}; _address_mode = {}; _address = {}; memset(_reg, 0, kRegistersCount); memset(_vram, 0, kVRAMSize); memset(_cram, 0, kCRAMSize); memset(_vsram, 0, kVSRAMSize); } static inline bool IsWriteToReg(const uint16_t value) { return ((value >> 13) & 0x7) == 4; } void VDP::writeData(const uint8_t address_mode, const uint16_t address, const uint16_t value) { switch (static_cast(address_mode & 0xf)) { case AddressMode::kVRAMWrite: if (address >= kVRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": VRAM @0x%04x: invalid address, ignoring", address); } return; } if (DEBUG_TRACE_VDP_ACCESS) { printf(": VRAM @0x%04x 0x%04x", address, value); } _vram[address] = (value >> 8) & 0xff;; _vram[address + 1] = value & 0xff;; return; case AddressMode::kCRAMWrite: if (address >= kCRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": CRAM @0x%04x: invalid address, ignoring", address); } return; } if (DEBUG_TRACE_VDP_ACCESS) { printf(": CRAM @0x%04x 0x%04x", address, value); } _cram[address] = (value >> 8) & 0xff;; _cram[address + 1] = value & 0xff;; return; case AddressMode::kVSRAMWrite: if (address >= kVSRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": VSRAM @0x%04x: invalid address, ignoring", address); } return; } if (DEBUG_TRACE_VDP_ACCESS) { printf(": VSRAM @0x%04x 0x%04x", address, value); } _vsram[address] = (value >> 8) & 0xff;; _vsram[address + 1] = value & 0xff;; return; case AddressMode::kVRAMRead: case AddressMode::kCRAMRead: case AddressMode::kVSRAMRead: break; } if (DEBUG_TRACE_VDP_ACCESS) { printf( ": 0x%02x(%s): invalid address_mode", address_mode, addressModeToString(address_mode)); } } void VDP::writeControl(const uint16_t value) { if (_control_write_second_word) { // Write second word // 0 0 0 0 0 0 0 0 // CD5 CD4 CD3 CD2 0 0 A15 A14 _address |= (value & 0x3) << 14; _address_mode |= ((value >> 4) & 0xf) << 2; _control_write_second_word = false; if (DEBUG_TRACE_VDP_ACCESS) { printf( ": New address_mode=0x%02x(%s), address=0x%04x", _address_mode, addressModeToString(_address_mode), _address); } const uint16_t mode_set_2 = _reg[static_cast(RegID::kModeSet2)]; const uint8_t dma_address_mode = (_address_mode >> 4) & 0x3; if (MODESET2_M1_GET(mode_set_2) && dma_address_mode) { const uint16_t destination_address = _address; const uint8_t increment = _reg[static_cast(RegID::kAutoIncrement)]; const uint16_t transfer_size = increment * (_reg[static_cast(RegID::kDMALengthCounterLow)] | (_reg[static_cast(RegID::kDMALengthCounterHigh)] << 8)); const uint8_t dma_action = DMASRCADRHIGH_DMD_GET(_reg[static_cast(RegID::kDMASourceAddressHigh)]); switch (dma_action) { case 0: case 1: { if (DEBUG_TRACE_VDP_ACCESS) { printf(": Running DMA Memory to VRAM"); } const uint32_t hi = _reg[static_cast(RegID::kDMASourceAddressHigh)]; const uint32_t mid = _reg[static_cast(RegID::kDMASourceAddressMid)]; const uint32_t lo = _reg[static_cast(RegID::kDMASourceAddressLow)]; const uint32_t source_address = (((hi & 0x7f) << 16) | (mid << 8) | lo) << 1; runDMAMemoryToVRAM( baseFromAddressMode(_address_mode), source_address, destination_address, transfer_size, increment); _address += transfer_size; } break; case 2: { if (DEBUG_TRACE_VDP_ACCESS) { printf(": Ready to run DMA VRAM Fill"); } _dma_ready = true; } break; case 3: { if (DEBUG_TRACE_VDP_ACCESS) { printf(": Running DMA VRAM Copy"); } runDMAVRAMCopy(baseFromAddressMode(_address_mode)); _address += transfer_size * increment / 2; } break; } } } else if (IsWriteToReg(value)) { // Write register // 1 0 0 RS4 RS3 RS2 RS1 RS0 // D7 D6 D5 D4 D3 D2 D1 D0 const uint8_t reg_num = (value >> 8) & 0x1f; const uint8_t reg_val = value & 0xff; if (DEBUG_TRACE_VDP_ACCESS) { traceRegWrite(stdout, reg_num, reg_val); } _reg[reg_num] = reg_val; // TODO print warnings of all invalid values or writes that ignore a mask // Some things to warn about: // - MODESET4 RS0 does not equal to MODESET4 RS1 // - Sprite attribute table mask ignored // - kScrollAAddress mask ignored // - kScrollBAddress mask ignored // - kScrollSize VCell is set to 2 // - kScrollSize HCell is set to 2 } else if (!_control_write_second_word) { // Write first word // CD1 CD0 A13 A12 A11 A10 A9 A8 // A7 A6 A5 A4 A3 A2 A1 A0 _address = value & 0x3fff; _address_mode = (value >> 14) & 0x3; _control_write_second_word = true; } } uint16_t VDP::readData(const uint8_t address_mode, const uint16_t address) { switch (static_cast(address_mode & 0xf)) { case AddressMode::kVRAMWrite: case AddressMode::kCRAMWrite: case AddressMode::kVSRAMWrite: break; case AddressMode::kVRAMRead: if (address >= kVRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": VRAM @0x%04x: invalid address, ignoring", address); } return 0; } else { const uint16_t value = (_vram[address] << 8) | _vram[address + 1]; if (DEBUG_TRACE_VDP_ACCESS) { printf(": VRAM @0x%04x 0x%04x", address, value); } return value; } case AddressMode::kCRAMRead: if (address >= kCRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": CRAM @0x%04x: invalid address, ignoring", address); } return 0; } else { const uint16_t value = (_cram[address] << 8) | _cram[address + 1]; if (DEBUG_TRACE_VDP_ACCESS) { printf(": CRAM @0x%04x 0x%04x", address, value); } return value; } case AddressMode::kVSRAMRead: if (address >= kVSRAMSize) { if (DEBUG_TRACE_VDP_ACCESS) { printf(": VSRAM @0x%04x: invalid address, ignoring", address); } return 0; } else { const uint16_t value = (_vsram[address] << 8) | _vsram[address + 1]; if (DEBUG_TRACE_VDP_ACCESS) { printf(": VSRAM @0x%04x 0x%04x", address, value); } return value; } } if (DEBUG_TRACE_VDP_ACCESS) { printf( ": 0x%02x(%s): invalid address_mode", address_mode, addressModeToString(address_mode)); } return 0; } uint16_t VDP::readStatusRegister() const { const uint16_t value = 0 | (!_status.fifo_not_empty << 9) | (_status.fifo_full << 8) | (_status.v_irq << 7) | (_status.sprites_overflow << 6) | (_status.sprites_collision << 5) | (_status.odd_frame << 4) | (_status.vblank << 3) | (_status.hblank << 2) | (_status.dma_busy << 1) | (_status.pal_mode << 0) ; if (DEBUG_TRACE_VDP_ACCESS) { fprintf(stdout, ": Status %04x", value); TraceRegBits(stdout, reg_status_bits_desc, value); } return value; } uint8_t* VDP::baseFromAddressMode(uint8_t address_mode) { switch (static_cast(address_mode & 0xf)) { case AddressMode::kVRAMWrite: return _vram; case AddressMode::kCRAMWrite: return _cram; case AddressMode::kVSRAMWrite: return _vsram; case AddressMode::kVRAMRead: return _vram; case AddressMode::kCRAMRead: return _cram; case AddressMode::kVSRAMRead: return _vsram; } UNREACHABLE; return nullptr; } const char* VDP::addressModeToString(const uint8_t address_mode) { // Bits other than CD5-CD0 cannot be set assert((address_mode >> 6) == 0); const uint8_t dma_address_mode = (address_mode >> 4) & 0x3; switch (static_cast(address_mode & 0xf)) { case AddressMode::kVRAMWrite: switch (dma_address_mode) { case 0: return "VRAM write"; case 1: return "DMA ???, VRAM write"; case 2: return "DMA Memory to VRAM/VRAM fill, VRAM write"; case 3: return "DMA VRAM copy, VRAM write"; } break; case AddressMode::kCRAMWrite: switch (dma_address_mode) { case 0: return "CRAM write"; case 1: return "DMA ???, CRAM write"; case 2: return "DMA Memory to VRAM/VRAM fill, CRAM write"; case 3: return "DMA VRAM copy, CRAM write"; } break; case AddressMode::kVSRAMWrite: switch (dma_address_mode) { case 0: return "VSRAM write"; case 1: return "DMA ???, VSRAM write"; case 2: return "DMA Memory to VRAM/VRAM fill, VSRAM write"; case 3: return "DMA VRAM copy, VSRAM write"; } break; case AddressMode::kVRAMRead: switch (dma_address_mode) { case 0: return "VRAM read"; case 1: return "DMA ???, VRAM read"; case 2: return "DMA Memory to VRAM/VRAM fill, VRAM read"; case 3: return "DMA VRAM copy, VRAM read"; } break; case AddressMode::kCRAMRead: switch (dma_address_mode) { case 0: return "CRAM read"; case 1: return "DMA ???, CRAM read"; case 2: return "DMA Memory to VRAM/VRAM fill, CRAM read"; case 3: return "DMA VRAM copy, CRAM read"; } break; case AddressMode::kVSRAMRead: switch (dma_address_mode) { case 0: return "VSRAM read"; case 1: return "DMA ???, VSRAM read"; case 2: return "DMA Memory to VRAM/VRAM fill, VSRAM read"; case 3: return "DMA VRAM copy, VSRAM read"; } break; } UNREACHABLE; return "Unknown"; } const char* VDP::RegIDToString(const RegID reg_id) { switch (reg_id) { case RegID::kModeSet1: return "ModeSet1"; case RegID::kModeSet2: return "ModeSet2"; case RegID::kScrollAAddress: return "ScrollAAddress"; case RegID::kWindowAddress: return "WindowAddress"; case RegID::kScrollBAddress: return "ScrollBAddress"; case RegID::kSpritesTableAddress: return "SpritesTableAddress"; case RegID::kBackgroundColor: return "BackgroundColor"; case RegID::kHint: return "Hint"; case RegID::kModeSet3: return "ModeSet3"; case RegID::kModeSet4: return "ModeSet4"; case RegID::kHScrollTableAddress: return "HScrollTableAddress"; case RegID::kAutoIncrement: return "AutoIncrement"; case RegID::kScrollSize: return "ScrollSize"; case RegID::kWindowHorizontalPosition: return "WindowHorizontalPosition"; case RegID::kWindowVertialPosition: return "WindowVertialPosition"; case RegID::kDMALengthCounterLow: return "DMACounterLow"; case RegID::kDMALengthCounterHigh: return "DMACounterHigh"; case RegID::kDMASourceAddressLow: return "DMASourceAddressLow"; case RegID::kDMASourceAddressMid: return "DMASourceAddressMid"; case RegID::kDMASourceAddressHigh: return "DMASourceAddressHigh"; case RegID::kRegistersCount: break; } return "Unknown"; } void VDP::traceRegWrite(FILE* const s, const uint8_t reg, const uint8_t val) const { const auto reg_id = static_cast(reg); fprintf(s, ": reg 0x%02x(%s) = 0x%02x", reg, VDP::RegIDToString(reg_id), val); switch (reg_id) { case VDP::RegID::kModeSet1: return TraceRegBits(s, reg_mode_set_1_bits_desc, val); case VDP::RegID::kModeSet2: return TraceRegBits(s, reg_mode_set_2_bits_desc, val); case VDP::RegID::kScrollAAddress: fprintf(s, " (0x%04x)", uint16_t(val & 0x38) << 10); break; case VDP::RegID::kWindowAddress: fprintf(s, " (0x%04x)", uint16_t(val & 0x3e) << 10); break; case VDP::RegID::kScrollBAddress: fprintf(s, " (0x%04x)", uint16_t(val & 0x7) << 13); break; case VDP::RegID::kSpritesTableAddress: fprintf(s, " (0x%04x)", uint16_t(val & 0x7f) << 9); break; case VDP::RegID::kBackgroundColor: break; case VDP::RegID::kHint: break; case VDP::RegID::kModeSet3: return TraceRegBits(s, reg_mode_set_3_bits_desc, val); case VDP::RegID::kModeSet4: return TraceRegBits(s, reg_mode_set_4_bits_desc, val); case VDP::RegID::kHScrollTableAddress: fprintf(s, " (0x%04x)", uint16_t(val & 0x3f) << 10); break; case VDP::RegID::kAutoIncrement: break; case VDP::RegID::kScrollSize: break; case VDP::RegID::kWindowHorizontalPosition: break; case VDP::RegID::kWindowVertialPosition: break; case VDP::RegID::kDMALengthCounterLow: break; case VDP::RegID::kDMALengthCounterHigh: break; case VDP::RegID::kDMASourceAddressLow: { const uint32_t hi_raw = _reg[static_cast(RegID::kDMASourceAddressHigh)]; const uint32_t hi = (hi_raw & ((hi_raw & 0x80) ? 0x3f : 0x7f)); const uint32_t mid = _reg[static_cast(RegID::kDMASourceAddressMid)]; const uint32_t lo = val; const uint32_t addr = ((hi << 16) | (mid << 8) | lo) << 1; fprintf(s, " (0x%08x)", addr); } break; case VDP::RegID::kDMASourceAddressMid: { const uint32_t hi_raw = _reg[static_cast(RegID::kDMASourceAddressHigh)]; const uint32_t hi = (hi_raw & ((hi_raw & 0x80) ? 0x3f : 0x7f)); const uint32_t mid = val; const uint32_t lo = _reg[static_cast(RegID::kDMASourceAddressLow)]; const uint32_t addr = ((hi << 16) | (mid << 8) | lo) << 1; fprintf(s, " (0x%08x)", addr); } break; case VDP::RegID::kDMASourceAddressHigh: { const char* op = (val & 0x80) ? ((val & 0x40) ? "VRAM Copy" : "VRAM Fill") : "MemToVRAM"; const uint32_t hi = (val & ((val & 0x80) ? 0x3f : 0x7f)); const uint32_t mid = _reg[static_cast(RegID::kDMASourceAddressMid)]; const uint32_t lo = _reg[static_cast(RegID::kDMASourceAddressLow)]; const uint32_t addr = ((hi << 16) | (mid << 8) | lo) << 1; fprintf(s, " (%s 0x%08x)", op, addr); } break; case VDP::RegID::kRegistersCount: break; } } void VDP::runDMAMemoryToVRAM( uint8_t* const base, const uint32_t source_address, const uint16_t destination_address, const uint16_t transfer_size, const uint8_t increment) { constexpr size_t kDumpWidth = 16; for (size_t i = 0, o = 0; i < transfer_size; i += 2, o += increment) { const uint32_t src_addr = source_address + i; const uint32_t dst_addr = destination_address + o; const uint16_t value = m68k_read_memory_16(src_addr); if (DEBUG_TRACE_VDP_ACCESS) { if (i % kDumpWidth == 0) { printf("\n%06x:%06x: ", src_addr, dst_addr); } printf("%04x ", value); } base[dst_addr + 0] = (value >> 8) & 0xff; base[dst_addr + 1] = value & 0xff; } } void VDP::runDMAVRAMFill( uint8_t* const base, const uint16_t destination_address, const uint16_t transfer_size, const uint8_t increment, const uint16_t filler) { if (DEBUG_TRACE_VDP_ACCESS) { printf( "\nVDP DMA Fill @%06x..%06x = 0x%04x", destination_address, destination_address + transfer_size, filler); } for (size_t i = 0, o = 0; i < transfer_size; i += 2, o += increment) { // TODO: There must some complex byte order shit be happening, depending // on odd/even destination address const uint16_t dst_addr = destination_address + o; const uint16_t value = filler; base[dst_addr + 0] = (value >> 8) & 0xff; base[dst_addr + 1] = value & 0xff; } } void VDP::runDMAVRAMCopy(uint8_t* base) { (void) base; // TODO }