More video, DMA and interrupt work

This commit is contained in:
Asnivor 2024-10-31 19:16:30 +00:00
parent 875a7d808c
commit e8a2093a74
8 changed files with 330 additions and 145 deletions

View File

@ -72,7 +72,7 @@ namespace BizHawk.Client.Common
["INTV"] = 59.92,
["ZXSpectrum_PAL"] = 50.080128205,
["AmstradCPC_PAL"] = 50.08012820512821, // = 1 / ((1024 * 312) / 16,000,000)
["AmstradCPC_PAL"] = 50.08012820512821, // = 1 / ((1024 * 312) / 16,000,000)
["UZE"] = 1125000.0 / 18733.0, // = 8 * 315000000 / 88 / 1820 / 262 ≈ 60.05444936742646666
["VEC"] = 50,
@ -82,8 +82,10 @@ namespace BizHawk.Client.Common
["TIC80"] = 60,
["ChannelF"] = 234375.0 / 3872.0, // (NTSCCarrier * 8 / 7) / (256 * 264)
// note: ChannelF II PAL timings might be slightly different...
// note: ChannelF II PAL timings might be slightly different...
["ChannelF_PAL"] = 15625.0 / 312.0, // 4000000 / (256 * 312)
["SuperVision"] = 50.08012820512821
};
public static double GetFrameRate(string systemId, bool pal)

View File

@ -34,7 +34,8 @@ namespace BizHawk.Emulation.Common
new(VSystemID.Raw.O2, "Odyssey2"),
new(VSystemID.Raw.VEC, "Vectrex"),
new(VSystemID.Raw.MSX, "MSX"),
new(VSystemID.Raw.NDS, "Nintendo DS")
new(VSystemID.Raw.NDS, "Nintendo DS"),
new(VSystemID.Raw.SuperVision, "Watara SuperVision")
};
public SystemInfo this[string systemId]

View File

@ -1,63 +0,0 @@
namespace BizHawk.Emulation.Cores.Consoles.SuperVision
{
public partial class ASIC
{
/// <summary>
/// The inbuilt LCD screen
/// </summary>
private LCD _screen;
/// <summary>
/// The current field being drawn (0 or 1)
/// </summary>
private int _field;
/// <summary>
/// VRAM byte read every 6 CPU cycles
/// </summary>
private byte _latchedVRAM;
/// <summary>
/// The current VRAM pointer - latched every 6 cpu cycles
/// </summary>
private int _currentVRAMPointer;
private int _currY;
private int _currX;
private void SetupScreen(SuperVision.SuperVisionSyncSettings superVisionSyncSettings)
{
_screen = new LCD(superVisionSyncSettings.ScreenType);
}
private void VideoClock()
{
if (FrameStart)
{
// initial V start value (limit to 8k size)
_currentVRAMPointer = (_regs[R_Y_SCROLL] * 0x30) * 0x1FFF;
}
else
{
_currentVRAMPointer++;
if (_currentVRAMPointer == 0x1FE0)
{
// wrap around
_currentVRAMPointer = 0;
}
}
if (_sv.FrameClock % 6 == 0)
{
// address lines are updated with a new VRAM address
_latchedVRAM = _sv.VRAM[_currentVRAMPointer];
}
}
}
}

View File

@ -41,37 +41,141 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
public const int R_SYSTEM_CONTROL = 0x26;
public const int R_IRQ_STATUS = 0x27;
private SuperVision _sv;
/// <summary>
/// Scanline length in cpu clocks
/// </summary>
public int CLOCK_WIDTH =>
((_regs[R_LCD_X_SIZE] & 0xFC) // topmost 6 bits of the X Size register
+ 4) // line latch pulse
* 6; // 6 clocks per pixel
/// <summary>
/// Number of scanlines in a field
/// </summary>
public int LINE_HEIGHT => _regs[R_LCD_Y_SIZE];
private SuperVision _sv;
private byte[] _regs = new byte[0x30];
/// <summary>
/// The inbuilt LCD screen
/// </summary>
public LCD Screen;
public ASIC(SuperVision sv, SuperVision.SuperVisionSyncSettings ss)
{
_sv = sv;
_screen = new LCD(ss.ScreenType);
Screen = new LCD(ss.ScreenType);
}
public bool FrameStart;
private int _intTimer;
private bool _intTimerEnabled;
private bool _intTimerChanged;
private int _nmiTimer;
private bool _intFlag;
private bool _dmaInProgress;
private int _dmaCounter;
private int _seqCounter;
private int _byteCounter;
private int _lineCounter;
private int _field;
private ushort _vramByteBuffer;
/// <summary>
/// ASIC is clocked at the same rate as the CPU
/// </summary>
public void Clock()
{
CheckInterrupt();
// According to the information presented in https://github.com/GrenderG/supervision_reveng_notes/blob/master/Supervision_Tech.txt
// it can be surmised that the ASIC rigidly sticks to a 6-phase sequencer, which is as follows:
// 0: CPU RDY line true / PixelCLK to LCD / Output 1/2 byte to LCD
// 1: DMA byte transfer to VRAM (if DMA is active) / CPU RDY line false (if DMA is active)
// 2: DMA byte transfer to VRAM (if DMA is active) / CPU RDY line false (if DMA is active)
// 3: DMA byte transfer to VRAM (if DMA is active) / CPU RDY line false (if DMA is active)
// 4: DMA byte transfer to VRAM (if DMA is active) / CPU RDY line false (if DMA is active)
// 5: DMA byte transfer to VRAM (if DMA is active) / CPU RDY line false (if DMA is active)
CheckDMA();
VideoClock();
// so DMA can transfer 5 bytes to VRAM every 6 clocks, the 6th clock being the 1/2 byte transfer to the LCD
switch (_seqCounter)
{
case 0:
// there is no DMA on this cycle so CPU can run freely
_sv._cpu.RDY = true;
// ASIC reads a byte from VRAM
byte data = 0xff; //todo
// shift the last read byte in the buffer and add the new byte to the start
_vramByteBuffer = (ushort) ((_vramByteBuffer << 8) | data);
// get the correct byte data based on the X Scroll register lower 2 bits
// this simulates a delay in the bits sent to the LCD
byte b = (byte) ((_vramByteBuffer >> (_regs[R_X_SCROLL] & 0b0000_0011)) & 0xff);
// depending on the field, a 4 bit sequence is sent to the LCD
// Field0: bits 0-2-4-6
// Field1: bits 1-3-5-7
byte lData = _field == 0
? (byte) ((b & 0b0000_0001) | ((b & 0b0000_0100) >> 1) | ((b & 0b0001_0000) >> 2) | ((b & 0b0100_0000) >> 3))
: (byte) ((b & 0b0000_0010) >> 1 | ((b & 0b0000_1000) >> 2) | ((b & 0b0010_0000) >> 3) | ((b & 0b1000_0000) >> 4));
bool lineEnd = _byteCounter == CLOCK_WIDTH - 1;
bool frameEnd = _lineCounter == LINE_HEIGHT && lineEnd && _field == 1;
// send 1/2 byte to the LCD
Screen.PixelClock(lData, _field, lineEnd, frameEnd);
_byteCounter++;
if (_byteCounter == CLOCK_WIDTH)
{
// end of scanline
_byteCounter = 0;
_lineCounter++;
if (_lineCounter == LINE_HEIGHT)
{
// end of field
_lineCounter = 0;
_field++;
if (_field == 2)
_field = 0; // wraparound
}
}
break;
default:
if (_dmaInProgress)
{
// perform DMA transfer
DoDMA();
_sv._cpu.RDY = !_dmaInProgress;
}
break;
}
_seqCounter++;
if (_seqCounter == 7)
_seqCounter = 0; // wraparound
CheckInterrupt();
AudioClock();
if (FrameStart)
FrameStart = false;
_sv.FrameClock++;
}
/// <summary>
/// The current prescaler value for the IRQ timer
/// </summary>
private int IntPrescaler => _regs[R_SYSTEM_CONTROL].Bit(4) ? 16384 : 256;
/// <summary>
@ -88,16 +192,63 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
_sv._cpu.NMI = true;
}
_intTimer--;
if (_intTimer <= 0)
if (_intTimerChanged)
{
// IRQ timer register has just been modified
_intTimerChanged = false;
_intTimerEnabled = true;
// prescaler reset
_intTimer = 0;
if (_regs[R_IRQ_TIMER] == 0)
{
if (_regs[R_SYSTEM_CONTROL].Bit(1))
{
// instant IRQ
_intFlag = true;
_intTimerEnabled = false;
// set IRQ Timer expired bit
_regs[R_IRQ_STATUS] = (byte) (_regs[R_IRQ_STATUS] | 2);
}
}
}
else if (_regs[R_IRQ_TIMER] > 0)
{
// timer will be counting down clocked by the prescaler
if (_intTimer++ == IntPrescaler)
{
// prescaler clock
_intTimer = 0;
// decrement timer
_regs[R_IRQ_TIMER]--;
}
}
else
{
// timer has expired
if (_intTimerEnabled)
{
_intFlag = true;
_intTimerEnabled = false;
// set IRQ Timer expired bit
_regs[R_IRQ_STATUS] = (byte) (_regs[R_IRQ_STATUS] | 2);
}
}
if (_intFlag && _regs[R_SYSTEM_CONTROL].Bit(1))
{
// IRQ enabled
_sv._cpu.IRQ = true;
_intFlag = false;
}
}
/// <summary>
/// DMA Control
/// Check whether DMA needs to start
/// </summary>
private void CheckDMA()
{
@ -105,22 +256,46 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
{
// DMA start requested
_dmaInProgress = true;
}
// Unset the DMA start bit
_regs[R_DMA_CONTROL] = (byte) (_regs[R_DMA_CONTROL] & ~(1 << 7));
}
}
/// <summary>
/// Perform a DMA transfer
/// </summary>
private void DoDMA()
{
if (_dmaInProgress)
{
ushort source = (ushort) (_regs[R_DMA_SOURCE_HIGH] << 8 | _regs[R_DMA_SOURCE_LOW]);
ushort dest = (ushort) (_regs[R_DMA_DEST_HIGH] << 8 | _regs[R_DMA_DEST_LOW]);
_dmaCounter++;
if (_dmaCounter == 4096)
if (_dmaCounter == 4096 || _dmaCounter == _regs[R_DMA_LENGTH] * 16)
{
// wrap around
// wraparound or length reached
_dmaCounter = 0;
_dmaInProgress = false;
}
else
{
ushort source = (ushort) (_regs[R_DMA_SOURCE_HIGH] << 8 | _regs[R_DMA_SOURCE_LOW]);
ushort dest = (ushort) (_regs[R_DMA_DEST_HIGH] << 8 | _regs[R_DMA_DEST_LOW]);
// transfer a byte from source to dest using DMA
_sv.WriteMemory(dest, _sv.ReadMemory(source));
// source registers incremented
source++;
_regs[R_DMA_SOURCE_HIGH] = (byte) (source >> 8);
_regs[R_DMA_SOURCE_LOW] = (byte) source;
// destination registers incremeneted
dest++;
_regs[R_DMA_DEST_HIGH] = (byte) (dest >> 8);
_regs[R_DMA_DEST_LOW] = (byte) dest;
}
}
}
/// <summary>
@ -135,45 +310,83 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
// LCD_X_Size
case 0x00:
case 0x04:
// Only the upper 6 bits of LCD_X_Size are usable. The lower 2 bits are ignored.
// the LCD size can only be changed in 4 pixel increments
_regs[R_LCD_X_SIZE] = value;
break;
// LCD_Y_Size
case 0x01:
case 0x05:
// LCD_Y_Size controls how many scanlines are shown in the field
// After the requisite number of scanlines, the LCD frame latch signal is output and the frame polarity line is toggled
_regs[R_LCD_Y_SIZE] = value;
break;
// X_Scroll
case 0x02:
case 0x06:
_regs[R_X_SCROLL] = value;
break;
// Y_Scroll
case 0x03:
case 0x07:
_regs[R_Y_SCROLL] = value;
break;
// DMA Source low
case 0x08:
_regs[R_DMA_SOURCE_LOW] = value;
break;
// DMA Source high
case 0x09:
_regs[R_DMA_SOURCE_HIGH] = value;
break;
// DMA Destination low
case 0x0A:
_regs[R_DMA_DEST_LOW] = value;
break;
// DMA Destination high
case 0x0B:
_regs[R_DMA_DEST_HIGH] = value;
break;
// DMA Length
case 0x0C:
// 8bit register
// This register selects how many bytes of data to move. The actual number of bytes to move is (L * 16).
// If the register is loaded with 0, a full 4096 bytes is moved.
_regs[R_DMA_LENGTH] = value;
break;
// DMA Control
case 0x0D:
// Start DMA when written with bit7 set
_regs[R_DMA_CONTROL] = value;
break;
// CH1_Flow (right only)
@ -244,9 +457,12 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
// This timer is clocked by a prescaler, which is reset when the timer is written to.
// This prescaler can divide the system clock by 256 or 16384.
// 8bits
_regs[R_IRQ_TIMER] = value;
_intTimerEnabled = true;
// reset prescaler?
_regs[R_SYSTEM_CONTROL] = (byte)(_regs[R_SYSTEM_CONTROL] & ~(1 << 4)); // Reset bit 4
//_regs[R_SYSTEM_CONTROL] = (byte)(_regs[R_SYSTEM_CONTROL] & ~(1 << 4)); // Reset bit 4
_intTimer = value * IntPrescaler;
@ -274,20 +490,16 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
_regs[regIndex] = value;
// lcd displayenable
_screen.DisplayEnable = value.Bit(3);
Screen.DisplayEnable = value.Bit(3);
// banking
_sv.BankSelect = value >> 5;
// writing to this register resets the LCD rendering system and makes it start rendering from the upper left corner, regardless of the bit pattern.
_screen.ResetPosition();
Screen.ResetPosition();
break;
// IRQ status
case 0x27:
break;
// CH4_Freq_Vol (left and right)
case 0x28:
case 0x2C:
@ -305,7 +517,8 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
// READONLY
case 0x20: // Controller
case 0x20: // Controller
case 0x27: // IRQ status
break;
// UNKNOWN
@ -440,6 +653,7 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
// IRQ timer
case 0x23:
result = _regs[R_IRQ_TIMER];
break;
// Reset IRQ timer flag
@ -457,6 +671,11 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
// IRQ status
case 0x27:
// bit0: DMA Audio System (1 == DMA audio finished)
// bit1: IRQ Timer expired (1 == expired)
result = _regs[regIndex];
break;
// CH4_Freq_Vol (left and right)
@ -500,18 +719,18 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
ser.Sync(nameof(_regs), ref _regs, false);
ser.Sync(nameof(FrameStart), ref FrameStart);
ser.Sync(nameof(_intTimer), ref _intTimer);
ser.Sync(nameof(_intTimerEnabled), ref _intTimerEnabled);
ser.Sync(nameof(_intTimerChanged), ref _intTimerChanged);
ser.Sync(nameof(_nmiTimer), ref _nmiTimer);
ser.Sync(nameof(_intFlag), ref _intFlag);
ser.Sync(nameof(_dmaInProgress), ref _dmaInProgress);
ser.Sync(nameof(_dmaCounter), ref _dmaCounter);
ser.Sync(nameof(_seqCounter), ref _seqCounter);
ser.Sync(nameof(_byteCounter), ref _byteCounter);
ser.Sync(nameof(_lineCounter), ref _lineCounter);
ser.Sync(nameof(_field), ref _field);
ser.Sync(nameof(_latchedVRAM), ref _latchedVRAM);
ser.Sync(nameof(_currentVRAMPointer), ref _currentVRAMPointer);
ser.Sync(nameof(_currY), ref _currY);
ser.Sync(nameof(_currX), ref _currX);
_screen.SyncState(ser);
ser.Sync(nameof(_vramByteBuffer), ref _vramByteBuffer);
Screen.SyncState(ser);
ser.EndSection();
}

View File

@ -27,10 +27,13 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
private readonly int[] _palette = new int[4];
public const int PEN_BUFFER_WIDTH = 160 * 2;
public const int PEN_BUFFER_HEIGHT = 160;
/// <summary>
/// The inbuilt screen is a 160*160 dot 2bpp monochrome LCD
/// </summary>
private int[] _penBuffer = new int[160 * 160 * 2];
private int[] _penBuffer = new int[PEN_BUFFER_WIDTH * PEN_BUFFER_HEIGHT];
/// <summary>
/// The output framebuffer
@ -90,13 +93,11 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
WritePixels(data, framePolarity);
// setup for next frame
_vPos = 0;
_hPos = 0;
ResetPosition();
}
else if (lineLatch)
{
// end of scanline
//
_hPos = 0;
_vPos++;
}
@ -115,13 +116,27 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
{
for (int i = 0; i < 4; i++)
{
_frameBuffer[(_vPos * 160 * 2) + (_hPos + framePolarity)] = (data >> i) & 0x01;
if (_hPos < PEN_BUFFER_WIDTH && _vPos < PEN_BUFFER_HEIGHT)
{
_penBuffer[(_vPos * 160 * 2) + (_hPos + framePolarity)] = (data >> i) & 0x01;
}
else
{
// bits out of bounds of the LCD screen
// data is discarded
}
_hPos += 2;
}
}
public void SetRates(int num, int dom)
{
VsyncNumerator = num;
VsyncDenominator = dom;
}
public int VirtualWidth => BufferWidth;
public int VirtualWidth => (int)(BufferWidth * 1.25);
public int VirtualHeight => BufferHeight;
public int BufferWidth => 160;
public int BufferHeight => 160;
@ -144,6 +159,7 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
{
ser.BeginSection("LCD");
ser.Sync(nameof(_frameBuffer), ref _frameBuffer, false);
ser.Sync(nameof(_penBuffer), ref _penBuffer, false);
ser.Sync(nameof(_hPos), ref _hPos);
ser.Sync(nameof(_vPos), ref _vPos);
ser.Sync(nameof(DisplayEnable), ref DisplayEnable);

View File

@ -11,9 +11,7 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
/// <summary>
/// 8K of VRAM which the CPU can access with 0 wait states
/// </summary>
public byte[] VRAM = new byte[0x2000];
public byte[] VRAM = new byte[0x2000];
/// <summary>
/// Bank select index
@ -30,53 +28,54 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
_cpuMemoryAccess = true;
byte result = 0xFF;
if (address < 0x2000)
var divider = address / 0x2000;
switch (divider)
{
// RAM
return WRAM[address];
}
case 0:
// WRAM
result = WRAM[address];
break;
if (address < 0x4000)
{
// port access
return ReadHardware(address);
}
case 1:
// IO address space
break;
if (address < 0x6000)
{
// VRAM
return VRAM[address - 0x4000];
}
case 2:
// VRAM
result = VRAM[address - 0x4000];
break;
if (address < 0x8000)
{
// nothing here
}
case 3:
// nothing here
break;
if (address < 0xC000)
{
// cartridge rom banking
// 0x8000 - 0xBFFF is selectable using the 3 bits from the SystemControl register
switch (BankSelect)
{
// first 16k
case 0:
return _cartridge.ReadByte((ushort)(address % 0x2000));
case 4:
// cartridge rom banking
// 0x8000 - 0xBFFF is selectable using the 3 bits from the SystemControl register
switch (BankSelect)
{
// first 16k
case 0:
result = _cartridge.ReadByte((ushort) (address % 0x2000));
break;
// second 16k
case 1:
return _cartridge.ReadByte((ushort)((address % 0x2000) + 0x2000));
// second 16k
case 1:
result = _cartridge.ReadByte((ushort) ((address % 0x2000) + 0x2000));
break;
// third 16k
case 2:
return _cartridge.ReadByte((ushort)((address % 0x2000) + 0x4000));
}
}
// third 16k
case 2:
result = _cartridge.ReadByte((ushort) ((address % 0x2000) + 0x4000));
break;
}
break;
if (address < 0xFFFF)
{
// fixed to the last 16K in the cart address space
return _cartridge.ReadByte((ushort)((address % 0x2000) + 0x6000));
case 5:
// fixed to the last 16K in the cart address space
result = _cartridge.ReadByte(address);
break;
}
return result;

View File

@ -18,7 +18,11 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
private int _frameClock;
private int _frame;
public int FrameClock => _frameClock;
public int FrameClock
{
get => _frameClock;
set => _frameClock = value;
}
public int Frame => _frame;
private void CalcClock()
@ -31,6 +35,10 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
2; // fields per frame
double refreshRate = _cpuClocksPerSecond / _cpuClocksPerFrame; // 50.8130081300813
_asic.Screen.SetRates(
(int) _cpuClocksPerSecond,
(int) _cpuClocksPerFrame);
}
public bool FrameAdvance(IController controller, bool render, bool renderSound)

View File

@ -28,6 +28,9 @@ namespace BizHawk.Emulation.Cores.Consoles.SuperVision
_tracer = new TraceBuffer(_cpu.TraceHeader);
_asic = new ASIC(this, _syncSettings);
CalcClock();
ser.Register<IVideoProvider>(_asic.Screen);
ser.Register<ITraceable>(_tracer);
ser.Register<IDisassemblable>(_cpu);
ser.Register<IStatable>(new StateSerializer(SyncState));