From 6fd499665b40b9e363afd50708c326d8eb0a4b47 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:12:10 +0200 Subject: [PATCH 1/6] Fix bug in sound channel 4 --- CoreBoy/sound/Lfsr.cs | 9 +++++---- CoreBoy/sound/SoundMode4.cs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CoreBoy/sound/Lfsr.cs b/CoreBoy/sound/Lfsr.cs index 3f50044..f2d8dd0 100644 --- a/CoreBoy/sound/Lfsr.cs +++ b/CoreBoy/sound/Lfsr.cs @@ -10,13 +10,14 @@ public class Lfsr public int NextBit(bool widthMode7) { - var x = ((Value & 1) ^ ((Value & 2) >> 1)) != 0; + var xor = ((Value & 1) ^ ((Value & 2) >> 1)); Value = Value >> 1; - Value = Value | (x ? (1 << 14) : 0); - + Value |= xor << 14; + if (widthMode7) { - Value = Value | (x ? (1 << 6) : 0); + Value |= (xor << 6); + Value &= 0x7F; } return 1 & ~Value; diff --git a/CoreBoy/sound/SoundMode4.cs b/CoreBoy/sound/SoundMode4.cs index 58ea80b..26f9a43 100644 --- a/CoreBoy/sound/SoundMode4.cs +++ b/CoreBoy/sound/SoundMode4.cs @@ -48,7 +48,7 @@ public override int Tick() if (_polynomialCounter.Tick()) { - _lastResult = _lfsr.NextBit((Nr3 & (1 << 3)) != 0); + _lastResult = _lfsr.NextBit(((Nr3 >> 3) & 1) != 0); } return _lastResult * _volumeEnvelope.GetVolume(); From 19e1938d127feeff438adc02d3ab03e3fb090c18 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:12:19 +0200 Subject: [PATCH 2/6] Add sound support for Windows --- CoreBoy/gui/WinSound.cs | 119 ++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 79 deletions(-) diff --git a/CoreBoy/gui/WinSound.cs b/CoreBoy/gui/WinSound.cs index 1f5254f..fc42936 100644 --- a/CoreBoy/gui/WinSound.cs +++ b/CoreBoy/gui/WinSound.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using CoreBoy.sound; using NAudio.Wave; using NAudio.Wave.SampleProviders; @@ -10,11 +11,14 @@ namespace CoreBoy.gui public class WinSound : ISoundOutput { + private readonly byte[] _buffer = new byte[BufferSize]; + private int _i = 0; private int _tick; private readonly int _divider; private AudioPlaybackEngine _engine; - private const int SampleRate = 22050; + private const int BufferSize = 1024; + public const int SampleRate = 22050; public WinSound() { @@ -29,6 +33,7 @@ public void Start() public void Stop() { _engine?.Dispose(); + _engine = null; } public void Play(int left, int right) @@ -39,36 +44,61 @@ public void Play(int left, int right) return; } - //Beep((uint)left, 5);*/ + left = (int)(left * 0.25); + right = (int)(right * 0.25); + + left = left < 0 ? 0 : (left > 255 ? 255 : left); + right = right < 0 ? 0 : (right > 255 ? 255 : right); + + _buffer[_i++] = (byte)left; + _buffer[_i++] = (byte)right; + if (_i > BufferSize / 2) + { + _engine?.PlaySound(_buffer, 0, _i); + _i = 0; + } + + // wait until audio is done playing this data + while (_engine?.GetQueuedAudioLength() > BufferSize) + { + Thread.Sleep(0); + } } } public class AudioPlaybackEngine : IDisposable { - private readonly IWavePlayer _outputDevice; + private IWavePlayer _outputDevice; private readonly MixingSampleProvider _mixer; + private readonly BufferedWaveProvider _bufferedWaveProvider; public AudioPlaybackEngine(int sampleRate = 44100, int channelCount = 2) { - _outputDevice = new WaveOutEvent(); + _outputDevice = new WasapiOut(); _mixer = new MixingSampleProvider(WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channelCount)) { ReadFully = true }; + _bufferedWaveProvider = new BufferedWaveProvider(WaveFormat.CreateCustomFormat(WaveFormatEncoding.Pcm, sampleRate, 2, sampleRate, 8, 8)) + { + ReadFully = true, + DiscardOnBufferOverflow = true + }; + + AddMixerInput(_bufferedWaveProvider.ToSampleProvider()); _outputDevice.Init(_mixer); _outputDevice.Play(); } - public void PlaySound(string fileName) + public int GetQueuedAudioLength() { - var input = new AudioFileReader(fileName); - AddMixerInput(new AutoDisposeFileReader(input)); + return _bufferedWaveProvider.BufferedBytes; } - public void PlaySound(CachedSound sound) + public void PlaySound(byte[] buffer, int offset, int count) { - AddMixerInput(new CachedSoundSampleProvider(sound)); + _bufferedWaveProvider.AddSamples(buffer, offset, count); } private void AddMixerInput(ISampleProvider input) @@ -91,77 +121,8 @@ private ISampleProvider ConvertToRightChannelCount(ISampleProvider input) public void Dispose() { + _outputDevice.Stop(); _outputDevice.Dispose(); } } - - public class AutoDisposeFileReader : ISampleProvider - { - private readonly AudioFileReader _reader; - private bool _isDisposed; - public AutoDisposeFileReader(AudioFileReader reader) - { - this._reader = reader; - this.WaveFormat = reader.WaveFormat; - } - - public int Read(float[] buffer, int offset, int count) - { - if (_isDisposed) - return 0; - - var read = _reader.Read(buffer, offset, count); - if (read == 0) - { - _reader.Dispose(); - _isDisposed = true; - } - return read; - } - - public WaveFormat WaveFormat { get; private set; } - } - public class CachedSoundSampleProvider : ISampleProvider - { - private readonly CachedSound _cachedSound; - private long _position; - - public CachedSoundSampleProvider(CachedSound cachedSound) - { - this._cachedSound = cachedSound; - } - - public int Read(float[] buffer, int offset, int count) - { - var availableSamples = _cachedSound.AudioData.Length - _position; - var samplesToCopy = Math.Min(availableSamples, count); - Array.Copy(_cachedSound.AudioData, _position, buffer, offset, samplesToCopy); - _position += samplesToCopy; - return (int)samplesToCopy; - } - - public WaveFormat WaveFormat { get { return _cachedSound.WaveFormat; } } - } - - public class CachedSound - { - public float[] AudioData { get; private set; } - public WaveFormat WaveFormat { get; private set; } - public CachedSound(string audioFileName) - { - using (var audioFileReader = new AudioFileReader(audioFileName)) - { - // TODO: could add resampling in here if required - WaveFormat = audioFileReader.WaveFormat; - var wholeFile = new List((int)(audioFileReader.Length / 4)); - var readBuffer = new float[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels]; - int samplesRead; - while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0) - { - wholeFile.AddRange(readBuffer.Take(samplesRead)); - } - AudioData = wholeFile.ToArray(); - } - } - } } \ No newline at end of file From 3fc4d256c5bcf240de017189c20779484e2ff182 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:33:04 +0200 Subject: [PATCH 3/6] Add custom WinForms display control for performance and sound choppyness --- CoreBoy.Windows/BitmapDisplayControl.cs | 226 +++++++++++++++++++++ CoreBoy.Windows/WinFormsEmulatorSurface.cs | 76 ++----- CoreBoy/gui/Emulator.cs | 2 +- 3 files changed, 246 insertions(+), 58 deletions(-) create mode 100644 CoreBoy.Windows/BitmapDisplayControl.cs diff --git a/CoreBoy.Windows/BitmapDisplayControl.cs b/CoreBoy.Windows/BitmapDisplayControl.cs new file mode 100644 index 0000000..eaf5454 --- /dev/null +++ b/CoreBoy.Windows/BitmapDisplayControl.cs @@ -0,0 +1,226 @@ +using System; +using System.Drawing; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Windows.Forms; +using CoreBoy.gpu; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Image = System.Drawing.Image; + +namespace CoreBoy.Windows +{ + public sealed class BitmapDisplayControl : Control, IDisplay + { + public static readonly int DisplayWidth = 160; + public static readonly int DisplayHeight = 144; + public static readonly float AspectRatio = DisplayWidth / (DisplayHeight * 1f); + + public static readonly int[] Colors = { 0xe6f8da, 0x99c886, 0x437969, 0x051f2a }; + + private readonly int[] _rgb; + private readonly MemoryStream _imageStream = new MemoryStream(); + private readonly Image _imageBuffer = new Image(DisplayWidth, DisplayHeight); + + private bool _doStop; + private bool _doRefresh; + private int _i; + + private readonly object _lockObject = new object(); + + public BitmapDisplayControl() + { + _rgb = new int[DisplayWidth * DisplayHeight]; + SetStyle(ControlStyles.Opaque | ControlStyles.Selectable, false); + SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.SupportsTransparentBackColor, true); + + BackColor = System.Drawing.Color.FromArgb(Colors[0]); + TabStop = false; + } + + public event FrameProducedEventHandler OnFrameProduced; + + bool IDisplay.Enabled + { + get => DisplayEnabled; + set => DisplayEnabled = value; + } + + public bool DisplayEnabled { get; set; } + + public void PutDmgPixel(int color) + { + _rgb[_i++] = Colors[color]; + _i %= _rgb.Length; + } + + public void PutColorPixel(int gbcRgb) + { + if (_i >= _rgb.Length) + { + return; + } + _rgb[_i++] = TranslateGbcRgb(gbcRgb); + } + + public static int TranslateGbcRgb(int gbcRgb) + { + var r = (gbcRgb >> 0) & 0x1f; + var g = (gbcRgb >> 5) & 0x1f; + var b = (gbcRgb >> 10) & 0x1f; + var result = (r * 8) << 16; + result |= (g * 8) << 8; + result |= (b * 8) << 0; + return result; + } + + public void RequestRefresh() + { + lock (_lockObject) + { + _doRefresh = true; + Monitor.PulseAll(_lockObject); + } + } + + public void WaitForRefresh() + { + lock (_lockObject) + { + while (_doRefresh) + { + try + { + Monitor.Wait(_lockObject, 1); + } + catch (ThreadInterruptedException) + { + break; + } + } + } + } + protected override void OnPaint(PaintEventArgs e) + { + if (InvokeRequired) + { + Invoke(new PaintEventHandler((_, __) => OnPaint(e))); + return; + } + + base.OnPaint(e); + + var width = ClientRectangle.Width; + var height = ClientRectangle.Height; + var aspectRatio = width * 1f / height; + + if (aspectRatio <= AspectRatio) + { + height = (int)Math.Floor(width / AspectRatio); + } + else + { + width = (int)Math.Floor(height * AspectRatio); + } + + var x = 0; + var y = 0; + if (width < ClientRectangle.Width) + { + x += (ClientRectangle.Width - width) / 2; + } + else + { + y += (ClientRectangle.Height - height) / 2; + } + + try + { + if (DisplayEnabled) + { + _imageStream.Seek(0, SeekOrigin.Begin); + _imageBuffer.SaveAsBmp(_imageStream); + using var img = Image.FromStream(_imageStream); + + e.Graphics.DrawImage(img, x, y, width, height); + } + else + { + using var brush = new SolidBrush(System.Drawing.Color.FromArgb(0xe6f8da)); + e.Graphics.FillRectangle(brush, x, y, width, height); + } + } + catch (ObjectDisposedException) { } + } + + public void SaveLastFrame(string path) + { + using var fs = File.OpenWrite(path); + _imageBuffer.SaveAsBmp(fs); + fs.Flush(); + fs.Close(); + } + + public void Run(CancellationToken token) + { + _doStop = false; + _doRefresh = false; + DisplayEnabled = true; + + while (!_doStop) + { + lock (_lockObject) + { + try + { + Monitor.Wait(_lockObject, 1); + } + catch (ThreadInterruptedException) + { + break; + } + } + + if (_doRefresh) + { + FillAndDrawBuffer(); + + lock (_lockObject) + { + _i = 0; + _doRefresh = false; + Monitor.PulseAll(_lockObject); + } + } + + _doStop = token.IsCancellationRequested; + } + } + + private void FillAndDrawBuffer() + { + try + { + var pi = 0; + while (pi < _rgb.Length) + { + var (r, g, b) = ToRgb(_rgb[pi]); + _imageBuffer[pi % DisplayWidth, pi++ / DisplayWidth] = new Rgba32((byte)r, (byte)g, (byte)b, 255); + } + + Invalidate(); + } + catch (ObjectDisposedException) { } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (int, int, int) ToRgb(int pixel) + { + var b = pixel & 255; + var g = (pixel >> 8) & 255; + var r = (pixel >> 16) & 255; + return (r, g, b); + } + } +} \ No newline at end of file diff --git a/CoreBoy.Windows/WinFormsEmulatorSurface.cs b/CoreBoy.Windows/WinFormsEmulatorSurface.cs index 727e4a2..ccb59f7 100644 --- a/CoreBoy.Windows/WinFormsEmulatorSurface.cs +++ b/CoreBoy.Windows/WinFormsEmulatorSurface.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Drawing; using System.IO; using System.Threading; @@ -14,17 +13,14 @@ public partial class WinFormsEmulatorSurface : Form, IController { private IButtonListener _listener; - private byte[] _lastFrame; private readonly MenuStrip _menu; - private readonly PictureBox _pictureBox; + private readonly BitmapDisplayControl _display; private readonly Dictionary _controls; private readonly Emulator _emulator; private readonly GameboyOptions _gameboyOptions; private CancellationTokenSource _cancellation; - private readonly object _updateLock = new object(); - public WinFormsEmulatorSurface() { InitializeComponent(); @@ -52,13 +48,13 @@ public WinFormsEmulatorSurface() } }); - Controls.Add(_pictureBox = new PictureBox + Controls.Add(_display = new BitmapDisplayControl { - Top = _menu.Height, - Width = BitmapDisplay.DisplayWidth * 5, - Height = BitmapDisplay.DisplayHeight * 5, - BackColor = Color.Black, - SizeMode = PictureBoxSizeMode.Zoom + BackColor = Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(230)))), ((int)(((byte)(248)))), ((int)(((byte)(218))))), + DisplayEnabled = false, + Dock = DockStyle.Fill, + Location = new Point(0, 44), + Size = new Size(1600, 1296) }); _controls = new Dictionary @@ -73,12 +69,16 @@ public WinFormsEmulatorSurface() {Keys.Back, Button.Select} }; - Height = _menu.Height + _pictureBox.Height + 50; - Width = _pictureBox.Width; + AutoScaleDimensions = new SizeF(192F, 192F); + AutoScaleMode = AutoScaleMode.Dpi; + ClientSize = new Size(1600, 1340); _cancellation = new CancellationTokenSource(); _gameboyOptions = new GameboyOptions(); - _emulator = new Emulator(_gameboyOptions); + _emulator = new Emulator(_gameboyOptions) + { + Display = _display + }; ConnectEmulatorToPanel(); } @@ -86,8 +86,7 @@ public WinFormsEmulatorSurface() private void ConnectEmulatorToPanel() { _emulator.Controller = this; - _emulator.Display.OnFrameProduced += UpdateDisplay; - + KeyDown += WinFormsEmulatorSurface_KeyDown; KeyUp += WinFormsEmulatorSurface_KeyUp; Closed += (_, e) => { _cancellation.Cancel(); }; @@ -99,7 +98,7 @@ private void StartEmulation() { _emulator.Stop(_cancellation); _cancellation = new CancellationTokenSource(); - _pictureBox.Image = null; + _display.DisplayEnabled = false; Thread.Sleep(100); } @@ -138,15 +137,7 @@ private void Screenshot() if (success) { - try - { - Monitor.Enter(_updateLock); - File.WriteAllBytes(sfd.FileName, _lastFrame); - } - finally - { - Monitor.Exit(_updateLock); - } + _display.SaveLastFrame(sfd.FileName); } _emulator.TogglePause(); @@ -172,39 +163,10 @@ private void WinFormsEmulatorSurface_KeyUp(object sender, KeyEventArgs e) public void SetButtonListener(IButtonListener listener) => _listener = listener; - protected override void OnResize(EventArgs e) - { - base.OnResize(e); - if (_pictureBox == null) return; - - _pictureBox.Width = Width; - _pictureBox.Height = Height - _menu.Height - 50; - } - - public void UpdateDisplay(object _, byte[] frame) - { - if (!Monitor.TryEnter(_updateLock)) return; - - try - { - _lastFrame = frame; - using var memoryStream = new MemoryStream(frame); - _pictureBox.Image = Image.FromStream(memoryStream); - } - catch - { - // YOLO - } - finally - { - Monitor.Exit(_updateLock); - } - } - protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); - _pictureBox.Dispose(); + _display.Dispose(); } } } \ No newline at end of file diff --git a/CoreBoy/gui/Emulator.cs b/CoreBoy/gui/Emulator.cs index 4cc32bf..d5bec38 100644 --- a/CoreBoy/gui/Emulator.cs +++ b/CoreBoy/gui/Emulator.cs @@ -13,7 +13,7 @@ namespace CoreBoy.gui public class Emulator: IRunnable { public Gameboy Gameboy { get; set; } - public IDisplay Display { get; set; } = new BitmapDisplay(); + public IDisplay Display { get; set; } public IController Controller { get; set; } = new NullController(); public SerialEndpoint SerialEndpoint { get; set; } = new NullSerialEndpoint(); public GameboyOptions Options { get; set; } From 23427b867632281f4f773b0ec1b7110b9c54c032 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:39:05 +0200 Subject: [PATCH 4/6] Sound volume --- CoreBoy/sound/Sound.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CoreBoy/sound/Sound.cs b/CoreBoy/sound/Sound.cs index 76d3cd7..0b895af 100644 --- a/CoreBoy/sound/Sound.cs +++ b/CoreBoy/sound/Sound.cs @@ -67,14 +67,11 @@ public void Tick() } } - left /= 4; - right /= 4; - var volumes = _ram.GetByte(0xff24); left *= ((volumes >> 4) & 0b111); right *= (volumes & 0b111); - _output.Play((byte) left, (byte) right); + _output.Play(left, right); } private IAddressSpace GetAddressSpace(int address) From e3ff660e5438a8ef9dbd9b649e4d5aec35640048 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:39:32 +0200 Subject: [PATCH 5/6] UI sizing and control order for DockStyle.Fill --- CoreBoy.Windows/WinFormsEmulatorSurface.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CoreBoy.Windows/WinFormsEmulatorSurface.cs b/CoreBoy.Windows/WinFormsEmulatorSurface.cs index ccb59f7..9cd550b 100644 --- a/CoreBoy.Windows/WinFormsEmulatorSurface.cs +++ b/CoreBoy.Windows/WinFormsEmulatorSurface.cs @@ -25,6 +25,15 @@ public WinFormsEmulatorSurface() { InitializeComponent(); + Controls.Add(_display = new BitmapDisplayControl + { + BackColor = Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(230)))), ((int)(((byte)(248)))), ((int)(((byte)(218))))), + DisplayEnabled = false, + Dock = DockStyle.Fill, + Location = new Point(0, 44), + Size = new Size(800, 720) + }); + Controls.Add(_menu = new MenuStrip { Items = @@ -48,15 +57,6 @@ public WinFormsEmulatorSurface() } }); - Controls.Add(_display = new BitmapDisplayControl - { - BackColor = Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(230)))), ((int)(((byte)(248)))), ((int)(((byte)(218))))), - DisplayEnabled = false, - Dock = DockStyle.Fill, - Location = new Point(0, 44), - Size = new Size(1600, 1296) - }); - _controls = new Dictionary { {Keys.Left, Button.Left}, @@ -71,7 +71,7 @@ public WinFormsEmulatorSurface() AutoScaleDimensions = new SizeF(192F, 192F); AutoScaleMode = AutoScaleMode.Dpi; - ClientSize = new Size(1600, 1340); + ClientSize = new Size(_display.Width, _display.Height + _menu.Height); _cancellation = new CancellationTokenSource(); _gameboyOptions = new GameboyOptions(); From 53dc11552e7c63c43ae448ad187a2b828d06e445 Mon Sep 17 00:00:00 2001 From: Wesley Cabus Date: Thu, 26 May 2022 20:49:09 +0200 Subject: [PATCH 6/6] Fix serial port bug to make Tetris run --- CoreBoy/Integer.cs | 6 ++ CoreBoy/gui/ConsoleWriteSerialEndpoint.cs | 4 +- CoreBoy/serial/SerialEndpoint.cs | 5 +- CoreBoy/serial/SerialPort.cs | 96 ++++++++++++++++++----- 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/CoreBoy/Integer.cs b/CoreBoy/Integer.cs index 6d146a3..9ec2b1c 100644 --- a/CoreBoy/Integer.cs +++ b/CoreBoy/Integer.cs @@ -6,5 +6,11 @@ public static string ToHexString(int address) { return $"{address} - THIS SHOULD BE A HEX ADDRESS"; } + + public static bool GetBit(this int byteValue, int position) => (byteValue & (1 << position)) != 0; + + public static int SetBit(this int byteValue, int position, bool value) => value ? SetBit(byteValue, position) : ClearBit(byteValue, position); + public static int SetBit(this int byteValue, int position) => (byteValue | (1 << position)) & 0xff; + public static int ClearBit(this int byteValue, int position) => ~(1 << position) & byteValue & 0xff; } } \ No newline at end of file diff --git a/CoreBoy/gui/ConsoleWriteSerialEndpoint.cs b/CoreBoy/gui/ConsoleWriteSerialEndpoint.cs index 29a4919..d157ec3 100644 --- a/CoreBoy/gui/ConsoleWriteSerialEndpoint.cs +++ b/CoreBoy/gui/ConsoleWriteSerialEndpoint.cs @@ -5,10 +5,12 @@ namespace CoreBoy.gui { public class ConsoleWriteSerialEndpoint : SerialEndpoint { + public bool externalClockPulsed() => false; + public int transfer(int b) { Console.Write((char) b); - return 0; + return (b << 1) & 0xFF; } } } \ No newline at end of file diff --git a/CoreBoy/serial/SerialEndpoint.cs b/CoreBoy/serial/SerialEndpoint.cs index 631bc00..9459e7a 100644 --- a/CoreBoy/serial/SerialEndpoint.cs +++ b/CoreBoy/serial/SerialEndpoint.cs @@ -2,15 +2,18 @@ namespace CoreBoy.serial { public interface SerialEndpoint { + bool externalClockPulsed(); int transfer(int outgoing); } public class NullSerialEndpoint : SerialEndpoint { + public bool externalClockPulsed() => false; + public int transfer(int outgoing) { - return 0; + return (outgoing << 1) & 0xFF; } } } \ No newline at end of file diff --git a/CoreBoy/serial/SerialPort.cs b/CoreBoy/serial/SerialPort.cs index 350311b..b05aa40 100644 --- a/CoreBoy/serial/SerialPort.cs +++ b/CoreBoy/serial/SerialPort.cs @@ -12,8 +12,8 @@ public class SerialPort : IAddressSpace private readonly SpeedMode _speedMode; private int _sb; private int _sc; - private bool _transferInProgress; private int _divider; + private int _shiftClock; public SerialPort(InterruptManager interruptManager, SerialEndpoint serialEndpoint, SpeedMode speedMode) { @@ -24,25 +24,41 @@ public SerialPort(InterruptManager interruptManager, SerialEndpoint serialEndpoi public void Tick() { - if (!_transferInProgress) + if (!TransferInProgress) { return; } - if (++_divider >= Gameboy.TicksPerSec / 8192 / _speedMode.GetSpeedMode()) + if (++_divider >= Gameboy.TicksPerSec / 8192 / (FastMode ? 4 : 1) / _speedMode.GetSpeedMode()) { - _transferInProgress = false; - try + var clockPulsed = false; + if (InternalClockEnabled || _serialEndpoint.externalClockPulsed()) { - _sb = _serialEndpoint.transfer(_sb); + _shiftClock++; + clockPulsed = true; } - catch (IOException e) + + if (_shiftClock >= 8) + { + TransferInProgress = false; + _interruptManager.RequestInterrupt(InterruptManager.InterruptType.Serial); + return; + } + + if (clockPulsed) { - Debug.WriteLine($"Can't transfer byte {e}"); - _sb = 0; + try + { + _sb = _serialEndpoint.transfer(_sb); + } + catch (IOException e) + { + Debug.WriteLine($"Can't transfer byte {e}"); + _sb = 0; + } } - _interruptManager.RequestInterrupt(InterruptManager.InterruptType.Serial); + _divider = 0; } } @@ -53,17 +69,15 @@ public bool Accepts(int address) public void SetByte(int address, int value) { - if (address == 0xff01) + if (address == 0xff01 && !TransferInProgress) { _sb = value; } else if (address == 0xff02) { - _sc = value; - if ((_sc & (1 << 7)) != 0) - { - StartTransfer(); - } + TransferInProgress = value.GetBit(7); + FastMode = value.GetBit(1); + InternalClockEnabled = value.GetBit(0); } } @@ -83,10 +97,54 @@ public int GetByte(int address) } } - private void StartTransfer() + private bool TransferInProgress + { + get => (_sc & (1 << 7)) != 0; + set + { + if (value) + { + _sc = _sc.SetBit(7); + _divider = 0; + _shiftClock = 0; + } + else + { + _sc = _sc.ClearBit(7); + } + } + } + + private bool FastMode { - _transferInProgress = true; - _divider = 0; + get => (_sc & 2) != 0; + set + { + if (value) + { + _sc = _sc.SetBit(1); + } + else + { + _sc = _sc.ClearBit(1); + } + } + } + + private bool InternalClockEnabled + { + get => (_sc & 1) != 0; + set + { + if (value) + { + _sc = _sc.SetBit(0); + } + else + { + _sc = _sc.ClearBit(0); + } + } } } } \ No newline at end of file