/******************************************************************************/ /* Mednafen Sega Saturn Emulation Module */ /******************************************************************************/ /* vdp1.cpp - VDP1 Emulation ** Copyright (C) 2015-2017 Mednafen Team ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the GNU General Public License ** as published by the Free Software Foundation; either version 2 ** of the License, or (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program; if not, write to the Free Software Foundation, Inc., ** 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // TODO: Check to see what registers are reset on reset. // TODO: Fix preclipping when raw system clipping values have bit12==1(sign bit?). // TODO: SS_SetPhysMemMap(0x05C80000, 0x05CFFFFF, FB[FBDrawWhich], sizeof(FB[0])); // (...but goes weird in 8bpp rotated mode...) // TODO: Test 1x1 line, polyline, sprite, and polygon. // TODO: Framebuffer swap/auto drawing start happens a bit too early, should happen near // end of hblank instead of the beginning. #include "ss.h" #include "scu.h" #include "vdp1.h" #include "vdp2.h" #include "vdp1_common.h" enum { VDP1_UpdateTimingGran = 263 }; enum { VDP1_IdleTimingGran = 1019 }; namespace MDFN_IEN_SS { namespace VDP1 { uint8 spr_w_shift_tab[8]; line_data LineSetup; uint8 gouraud_lut[0x40]; uint16 VRAM[0x40000]; uint16 FB[2][0x20000]; bool FBDrawWhich; static bool FBManualPending; static bool FBVBErasePending; static bool FBVBEraseActive; static sscpu_timestamp_t FBVBEraseLastTS; int32 SysClipX, SysClipY; int32 UserClipX0, UserClipY0, UserClipX1, UserClipY1; int32 LocalX, LocalY; static uint32 CurCommandAddr; static int32 RetCommandAddr; static bool DrawingActive; static uint16 LOPR; static uint16 EWDR; // Erase/Write Data static uint16 EWLR; // Erase/Write Upper Left Coordinate static uint16 EWRR; // Erase/Write Lower Right Coordinate static struct { bool rot8; uint32 fb_x_mask; uint32 y_start; uint32 x_start; uint32 y_end; uint32 x_bound; uint16 fill_data; } EraseParams; static uint32 EraseYCounter; uint8 TVMR; uint8 FBCR; uint8 PTMR; static uint8 EDSR; static bool vb_status, hb_status; static sscpu_timestamp_t lastts; static int32 CycleCounter; static bool vbcdpending; void Init(void) { vbcdpending = false; for(int i = 0; i < 0x40; i++) { gouraud_lut[i] = std::min(31, std::max(0, i - 16)); } for(int i = 0; i < 8; i++) { spr_w_shift_tab[i] = (7 - i) / 3; } // // SS_SetPhysMemMap(0x05C00000, 0x05C7FFFF, VRAM, sizeof(VRAM), true); AddMemoryDomain("VDP1 Ram", VRAM, sizeof(VRAM), MEMORYAREA_FLAGS_WRITABLE | MEMORYAREA_FLAGS_YUGEENDIAN | MEMORYAREA_FLAGS_WORDSIZE2); AddMemoryDomain("VDP1 Framebuffer", FB, sizeof(FB), MEMORYAREA_FLAGS_WRITABLE | MEMORYAREA_FLAGS_YUGEENDIAN | MEMORYAREA_FLAGS_WORDSIZE2); //SS_SetPhysMemMap(0x05C80000, 0x05CFFFFF, FB[FBDrawWhich], sizeof(FB[0]), true); vb_status = false; hb_status = false; lastts = 0; FBVBEraseLastTS = 0; } void Reset(bool powering_up) { if(powering_up) { for(unsigned i = 0; i < 0x40000; i++) { uint16 val; if((i & 0xF) == 0) val = 0x8000; else if(i & 0x1) val = 0x5555; else val = 0xAAAA; VRAM[i] = val; } for(unsigned fb = 0; fb < 2; fb++) for(unsigned i = 0; i < 0x20000; i++) FB[fb][i] = 0xFFFF; memset(&LineSetup, 0, sizeof(LineSetup)); // // // EWDR = 0; EWLR = 0; EWRR = 0; TVMR = 0; FBCR = 0; } UserClipX0 = 0; UserClipY0 = 0; UserClipX1 = 0; UserClipY1 = 0; SysClipX = 0; SysClipY = 0; LocalX = 0; LocalY = 0; FBDrawWhich = 0; //SS_SetPhysMemMap(0x05C80000, 0x05CFFFFF, FB[FBDrawWhich], sizeof(FB[0]), true); FBManualPending = false; FBVBErasePending = false; FBVBEraseActive = false; LOPR = 0; CurCommandAddr = 0; RetCommandAddr = -1; DrawingActive = false; PTMR = 0; EDSR = 0; memset(&EraseParams, 0, sizeof(EraseParams)); EraseYCounter = ~0U; CycleCounter = 0; } static int32 CMD_SetUserClip(const uint16* cmd_data) { UserClipX0 = cmd_data[0x6] & 0x3FF; UserClipY0 = cmd_data[0x7] & 0x1FF; UserClipX1 = cmd_data[0xA] & 0x3FF; UserClipY1 = cmd_data[0xB] & 0x1FF; return 0; } int32 CMD_SetSystemClip(const uint16* cmd_data) { SysClipX = cmd_data[0xA] & 0x3FF; SysClipY = cmd_data[0xB] & 0x1FF; return 0; } int32 CMD_SetLocalCoord(const uint16* cmd_data) { LocalX = sign_x_to_s32(11, cmd_data[0x6] & 0x7FF); LocalY = sign_x_to_s32(11, cmd_data[0x7] & 0x7FF); return 0; } template static uint32 MDFN_FASTCALL TexFetch(uint32 x) { const uint32 base = LineSetup.tex_base; const bool ECD = ECDSPDMode & 0x10; const bool SPD = ECDSPDMode & 0x08; const unsigned ColorMode = ECDSPDMode & 0x07; uint32 rtd; uint32 ret_or = 0; switch(ColorMode) { case 0: // 16 colors, color bank rtd = (VRAM[(base + (x >> 2)) & 0x3FFFF] >> (((x & 0x3) ^ 0x3) << 2)) & 0xF; if(!ECD && rtd == 0xF) { LineSetup.ec_count--; return -1; } ret_or = LineSetup.cb_or; if(!SPD) ret_or |= (int32)(rtd - 1) >> 31; return rtd | ret_or; case 1: // 16 colors, LUT rtd = (VRAM[(base + (x >> 2)) & 0x3FFFF] >> (((x & 0x3) ^ 0x3) << 2)) & 0xF; if(!ECD && rtd == 0xF) { LineSetup.ec_count--; return -1; } if(!SPD) ret_or |= (int32)(rtd - 1) >> 31; return LineSetup.CLUT[rtd] | ret_or; case 2: // 64 colors, color bank rtd = (VRAM[(base + (x >> 1)) & 0x3FFFF] >> (((x & 0x1) ^ 0x1) << 3)) & 0xFF; if(!ECD && rtd == 0xFF) { LineSetup.ec_count--; return -1; } ret_or = LineSetup.cb_or; if(!SPD) ret_or |= (int32)(rtd - 1) >> 31; return (rtd & 0x3F) | ret_or; case 3: // 128 colors, color bank rtd = (VRAM[(base + (x >> 1)) & 0x3FFFF] >> (((x & 0x1) ^ 0x1) << 3)) & 0xFF; if(!ECD && rtd == 0xFF) { LineSetup.ec_count--; return -1; } ret_or = LineSetup.cb_or; if(!SPD) ret_or |= (int32)(rtd - 1) >> 31; return (rtd & 0x7F) | ret_or; case 4: // 256 colors, color bank rtd = (VRAM[(base + (x >> 1)) & 0x3FFFF] >> (((x & 0x1) ^ 0x1) << 3)) & 0xFF; if(!ECD && rtd == 0xFF) { LineSetup.ec_count--; return -1; } ret_or = LineSetup.cb_or; if(!SPD) ret_or |= (int32)(rtd - 1) >> 31; return rtd | ret_or; case 5: // 32K colors, RGB case 6: case 7: if(ColorMode >= 6) rtd = VRAM[0]; else rtd = VRAM[(base + x) & 0x3FFFF]; if(!ECD && (rtd & 0xC000) == 0x4000) { LineSetup.ec_count--; return -1; } if(!SPD) ret_or |= (int32)(rtd - 0x4000) >> 31; return rtd | ret_or; } } extern uint32 (MDFN_FASTCALL *const TexFetchTab[0x20])(uint32 x) = { #define TF(a) (TexFetch) TF(0x00), TF(0x01), TF(0x02), TF(0x03), TF(0x04), TF(0x05), TF(0x06), TF(0x07), TF(0x08), TF(0x09), TF(0x0A), TF(0x0B), TF(0x0C), TF(0x0D), TF(0x0E), TF(0x0F), TF(0x10), TF(0x11), TF(0x12), TF(0x13), TF(0x14), TF(0x15), TF(0x16), TF(0x17), TF(0x18), TF(0x19), TF(0x1A), TF(0x1B), TF(0x1C), TF(0x1D), TF(0x1E), TF(0x1F), #undef TF }; /* Notes: When vblank starts: Abort command processing, and if VBE=1, erase framebuffer just displayed according to set values. When vblank ends: Abort framebuffer erase, swap framebuffer, and if (PTMR&2) start command processing. See if EDSR and LOPR are modified or not when PTMR=0 and an auto framebuffer swap occurs. FB erase params are latched at framebuffer swap time probably. VBE=1 is persistent. */ sscpu_timestamp_t Update(sscpu_timestamp_t timestamp) { if(MDFN_UNLIKELY(timestamp < lastts)) { // Don't else { } normal execution, since this bug condition miiight occur in the call from SetHBVB(), // and we need drawing to start ASAP before silly games overwrite the beginning of the command table. // SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] [BUG] timestamp(%d) < lastts(%d)", timestamp, lastts); timestamp = lastts; } // // // int32 cycles = timestamp - lastts; lastts = timestamp; CycleCounter += cycles; if(CycleCounter > VDP1_UpdateTimingGran) CycleCounter = VDP1_UpdateTimingGran; if(CycleCounter > 0 && SCU_CheckVDP1HaltKludge()) { //puts("Kludge"); CycleCounter = 0; } else if(DrawingActive) { while(CycleCounter > 0) { uint16 cmd_data[0x10]; // Fetch command data memcpy(cmd_data, &VRAM[CurCommandAddr], sizeof(cmd_data)); CycleCounter -= 16; //SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Command @ 0x%06x: 0x%04x\n", CurCommandAddr, cmd_data[0]); if(MDFN_LIKELY(!(cmd_data[0] & 0xC000))) { const unsigned cc = cmd_data[0] & 0xF; if(MDFN_UNLIKELY(cc >= 0xC)) { DrawingActive = false; break; } else { static int32 (*const command_table[0xC])(const uint16* cmd_data) = { /* 0x0 */ /* 0x1 */ /* 0x2 */ /* 0x3 */ CMD_NormalSprite, CMD_ScaledSprite, CMD_DistortedSprite, CMD_DistortedSprite, /* 0x4 */ /* 0x5 */ /* 0x6 */ /* 0x7 */ CMD_Polygon, CMD_Polyline, CMD_Line, CMD_Polyline, /* 0x8*/ /* 0x9 */ /* 0xA */ /* 0xB */ CMD_SetUserClip, CMD_SetSystemClip, CMD_SetLocalCoord, CMD_SetUserClip }; CycleCounter -= command_table[cc](cmd_data); } } else if(MDFN_UNLIKELY(cmd_data[0] & 0x8000)) { SS_DBGTI(SS_DBG_VDP1, "[VDP1] Drawing finished at 0x%05x", CurCommandAddr); DrawingActive = false; EDSR |= 0x2; // TODO: Does EDSR reflect IRQ out status? SCU_SetInt(SCU_INT_VDP1, true); SCU_SetInt(SCU_INT_VDP1, false); break; } CurCommandAddr = (CurCommandAddr + 0x10) & 0x3FFFF; switch((cmd_data[0] >> 12) & 0x3) { case 0: break; case 1: CurCommandAddr = (cmd_data[1] << 2) &~ 0xF; break; case 2: if(RetCommandAddr < 0) RetCommandAddr = CurCommandAddr; CurCommandAddr = (cmd_data[1] << 2) &~ 0xF; break; case 3: if(RetCommandAddr >= 0) { CurCommandAddr = RetCommandAddr; RetCommandAddr = -1; } break; } } } return timestamp + (DrawingActive ? std::max(VDP1_UpdateTimingGran, 0 - CycleCounter) : VDP1_IdleTimingGran); } // Draw-clear minimum x amount is 2(16-bit units) for normal and 8bpp, and 8 for rotate...actually, seems like // rotate being enabled forces vblank erase mode somehow. static void StartDrawing(void) { if(DrawingActive) { SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Drawing interrupted by new drawing start request."); } SS_DBGTI(SS_DBG_VDP1, "[VDP1] Started drawing to framebuffer %d.", FBDrawWhich); // On draw start, clear CEF. EDSR &= ~0x2; CurCommandAddr = 0; RetCommandAddr = -1; DrawingActive = true; CycleCounter = VDP1_UpdateTimingGran; } void SetHBVB(const sscpu_timestamp_t event_timestamp, const bool new_hb_status, const bool new_vb_status) { const bool old_hb_status = hb_status; const bool old_vb_status = vb_status; hb_status = new_hb_status; vb_status = new_vb_status; if(MDFN_UNLIKELY(vbcdpending & hb_status & (old_hb_status ^ hb_status))) { vbcdpending = false; if(vb_status) // Going into v-blank { // // v-blank erase // if((TVMR & TVMR_VBE) || FBVBErasePending) { SS_DBGTI(SS_DBG_VDP1, "[VDP1] VB erase start of framebuffer %d.", !FBDrawWhich); FBVBErasePending = false; FBVBEraseActive = true; FBVBEraseLastTS = event_timestamp; } } else // Leaving v-blank { // Run vblank erase at end of vblank all at once(not strictly accurate, but should only have visible side effects wrt the debugger and reset). if(FBVBEraseActive) { int32 count = event_timestamp - FBVBEraseLastTS; //printf("%d %d, %d\n", event_timestamp, FBVBEraseLastTS, count); // // // uint32 y = EraseParams.y_start; do { uint16* fbyptr; uint32 x = EraseParams.x_start; fbyptr = &FB[!FBDrawWhich][(y & 0xFF) << 9]; if(EraseParams.rot8) fbyptr += (y & 0x100); count -= 8; do { for(unsigned sub = 0; sub < 8; sub++) { //printf("%d %d:%d %04x\n", FBDrawWhich, x, y, fill_data); //printf("%lld\n", &fbyptr[x & fb_x_mask] - FB[!FBDrawWhich]); fbyptr[x & EraseParams.fb_x_mask] = EraseParams.fill_data; x++; } count -= 8; if(MDFN_UNLIKELY(count <= 0)) { SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] VB erase of framebuffer %d ran out of time.", !FBDrawWhich); goto AbortVBErase; } } while(x < EraseParams.x_bound); } while(++y <= EraseParams.y_end); AbortVBErase:; // FBVBEraseActive = false; } // // // // if(!(FBCR & FBCR_FCM) || (FBManualPending && (FBCR & FBCR_FCT))) // Swap framebuffers { if(DrawingActive) { SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Drawing aborted by framebuffer swap."); DrawingActive = false; } FBDrawWhich = !FBDrawWhich; SS_DBGTI(SS_DBG_VDP1, "[VDP1] Displayed framebuffer changed to %d.", !FBDrawWhich); // On fb swap, copy CEF to BEF, clear CEF, and copy COPR to LOPR. EDSR = EDSR >> 1; LOPR = CurCommandAddr >> 2; // EraseParams.rot8 = (TVMR & (TVMR_8BPP | TVMR_ROTATE)) == (TVMR_8BPP | TVMR_ROTATE); EraseParams.fb_x_mask = EraseParams.rot8 ? 0xFF : 0x1FF; EraseParams.y_start = EWLR & 0x1FF; EraseParams.x_start = ((EWLR >> 9) & 0x3F) << 3; EraseParams.y_end = EWRR & 0x1FF; EraseParams.x_bound = ((EWRR >> 9) & 0x7F) << 3; EraseParams.fill_data = EWDR; // if(PTMR & 0x2) // Start drawing(but only if we swapped the frame) { StartDrawing(); SS_SetEventNT(&events[SS_EVENT_VDP1], Update(event_timestamp)); } } if(!(FBCR & FBCR_FCM) || (FBManualPending && !(FBCR & FBCR_FCT))) { if(TVMR & TVMR_ROTATE) { EraseYCounter = ~0U; FBVBErasePending = true; } else { EraseYCounter = EraseParams.y_start; } } FBManualPending = false; } } vbcdpending |= old_vb_status ^ vb_status; } bool GetLine(const int line, uint16* buf, unsigned w, uint32 rot_x, uint32 rot_y, uint32 rot_xinc, uint32 rot_yinc) { bool ret = false; // // // if(TVMR & TVMR_ROTATE) { const uint16* fbptr = FB[!FBDrawWhich]; if(TVMR & TVMR_8BPP) { for(unsigned i = 0; MDFN_LIKELY(i < w); i++) { const uint32 fb_x = rot_x >> 9; const uint32 fb_y = rot_y >> 9; if((fb_x | fb_y) &~ 0x1FF) buf[i] = 0; // Not 0xFF00 else { const uint16* fbyptr = fbptr + ((fb_y & 0xFF) << 9); uint8 tmp = ne16_rbo_be(fbyptr, (fb_x & 0x1FF) | ((fb_y & 0x100) << 1)); buf[i] = 0xFF00 | tmp; } rot_x += rot_xinc; rot_y += rot_yinc; } } else { for(unsigned i = 0; MDFN_LIKELY(i < w); i++) { const uint32 fb_x = rot_x >> 9; const uint32 fb_y = rot_y >> 9; if((fb_x &~ 0x1FF) | (fb_y &~ 0xFF)) buf[i] = 0; else buf[i] = fbptr[(fb_y << 9) + fb_x]; rot_x += rot_xinc; rot_y += rot_yinc; } } } else { const uint16* fbyptr = &FB[!FBDrawWhich][(line & 0xFF) << 9]; if(TVMR & TVMR_8BPP) ret = true; for(unsigned i = 0; MDFN_LIKELY(i < w); i++) buf[i] = fbyptr[i]; } // // // if(EraseYCounter <= EraseParams.y_end) { uint16* fbyptr; uint32 x = EraseParams.x_start; fbyptr = &FB[!FBDrawWhich][(EraseYCounter & 0xFF) << 9]; if(EraseParams.rot8) fbyptr += (EraseYCounter & 0x100); do { for(unsigned sub = 0; sub < 2; sub++) { //printf("%d %d:%d %04x\n", FBDrawWhich, x, y, fill_data); //printf("%lld\n", &fbyptr[x & fb_x_mask] - FB[!FBDrawWhich]); fbyptr[x & EraseParams.fb_x_mask] = EraseParams.fill_data; x++; } } while(x < EraseParams.x_bound); EraseYCounter++; } return ret; } void AdjustTS(const int32 delta) { lastts += delta; if(FBVBEraseActive) FBVBEraseLastTS += delta; } static INLINE void WriteReg(const unsigned which, const uint16 value) { SS_SetEventNT(&events[SS_EVENT_VDP2], VDP2::Update(SH7095_mem_timestamp)); sscpu_timestamp_t nt = Update(SH7095_mem_timestamp); SS_DBGTI(SS_DBG_VDP1_REGW, "[VDP1] Register write: 0x%02x: 0x%04x", which << 1, value); switch(which) { default: SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Unknown write of value 0x%04x to register 0x%02x", value, which << 1); break; case 0x0: // TVMR TVMR = value & 0xF; break; case 0x1: // FBCR FBCR = value & 0x1F; FBManualPending |= value & 0x2; break; case 0x2: // PTMR PTMR = (value & 0x3); if(value & 0x1) { StartDrawing(); nt = SH7095_mem_timestamp + 1; } break; case 0x3: // EWDR EWDR = value; break; case 0x4: // EWLR EWLR = value & 0x7FFF; break; case 0x5: // EWRR EWRR = value; break; case 0x6: // ENDR if(DrawingActive) { DrawingActive = false; if(CycleCounter < 0) CycleCounter = 0; nt = SH7095_mem_timestamp + VDP1_IdleTimingGran; SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Program forced termination of VDP1 drawing."); } break; } SS_SetEventNT(&events[SS_EVENT_VDP1], nt); } static INLINE uint16 ReadReg(const unsigned which) { switch(which) { default: SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] Unknown read from register 0x%02x", which); return 0; case 0x8: // EDSR return EDSR; case 0x9: // LOPR return LOPR; case 0xA: // COPR return CurCommandAddr >> 2; case 0xB: // MODR return (0x1 << 12) | ((PTMR & 0x2) << 7) | ((FBCR & 0x1E) << 3) | (TVMR << 0); } } void Write8_DB(uint32 A, uint16 DB) { A &= 0x1FFFFF; if(A < 0x80000) { ne16_wbo_be(VRAM, A, DB >> (((A & 1) ^ 1) << 3) ); return; } if(A < 0x100000) { uint32 FBA = A; if((TVMR & (TVMR_8BPP | TVMR_ROTATE)) == (TVMR_8BPP | TVMR_ROTATE)) FBA = (FBA & 0x1FF) | ((FBA << 1) & 0x3FC00) | ((FBA >> 8) & 0x200); ne16_wbo_be(FB[FBDrawWhich], FBA & 0x3FFFF, DB >> (((A & 1) ^ 1) << 3) ); return; } SS_DBGTI(SS_DBG_WARNING | SS_DBG_VDP1, "[VDP1] 8-bit write to 0x%08x(DB=0x%04x)", A, DB); WriteReg((A - 0x100000) >> 1, DB); } void Write16_DB(uint32 A, uint16 DB) { A &= 0x1FFFFE; if(A < 0x80000) { VRAM[A >> 1] = DB; return; } if(A < 0x100000) { uint32 FBA = A; if((TVMR & (TVMR_8BPP | TVMR_ROTATE)) == (TVMR_8BPP | TVMR_ROTATE)) FBA = (FBA & 0x1FF) | ((FBA << 1) & 0x3FC00) | ((FBA >> 8) & 0x200); FB[FBDrawWhich][(FBA >> 1) & 0x1FFFF] = DB; return; } WriteReg((A - 0x100000) >> 1, DB); } uint16 Read16_DB(uint32 A) { A &= 0x1FFFFE; if(A < 0x080000) return VRAM[A >> 1]; if(A < 0x100000) { uint32 FBA = A; if((TVMR & (TVMR_8BPP | TVMR_ROTATE)) == (TVMR_8BPP | TVMR_ROTATE)) FBA = (FBA & 0x1FF) | ((FBA << 1) & 0x3FC00) | ((FBA >> 8) & 0x200); return FB[FBDrawWhich][(FBA >> 1) & 0x1FFFF]; } return ReadReg((A - 0x100000) >> 1); } uint8 PeekVRAM(const uint32 addr) { return ne16_rbo_be(VRAM, addr & 0x7FFFF); } void PokeVRAM(const uint32 addr, const uint8 val) { ne16_wbo_be(VRAM, addr & 0x7FFFF, val); } /*void MakeDump(const std::string& path) { FileStream fp(path, FileStream::MODE_WRITE); for(unsigned i = 0; i < 0x40000; i++) fp.print_format("0x%04x, ", VRAM[i]); fp.close(); }*/ } }