mirror of
https://github.com/livingcomputermuseum/ContrAlto.git
synced 2026-01-17 00:23:24 +00:00
404 lines
13 KiB
C#
404 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using Contralto.CPU;
|
|
|
|
namespace Contralto.Display
|
|
{
|
|
/// <summary>
|
|
/// DisplayController implements hardware controlling the virtual electron beam
|
|
/// as it scans across the screen. It implements the logic of the display's sync generator
|
|
/// and wakes up the DVT and DHT tasks as necessary during a display field.
|
|
/// </summary>
|
|
public class DisplayController
|
|
{
|
|
public DisplayController(AltoSystem system)
|
|
{
|
|
_system = system;
|
|
Reset();
|
|
}
|
|
|
|
public void AttachDisplay(IAltoDisplay display)
|
|
{
|
|
_display = display;
|
|
}
|
|
|
|
public void DetachDisplay()
|
|
{
|
|
_display = null;
|
|
}
|
|
|
|
public int Fields
|
|
{
|
|
get { return _fields; }
|
|
set { _fields = value; }
|
|
}
|
|
|
|
public bool DWTBLOCK
|
|
{
|
|
get { return _dwtBlocked; }
|
|
set
|
|
{
|
|
_dwtBlocked = value;
|
|
CheckWordWakeup();
|
|
}
|
|
}
|
|
|
|
public bool DHTBLOCK
|
|
{
|
|
get { return _dhtBlocked; }
|
|
set
|
|
{
|
|
_dhtBlocked = value;
|
|
CheckWordWakeup();
|
|
}
|
|
}
|
|
|
|
public bool FIFOFULL
|
|
{
|
|
get
|
|
{
|
|
return _dataBuffer.Count >= 14;
|
|
}
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
_evenField = false;
|
|
_scanline = 0;
|
|
_word = 0;
|
|
_dwtBlocked = true;
|
|
_dhtBlocked = false;
|
|
_dataBuffer.Clear();
|
|
|
|
if (_system.CPU != null)
|
|
{
|
|
CheckWordWakeup();
|
|
}
|
|
|
|
_whiteOnBlack = _whiteOnBlackLatch = false;
|
|
_lowRes = _lowResLatch = false;
|
|
_swModeLatch = false;
|
|
|
|
_cursorReg = 0;
|
|
_cursorX = 0;
|
|
_cursorRegLatch = false;
|
|
_cursorXLatch = false;
|
|
|
|
_verticalBlankScanlineWakeup = new Event(_verticalBlankDuration, null, VerticalBlankScanlineCallback);
|
|
_horizontalWakeup = new Event(_horizontalBlankDuration, null, HorizontalBlankEndCallback);
|
|
_wordWakeup = new Event(_wordDuration, null, WordCallback);
|
|
|
|
// Kick things off
|
|
_system.Scheduler.Schedule(_verticalBlankScanlineWakeup);
|
|
}
|
|
|
|
private void FieldStart()
|
|
{
|
|
// Start of Vertical Blanking (end of last field). This lasts for 34 scanline times or so.
|
|
_evenField = !_evenField;
|
|
|
|
// Wakeup DVT
|
|
_system.CPU.WakeupTask(TaskType.DisplayVertical);
|
|
|
|
// Block DHT, DWT
|
|
_system.CPU.BlockTask(TaskType.DisplayHorizontal);
|
|
_system.CPU.BlockTask(TaskType.DisplayWord);
|
|
|
|
// TODO: this eliminates the garbage on the top two lines of the screen. We are likely
|
|
// not stopping the Word task at the right point at the end of the field, leading to it reading
|
|
// garbage for the first word(s) or so of the next field...
|
|
// OR: we need more time at the start of the scanline before rendering starts...
|
|
// Fix this right!
|
|
_system.CPU.Tasks[(int)TaskType.DisplayWord].Reset();
|
|
|
|
|
|
_fields++;
|
|
|
|
_scanline = _evenField ? 0 : 1;
|
|
|
|
_vblankScanlineCount = 0;
|
|
|
|
|
|
_dataBuffer.Clear();
|
|
|
|
// Schedule wakeup for first scanline of vblank
|
|
_verticalBlankScanlineWakeup.TimestampNsec = _verticalBlankScanlineDuration;
|
|
_system.Scheduler.Schedule(_verticalBlankScanlineWakeup);
|
|
}
|
|
|
|
private void VerticalBlankScanlineCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
// End of VBlank scanline.
|
|
_vblankScanlineCount++;
|
|
|
|
// Run MRT
|
|
_system.CPU.WakeupTask(TaskType.MemoryRefresh);
|
|
|
|
// Run Ethernet if a countdown wakeup is in progress
|
|
if (_system.EthernetController.CountdownWakeup)
|
|
{
|
|
_system.CPU.WakeupTask(TaskType.Ethernet);
|
|
}
|
|
|
|
if (_vblankScanlineCount > (_evenField ? 33 : 34))
|
|
{
|
|
// End of vblank:
|
|
// Wake up DHT
|
|
_system.CPU.WakeupTask(TaskType.DisplayHorizontal);
|
|
|
|
_dataBuffer.Clear();
|
|
|
|
DWTBLOCK = false;
|
|
DHTBLOCK = false;
|
|
|
|
// Run CURT
|
|
_system.CPU.WakeupTask(TaskType.Cursor);
|
|
|
|
// Schedule HBlank wakeup for end of first HBlank
|
|
_horizontalWakeup.TimestampNsec = _horizontalBlankDuration - skewNsec;
|
|
_system.Scheduler.Schedule(_horizontalWakeup);
|
|
}
|
|
else
|
|
{
|
|
// Do the next vblank scanline
|
|
_verticalBlankScanlineWakeup.TimestampNsec = _verticalBlankScanlineDuration;
|
|
_system.Scheduler.Schedule(_verticalBlankScanlineWakeup);
|
|
}
|
|
}
|
|
|
|
private void HorizontalBlankEndCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
// Reset scanline word counter
|
|
_word = 0;
|
|
|
|
// Deal with cursor latches for this scanline
|
|
if (_cursorRegLatch)
|
|
{
|
|
_cursorRegLatched = _cursorReg;
|
|
_cursorRegLatch = false;
|
|
}
|
|
|
|
if (_cursorXLatch)
|
|
{
|
|
_cursorXLatched = _cursorX;
|
|
_cursorXLatch = false;
|
|
}
|
|
|
|
// Schedule wakeup for first word on this scanline
|
|
// TODO: the delay below is chosen to reduce flicker on first scanline;
|
|
// investigate.
|
|
_wordWakeup.TimestampNsec = _wordDuration * 3;
|
|
_system.Scheduler.Schedule(_wordWakeup);
|
|
}
|
|
|
|
private void WordCallback(ulong timeNsec, ulong skewNsec, object context)
|
|
{
|
|
if (_display == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Dequeue a word (if available) and draw it to the screen.
|
|
ushort displayWord = (ushort)(_whiteOnBlack ? 0 : 0xffff);
|
|
if (_dataBuffer.Count > 0)
|
|
{
|
|
displayWord = _whiteOnBlack ? _dataBuffer.Dequeue() : (ushort)~_dataBuffer.Dequeue();
|
|
CheckWordWakeup();
|
|
}
|
|
|
|
_display.DrawDisplayWord(_scanline, _word, displayWord, _lowRes);
|
|
|
|
// Merge in cursor word:
|
|
// Calculate X offset of current word
|
|
int xOffset = _word * (_lowRes ? 32 : 16);
|
|
|
|
_word++;
|
|
|
|
if (_word >= (_lowRes ? _scanlineWords / 2 : _scanlineWords))
|
|
{
|
|
// End of scanline.
|
|
// Move to next.
|
|
|
|
// Draw cursor for this scanline first
|
|
if (_cursorXLatched < 606)
|
|
{
|
|
_display.DrawCursorWord(_scanline, _cursorXLatched, _whiteOnBlack, _cursorRegLatched);
|
|
}
|
|
|
|
_scanline += 2;
|
|
|
|
if (_scanline >= 808)
|
|
{
|
|
// Done with field.
|
|
|
|
// Draw the completed field to the emulated display.
|
|
_display.Render();
|
|
|
|
// And start over
|
|
FieldStart();
|
|
}
|
|
else
|
|
{
|
|
// More scanlines to do.
|
|
|
|
// Run CURT and MRT at end of scanline
|
|
_system.CPU.WakeupTask(TaskType.Cursor);
|
|
_system.CPU.WakeupTask(TaskType.MemoryRefresh);
|
|
|
|
// Schedule HBlank wakeup for end of next HBlank
|
|
_horizontalWakeup.TimestampNsec = _horizontalBlankDuration - skewNsec;
|
|
_system.Scheduler.Schedule(_horizontalWakeup);
|
|
DWTBLOCK = false;
|
|
_dataBuffer.Clear();
|
|
|
|
// Deal with SWMODE latches for the scanline we're about to draw
|
|
if (_swModeLatch)
|
|
{
|
|
_lowRes = _lowResLatch;
|
|
_whiteOnBlack = _whiteOnBlackLatch;
|
|
_swModeLatch = false;
|
|
}
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// More words to do.
|
|
// Schedule wakeup for next word
|
|
if (_lowRes)
|
|
{
|
|
_wordWakeup.TimestampNsec = _wordDuration * 2 - skewNsec;
|
|
}
|
|
else
|
|
{
|
|
_wordWakeup.TimestampNsec = _wordDuration - skewNsec;
|
|
}
|
|
_system.Scheduler.Schedule(_wordWakeup);
|
|
}
|
|
}
|
|
|
|
private void CheckWordWakeup()
|
|
{
|
|
if (FIFOFULL ||
|
|
DHTBLOCK ||
|
|
DWTBLOCK)
|
|
{
|
|
// If the fifo is full or either the horizontal or word tasks have blocked,
|
|
// the word task must be blocked.
|
|
_system.CPU.BlockTask(TaskType.DisplayWord);
|
|
}
|
|
else if (!FIFOFULL &&
|
|
!DHTBLOCK &&
|
|
!DWTBLOCK)
|
|
{
|
|
//
|
|
// "If the DWT has not executed a BLOCK, if DHT is not blocked, and if the
|
|
// buffer is not full, DWT wakeups are generated."
|
|
//
|
|
_system.CPU.WakeupTask(TaskType.DisplayWord);
|
|
}
|
|
}
|
|
|
|
public void LoadDDR(ushort word)
|
|
{
|
|
_dataBuffer.Enqueue(word);
|
|
|
|
// Sanity check: data length should never exceed 16 words.
|
|
if (_dataBuffer.Count > 16)
|
|
{
|
|
_dataBuffer.Dequeue();
|
|
}
|
|
|
|
CheckWordWakeup();
|
|
}
|
|
|
|
public void LoadXPREG(ushort word)
|
|
{
|
|
if (!_cursorXLatch)
|
|
{
|
|
_cursorXLatch = true;
|
|
_cursorX = (ushort)(~word);
|
|
}
|
|
}
|
|
|
|
public void LoadCSR(ushort word)
|
|
{
|
|
if (!_cursorRegLatch)
|
|
{
|
|
_cursorRegLatch = true;
|
|
_cursorReg = (ushort)word;
|
|
}
|
|
}
|
|
|
|
public void SETMODE(ushort word)
|
|
{
|
|
// These take effect at the beginning of the next scanline.
|
|
_lowResLatch = (word & 0x8000) != 0;
|
|
_whiteOnBlackLatch = (word & 0x4000) != 0;
|
|
_swModeLatch = true;
|
|
}
|
|
|
|
public bool EVENFIELD
|
|
{
|
|
get { return _evenField; }
|
|
}
|
|
|
|
private enum DisplayState
|
|
{
|
|
Invalid = 0,
|
|
VerticalBlank,
|
|
VisibleScanline,
|
|
HorizontalBlank,
|
|
}
|
|
|
|
// MODE data
|
|
private bool _evenField;
|
|
private bool _lowRes;
|
|
private bool _lowResLatch;
|
|
private bool _whiteOnBlack;
|
|
private bool _whiteOnBlackLatch;
|
|
private bool _swModeLatch;
|
|
|
|
// Cursor data
|
|
private bool _cursorRegLatch;
|
|
private ushort _cursorReg;
|
|
private ushort _cursorRegLatched;
|
|
private bool _cursorXLatch;
|
|
private ushort _cursorX;
|
|
private ushort _cursorXLatched;
|
|
|
|
// Indicates whether the DWT or DHT blocked itself
|
|
// in which case they cannot be reawakened until the next field.
|
|
private bool _dwtBlocked;
|
|
private bool _dhtBlocked;
|
|
|
|
private int _scanline;
|
|
private int _word;
|
|
private const int _scanlineWords = 38;
|
|
|
|
private Queue<ushort> _dataBuffer = new Queue<ushort>(16);
|
|
|
|
private AltoSystem _system;
|
|
private IAltoDisplay _display;
|
|
|
|
private int _fields;
|
|
|
|
// Timing constants
|
|
// 38uS per scanline; 6uS for hblank.
|
|
// ~35 scanlines for vblank (1330uS)
|
|
private const double _scale = 1.0;
|
|
private const ulong _verticalBlankDuration = (ulong)(665000.0 * _scale); // 665uS
|
|
private const ulong _verticalBlankScanlineDuration = (ulong)(38080 * _scale); // 38uS
|
|
private const ulong _horizontalBlankDuration = (ulong)(6084 * _scale); // 6uS
|
|
private const ulong _wordDuration = (ulong)(842.0 * _scale); // 32/38uS
|
|
|
|
private int _vblankScanlineCount;
|
|
|
|
//
|
|
// Scheduler events
|
|
//
|
|
private Event _verticalBlankScanlineWakeup;
|
|
private Event _horizontalWakeup;
|
|
private Event _wordWakeup;
|
|
}
|
|
}
|