diff --git a/Assets/Plugins/Essgee.meta b/Assets/Plugins/Essgee.meta
new file mode 100644
index 0000000..d4cbc45
--- /dev/null
+++ b/Assets/Plugins/Essgee.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 88819cf01cddd694b9da30d934d2a564
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/AppEnvironment.cs b/Assets/Plugins/Essgee/AppEnvironment.cs
new file mode 100644
index 0000000..825b9be
--- /dev/null
+++ b/Assets/Plugins/Essgee/AppEnvironment.cs
@@ -0,0 +1,17 @@
+using UnityEngine;
+
+public static class AppEnvironment
+{
+#if DEBUG
+    public static readonly bool DebugMode = true;
+#else
+			public static readonly bool DebugMode = false;
+#endif
+    public static readonly bool EnableCustomUnhandledExceptionHandler = true;
+    public static readonly bool TemporaryDisableCustomExceptionForm = false;
+
+    public static readonly bool EnableLogger = false;
+    public static readonly bool EnableSuperSlowCPULogger = false;
+
+    public static readonly bool EnableOpenGLDebug = false;
+}
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/AppEnvironment.cs.meta b/Assets/Plugins/Essgee/AppEnvironment.cs.meta
new file mode 100644
index 0000000..197f722
--- /dev/null
+++ b/Assets/Plugins/Essgee/AppEnvironment.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ce38d9b5f910b4c4185e3fab7eb45ecf
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Configuration.cs b/Assets/Plugins/Essgee/Configuration.cs
new file mode 100644
index 0000000..b41c4a1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Configuration.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.Drawing;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Utilities;
+
+namespace Essgee
+{
+	public class Configuration
+	{
+		public const int RecentFilesCapacity = 15;
+		public const string DefaultShaderName = "Basic";
+
+		public bool LimitFps { get; set; }
+		public bool ShowFps { get; set; }
+		public bool Mute { get; set; }
+		public float Volume { get; set; }
+		public int SampleRate { get; set; }
+		public bool LowPassFilter { get; set; }
+		public int ScreenSize { get; set; }
+		[JsonConverter(typeof(StringEnumConverter))]
+		public ScreenSizeMode ScreenSizeMode { get; set; }
+		public string LastShader { get; set; }
+		public bool EnableXInput { get; set; }
+		public bool EnableRumble { get; set; }
+		public bool AutoPause { get; set; }
+
+		public List<string> RecentFiles { get; set; }
+
+		[JsonConverter(typeof(InterfaceDictionaryConverter<IConfiguration>))]
+		public Dictionary<string, IConfiguration> Machines { get; set; }
+
+		public Dictionary<string, Point> DebugWindows { get; set; }
+
+		public Configuration()
+		{
+			LimitFps = true;
+			ShowFps = false;
+			Mute = false;
+			Volume = 1.0f;
+			SampleRate = 44100;
+			LowPassFilter = true;
+			ScreenSize = 2;
+			ScreenSizeMode = ScreenSizeMode.Scale;
+			LastShader = DefaultShaderName;
+			EnableXInput = false;
+			EnableRumble = false;
+			AutoPause = true;
+
+			RecentFiles = new List<string>(RecentFilesCapacity);
+
+			Machines = new Dictionary<string, IConfiguration>();
+
+			DebugWindows = new Dictionary<string, Point>();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Configuration.cs.meta b/Assets/Plugins/Essgee/Configuration.cs.meta
new file mode 100644
index 0000000..575b88c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Configuration.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1cb6affe10cc31441b6301f115fbc2ab
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation.meta b/Assets/Plugins/Essgee/Emulation.meta
new file mode 100644
index 0000000..309068c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 289897a5723c6484ca800ed09fb528f5
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Audio.meta b/Assets/Plugins/Essgee/Emulation/Audio.meta
new file mode 100644
index 0000000..73a47ea
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 957a410fe17ff6242ab46d90146f525d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs
new file mode 100644
index 0000000..b1bbc7f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class CGBAudio
+	{
+		public class CGBWave : Wave, IDMGAudioChannel
+		{
+			public override void Reset()
+			{
+				base.Reset();
+
+				for (var i = 0; i < sampleBuffer.Length; i += 2)
+				{
+					sampleBuffer[i + 0] = 0x00;
+					sampleBuffer[i + 1] = 0xFF;
+				}
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs.meta
new file mode 100644
index 0000000..23d2fff
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.Wave.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 595436ed6c508904399695fee24279ff
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs
new file mode 100644
index 0000000..9f32f39
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class CGBAudio : DMGAudio, IAudio
+	{
+		public CGBAudio()
+		{
+			channelSampleBuffer = new List<short>[numChannels];
+			for (int i = 0; i < numChannels; i++) channelSampleBuffer[i] = new List<short>();
+
+			mixedSampleBuffer = new List<short>();
+
+			channel1 = new Square(true);
+			channel2 = new Square(false);
+			channel3 = new CGBWave();
+			channel4 = new Noise();
+
+			samplesPerFrame = cyclesPerFrame = cyclesPerSample = -1;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs.meta
new file mode 100644
index 0000000..58d2261
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/CGBAudio.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 40f2c16e17787974881d5614068c46b7
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs
new file mode 100644
index 0000000..0ba3a39
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs
@@ -0,0 +1,218 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class DMGAudio
+	{
+		public class Noise : IDMGAudioChannel
+		{
+			static readonly int[] divisors = new int[]
+			{
+				8, 16, 32, 48, 64, 80, 96, 112
+			};
+
+			// NR41
+			byte lengthLoad;
+
+			// NR42
+			byte envelopeStartingVolume, envelopePeriodReload;
+			bool envelopeAddMode;
+
+			// NR43
+			byte clockShift, divisorCode;
+			bool lfsrWidthMode;
+
+			// NR44
+			bool trigger, lengthEnable;
+
+			//
+
+			// Noise
+			int noiseCounter;
+			ushort lfsr;
+
+			// Envelope
+			int volume, envelopeCounter;
+			bool isEnvelopeUpdateEnabled;
+
+			// Misc
+			bool isChannelEnabled, isDacEnabled;
+			int lengthCounter;
+
+			public int OutputVolume { get; private set; }
+
+			public bool IsActive { get { return lengthCounter != 0; } }
+
+			public Noise()
+			{
+				//
+			}
+
+			public void Reset()
+			{
+				noiseCounter = 0;
+				lfsr = 0;
+
+				volume = 15;
+				envelopeCounter = 0;
+				isEnvelopeUpdateEnabled = false;
+
+				isChannelEnabled = isDacEnabled = false;
+				lengthCounter = 0;
+
+				OutputVolume = volume;
+			}
+
+			public void LengthCounterClock()
+			{
+				if (lengthCounter > 0 && lengthEnable)
+				{
+					lengthCounter--;
+					if (lengthCounter == 0)
+						isChannelEnabled = false;
+				}
+			}
+
+			public void SweepClock()
+			{
+				throw new Exception("Channel type does not support sweep");
+			}
+
+			public void VolumeEnvelopeClock()
+			{
+				envelopeCounter--;
+				if (envelopeCounter == 0)
+				{
+					envelopeCounter = envelopePeriodReload;
+
+					if (isEnvelopeUpdateEnabled)
+					{
+						var newVolume = volume;
+						if (envelopeAddMode) newVolume++;
+						else newVolume--;
+
+						if (newVolume >= 0 && newVolume <= 15)
+							volume = newVolume;
+						else
+							isEnvelopeUpdateEnabled = false;
+					}
+				}
+			}
+
+			public void Step()
+			{
+				if (!isChannelEnabled) return;
+
+				noiseCounter--;
+				if (noiseCounter == 0)
+				{
+					noiseCounter = divisors[divisorCode] << clockShift;
+
+					var result = (lfsr & 0b1) ^ ((lfsr >> 1) & 0b1);
+					lfsr = (ushort)((lfsr >> 1) | (result << 14));
+
+					if (lfsrWidthMode)
+						lfsr = (ushort)((lfsr & 0b10111111) | (result << 6));
+				}
+
+				OutputVolume = isDacEnabled && ((lfsr & 0b1) == 0) ? volume : 0;
+			}
+
+			private void Trigger()
+			{
+				isChannelEnabled = true;
+
+				if (lengthCounter == 0) lengthCounter = 64;
+
+				noiseCounter = divisors[divisorCode] << clockShift;
+				volume = envelopeStartingVolume;
+				envelopeCounter = envelopePeriodReload;
+				isEnvelopeUpdateEnabled = true;
+
+				lfsr = 0x7FFF;
+			}
+
+			public void WritePort(byte port, byte value)
+			{
+				switch (port)
+				{
+					case 0:
+						break;
+
+					case 1:
+						lengthLoad = (byte)((value >> 0) & 0b111111);
+
+						lengthCounter = 64 - lengthLoad;
+						break;
+
+					case 2:
+						envelopeStartingVolume = (byte)((value >> 4) & 0b1111);
+						envelopeAddMode = ((value >> 3) & 0b1) == 0b1;
+						envelopePeriodReload = (byte)((value >> 0) & 0b111);
+
+						isDacEnabled = ((value >> 3) & 0b11111) != 0;
+						break;
+
+					case 3:
+						clockShift = (byte)((value >> 4) & 0b1111);
+						lfsrWidthMode = ((value >> 3) & 0b1) == 0b1;
+						divisorCode = (byte)((value >> 0) & 0b111);
+						break;
+
+					case 4:
+						trigger = ((value >> 7) & 0b1) == 0b1;
+						lengthEnable = ((value >> 6) & 0b1) == 0b1;
+
+						if (trigger) Trigger();
+						break;
+				}
+			}
+
+			public byte ReadPort(byte port)
+			{
+				switch (port)
+				{
+					case 0:
+						return 0xFF;
+
+					case 1:
+						return 0xFF;
+
+					case 2:
+						return (byte)(
+							(envelopeStartingVolume << 4) |
+							(envelopeAddMode ? (1 << 3) : 0) |
+							(envelopePeriodReload << 0));
+
+					case 3:
+						return (byte)(
+							(clockShift << 4) |
+							(lfsrWidthMode ? (1 << 3) : 0) |
+							(divisorCode << 0));
+
+					case 4:
+						return (byte)(
+							0xBF |
+							(lengthEnable ? (1 << 6) : 0));
+
+					default:
+						return 0xFF;
+				}
+			}
+
+			public void WriteWaveRam(byte offset, byte value)
+			{
+				throw new Exception("Channel type does have Wave RAM");
+			}
+
+			public byte ReadWaveRam(byte offset)
+			{
+				throw new Exception("Channel type does have Wave RAM");
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs.meta
new file mode 100644
index 0000000..8d27ee9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Noise.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0992ef0e8260e124c9667aea4433025c
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs
new file mode 100644
index 0000000..fff6359
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs
@@ -0,0 +1,273 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class DMGAudio
+	{
+		public class Square : IDMGAudioChannel
+		{
+			static readonly bool[,] dutyCycleTable = new bool[,]
+			{
+				{ false, false, false, false, false, false, false, true,  },	// 00000001    12.5%
+				{ true,  false, false, false, false, false, false, true,  },	// 10000001    25%
+				{ true,  false, false, false, false, true,  true,  true,  },	// 10000111    50%
+				{ false, true,  true,  true,  true,  true,  true,  false, }		// 01111110    75%
+			};
+
+			// NR10/20
+			byte sweepPeriodReload, sweepShift;
+			bool sweepNegate;
+
+			// NR11/21
+			byte dutyCycle, lengthLoad;
+
+			// NR12/22
+			byte envelopeStartingVolume, envelopePeriodReload;
+			bool envelopeAddMode;
+
+			// NR13/23
+			byte frequencyLSB;
+
+			// NR14/24
+			bool trigger, lengthEnable;
+			byte frequencyMSB;
+
+			//
+
+			readonly bool channelSupportsSweep;
+
+			// Sweep
+			bool isSweepEnabled;
+			int sweepCounter, sweepFreqShadow;
+
+			// Frequency
+			int frequencyCounter;
+
+			// Envelope
+			int volume, envelopeCounter;
+			bool isEnvelopeUpdateEnabled;
+
+			// Misc
+			bool isChannelEnabled, isDacEnabled;
+			int lengthCounter, dutyCounter;
+
+			public int OutputVolume { get; private set; }
+
+			public bool IsActive { get { return lengthCounter != 0; } }
+
+			public Square(bool hasSweep)
+			{
+				channelSupportsSweep = hasSweep;
+			}
+
+			public void Reset()
+			{
+				isSweepEnabled = false;
+				sweepCounter = sweepFreqShadow = 0;
+
+				frequencyCounter = 0;
+
+				volume = 15;
+				envelopeCounter = 0;
+				isEnvelopeUpdateEnabled = false;
+
+				isChannelEnabled = isDacEnabled = false;
+				lengthCounter = dutyCounter = 0;
+
+				OutputVolume = volume;
+			}
+
+			public void LengthCounterClock()
+			{
+				if (lengthCounter > 0 && lengthEnable)
+				{
+					lengthCounter--;
+					if (lengthCounter == 0)
+						isChannelEnabled = false;
+				}
+			}
+
+			public void SweepClock()
+			{
+				if (!channelSupportsSweep) return;
+
+				sweepCounter--;
+				if (sweepCounter == 0)
+				{
+					sweepCounter = sweepPeriodReload;
+
+					if (isSweepEnabled && sweepPeriodReload != 0)
+					{
+						var newFrequency = PerformSweepCalculations();
+						if (newFrequency <= 2047 && sweepShift != 0)
+						{
+							sweepFreqShadow = newFrequency;
+							frequencyMSB = (byte)((newFrequency >> 8) & 0b111);
+							frequencyLSB = (byte)(newFrequency & 0xFF);
+							PerformSweepCalculations();
+						}
+					}
+				}
+			}
+
+			public void VolumeEnvelopeClock()
+			{
+				envelopeCounter--;
+				if (envelopeCounter == 0)
+				{
+					envelopeCounter = envelopePeriodReload;
+
+					if (isEnvelopeUpdateEnabled)
+					{
+						var newVolume = volume;
+						if (envelopeAddMode) newVolume++;
+						else newVolume--;
+
+						if (newVolume >= 0 && newVolume <= 15)
+							volume = newVolume;
+						else
+							isEnvelopeUpdateEnabled = false;
+					}
+				}
+			}
+
+			public void Step()
+			{
+				if (!isChannelEnabled) return;
+
+				frequencyCounter--;
+				if (frequencyCounter == 0)
+				{
+					frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 4;
+					dutyCounter++;
+					dutyCounter %= 8;
+				}
+
+				OutputVolume = isDacEnabled && dutyCycleTable[dutyCycle, dutyCounter] ? volume : 0;
+			}
+
+			private void Trigger()
+			{
+				isChannelEnabled = true;
+
+				if (lengthCounter == 0) lengthCounter = 64;
+
+				frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 4;
+				volume = envelopeStartingVolume;
+				envelopeCounter = envelopePeriodReload;
+				isEnvelopeUpdateEnabled = true;
+
+				if (channelSupportsSweep)
+				{
+					sweepFreqShadow = (frequencyMSB << 8) | frequencyLSB;
+					sweepCounter = sweepPeriodReload;
+					isSweepEnabled = sweepPeriodReload != 0 || sweepShift != 0;
+					if (sweepShift != 0)
+						PerformSweepCalculations();
+				}
+			}
+
+			private int PerformSweepCalculations()
+			{
+				var newFrequency = sweepFreqShadow >> sweepShift;
+				if (sweepNegate) newFrequency = -newFrequency;
+				newFrequency += sweepFreqShadow;
+				if (newFrequency > 2047) isChannelEnabled = false;
+				return newFrequency;
+			}
+
+			public void WritePort(byte port, byte value)
+			{
+				switch (port)
+				{
+					case 0:
+						if (channelSupportsSweep)
+						{
+							sweepPeriodReload = (byte)((value >> 4) & 0b111);
+							sweepNegate = ((value >> 3) & 0b1) == 0b1;
+							sweepShift = (byte)((value >> 0) & 0b111);
+						}
+						break;
+
+					case 1:
+						dutyCycle = (byte)((value >> 6) & 0b11);
+						lengthLoad = (byte)((value >> 0) & 0b111111);
+
+						lengthCounter = 64 - lengthLoad;
+						break;
+
+					case 2:
+						envelopeStartingVolume = (byte)((value >> 4) & 0b1111);
+						envelopeAddMode = ((value >> 3) & 0b1) == 0b1;
+						envelopePeriodReload = (byte)((value >> 0) & 0b111);
+
+						isDacEnabled = ((value >> 3) & 0b11111) != 0;
+						break;
+
+					case 3:
+						frequencyLSB = value;
+						break;
+
+					case 4:
+						trigger = ((value >> 7) & 0b1) == 0b1;
+						lengthEnable = ((value >> 6) & 0b1) == 0b1;
+						frequencyMSB = (byte)((value >> 0) & 0b111);
+
+						if (trigger) Trigger();
+						break;
+				}
+			}
+
+			public byte ReadPort(byte port)
+			{
+				switch (port)
+				{
+					case 0:
+						if (channelSupportsSweep)
+						{
+							return (byte)(
+								0x80 |
+								(sweepPeriodReload << 4) |
+								(sweepNegate ? (1 << 3) : 0) |
+								(sweepShift << 0));
+						}
+						else
+							return 0xFF;
+
+					case 1:
+						return (byte)(
+							0x3F |
+							(dutyCycle << 6));
+
+					case 2:
+						return (byte)(
+							(envelopeStartingVolume << 4) |
+							(envelopeAddMode ? (1 << 3) : 0) |
+							(envelopePeriodReload << 0));
+
+					case 4:
+						return (byte)(
+							0xBF |
+							(lengthEnable ? (1 << 6) : 0));
+
+					default:
+						return 0xFF;
+				}
+			}
+
+			public void WriteWaveRam(byte offset, byte value)
+			{
+				throw new Exception("Channel type does have Wave RAM");
+			}
+
+			public byte ReadWaveRam(byte offset)
+			{
+				throw new Exception("Channel type does have Wave RAM");
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs.meta
new file mode 100644
index 0000000..639fff9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Square.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: eab50abebc5d5f341889e031ded6a571
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs
new file mode 100644
index 0000000..d5434a4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class DMGAudio
+	{
+		public class Wave : IDMGAudioChannel
+		{
+			// NR30
+			bool isDacEnabled;
+
+			// NR31
+			byte lengthLoad;
+
+			// NR32
+			byte volumeCode;
+
+			// NR33
+			byte frequencyLSB;
+
+			// NR34
+			bool trigger, lengthEnable;
+			byte frequencyMSB;
+
+			// Wave
+			protected byte[] sampleBuffer;
+			int frequencyCounter, positionCounter, volume;
+
+			// Misc
+			bool isChannelEnabled;
+			int lengthCounter;
+
+			public int OutputVolume { get; private set; }
+
+			public bool IsActive { get { return isDacEnabled; } }   // TODO: correct? lengthCounter check makes Zelda Oracle games hang
+
+			public Wave()
+			{
+				sampleBuffer = new byte[16];
+			}
+
+			public virtual void Reset()
+			{
+				for (var i = 0; i < sampleBuffer.Length; i++) sampleBuffer[i] = (byte)StandInfo.Random.Next(255);
+				frequencyCounter = positionCounter = 0;
+				volume = 15;
+
+				isChannelEnabled = isDacEnabled = false;
+				lengthCounter = 0;
+
+				OutputVolume = volume;
+			}
+
+			public void LengthCounterClock()
+			{
+				if (lengthCounter > 0 && lengthEnable)
+				{
+					lengthCounter--;
+					if (lengthCounter == 0)
+						isChannelEnabled = false;
+				}
+			}
+
+			public void SweepClock()
+			{
+				throw new Exception("Channel type does not support sweep");
+			}
+
+			public void VolumeEnvelopeClock()
+			{
+				throw new Exception("Channel type does not support envelope");
+			}
+
+			public void Step()
+			{
+				if (!isChannelEnabled) return;
+
+				frequencyCounter--;
+				if (frequencyCounter == 0)
+				{
+					frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 2;
+					positionCounter++;
+					positionCounter %= 32;
+
+					var value = sampleBuffer[positionCounter / 2];
+					if ((positionCounter & 0b1) == 0) value >>= 4;
+					value &= 0b1111;
+
+					if (volumeCode != 0)
+						volume = value >> (volumeCode - 1);
+					else
+						volume = 0;
+				}
+
+				OutputVolume = isDacEnabled ? volume : 0;
+			}
+
+			private void Trigger()
+			{
+				isChannelEnabled = true;
+
+				if (lengthCounter == 0) lengthCounter = 256;
+
+				frequencyCounter = (2048 - ((frequencyMSB << 8) | frequencyLSB)) * 2;
+				positionCounter = 0;
+			}
+
+			public void WritePort(byte port, byte value)
+			{
+				switch (port)
+				{
+					case 0:
+						isDacEnabled = ((value >> 7) & 0b1) == 0b1;
+						break;
+
+					case 1:
+						lengthLoad = value;
+
+						lengthCounter = 256 - lengthLoad;
+						break;
+
+					case 2:
+						volumeCode = (byte)((value >> 5) & 0b11);
+						break;
+
+					case 3:
+						frequencyLSB = value;
+						break;
+
+					case 4:
+						trigger = ((value >> 7) & 0b1) == 0b1;
+						lengthEnable = ((value >> 6) & 0b1) == 0b1;
+						frequencyMSB = (byte)((value >> 0) & 0b111);
+
+						if (trigger) Trigger();
+						break;
+				}
+			}
+
+			public byte ReadPort(byte port)
+			{
+				switch (port)
+				{
+					case 0:
+						return (byte)(
+							0x7F |
+							(isDacEnabled ? (1 << 7) : 0));
+
+					case 1:
+						return 0xFF;
+
+					case 2:
+						return (byte)(
+							0x9F |
+							(volumeCode << 5));
+
+					case 4:
+						return (byte)(
+							0xBF |
+							(lengthEnable ? (1 << 6) : 0));
+
+					default:
+						return 0xFF;
+				}
+			}
+
+			// TODO: more details on behavior on access w/ channel enabled
+
+			public void WriteWaveRam(byte offset, byte value)
+			{
+				if (!isDacEnabled)
+					sampleBuffer[offset & (sampleBuffer.Length - 1)] = value;
+				else
+					sampleBuffer[positionCounter & (sampleBuffer.Length - 1)] = value;
+			}
+
+			public byte ReadWaveRam(byte offset)
+			{
+				if (!isDacEnabled)
+					return sampleBuffer[offset & (sampleBuffer.Length - 1)];
+				else
+					return 0xFF;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs.meta
new file mode 100644
index 0000000..acfa2fa
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.Wave.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a9f4769005789914785d05d63578c931
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs
new file mode 100644
index 0000000..4a17e69
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs
@@ -0,0 +1,418 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Audio
+{
+	public partial class DMGAudio : IAudio
+	{
+		// https://gbdev.gg8.se/wiki/articles/Gameboy_sound_hardware
+		// http://emudev.de/gameboy-emulator/bleeding-ears-time-to-add-audio/
+		// https://github.com/GhostSonic21/GhostBoy/blob/master/GhostBoy/APU.cpp
+
+		protected const int numChannels = 4;
+
+		protected const string channel1OptionName = "AudioEnableCh1Square";
+		protected const string channel2OptionName = "AudioEnableCh2Square";
+		protected const string channel3OptionName = "AudioEnableCh3Wave";
+		protected const string channel4OptionName = "AudioEnableCh4Noise";
+
+		protected IDMGAudioChannel channel1, channel2, channel3, channel4;
+
+		// FF24 - NR50
+		byte[] volumeRightLeft;
+		bool[] vinEnableRightLeft;
+
+		// FF25 - NR51
+		bool[] channel1Enable, channel2Enable, channel3Enable, channel4Enable;
+
+		// FF26 - NR52
+		bool isSoundHwEnabled;
+
+		protected int frameSequencerReload, frameSequencerCounter, frameSequencer;
+
+		protected List<short>[] channelSampleBuffer;
+		protected List<short> mixedSampleBuffer;
+		public virtual event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples;
+		public virtual void OnEnqueueSamples(EnqueueSamplesEventArgs e) { EnqueueSamples?.Invoke(this, e); }
+
+		protected int sampleRate, numOutputChannels;
+
+		//
+
+		double clockRate, refreshRate;
+		protected int samplesPerFrame, cyclesPerFrame, cyclesPerSample;
+		[StateRequired]
+		int sampleCycleCount, frameCycleCount;
+
+		protected bool channel1ForceEnable, channel2ForceEnable, channel3ForceEnable, channel4ForceEnable;
+
+		public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[]
+		{
+			(channel1OptionName, "Channel 1 (Square)"),
+			(channel2OptionName, "Channel 2 (Square)"),
+			(channel3OptionName, "Channel 3 (Wave)"),
+			(channel4OptionName, "Channel 4 (Noise)")
+		};
+
+		public DMGAudio()
+		{
+			channelSampleBuffer = new List<short>[numChannels];
+			for (int i = 0; i < numChannels; i++) channelSampleBuffer[i] = new List<short>();
+
+			mixedSampleBuffer = new List<short>();
+
+			channel1 = new Square(true);
+			channel2 = new Square(false);
+			channel3 = new Wave();
+			channel4 = new Noise();
+
+			samplesPerFrame = cyclesPerFrame = cyclesPerSample = -1;
+
+			channel1ForceEnable = true;
+			channel2ForceEnable = true;
+			channel3ForceEnable = true;
+			channel4ForceEnable = true;
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			switch (name)
+			{
+				case channel1OptionName: return channel1ForceEnable;
+				case channel2OptionName: return channel2ForceEnable;
+				case channel3OptionName: return channel3ForceEnable;
+				case channel4OptionName: return channel4ForceEnable;
+				default: return null;
+			}
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			switch (name)
+			{
+				case channel1OptionName: channel1ForceEnable = (bool)value; break;
+				case channel2OptionName: channel2ForceEnable = (bool)value; break;
+				case channel3OptionName: channel3ForceEnable = (bool)value; break;
+				case channel4OptionName: channel4ForceEnable = (bool)value; break;
+			}
+		}
+
+		public void SetSampleRate(int rate)
+		{
+			sampleRate = rate;
+			ConfigureTimings();
+		}
+
+		public void SetOutputChannels(int channels)
+		{
+			numOutputChannels = channels;
+			ConfigureTimings();
+		}
+
+		public void SetClockRate(double clock)
+		{
+			clockRate = clock;
+			ConfigureTimings();
+		}
+
+		public void SetRefreshRate(double refresh)
+		{
+			refreshRate = refresh;
+			ConfigureTimings();
+		}
+
+		private void ConfigureTimings()
+		{
+			samplesPerFrame = (int)(sampleRate / refreshRate);
+			cyclesPerFrame = (int)Math.Round(clockRate / refreshRate);
+			cyclesPerSample = (cyclesPerFrame / samplesPerFrame);
+
+			volumeRightLeft = new byte[numOutputChannels];
+			vinEnableRightLeft = new bool[numOutputChannels];
+
+			channel1Enable = new bool[numOutputChannels];
+			channel2Enable = new bool[numOutputChannels];
+			channel3Enable = new bool[numOutputChannels];
+			channel4Enable = new bool[numOutputChannels];
+
+			FlushSamples();
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			if (samplesPerFrame == -1) throw new EmulationException("GB PSG: Timings not configured, invalid samples per frame");
+			if (cyclesPerFrame == -1) throw new EmulationException("GB PSG: Timings not configured, invalid cycles per frame");
+			if (cyclesPerSample == -1) throw new EmulationException("GB PSG: Timings not configured, invalid cycles per sample");
+		}
+
+		public virtual void Shutdown()
+		{
+			//
+		}
+
+		public virtual void Reset()
+		{
+			FlushSamples();
+
+			channel1.Reset();
+			channel2.Reset();
+			channel3.Reset();
+			channel4.Reset();
+
+			for (var i = 0; i < numOutputChannels; i++)
+			{
+				volumeRightLeft[i] = 0;
+				vinEnableRightLeft[i] = false;
+
+				channel1Enable[i] = false;
+				channel2Enable[i] = false;
+				channel3Enable[i] = false;
+				channel4Enable[i] = false;
+			}
+
+			frameSequencerReload = (int)(clockRate / 512);
+			frameSequencerCounter = frameSequencerReload;
+			frameSequencer = 0;
+
+			sampleCycleCount = frameCycleCount = 0;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			if (!isSoundHwEnabled) return;
+
+			sampleCycleCount += clockCyclesInStep;
+			frameCycleCount += clockCyclesInStep;
+
+			for (int i = 0; i < clockCyclesInStep; i++)
+			{
+				frameSequencerCounter--;
+				if (frameSequencerCounter == 0)
+				{
+					frameSequencerCounter = frameSequencerReload;
+
+					switch (frameSequencer)
+					{
+						case 0:
+							channel1.LengthCounterClock();
+							channel2.LengthCounterClock();
+							channel3.LengthCounterClock();
+							channel4.LengthCounterClock();
+							break;
+
+						case 1:
+							break;
+
+						case 2:
+							channel1.SweepClock();
+							channel1.LengthCounterClock();
+							channel2.LengthCounterClock();
+							channel3.LengthCounterClock();
+							channel4.LengthCounterClock();
+							break;
+
+						case 3:
+							break;
+
+						case 4:
+							channel1.LengthCounterClock();
+							channel2.LengthCounterClock();
+							channel3.LengthCounterClock();
+							channel4.LengthCounterClock();
+							break;
+
+						case 5:
+							break;
+
+						case 6:
+							channel1.SweepClock();
+							channel1.LengthCounterClock();
+							channel2.LengthCounterClock();
+							channel3.LengthCounterClock();
+							channel4.LengthCounterClock();
+							break;
+
+						case 7:
+							channel1.VolumeEnvelopeClock();
+							channel2.VolumeEnvelopeClock();
+							channel4.VolumeEnvelopeClock();
+							break;
+					}
+
+					frameSequencer++;
+					if (frameSequencer >= 8)
+						frameSequencer = 0;
+				}
+
+				channel1.Step();
+				channel2.Step();
+				channel3.Step();
+				channel4.Step();
+			}
+
+			if (sampleCycleCount >= cyclesPerSample)
+			{
+				GenerateSample();
+
+				sampleCycleCount -= cyclesPerSample;
+			}
+
+			if (mixedSampleBuffer.Count >= (samplesPerFrame * numOutputChannels))
+			{
+				OnEnqueueSamples(new EnqueueSamplesEventArgs(
+					numChannels,
+					channelSampleBuffer.Select(x => x.ToArray()).ToArray(),
+					new bool[] { !channel1ForceEnable, !channel2ForceEnable, !channel3ForceEnable, !channel4ForceEnable },
+					mixedSampleBuffer.ToArray()));
+
+				FlushSamples();
+			}
+
+			if (frameCycleCount >= cyclesPerFrame)
+			{
+				frameCycleCount -= cyclesPerFrame;
+				sampleCycleCount = frameCycleCount;
+			}
+		}
+
+		protected virtual void GenerateSample()
+		{
+			for (int i = 0; i < numOutputChannels; i++)
+			{
+				/* Generate samples */
+				var ch1 = (short)(((channel1Enable[i] ? channel1.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8);
+				var ch2 = (short)(((channel2Enable[i] ? channel2.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8);
+				var ch3 = (short)(((channel3Enable[i] ? channel3.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8);
+				var ch4 = (short)(((channel4Enable[i] ? channel4.OutputVolume : 0) * (volumeRightLeft[i] + 1)) << 8);
+
+				channelSampleBuffer[0].Add(ch1);
+				channelSampleBuffer[1].Add(ch2);
+				channelSampleBuffer[2].Add(ch3);
+				channelSampleBuffer[3].Add(ch4);
+
+				/* Mix samples */
+				var mixed = 0;
+				if (channel1ForceEnable) mixed += ch1;
+				if (channel2ForceEnable) mixed += ch2;
+				if (channel3ForceEnable) mixed += ch3;
+				if (channel4ForceEnable) mixed += ch4;
+				mixed /= numChannels;
+
+				mixedSampleBuffer.Add((short)mixed);
+			}
+		}
+
+		public void FlushSamples()
+		{
+			for (int i = 0; i < numChannels; i++)
+				channelSampleBuffer[i].Clear();
+
+			mixedSampleBuffer.Clear();
+		}
+
+		public virtual byte ReadPort(byte port)
+		{
+			// Channels
+			if (port >= 0x10 && port <= 0x14)
+				return channel1.ReadPort((byte)(port - 0x10));
+			else if (port >= 0x15 && port <= 0x19)
+				return channel2.ReadPort((byte)(port - 0x15));
+			else if (port >= 0x1A && port <= 0x1E)
+				return channel3.ReadPort((byte)(port - 0x1A));
+			else if (port >= 0x1F && port <= 0x23)
+				return channel4.ReadPort((byte)(port - 0x1F));
+
+			// Channel 3 Wave RAM
+			else if (port >= 0x30 && port <= 0x3F)
+				return channel3.ReadWaveRam((byte)(port - 0x30));
+
+			// Control ports
+			else
+				switch (port)
+				{
+					case 0x24:
+						return (byte)(
+							(vinEnableRightLeft[1] ? (1 << 7) : 0) |
+							(volumeRightLeft[1] << 4) |
+							(vinEnableRightLeft[0] ? (1 << 3) : 0) |
+							(volumeRightLeft[0] << 0));
+
+					case 0x25:
+						return (byte)(
+							(channel4Enable[1] ? (1 << 7) : 0) |
+							(channel3Enable[1] ? (1 << 6) : 0) |
+							(channel2Enable[1] ? (1 << 5) : 0) |
+							(channel1Enable[1] ? (1 << 4) : 0) |
+							(channel4Enable[0] ? (1 << 3) : 0) |
+							(channel3Enable[0] ? (1 << 2) : 0) |
+							(channel2Enable[0] ? (1 << 1) : 0) |
+							(channel1Enable[0] ? (1 << 0) : 0));
+
+					case 0x26:
+						return (byte)(
+							0x70 |
+							(isSoundHwEnabled ? (1 << 7) : 0) |
+							(channel4.IsActive ? (1 << 3) : 0) |
+							(channel3.IsActive ? (1 << 2) : 0) |
+							(channel2.IsActive ? (1 << 1) : 0) |
+							(channel1.IsActive ? (1 << 0) : 0));
+
+					default:
+						return 0xFF;
+				}
+		}
+
+		public virtual void WritePort(byte port, byte value)
+		{
+			// Channels
+			if (port >= 0x10 && port <= 0x14)
+				channel1.WritePort((byte)(port - 0x10), value);
+			else if (port >= 0x15 && port <= 0x19)
+				channel2.WritePort((byte)(port - 0x15), value);
+			else if (port >= 0x1A && port <= 0x1E)
+				channel3.WritePort((byte)(port - 0x1A), value);
+			else if (port >= 0x1F && port <= 0x23)
+				channel4.WritePort((byte)(port - 0x1F), value);
+
+			// Channel 3 Wave RAM
+			else if (port >= 0x30 && port <= 0x3F)
+				channel3.WriteWaveRam((byte)(port - 0x30), value);
+
+			// Control ports
+			else
+				switch (port)
+				{
+					case 0x24:
+						vinEnableRightLeft[1] = ((value >> 7) & 0b1) == 0b1;
+						volumeRightLeft[1] = (byte)((value >> 4) & 0b111);
+						vinEnableRightLeft[0] = ((value >> 3) & 0b1) == 0b1;
+						volumeRightLeft[0] = (byte)((value >> 0) & 0b111);
+						break;
+
+					case 0x25:
+						channel4Enable[1] = ((value >> 7) & 0b1) == 0b1;
+						channel3Enable[1] = ((value >> 6) & 0b1) == 0b1;
+						channel2Enable[1] = ((value >> 5) & 0b1) == 0b1;
+						channel1Enable[1] = ((value >> 4) & 0b1) == 0b1;
+						channel4Enable[0] = ((value >> 3) & 0b1) == 0b1;
+						channel3Enable[0] = ((value >> 2) & 0b1) == 0b1;
+						channel2Enable[0] = ((value >> 1) & 0b1) == 0b1;
+						channel1Enable[0] = ((value >> 0) & 0b1) == 0b1;
+						break;
+
+					case 0x26:
+						isSoundHwEnabled = ((value >> 7) & 0b1) == 0b1;
+						break;
+				}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs.meta
new file mode 100644
index 0000000..d9dc4d6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/DMGAudio.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b1912726afeeb8a48bd9e35bf55fe8e3
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs b/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs
new file mode 100644
index 0000000..3254c45
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.Audio
+{
+	interface IAudio
+	{
+		event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples;
+		void OnEnqueueSamples(EnqueueSamplesEventArgs e);
+
+		(string Name, string Description)[] RuntimeOptions { get; }
+
+		object GetRuntimeOption(string name);
+		void SetRuntimeOption(string name, object value);
+
+		void Startup();
+		void Shutdown();
+		void Reset();
+		void Step(int clockCyclesInStep);
+
+		void SetSampleRate(int rate);
+		void SetOutputChannels(int channels);
+		void SetClockRate(double clock);
+		void SetRefreshRate(double refresh);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs.meta
new file mode 100644
index 0000000..ced8ebc
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/IAudio.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c7d71d4c287c7024f88c3e76da01431f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs b/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs
new file mode 100644
index 0000000..8a37c6e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Audio
+{
+	public interface IDMGAudioChannel
+	{
+		int OutputVolume { get; }
+		bool IsActive { get; }
+
+		void Reset();
+		void LengthCounterClock();
+		void SweepClock();
+		void VolumeEnvelopeClock();
+		void Step();
+
+		void WritePort(byte port, byte value);
+		byte ReadPort(byte port);
+		void WriteWaveRam(byte offset, byte value);
+		byte ReadWaveRam(byte offset);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs.meta
new file mode 100644
index 0000000..beebd85
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/IDMGAudioChannel.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b23834762d9307449a8e4b923fb2688b
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs b/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs
new file mode 100644
index 0000000..41dd4c6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs
@@ -0,0 +1,387 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Audio
+{
+	public class SN76489 : IAudio
+	{
+		/* http://www.smspower.org/Development/SN76489 */
+		/* Differences in various system's PSGs: http://forums.nesdev.com/viewtopic.php?p=190216#p190216 */
+
+		protected const int numChannels = 4, numToneChannels = 3, noiseChannelIndex = 3;
+
+		protected const string channel1OptionName = "AudioEnableCh1Square";
+		protected const string channel2OptionName = "AudioEnableCh2Square";
+		protected const string channel3OptionName = "AudioEnableCh3Square";
+		protected const string channel4OptionName = "AudioEnableCh4Noise";
+
+		/* Noise generation constants */
+		protected virtual ushort noiseLfsrMask => 0x7FFF;
+		protected virtual ushort noiseTappedBits => 0x0003;     /* Bits 0 and 1 */
+		protected virtual int noiseBitShift => 14;
+
+		/* Sample generation & event handling */
+		protected List<short>[] channelSampleBuffer;
+		protected List<short> mixedSampleBuffer;
+		public virtual event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples;
+		public virtual void OnEnqueueSamples(EnqueueSamplesEventArgs e) { EnqueueSamples?.Invoke(this, e); }
+
+		/* Audio output variables */
+		protected int sampleRate, numOutputChannels;
+
+		/* Channel registers */
+		[StateRequired]
+		protected ushort[] volumeRegisters;     /* Channels 0-3: 4 bits */
+		[StateRequired]
+		protected ushort[] toneRegisters;       /* Channels 0-2 (tone): 10 bits; channel 3 (noise): 3 bits */
+
+		/* Channel counters */
+		[StateRequired]
+		protected ushort[] channelCounters;     /* 10-bit counters */
+		[StateRequired]
+		protected bool[] channelOutput;
+
+		/* Volume attenuation table */
+		protected short[] volumeTable;          /* 2dB change per volume register step */
+
+		/* Latched channel/type */
+		[StateRequired]
+		byte latchedChannel, latchedType;
+
+		/* Linear-feedback shift register, for noise generation */
+		[StateRequired]
+		protected ushort noiseLfsr;             /* 15-bit */
+
+		/* Timing variables */
+		double clockRate, refreshRate;
+		int samplesPerFrame, cyclesPerFrame, cyclesPerSample;
+		[StateRequired]
+		int sampleCycleCount, frameCycleCount, dividerCount;
+
+		/* User-facing channel toggles */
+		protected bool channel1ForceEnable, channel2ForceEnable, channel3ForceEnable, channel4ForceEnable;
+
+		public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[]
+		{
+			(channel1OptionName, "Channel 1 (Square)"),
+			(channel2OptionName, "Channel 2 (Square)"),
+			(channel3OptionName, "Channel 3 (Square)"),
+			(channel4OptionName, "Channel 4 (Noise)")
+		};
+
+		public SN76489()
+		{
+			channelSampleBuffer = new List<short>[numChannels];
+			for (int i = 0; i < numChannels; i++) channelSampleBuffer[i] = new List<short>();
+
+			mixedSampleBuffer = new List<short>();
+
+			volumeRegisters = new ushort[numChannels];
+			toneRegisters = new ushort[numChannels];
+
+			channelCounters = new ushort[numChannels];
+			channelOutput = new bool[numChannels];
+
+			volumeTable = new short[16];
+			for (int i = 0; i < volumeTable.Length; i++)
+				volumeTable[i] = (short)(short.MaxValue * Math.Pow(2.0, i * -2.0 / 6.0));
+			volumeTable[15] = 0;
+
+			samplesPerFrame = cyclesPerFrame = cyclesPerSample = -1;
+
+			channel1ForceEnable = true;
+			channel2ForceEnable = true;
+			channel3ForceEnable = true;
+			channel4ForceEnable = true;
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			switch (name)
+			{
+				case channel1OptionName: return channel1ForceEnable;
+				case channel2OptionName: return channel2ForceEnable;
+				case channel3OptionName: return channel3ForceEnable;
+				case channel4OptionName: return channel4ForceEnable;
+				default: return null;
+			}
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			switch (name)
+			{
+				case channel1OptionName: channel1ForceEnable = (bool)value; break;
+				case channel2OptionName: channel2ForceEnable = (bool)value; break;
+				case channel3OptionName: channel3ForceEnable = (bool)value; break;
+				case channel4OptionName: channel4ForceEnable = (bool)value; break;
+			}
+		}
+
+		public void SetSampleRate(int rate)
+		{
+			sampleRate = rate;
+			ConfigureTimings();
+		}
+
+		public void SetOutputChannels(int channels)
+		{
+			numOutputChannels = channels;
+			ConfigureTimings();
+		}
+
+		public void SetClockRate(double clock)
+		{
+			clockRate = clock;
+			ConfigureTimings();
+		}
+
+		public void SetRefreshRate(double refresh)
+		{
+			refreshRate = refresh;
+			ConfigureTimings();
+		}
+
+		private void ConfigureTimings()
+		{
+			samplesPerFrame = (int)(sampleRate / refreshRate);
+			cyclesPerFrame = (int)(clockRate / refreshRate);
+			cyclesPerSample = (cyclesPerFrame / samplesPerFrame);
+
+			FlushSamples();
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			if (samplesPerFrame == -1) throw new EmulationException("SN76489: Timings not configured, invalid samples per frame");
+			if (cyclesPerFrame == -1) throw new EmulationException("SN76489: Timings not configured, invalid cycles per frame");
+			if (cyclesPerSample == -1) throw new EmulationException("SN76489: Timings not configured, invalid cycles per sample");
+		}
+
+		public virtual void Shutdown()
+		{
+			//
+		}
+
+		public virtual void Reset()
+		{
+			FlushSamples();
+
+			latchedChannel = latchedType = 0x00;
+			noiseLfsr = 0x4000;
+
+			for (int i = 0; i < numChannels; i++)
+			{
+				volumeRegisters[i] = 0x000F;
+				toneRegisters[i] = 0x0000;
+			}
+
+			sampleCycleCount = frameCycleCount = dividerCount = 0;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			sampleCycleCount += clockCyclesInStep;
+			frameCycleCount += clockCyclesInStep;
+
+			for (int i = 0; i < clockCyclesInStep; i++)
+			{
+				dividerCount++;
+				if (dividerCount == 16)
+				{
+					for (int ch = 0; ch < numToneChannels; ch++)
+						StepToneChannel(ch);
+					StepNoiseChannel();
+
+					dividerCount = 0;
+				}
+			}
+
+			if (sampleCycleCount >= cyclesPerSample)
+			{
+				GenerateSample();
+
+				sampleCycleCount -= cyclesPerSample;
+			}
+
+			if (mixedSampleBuffer.Count >= (samplesPerFrame * numOutputChannels))
+			{
+				OnEnqueueSamples(new EnqueueSamplesEventArgs(
+					numChannels,
+					channelSampleBuffer.Select(x => x.ToArray()).ToArray(),
+					new bool[] { !channel1ForceEnable, !channel2ForceEnable, !channel3ForceEnable, !channel4ForceEnable },
+					mixedSampleBuffer.ToArray()));
+
+				FlushSamples();
+			}
+
+			if (frameCycleCount >= cyclesPerFrame)
+			{
+				frameCycleCount -= cyclesPerFrame;
+				sampleCycleCount = frameCycleCount;
+			}
+		}
+
+		private void StepToneChannel(int ch)
+		{
+			/* Check for counter underflow */
+			if ((channelCounters[ch] & 0x03FF) > 0)
+				channelCounters[ch]--;
+
+			/* Counter underflowed, reload and flip output bit, then generate sample */
+			if ((channelCounters[ch] & 0x03FF) == 0)
+			{
+				channelCounters[ch] = (ushort)(toneRegisters[ch] & 0x03FF);
+				channelOutput[ch] = !channelOutput[ch];
+			}
+		}
+
+		private void StepNoiseChannel()
+		{
+			int chN = noiseChannelIndex;
+			{
+				/* Check for counter underflow */
+				if ((channelCounters[chN] & 0x03FF) > 0)
+					channelCounters[chN]--;
+
+				/* Counter underflowed, reload and flip output bit */
+				if ((channelCounters[chN] & 0x03FF) == 0)
+				{
+					switch (toneRegisters[chN] & 0x3)
+					{
+						case 0x0: channelCounters[chN] = 0x10; break;
+						case 0x1: channelCounters[chN] = 0x20; break;
+						case 0x2: channelCounters[chN] = 0x40; break;
+						case 0x3: channelCounters[chN] = (ushort)(toneRegisters[2] & 0x03FF); break;
+					}
+					channelOutput[chN] = !channelOutput[chN];
+
+					if (channelOutput[chN])
+					{
+						/* Check noise type, then generate sample */
+						bool isWhiteNoise = (((toneRegisters[chN] >> 2) & 0x1) == 0x1);
+
+						ushort newLfsrBit = (ushort)((isWhiteNoise ? CheckParity((ushort)(noiseLfsr & noiseTappedBits)) : (noiseLfsr & 0x01)) << noiseBitShift);
+
+						noiseLfsr = (ushort)((newLfsrBit | (noiseLfsr >> 1)) & noiseLfsrMask);
+					}
+				}
+			}
+		}
+
+		protected virtual void GenerateSample()
+		{
+			for (int i = 0; i < numOutputChannels; i++)
+			{
+				/* Generate samples */
+				var ch1 = (short)(volumeTable[volumeRegisters[0]] * ((toneRegisters[0] < 2 ? true : channelOutput[0]) ? 1.0 : 0.0));
+				var ch2 = (short)(volumeTable[volumeRegisters[1]] * ((toneRegisters[1] < 2 ? true : channelOutput[1]) ? 1.0 : 0.0));
+				var ch3 = (short)(volumeTable[volumeRegisters[2]] * ((toneRegisters[2] < 2 ? true : channelOutput[2]) ? 1.0 : 0.0));
+				var ch4 = (short)(volumeTable[volumeRegisters[3]] * (noiseLfsr & 0x1));
+
+				channelSampleBuffer[0].Add(ch1);
+				channelSampleBuffer[1].Add(ch2);
+				channelSampleBuffer[2].Add(ch3);
+				channelSampleBuffer[3].Add(ch4);
+
+				/* Mix samples */
+				var mixed = 0;
+				if (channel1ForceEnable) mixed += ch1;
+				if (channel2ForceEnable) mixed += ch2;
+				if (channel3ForceEnable) mixed += ch3;
+				if (channel4ForceEnable) mixed += ch4;
+				mixed /= numChannels;
+
+				mixedSampleBuffer.Add((short)mixed);
+			}
+		}
+
+		public void FlushSamples()
+		{
+			for (int i = 0; i < numChannels; i++)
+				channelSampleBuffer[i].Clear();
+
+			mixedSampleBuffer.Clear();
+		}
+
+		private ushort CheckParity(ushort val)
+		{
+			val ^= (ushort)(val >> 8);
+			val ^= (ushort)(val >> 4);
+			val ^= (ushort)(val >> 2);
+			val ^= (ushort)(val >> 1);
+			return (ushort)(val & 0x1);
+		}
+
+		public virtual byte ReadPort(byte port)
+		{
+			throw new EmulationException("SN76489: Cannot read ports");
+		}
+
+		public virtual void WritePort(byte port, byte data)
+		{
+			if (IsBitSet(data, 7))
+			{
+				/* LATCH/DATA byte; get channel (0-3) and type (0 is tone/noise, 1 is volume) */
+				latchedChannel = (byte)((data >> 5) & 0x03);
+				latchedType = (byte)((data >> 4) & 0x01);
+
+				/* Mask off non-data bits */
+				data &= 0x0F;
+
+				/* If target is channel 3 noise (3 bits), mask off highest bit */
+				if (latchedChannel == 3 && latchedType == 0)
+					data &= 0x07;
+
+				/* Write to register */
+				if (latchedType == 0)
+				{
+					/* Data is tone/noise */
+					toneRegisters[latchedChannel] = (ushort)((toneRegisters[latchedChannel] & 0x03F0) | data);
+				}
+				else
+				{
+					/* Data is volume */
+					volumeRegisters[latchedChannel] = data;
+				}
+			}
+			else
+			{
+				/* DATA byte; mask off non-data bits */
+				data &= 0x3F;
+
+				/* Write to register */
+				if (latchedType == 0)
+				{
+					/* Data is tone/noise */
+					if (latchedChannel == 3)
+					{
+						/* Target is channel 3 noise, mask off excess bits and write to low bits of register */
+						toneRegisters[latchedChannel] = (ushort)(data & 0x07);
+					}
+					else
+					{
+						/* Target is not channel 3 noise, write to high bits of register */
+						toneRegisters[latchedChannel] = (ushort)((toneRegisters[latchedChannel] & 0x000F) | (data << 4));
+					}
+				}
+				else
+				{
+					/* Data is volume; mask off excess bits and write to low bits of register */
+					volumeRegisters[latchedChannel] = (ushort)(data & 0x0F);
+				}
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs.meta
new file mode 100644
index 0000000..2369837
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SN76489.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5b081ff484b016a4faafebb32b5d6bd1
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs b/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs
new file mode 100644
index 0000000..905d3cd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs
@@ -0,0 +1,77 @@
+using System;
+
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Audio
+{
+	public class SegaGGPSG : SegaSMSPSG
+	{
+		public const int PortStereoControl = 0x06;
+
+		[StateRequired]
+		readonly bool[] channel0Enable, channel1Enable, channel2Enable, channel3Enable;
+
+		public SegaGGPSG() : base()
+		{
+			channel0Enable = new bool[2];
+			channel1Enable = new bool[2];
+			channel2Enable = new bool[2];
+			channel3Enable = new bool[2];
+		}
+
+		public override void Reset()
+		{
+			base.Reset();
+
+			WritePort(PortStereoControl, 0xFF);
+		}
+
+		protected override void GenerateSample()
+		{
+			for (int i = 0; i < numOutputChannels; i++)
+			{
+				/* Generate samples */
+				var ch1 = (channel0Enable[i] ? (short)(volumeTable[volumeRegisters[0]] * ((toneRegisters[0] < 2 ? true : channelOutput[0]) ? 1.0 : 0.0)) : (short)0);
+				var ch2 = (channel1Enable[i] ? (short)(volumeTable[volumeRegisters[1]] * ((toneRegisters[1] < 2 ? true : channelOutput[1]) ? 1.0 : 0.0)) : (short)0);
+				var ch3 = (channel2Enable[i] ? (short)(volumeTable[volumeRegisters[2]] * ((toneRegisters[2] < 2 ? true : channelOutput[2]) ? 1.0 : 0.0)) : (short)0);
+				var ch4 = (channel3Enable[i] ? (short)(volumeTable[volumeRegisters[3]] * (noiseLfsr & 0x1)) : (short)0);
+
+				channelSampleBuffer[0].Add(ch1);
+				channelSampleBuffer[1].Add(ch2);
+				channelSampleBuffer[2].Add(ch3);
+				channelSampleBuffer[3].Add(ch4);
+
+				/* Mix samples */
+				var mixed = 0;
+				if (channel1ForceEnable) mixed += ch1;
+				if (channel2ForceEnable) mixed += ch2;
+				if (channel3ForceEnable) mixed += ch3;
+				if (channel4ForceEnable) mixed += ch4;
+				mixed /= numChannels;
+
+				mixedSampleBuffer.Add((short)mixed);
+			}
+		}
+
+		public override void WritePort(byte port, byte data)
+		{
+			if (port == PortStereoControl)
+			{
+				/* Stereo control */
+				channel0Enable[0] = ((data & 0x10) != 0);   /* Ch1 Left */
+				channel0Enable[1] = ((data & 0x01) != 0);   /* Ch1 Right */
+
+				channel1Enable[0] = ((data & 0x20) != 0);   /* Ch2 Left */
+				channel1Enable[1] = ((data & 0x02) != 0);   /* Ch2 Right */
+
+				channel2Enable[0] = ((data & 0x40) != 0);   /* Ch3 Left */
+				channel2Enable[1] = ((data & 0x04) != 0);   /* Ch3 Right */
+
+				channel3Enable[0] = ((data & 0x80) != 0);   /* Ch4 Left */
+				channel3Enable[1] = ((data & 0x08) != 0);   /* Ch4 Right */
+			}
+			else
+				base.WritePort(port, data);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs.meta
new file mode 100644
index 0000000..0be21bf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SegaGGPSG.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 63b4d9dd3dcd81942aab4e783f7ca197
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs b/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs
new file mode 100644
index 0000000..1115492
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs
@@ -0,0 +1,23 @@
+using System;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.Audio
+{
+	public class SegaSMSPSG : SN76489
+	{
+		/* LFSR is 16 bits, tapped bits are 0 and 3 (mask 0x0009), going into bit 15 */
+		protected override ushort noiseLfsrMask => 0xFFFF;
+		protected override ushort noiseTappedBits => 0x0009;
+		protected override int noiseBitShift => 15;
+
+		public SegaSMSPSG() : base() { }
+
+		public override void Reset()
+		{
+			base.Reset();
+
+			noiseLfsr = 0x8000;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs.meta b/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs.meta
new file mode 100644
index 0000000..69fd565
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Audio/SegaSMSPSG.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bdf808e342a96d74fbbe4a6ea16509eb
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU.meta b/Assets/Plugins/Essgee/Emulation/CPU.meta
new file mode 100644
index 0000000..3717bf1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ac5ab16607ba9574992b980c27ee0bba
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs b/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs
new file mode 100644
index 0000000..d5e4f8f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	interface ICPU
+	{
+		void Startup();
+		void Shutdown();
+		void Reset();
+		int Step();
+
+		void SetStackPointer(ushort value);
+		void SetProgramCounter(ushort value);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs.meta
new file mode 100644
index 0000000..7461968
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/ICPU.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d1eb29143cb5d8e49bb3ebe09e89a215
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs
new file mode 100644
index 0000000..bc37762
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83
+	{
+		public static class CycleCounts
+		{
+			public const int AdditionalJumpCond8Taken = 4;
+			public const int AdditionalRetCondTaken = 12;
+			public const int AdditionalCallCondTaken = 12;
+
+			// 32 cycles == dummy
+			public static readonly int[] NoPrefix = new int[]
+			{
+				4,  12, 8,  8,  4,  4,  8,  4,      20, 8,  8,  8,  4,  4,  8,  4,  /* 0x00 - 0x0F */
+				4,  12, 8,  8,  4,  4,  8,  4,      12, 8,  8,  8,  4,  4,  8,  4,  /* 0x10 - 0x1F */
+				8,  12, 8,  8,  4,  4,  8,  4,      8,  8,  8,  8,  4,  4,  8,  4,  /* 0x20 - 0x2F */
+				8,  12, 8,  8,  12, 12, 12, 4,      8,  8,  8,  8,  4,  4,  8,  4,  /* 0x30 - 0x3F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x40 - 0x4F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x50 - 0x5F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x60 - 0x6F */
+				8,  8,  8,  8,  8,  8,  4,  8,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x70 - 0x7F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x80 - 0x8F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0x90 - 0x9F */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0xA0 - 0xAF */
+				4,  4,  4,  4,  4,  4,  8,  4,      4,  4,  4,  4,  4,  4,  8,  4,  /* 0xB0 - 0xBF */
+				8,  12, 12, 16, 12, 16, 8,  16,     8,  16, 12, 32, 12, 24, 8,  16, /* 0xC0 - 0xCF */
+				8,  12, 12, 32, 12, 16, 8,  16,     8,  16, 12, 32, 12, 32, 8,  16, /* 0xD0 - 0xDF */
+				12, 12, 8,  32, 32, 16, 8,  16,     16, 4,  16, 32, 32, 32, 8,  16, /* 0xE0 - 0xEF */
+				12, 12, 8,  4,  32, 16, 8,  16,     12, 8,  16, 4,  32, 32, 8,  16	/* 0xF0 - 0xFF */
+			};
+
+			public static readonly int[] PrefixCB = new int[]
+			{
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x00 - 0x0F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x10 - 0x1F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x20 - 0x2F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x30 - 0x3F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x40 - 0x4F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x50 - 0x5F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x60 - 0x6F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x70 - 0x7F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x80 - 0x8F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0x90 - 0x9F */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0xA0 - 0xAF */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0xB0 - 0xBF */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0xC0 - 0xCF */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0xD0 - 0xDF */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8,  /* 0xE0 - 0xEF */
+				8,  8,  8,  8,  8,  8,  16, 8,      8,  8,  8,  8,  8,  8,  16, 8   /* 0xF0 - 0xFF */
+			};
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs.meta
new file mode 100644
index 0000000..cd0268b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.CycleCounts.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e1d2dab87d28f044ca71114b277aee3a
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs
new file mode 100644
index 0000000..8b3a717
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs
@@ -0,0 +1,205 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83
+	{
+		static readonly string[] opcodeMnemonicNoPrefix =
+		{
+		/*  +00                         +01                         +02                         +03                         +04                         +05                         +06                         +07                         */
+			"NOP",                      "LD BC, 0x{0:X4}",          "LD (BC), A",               "INC BC",                   "INC B",                    "DEC B",                    "LD B, 0x{0:X2}",           "RLCA",                     /* 0x00 */
+			"LD (0x{0:X4}), SP",        "ADD HL, BC",               "LD A, (BC)",               "DEC BC",                   "INC C",                    "DEC C",                    "LD C, 0x{0:X2}",           "RRCA",                     /* 0x08 */
+			"STOP",                     "LD DE, 0x{0:X4}",          "LD (DE), A",               "INC DE",                   "INC D",                    "DEC D",                    "LD D, 0x{0:X2}",           "RLA",                      /* 0x10 */
+			"JR 0x{0:X2}",              "ADD HL, DE",               "LD A, (DE)",               "DEC DE",                   "INC E",                    "DEC E",                    "LD E, 0x{0:X2}",           "RRA",                      /* 0x18 */
+			"JR NZ, 0x{0:X2}",          "LD HL, 0x{0:X4}",          "LDI (HL), A",              "INC HL",                   "INC H",                    "DEC H",                    "LD H, 0x{0:X2}",           "DAA",                      /* 0x20 */
+			"JR Z, 0x{0:X2}",           "ADD HL, HL",               "LDI A, (HL)",              "DEC HL",                   "INC L",                    "DEC L",                    "LD L, 0x{0:X2}",           "CPL",                      /* 0x28 */
+			"JR NC, 0x{0:X2}",          "LD SP, 0x{0:X4}",          "LDD (HL), A",              "INC SP",                   "INC (HL)",                 "DEC (HL)",                 "LD (HL), 0x{0:X2}",        "SCF",                      /* 0x30 */
+			"JR C, 0x{0:X2}",           "ADD HL, SP",               "LDD A, (HL)",              "DEC SP",                   "INC A",                    "DEC A",                    "LD A, 0x{0:X2}",           "CCF",                      /* 0x38 */
+			"LD B, B",                  "LD B, C",                  "LD B, D",                  "LD B, E",                  "LD B, H",                  "LD B, L",                  "LD B, (HL)",               "LD B, A",                  /* 0x40 */
+			"LD C, B",                  "LD C, C",                  "LD C, D",                  "LD C, E",                  "LD C, H",                  "LD C, L",                  "LD C, (HL)",               "LD C, A",                  /* 0x48 */
+			"LD D, B",                  "LD D, C",                  "LD D, D",                  "LD D, E",                  "LD D, H",                  "LD D, L",                  "LD D, (HL)",               "LD D, A",                  /* 0x50 */
+			"LD E, B",                  "LD E, C",                  "LD E, D",                  "LD E, E",                  "LD E, H",                  "LD E, L",                  "LD E, (HL)",               "LD E, A",                  /* 0x58 */
+			"LD H, B",                  "LD H, C",                  "LD H, D",                  "LD H, E",                  "LD H, H",                  "LD H, L",                  "LD H, (HL)",               "LD H, A",                  /* 0x60 */
+			"LD L, B",                  "LD L, C",                  "LD L, D",                  "LD L, E",                  "LD L, H",                  "LD L, L",                  "LD L, (HL)",               "LD L, A",                  /* 0x68 */
+			"LD (HL), B",               "LD (HL), C",               "LD (HL), D",               "LD (HL), E",               "LD (HL), H",               "LD (HL), L",               "HALT",                     "LD (HL), A",               /* 0x70 */
+			"LD A, B",                  "LD A, C",                  "LD A, D",                  "LD A, E",                  "LD A, H",                  "LD A, L",                  "LD A, (HL)",               "LD A, A",                  /* 0x78 */
+			"ADD B",                    "ADD C",                    "ADD D",                    "ADD E",                    "ADD H",                    "ADD L",                    "ADD (HL)",                 "ADD A",                    /* 0x80 */
+			"ADC B",                    "ADC C",                    "ADC D",                    "ADC E",                    "ADC H",                    "ADC L",                    "ADC (HL)",                 "ADC A",                    /* 0x88 */
+			"SUB B",                    "SUB C",                    "SUB D",                    "SUB E",                    "SUB H",                    "SUB L",                    "SUB (HL)",                 "SUB A",                    /* 0x90 */
+			"SBC B",                    "SBC C",                    "SBC D",                    "SBC E",                    "SBC H",                    "SBC L",                    "SBC (HL)",                 "SBC A",                    /* 0x98 */
+			"AND B",                    "AND C",                    "AND D",                    "AND E",                    "AND H",                    "AND L",                    "AND (HL)",                 "AND A",                    /* 0xA0 */
+			"XOR B",                    "XOR C",                    "XOR D",                    "XOR E",                    "XOR H",                    "XOR L",                    "XOR (HL)",                 "XOR A",                    /* 0xA8 */
+			"OR B",                     "OR C",                     "OR D",                     "OR E",                     "OR H",                     "OR L",                     "OR (HL)",                  "OR A",                     /* 0xB0 */
+			"CP B",                     "CP C",                     "CP D",                     "CP E",                     "CP H",                     "CP L",                     "CP (HL)",                  "CP A",                     /* 0xB8 */
+			"RET NZ",                   "POP BC",                   "JP NZ, 0x{0:X4}",          "JP 0x{0:X4}",              "CALL NZ, 0x{0:X4}",        "PUSH BC",                  "ADD 0x{0:X2}",             "RST 00",                   /* 0xC0 */
+			"RET Z",                    "RET",                      "JP Z, 0x{0:X4}",           string.Empty,               "CALL Z, 0x{0:X4}",         "CALL 0x{0:X4}",            "ADC 0x{0:X2}",             "RST 08",                   /* 0xC8 */
+			"RET NC",                   "POP DE",                   "JP NC, 0x{0:X4}",          ".DB 0xD3",                 "CALL NC, 0x{0:X4}",        "PUSH DE",                  "SUB 0x{0:X2}",             "RST 10",                   /* 0xD0 */
+			"RET C",                    "RETI",                     "JP C, 0x{0:X4}",           ".DB 0xDB",                 "CALL C, 0x{0:X4}",         ".DB 0xDD",                 "SBC 0x{0:X2}",             "RST 18",                   /* 0xD8 */
+			"LD (FF00+0x{0:X2}), A",    "POP HL",                   "LD (FF00+C), A",           ".DB 0xE3",                 ".DB 0xE4",                 "PUSH HL",                  "AND 0x{0:X2}",             "RST 20",                   /* 0xE0 */
+			"ADD SP, 0x{0:X2}",         "LD PC, HL",                "LD (0x{0:X4}), A",         ".DB 0xEB",                 ".DB 0xEC",                 ".DB 0xED",                 "XOR 0x{0:X2}",             "RST 28",                   /* 0xE8 */
+			"LD A, (FF00+0x{0:X2})",    "POP AF",                   "LD A, (FF00+C)",           "DI",                       ".DB 0xF4",                 "PUSH AF",                  "OR 0x{0:X2}",              "RST 30",                   /* 0xF0 */
+			"LD HL, SP+0x{0:X2}",       "LD SP, HL",                "LD A, (0x{0:X4})",         "EI",                       ".DB 0xFC",                 ".DB 0xFD",                 "CP 0x{0:X2}",              "RST 38"                    /* 0xF8 */
+		};
+
+		static readonly int[] opcodeLengthNoPrefix =
+		{
+			1, 3, 1, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1, 1, 2, 1,
+			2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
+			2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
+			2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 3, 3, 3, 1, 2, 1, 1, 1, 3, 2, 3, 3, 2, 1,
+			1, 1, 3, 1, 3, 1, 2, 1, 1, 1, 3, 1, 3, 1, 2, 1,
+			2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 1, 1, 2, 1,
+			2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 1, 1, 2, 1
+		};
+
+		static readonly string[] opcodeMnemonicPrefixCB = new string[]
+		{
+		/*  +00                         +01                         +02                         +03                         +04                         +05                         +06                         +07                         */
+			"RLC B",                    "RLC C",                    "RLC D",                    "RLC E",                    "RLC H",                    "RLC L",                    "RLC (HL)",                 "RLC A",                    /* 0x00 */
+			"RRC B",                    "RRC C",                    "RRC D",                    "RRC E",                    "RRC H",                    "RRC L",                    "RRC (HL)",                 "RRC A",                    /* 0x08 */
+			"RL B",                     "RL C",                     "RL D",                     "RL E",                     "RL H",                     "RL L",                     "RL (HL)",                  "RL A",                     /* 0x10 */
+			"RR B",                     "RR C",                     "RR D",                     "RR E",                     "RR H",                     "RR L",                     "RR (HL)",                  "RR A",                     /* 0x18 */
+			"SLA B",                    "SLA C",                    "SLA D",                    "SLA E",                    "SLA H",                    "SLA L",                    "SLA (HL)",                 "SLA A",                    /* 0x20 */
+			"SRA B",                    "SRA C",                    "SRA D",                    "SRA E",                    "SRA H",                    "SRA L",                    "SRA (HL)",                 "SRA A",                    /* 0x28 */
+			"SWAP B",                   "SWAP C",                   "SWAP D",                   "SWAP E",                   "SWAP H",                   "SWAP L",                   "SWAP (HL)",                "SWAP A",                   /* 0x30 */
+			"SRL B",                    "SRL C",                    "SRL D",                    "SRL E",                    "SRL H",                    "SRL L",                    "SRL (HL)",                 "SRL A",                    /* 0x38 */
+			"BIT 0, B",                 "BIT 0, C",                 "BIT 0, D",                 "BIT 0, E",                 "BIT 0, H",                 "BIT 0, L",                 "BIT 0, (HL)",              "BIT 0, A",                 /* 0x40 */
+			"BIT 1, B",                 "BIT 1, C",                 "BIT 1, D",                 "BIT 1, E",                 "BIT 1, H",                 "BIT 1, L",                 "BIT 1, (HL)",              "BIT 1, A",                 /* 0x48 */
+			"BIT 2, B",                 "BIT 2, C",                 "BIT 2, D",                 "BIT 2, E",                 "BIT 2, H",                 "BIT 2, L",                 "BIT 2, (HL)",              "BIT 2, A",                 /* 0x50 */
+			"BIT 3, B",                 "BIT 3, C",                 "BIT 3, D",                 "BIT 3, E",                 "BIT 3, H",                 "BIT 3, L",                 "BIT 3, (HL)",              "BIT 3, A",                 /* 0x58 */
+			"BIT 4, B",                 "BIT 4, C",                 "BIT 4, D",                 "BIT 4, E",                 "BIT 4, H",                 "BIT 4, L",                 "BIT 4, (HL)",              "BIT 4, A",                 /* 0x60 */
+			"BIT 5, B",                 "BIT 5, C",                 "BIT 5, D",                 "BIT 5, E",                 "BIT 5, H",                 "BIT 5, L",                 "BIT 5, (HL)",              "BIT 5, A",                 /* 0x68 */
+			"BIT 6, B",                 "BIT 6, C",                 "BIT 6, D",                 "BIT 6, E",                 "BIT 6, H",                 "BIT 6, L",                 "BIT 6, (HL)",              "BIT 6, A",                 /* 0x70 */
+			"BIT 7, B",                 "BIT 7, C",                 "BIT 7, D",                 "BIT 7, E",                 "BIT 7, H",                 "BIT 7, L",                 "BIT 7, (HL)",              "BIT 7, A",                 /* 0x78 */
+			"RES 0, B",                 "RES 0, C",                 "RES 0, D",                 "RES 0, E",                 "RES 0, H",                 "RES 0, L",                 "RES 0, (HL)",              "RES 0, A",                 /* 0x80 */
+			"RES 1, B",                 "RES 1, C",                 "RES 1, D",                 "RES 1, E",                 "RES 1, H",                 "RES 1, L",                 "RES 1, (HL)",              "RES 1, A",                 /* 0x88 */
+			"RES 2, B",                 "RES 2, C",                 "RES 2, D",                 "RES 2, E",                 "RES 2, H",                 "RES 2, L",                 "RES 2, (HL)",              "RES 2, A",                 /* 0x90 */
+			"RES 3, B",                 "RES 3, C",                 "RES 3, D",                 "RES 3, E",                 "RES 3, H",                 "RES 3, L",                 "RES 3, (HL)",              "RES 3, A",                 /* 0x98 */
+			"RES 4, B",                 "RES 4, C",                 "RES 4, D",                 "RES 4, E",                 "RES 4, H",                 "RES 4, L",                 "RES 4, (HL)",              "RES 4, A",                 /* 0xA0 */
+			"RES 5, B",                 "RES 5, C",                 "RES 5, D",                 "RES 5, E",                 "RES 5, H",                 "RES 5, L",                 "RES 5, (HL)",              "RES 5, A",                 /* 0xA8 */
+			"RES 6, B",                 "RES 6, C",                 "RES 6, D",                 "RES 6, E",                 "RES 6, H",                 "RES 6, L",                 "RES 6, (HL)",              "RES 6, A",                 /* 0xB0 */
+			"RES 7, B",                 "RES 7, C",                 "RES 7, D",                 "RES 7, E",                 "RES 7, H",                 "RES 7, L",                 "RES 7, (HL)",              "RES 7, A",                 /* 0xB8 */
+			"SET 0, B",                 "SET 0, C",                 "SET 0, D",                 "SET 0, E",                 "SET 0, H",                 "SET 0, L",                 "SET 0, (HL)",              "SET 0, A",                 /* 0xC0 */
+			"SET 1, B",                 "SET 1, C",                 "SET 1, D",                 "SET 1, E",                 "SET 1, H",                 "SET 1, L",                 "SET 1, (HL)",              "SET 1, A",                 /* 0xC8 */
+			"SET 2, B",                 "SET 2, C",                 "SET 2, D",                 "SET 2, E",                 "SET 2, H",                 "SET 2, L",                 "SET 2, (HL)",              "SET 2, A",                 /* 0xD0 */
+			"SET 3, B",                 "SET 3, C",                 "SET 3, D",                 "SET 3, E",                 "SET 3, H",                 "SET 3, L",                 "SET 3, (HL)",              "SET 3, A",                 /* 0xD8 */
+			"SET 4, B",                 "SET 4, C",                 "SET 4, D",                 "SET 4, E",                 "SET 4, H",                 "SET 4, L",                 "SET 4, (HL)",              "SET 4, A",                 /* 0xE0 */
+			"SET 5, B",                 "SET 5, C",                 "SET 5, D",                 "SET 5, E",                 "SET 5, H",                 "SET 5, L",                 "SET 5, (HL)",              "SET 5, A",                 /* 0xE8 */
+			"SET 6, B",                 "SET 6, C",                 "SET 6, D",                 "SET 6, E",                 "SET 6, H",                 "SET 6, L",                 "SET 6, (HL)",              "SET 6, A",                 /* 0xF0 */
+			"SET 7, B",                 "SET 7, C",                 "SET 7, D",                 "SET 7, E",                 "SET 7, H",                 "SET 7, L",                 "SET 7, (HL)",              "SET 7, A"                  /* 0xF8 */
+		};
+
+		static readonly int[] opcodeLengthPrefixCB = new int[]
+		{
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+		};
+
+		public static string PrintRegisters(SM83 cpu)
+		{
+			return $"AF:{cpu.af.Word:X4} BC:{cpu.bc.Word:X4} DE:{cpu.de.Word:X4} HL:{cpu.hl.Word:X4} SP:{cpu.sp:X4}";
+		}
+
+		public static string PrintFlags(SM83 cpu)
+		{
+			return $"{(cpu.IsFlagSet(Flags.Zero) ? "Z" : "-")}{(cpu.IsFlagSet(Flags.Subtract) ? "N" : "-")}{(cpu.IsFlagSet(Flags.HalfCarry) ? "H" : "-")}{(cpu.IsFlagSet(Flags.Carry) ? "C" : "-")}";
+		}
+
+		public static string PrintInterrupt(SM83 cpu)
+		{
+			var intFlags = (InterruptSource)cpu.memoryReadDelegate(0xFF0F);
+			var intEnable = (InterruptSource)cpu.memoryReadDelegate(0xFFFF);
+
+			var intFlagsString =
+				$"{((intFlags & InterruptSource.VBlank) != 0 ? "V" : "-")}" +
+				$"{((intFlags & InterruptSource.LCDCStatus) != 0 ? "L" : "-")}" +
+				$"{((intFlags & InterruptSource.TimerOverflow) != 0 ? "T" : "-")}" +
+				$"{((intFlags & InterruptSource.SerialIO) != 0 ? "S" : "-")}" +
+				$"{((intFlags & InterruptSource.Keypad) != 0 ? "K" : "-")}";
+
+			var intEnableString =
+				$"{((intEnable & InterruptSource.VBlank) != 0 ? "V" : "-")}" +
+				$"{((intEnable & InterruptSource.LCDCStatus) != 0 ? "L" : "-")}" +
+				$"{((intEnable & InterruptSource.TimerOverflow) != 0 ? "T" : "-")}" +
+				$"{((intEnable & InterruptSource.SerialIO) != 0 ? "S" : "-")}" +
+				$"{((intEnable & InterruptSource.Keypad) != 0 ? "K" : "-")}";
+
+			return $"IME:{(cpu.ime ? "1" : "0")} IF:{intFlagsString} IE:{intEnableString}";
+		}
+
+		public static string DisassembleOpcode(SM83 cpu, ushort address)
+		{
+			var opcode = DisassembleGetOpcodeBytes(cpu, address);
+			return $"0x{address:X4} | {DisassembleMakeByteString(cpu, opcode).PadRight(15)} | {DisassembleMakeMnemonicString(cpu, opcode)}";
+		}
+
+		public static byte[] DisassembleGetOpcodeBytes(SM83 cpu, ushort address)
+		{
+			var opcode = new byte[3];
+			for (int i = 0; i < opcode.Length; i++)
+				opcode[i] = address + i <= 0xFFFF ? cpu.memoryReadDelegate((ushort)(address + i)) : (byte)0;
+			return opcode;
+		}
+
+		public static int DisassembleGetOpcodeLen(SM83 cpu, byte[] opcode)
+		{
+			if (opcode[0] == 0xCB)
+				return opcodeLengthPrefixCB[opcode[1]];
+			else
+				return opcodeLengthNoPrefix[opcode[0]];
+		}
+
+		public static string DisassembleMakeByteString(SM83 cpu, byte[] opcode)
+		{
+			return string.Join(" ", opcode.Select(x => $"{x:X2}").Take(DisassembleGetOpcodeLen(cpu, opcode)));
+		}
+
+		public static string DisassembleMakeMnemonicString(SM83 cpu, byte[] opcode)
+		{
+			var len = DisassembleGetOpcodeLen(cpu, opcode);
+			var start = opcode[0] == 0xCB ? 1 : 0;
+			var mnemonics = opcode[0] == 0xCB ? opcodeMnemonicPrefixCB : opcodeMnemonicNoPrefix;
+
+			switch (len - start)
+			{
+				case 1: return mnemonics[opcode[start]];
+				case 2: return string.Format(mnemonics[opcode[start]], opcode[start + 1]);
+				case 3: return string.Format(mnemonics[opcode[start]], (opcode[start + 2] << 8 | opcode[start + 1]));
+				default: return string.Empty;
+			}
+		}
+
+		private string MakeUnimplementedOpcodeString(string prefix, ushort address)
+		{
+			var opcode = DisassembleGetOpcodeBytes(this, address);
+			return $"Unimplemented {(prefix != string.Empty ? prefix + " " : prefix)}opcode {DisassembleMakeByteString(this, opcode)} ({DisassembleMakeMnemonicString(this, opcode)})";
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs.meta
new file mode 100644
index 0000000..d48ca21
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Disassembly.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7de4aca2c5f71b94b9509b8ad7b6ef8f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs
new file mode 100644
index 0000000..cef4865
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83
+	{
+		static SimpleOpcodeDelegate[] opcodesNoPrefix = new SimpleOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new SimpleOpcodeDelegate((c) => { /* NOP */ }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.bc.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftAccumulatorCircular(); }),
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory16(c.ReadMemory16(c.pc), c.sp); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterFromMemory8(ref c.af.High, c.bc.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightAccumulatorCircular(); }),
+			/* 0x10 */
+			new SimpleOpcodeDelegate((c) => { c.Stop(); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.de.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftAccumulator(); }),
+			new SimpleOpcodeDelegate((c) => { c.Jump8(); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterFromMemory8(ref c.af.High, c.de.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightAccumulator(); }),
+			/* 0x20 */
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory8(c.hl.Word++, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.DecimalAdjustAccumulator(); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8(c.hl.Word++); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High ^= 0xFF; c.SetFlag(Flags.Subtract | Flags.HalfCarry); }),
+			/* 0x30 */
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory8(c.hl.Word--, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.IncrementMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.DecrementMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.SetFlag(Flags.Carry); c.ClearFlag(Flags.Subtract | Flags.HalfCarry); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8(c.hl.Word--);}),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.ClearFlag(Flags.Subtract); c.ClearFlag(Flags.HalfCarry); c.SetClearFlagConditional(Flags.Carry, !c.IsFlagSet(Flags.Carry)); }),
+			/* 0x40 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.bc.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.bc.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.af.High, false); }),
+			/* 0x50 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.de.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.de.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.af.High, false); }),
+			/* 0x60 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.hl.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.hl.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.af.High, false); }),
+			/* 0x70 */
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.EnterHaltState(); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.af.High, false); }),
+			/* 0x80 */
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.hl.Word), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.hl.Word), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.af.High, true); }),
+			/* 0x90 */
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.hl.Word), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.hl.Word), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.af.High, true); }),
+			/* 0xA0 */
+			new SimpleOpcodeDelegate((c) => { c.And8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.af.High); }),
+			/* 0xB0 */
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.af.High); }),
+			/* 0xC0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.bc); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(true); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.bc); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.pc++), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0000); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { /* CB - handled elsewhere */ }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Call16(); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.pc++), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0008); }),
+			/* 0xD0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.de); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.de); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.pc++), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0010); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.ime = true; c.imeDelay = false; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.pc++), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0018); }),
+			/* 0xE0 */
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory8((ushort)(0xFF00 + c.ReadMemory8(c.pc++)), c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory8((ushort)(0xFF00 + c.bc.Low), c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0020); }),
+			new SimpleOpcodeDelegate((c) => { c.AddSPNN(); }),
+			new SimpleOpcodeDelegate((c) => { c.pc = c.hl.Word; }),
+			new SimpleOpcodeDelegate((c) => { c.WriteMemory8(c.ReadMemory16(c.pc), c.af.High); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0028); }),
+			/* 0xF0 */
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8((ushort)(0xFF00 + c.ReadMemory8(c.pc++))); }),
+			new SimpleOpcodeDelegate((c) => { c.PopAF(); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8((ushort)(0xFF00 + c.bc.Low)); }),
+			new SimpleOpcodeDelegate((c) => { c.DisableInterrupts(); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.af); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0030); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadHLSPNN(); }),
+			new SimpleOpcodeDelegate((c) => { c.sp = c.hl.Word; }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8(c.ReadMemory16(c.pc)); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.EnableInterrupts(); }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.pc--; }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0038); })
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs.meta
new file mode 100644
index 0000000..4c94393
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesNoPrefix.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e4fe2d8ddc2d10849b6c7c6d64db0d4f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs
new file mode 100644
index 0000000..60f7f9e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83
+	{
+		static SimpleOpcodeDelegate[] opcodesPrefixCB = new SimpleOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.af.High); }),
+			/* 0x10 */
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.af.High); }),
+			/* 0x20 */
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.af.High); }),
+			/* 0x30 */
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Swap(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.af.High); }),
+			/* 0x40 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 1); }),
+			/* 0x50 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 3); }),
+			/* 0x60 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 5); }),
+			/* 0x70 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 7); }),
+			/* 0x80 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 1); }),
+			/* 0x90 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 3); }),
+			/* 0xA0 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 5); }),
+			/* 0xB0 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 7); }),
+			/* 0xC0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 1); }),
+			/* 0xD0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 3); }),
+			/* 0xE0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 5); }),
+			/* 0xF0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 7); }),
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs.meta
new file mode 100644
index 0000000..6e4d3df
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.OpcodesPrefixCB.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 36c6024a407a43f43bf41f1e38274ad9
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs
new file mode 100644
index 0000000..9bb458e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83
+	{
+		[DebuggerDisplay("{Word}")]
+		[StructLayout(LayoutKind.Explicit)]
+		[Serializable]
+		public struct Register
+		{
+			[NonSerialized]
+			[FieldOffset(0)]
+			public byte Low;
+			[NonSerialized]
+			[FieldOffset(1)]
+			public byte High;
+
+			[FieldOffset(0)]
+			public ushort Word;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs.meta
new file mode 100644
index 0000000..024f0ea
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.Register.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e29886b06189649459149576fcb83444
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs
new file mode 100644
index 0000000..7040a66
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs
@@ -0,0 +1,977 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class SM83 : ICPU
+	{
+		[Flags]
+		enum Flags : byte
+		{
+			UnusedBit0 = (1 << 0),          /* (0) */
+			UnusedBit1 = (1 << 1),          /* (0) */
+			UnusedBit2 = (1 << 2),          /* (0) */
+			UnusedBit3 = (1 << 3),          /* (0) */
+			Carry = (1 << 4),               /* C */
+			HalfCarry = (1 << 5),           /* H */
+			Subtract = (1 << 6),            /* N */
+			Zero = (1 << 7)                 /* Z */
+		}
+
+		[Flags]
+		public enum InterruptSource : byte
+		{
+			VBlank = 0,
+			LCDCStatus = 1,
+			TimerOverflow = 2,
+			SerialIO = 3,
+			Keypad = 4
+		}
+
+		public delegate byte MemoryReadDelegate(ushort address);
+		public delegate void MemoryWriteDelegate(ushort address, byte value);
+
+		public delegate void RequestInterruptDelegate(InterruptSource source);
+
+		delegate void SimpleOpcodeDelegate(SM83 c);
+
+		protected MemoryReadDelegate memoryReadDelegate;
+		protected MemoryWriteDelegate memoryWriteDelegate;
+
+		[StateRequired]
+		protected Register af, bc, de, hl;
+		[StateRequired]
+		protected ushort sp, pc;
+
+		[StateRequired]
+		protected bool ime, imeDelay, halt, doHaltBug;
+
+		[StateRequired]
+		protected byte op;
+
+		[StateRequired]
+		protected int currentCycles;
+
+		string logFile;
+		int numLogEntries;
+		string[] logEntries;
+
+		public SM83(MemoryReadDelegate memoryRead, MemoryWriteDelegate memoryWrite)
+		{
+			af = bc = de = hl = new Register();
+
+			memoryReadDelegate = memoryRead;
+			memoryWriteDelegate = memoryWrite;
+
+			if (AppEnvironment.EnableSuperSlowCPULogger)
+			{
+				logFile = @"D:\Temp\Essgee\log-lr35902.txt";
+				numLogEntries = 0;
+				logEntries = new string[2000];
+			}
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			if (memoryReadDelegate == null) throw new EmulationException("SM83: Memory read method is null");
+			if (memoryWriteDelegate == null) throw new EmulationException("SM83: Memory write method is null");
+		}
+
+		public virtual void Shutdown()
+		{
+			if (AppEnvironment.EnableSuperSlowCPULogger && logEntries != null)
+			{
+				System.IO.File.AppendAllText(logFile, string.Join("", logEntries.Take(numLogEntries)));
+			}
+
+			//
+		}
+
+		public virtual void Reset()
+		{
+			af.Word = bc.Word = de.Word = hl.Word = 0;
+			pc = 0;
+			sp = 0;
+
+			ime = imeDelay = halt = doHaltBug = false;
+
+			currentCycles = 0;
+		}
+
+		public int Step()
+		{
+			currentCycles = 0;
+
+			/* Handle delayed interrupt enable */
+			if (imeDelay)
+			{
+				ime = true;
+				imeDelay = false;
+			}
+
+			/* Check interrupts */
+			HandleInterrupts();
+
+			if (halt)
+			{
+				/* CPU halted */
+				currentCycles = 4;
+			}
+			else
+			{
+				if (AppEnvironment.EnableSuperSlowCPULogger && logEntries != null)
+				{
+					string disasm = string.Format("{0} | {1} | {2} | {3}\n", DisassembleOpcode(this, pc).PadRight(48), PrintRegisters(this), PrintFlags(this), PrintInterrupt(this));
+					logEntries[numLogEntries++] = disasm;
+					if (numLogEntries >= logEntries.Length)
+					{
+						System.IO.File.AppendAllText(logFile, string.Join("", logEntries));
+						numLogEntries = 0;
+					}
+				}
+
+				/* Do HALT bug */
+				if (doHaltBug)
+				{
+					pc--;
+					doHaltBug = false;
+				}
+
+				/* Fetch and execute opcode */
+				op = ReadMemory8(pc++);
+				switch (op)
+				{
+					case 0xCB: ExecuteOpCB(); break;
+					default: ExecuteOpcodeNoPrefix(op); break;
+				}
+			}
+
+			return currentCycles;
+		}
+
+		#region Opcode Execution and Cycle Management
+
+		private void ExecuteOpcodeNoPrefix(byte op)
+		{
+			opcodesNoPrefix[op](this);
+			currentCycles += CycleCounts.NoPrefix[op];
+		}
+
+		private void ExecuteOpCB()
+		{
+			byte cbOp = ReadMemory8(pc++);
+			opcodesPrefixCB[cbOp](this);
+			currentCycles += CycleCounts.PrefixCB[cbOp];
+		}
+
+		#endregion
+
+		#region Helpers (Flags, etc.)
+
+		public void SetStackPointer(ushort value)
+		{
+			sp = value;
+		}
+
+		public void SetProgramCounter(ushort value)
+		{
+			pc = value;
+		}
+
+		public void SetRegisterAF(ushort value)
+		{
+			af.Word = value;
+		}
+
+		public void SetRegisterBC(ushort value)
+		{
+			bc.Word = value;
+		}
+
+		public void SetRegisterDE(ushort value)
+		{
+			de.Word = value;
+		}
+
+		public void SetRegisterHL(ushort value)
+		{
+			hl.Word = value;
+		}
+
+		private void SetFlag(Flags flags)
+		{
+			af.Low |= (byte)flags;
+		}
+
+		private void ClearFlag(Flags flags)
+		{
+			af.Low &= (byte)~flags;
+		}
+
+		private void SetClearFlagConditional(Flags flags, bool condition)
+		{
+			if (condition)
+				af.Low |= (byte)flags;
+			else
+				af.Low &= (byte)~flags;
+		}
+
+		private bool IsFlagSet(Flags flags)
+		{
+			return (((Flags)af.Low & flags) == flags);
+		}
+
+		#endregion
+
+		#region Interrupt, Halt and Stop State Handling
+
+		public void RequestInterrupt(InterruptSource source)
+		{
+			memoryWriteDelegate(0xFF0F, (byte)(memoryReadDelegate(0xFF0F) | (byte)(1 << (byte)source)));
+		}
+
+		private void HandleInterrupts()
+		{
+			var intEnable = memoryReadDelegate(0xFFFF);
+			var intFlags = memoryReadDelegate(0xFF0F);
+
+			if ((intEnable & intFlags) != 0)
+			{
+				LeaveHaltState();
+
+				if (ime)
+				{
+					if (ServiceInterrupt(InterruptSource.VBlank, intEnable, intFlags)) return;
+					if (ServiceInterrupt(InterruptSource.LCDCStatus, intEnable, intFlags)) return;
+					if (ServiceInterrupt(InterruptSource.TimerOverflow, intEnable, intFlags)) return;
+					if (ServiceInterrupt(InterruptSource.SerialIO, intEnable, intFlags)) return;
+					if (ServiceInterrupt(InterruptSource.Keypad, intEnable, intFlags)) return;
+				}
+			}
+		}
+
+		private bool ServiceInterrupt(InterruptSource intSource, byte intEnable, byte intFlags)
+		{
+			var intSourceBit = (byte)(1 << (byte)intSource);
+
+			if (((intEnable & intSourceBit) == intSourceBit) && ((intFlags & intSourceBit) == intSourceBit))
+			{
+				ime = false;
+
+				currentCycles += 20;
+
+				return RestartFromInterrupt(intSource);
+			}
+
+			return false;
+		}
+
+		protected virtual void EnterHaltState()
+		{
+			if (ime)
+			{
+				halt = true;
+				pc--;
+			}
+			else
+			{
+				if ((memoryReadDelegate(0xFF0F) & memoryReadDelegate(0xFFFF) & 0x1F) != 0)
+					doHaltBug = true;
+				else
+					halt = true;
+			}
+		}
+
+		private void LeaveHaltState()
+		{
+			if (halt)
+			{
+				halt = false;
+				if (ime)
+					pc++;
+			}
+		}
+
+		#endregion
+
+		#region Memory Access Functions
+
+		private byte ReadMemory8(ushort address)
+		{
+			return memoryReadDelegate(address);
+		}
+
+		private void WriteMemory8(ushort address, byte value)
+		{
+			memoryWriteDelegate(address, value);
+		}
+
+		private ushort ReadMemory16(ushort address)
+		{
+			return (ushort)((memoryReadDelegate((ushort)(address + 1)) << 8) | memoryReadDelegate(address));
+		}
+
+		private void WriteMemory16(ushort address, ushort value)
+		{
+			memoryWriteDelegate(address, (byte)(value & 0xFF));
+			memoryWriteDelegate((ushort)(address + 1), (byte)(value >> 8));
+		}
+
+		#endregion
+
+		#region Opcodes: 8-Bit Load Group
+
+		protected void LoadRegisterFromMemory8(ref byte register, ushort address, bool specialRegs)
+		{
+			LoadRegister8(ref register, ReadMemory8(address), specialRegs);
+		}
+
+		protected void LoadRegisterImmediate8(ref byte register, bool specialRegs)
+		{
+			LoadRegister8(ref register, ReadMemory8(pc++), specialRegs);
+		}
+
+		protected void LoadRegister8(ref byte register, byte value, bool specialRegs)
+		{
+			register = value;
+		}
+
+		protected void LoadMemory8(ushort address, byte value)
+		{
+			WriteMemory8(address, value);
+		}
+
+		#endregion
+
+		#region Opcodes: 16-Bit Load Group
+
+		protected void LoadRegisterImmediate16(ref ushort register)
+		{
+			LoadRegister16(ref register, ReadMemory16(pc));
+			pc += 2;
+		}
+
+		protected void LoadRegister16(ref ushort register, ushort value)
+		{
+			register = value;
+		}
+
+		protected void Push(Register register)
+		{
+			WriteMemory8(--sp, register.High);
+			WriteMemory8(--sp, register.Low);
+		}
+
+		protected void Pop(ref Register register)
+		{
+			register.Low = ReadMemory8(sp++);
+			register.High = ReadMemory8(sp++);
+		}
+
+		#endregion
+
+		#region Opcodes: 8-Bit Arithmetic Group
+
+		protected void Add8(byte operand, bool withCarry)
+		{
+			int operandWithCarry = (operand + (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+			int result = (af.High + operandWithCarry);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.Carry, (result > 0xFF));
+
+			af.High = (byte)result;
+		}
+
+		protected void Subtract8(byte operand, bool withCarry)
+		{
+			int operandWithCarry = (operand + (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+			int result = (af.High - operandWithCarry);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.Carry, (af.High < operandWithCarry));
+
+			af.High = (byte)result;
+		}
+
+		protected void And8(byte operand)
+		{
+			int result = (af.High & operand);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			ClearFlag(Flags.Subtract);
+			SetFlag(Flags.HalfCarry);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Or8(byte operand)
+		{
+			int result = (af.High | operand);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Xor8(byte operand)
+		{
+			int result = (af.High ^ operand);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Cp8(byte operand)
+		{
+			int result = (af.High - operand);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.Carry, (af.High < operand));
+		}
+
+		protected void Increment8(ref byte register)
+		{
+			byte result = (byte)(register + 1);
+
+			SetClearFlagConditional(Flags.Zero, (result == 0x00));
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, ((register & 0x0F) == 0x0F));
+			// C
+
+			register = result;
+		}
+
+		protected void IncrementMemory8(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			Increment8(ref value);
+			WriteMemory8(address, value);
+		}
+
+		protected void Decrement8(ref byte register)
+		{
+			byte result = (byte)(register - 1);
+
+			SetClearFlagConditional(Flags.Zero, (result == 0x00));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, ((register & 0x0F) == 0x00));
+			// C
+
+			register = result;
+		}
+
+		protected void DecrementMemory8(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			Decrement8(ref value);
+			WriteMemory8(address, value);
+		}
+
+		#endregion
+
+		#region Opcodes: General-Purpose Arithmetic and CPU Control Group
+
+		protected void DecimalAdjustAccumulator()
+		{
+			int value = af.High;
+
+			if (!IsFlagSet(Flags.Subtract))
+			{
+				if (IsFlagSet(Flags.HalfCarry) || ((value & 0x0F) > 9))
+					value += 0x06;
+				if (IsFlagSet(Flags.Carry) || (value > 0x9F))
+					value += 0x60;
+			}
+			else
+			{
+				if (IsFlagSet(Flags.HalfCarry))
+					value = (value - 0x06) & 0xFF;
+				if (IsFlagSet(Flags.Carry))
+					value -= 0x60;
+			}
+
+			ClearFlag(Flags.HalfCarry);
+			ClearFlag(Flags.Zero);
+
+			if ((value & 0x100) != 0) SetFlag(Flags.Carry);
+
+			value &= 0xFF;
+
+			if (value == 0) SetFlag(Flags.Zero);
+
+			af.High = (byte)value;
+		}
+
+		protected void Negate()
+		{
+			int result = (0 - af.High);
+
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, ((0 - (af.High & 0x0F)) < 0));
+			SetClearFlagConditional(Flags.Carry, (af.High != 0x00));
+
+			af.High = (byte)result;
+		}
+
+		protected void EnableInterrupts()
+		{
+			ime = false;
+			imeDelay = true;
+		}
+
+		protected void DisableInterrupts()
+		{
+			ime = false;
+		}
+
+		#endregion
+
+		#region Opcodes: 16-Bit Arithmetic Group
+
+		protected void Add16(ref Register dest, ushort operand)
+		{
+			int operandWithCarry = (short)operand;
+			int result = (dest.Word + operandWithCarry);
+
+			// Z
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((dest.Word & 0x0FFF) + (operandWithCarry & 0x0FFF)) > 0x0FFF));
+			SetClearFlagConditional(Flags.Carry, (((dest.Word & 0xFFFF) + (operandWithCarry & 0xFFFF)) > 0xFFFF));
+
+			dest.Word = (ushort)result;
+		}
+
+		protected void Increment16(ref ushort register)
+		{
+			register++;
+		}
+
+		protected void Decrement16(ref ushort register)
+		{
+			register--;
+		}
+
+		#endregion
+
+		#region Opcodes: Rotate and Shift Group
+
+		protected byte RotateLeft(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateLeft(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateLeft(ref byte value)
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+			if (isCarrySet) SetBit(ref value, 0);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte RotateLeftCircular(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateLeftCircular(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateLeftCircular(ref byte value)
+		{
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+			if (isMsbSet) SetBit(ref value, 0);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte RotateRight(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateRight(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateRight(ref byte value)
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+			if (isCarrySet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected byte RotateRightCircular(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateRightCircular(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateRightCircular(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+			if (isLsbSet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected void RotateLeftAccumulator()
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isMsbSet = IsBitSet(af.High, 7);
+			af.High <<= 1;
+			if (isCarrySet) SetBit(ref af.High, 0);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected void RotateLeftAccumulatorCircular()
+		{
+			bool isMsbSet = IsBitSet(af.High, 7);
+			af.High <<= 1;
+			if (isMsbSet) SetBit(ref af.High, 0);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected void RotateRightAccumulator()
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isLsbSet = IsBitSet(af.High, 0);
+			af.High >>= 1;
+			if (isCarrySet) SetBit(ref af.High, 7);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected void RotateRightAccumulatorCircular()
+		{
+			bool isLsbSet = IsBitSet(af.High, 0);
+			af.High >>= 1;
+			if (isLsbSet) SetBit(ref af.High, 7);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected byte ShiftLeftArithmetic(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftLeftArithmetic(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftLeftArithmetic(ref byte value)
+		{
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte ShiftRightArithmetic(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftRightArithmetic(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftRightArithmetic(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			bool isMsbSet = IsBitSet(value, 7);
+			value >>= 1;
+			if (isMsbSet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected byte ShiftRightLogical(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftRightLogical(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftRightLogical(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		#endregion
+
+		#region Opcodes: Bit Set, Reset and Test Group
+
+		protected byte SetBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+			SetBit(ref value, bit);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void SetBit(ref byte value, int bit)
+		{
+			value |= (byte)(1 << bit);
+		}
+
+		protected byte ResetBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+			ResetBit(ref value, bit);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ResetBit(ref byte value, int bit)
+		{
+			value &= (byte)~(1 << bit);
+		}
+
+		protected void TestBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+
+			TestBit(value, bit);
+		}
+
+		protected void TestBit(byte value, int bit)
+		{
+			bool isBitSet = ((value & (1 << bit)) != 0);
+
+			SetClearFlagConditional(Flags.Zero, !isBitSet);
+			ClearFlag(Flags.Subtract);
+			SetFlag(Flags.HalfCarry);
+			// C
+		}
+
+		#endregion
+
+		#region Opcodes: Jump Group
+
+		protected void Jump8()
+		{
+			pc += (ushort)(((sbyte)ReadMemory8(pc)) + 1);
+		}
+
+		protected void JumpConditional8(bool condition)
+		{
+			if (condition)
+			{
+				Jump8();
+				currentCycles += CycleCounts.AdditionalJumpCond8Taken;
+			}
+			else
+				pc++;
+		}
+
+		protected void JumpConditional16(bool condition)
+		{
+			if (condition)
+				pc = ReadMemory16(pc);
+			else
+				pc += 2;
+		}
+
+		#endregion
+
+		#region Opcodes: Call and Return Group
+
+		protected void Call16()
+		{
+			WriteMemory8(--sp, (byte)((pc + 2) >> 8));
+			WriteMemory8(--sp, (byte)((pc + 2) & 0xFF));
+			pc = ReadMemory16(pc);
+		}
+
+		protected void CallConditional16(bool condition)
+		{
+			if (condition)
+			{
+				Call16();
+				currentCycles += CycleCounts.AdditionalCallCondTaken;
+			}
+			else
+				pc += 2;
+		}
+
+		protected void Return()
+		{
+			pc = ReadMemory16(sp);
+			sp += 2;
+		}
+
+		protected void ReturnConditional(bool condition)
+		{
+			if (condition)
+			{
+				Return();
+				currentCycles += CycleCounts.AdditionalRetCondTaken;
+			}
+		}
+
+		protected void Restart(ushort address)
+		{
+			WriteMemory8(--sp, (byte)(pc >> 8));
+			WriteMemory8(--sp, (byte)(pc & 0xFF));
+			pc = address;
+		}
+
+		protected bool RestartFromInterrupt(InterruptSource intSource)
+		{
+			// https://github.com/Gekkio/mooneye-gb/blob/ca7ff30/tests/acceptance/interrupts/ie_push.s
+
+			var address = (ushort)(0x0040 + (byte)((int)intSource << 3));
+			var intSourceBit = (byte)(1 << (byte)intSource);
+
+			WriteMemory8(--sp, (byte)(pc >> 8));
+
+			var newIntEnable = memoryReadDelegate(0xFFFF);
+			var continueRestart = (newIntEnable & intSourceBit) != 0;
+
+			WriteMemory8(--sp, (byte)(pc & 0xFF));
+
+			if (continueRestart)
+			{
+				pc = address;
+				memoryWriteDelegate(0xFF0F, (byte)(memoryReadDelegate(0xFF0F) & (byte)~intSourceBit));
+			}
+			else
+				pc = 0x0000;
+
+			return continueRestart;
+		}
+
+		#endregion
+
+		#region Opcodes: SM83-specific Opcodes
+
+		protected void PopAF()
+		{
+			af.Low = (byte)(ReadMemory8(sp++) & 0xF0);
+			af.High = ReadMemory8(sp++);
+		}
+
+		protected void Swap(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			Swap(ref value);
+			WriteMemory8(address, value);
+		}
+
+		protected void Swap(ref byte value)
+		{
+			value = (byte)((value & 0xF0) >> 4 | (value & 0x0F) << 4);
+
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.HalfCarry);
+			ClearFlag(Flags.Carry);
+		}
+
+		protected virtual void Stop()
+		{
+			pc++;
+		}
+
+		private void AddSPNN()
+		{
+			byte offset = ReadMemory8(pc++);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((sp & 0x0F) + (offset & 0x0F)) > 0x0F));
+			SetClearFlagConditional(Flags.Carry, (((sp & 0xFF) + (byte)(offset & 0xFF)) > 0xFF));
+
+			sp = (ushort)(sp + (sbyte)offset);
+		}
+
+		private void LoadHLSPNN()
+		{
+			byte offset = ReadMemory8(pc++);
+
+			ClearFlag(Flags.Zero);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.HalfCarry, (((sp & 0x0F) + (offset & 0x0F)) > 0x0F));
+			SetClearFlagConditional(Flags.Carry, (((sp & 0xFF) + (byte)(offset & 0xFF)) > 0xFF));
+
+			hl.Word = (ushort)(sp + (sbyte)offset);
+		}
+
+		#endregion
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs.meta
new file mode 100644
index 0000000..00c9c49
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 18c6dc23d0278e847b2cc8483998c6fc
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs b/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs
new file mode 100644
index 0000000..9948871
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public class SM83CGB : SM83
+	{
+		// TODO: better way of implementing this?
+		public bool IsDoubleSpeed { get; private set; }
+
+		public SM83CGB(MemoryReadDelegate memoryRead, MemoryWriteDelegate memoryWrite) : base(memoryRead, memoryWrite) { }
+
+		protected override void EnterHaltState()
+		{
+			if (ime)
+			{
+				halt = true;
+				pc--;
+			}
+			else
+			{
+				if ((memoryReadDelegate(0xFF0F) & memoryReadDelegate(0xFFFF) & 0x1F) != 0)
+					currentCycles += 8;
+				else
+					halt = true;
+			}
+		}
+
+		protected override void Stop()
+		{
+			pc++;
+
+			// Perform speed switch; get IO register value
+			var key1 = memoryReadDelegate(0xFF4D);
+
+			// Is speed switch pending?
+			if ((key1 & 0b1) != 0)
+			{
+				// Clear pending flag
+				key1 &= 0xFE;
+
+				if (((key1 >> 7) & 0b1) != 0)
+				{
+					// Was double speed, now normal speed
+					key1 &= 0x7F;
+					IsDoubleSpeed = false;
+				}
+				else
+				{
+					// Was normal speed, now double speed
+					key1 |= 0x80;
+					IsDoubleSpeed = true;
+				}
+
+				// Write register value
+				memoryWriteDelegate(0xFF4D, key1);
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs.meta
new file mode 100644
index 0000000..7692894
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/SM83CGB.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cf45a7372b4385e429895c45fe9f4db0
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs
new file mode 100644
index 0000000..210cc18
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		public static class CycleCounts
+		{
+			public const int AdditionalJumpCond8Taken = 5;
+			public const int AdditionalRetCondTaken = 6;
+			public const int AdditionalCallCondTaken = 7;
+			public const int AdditionalRepeatByteOps = 5;
+			public const int AdditionalDDFDOps = 4;
+			public const int AdditionalDDFDCBOps = 8;
+
+			public static readonly int[] NoPrefix = new int[]
+			{
+				4,  10, 7,  6,  4,  4,  7,  4,      4,  11, 7,  6,  4,  4,  7,  4,  /* 0x00 - 0x0F */
+				8,  10, 7,  6,  4,  4,  7,  4,      12, 11, 7,  6,  4,  4,  7,  4,  /* 0x10 - 0x1F */
+				7,  10, 16, 6,  4,  4,  7,  4,      7,  11, 16, 6,  4,  4,  4,  4,  /* 0x20 - 0x2F */
+				7,  10, 13, 6,  11, 11, 10, 4,      7,  11, 13, 6,  4,  4,  7,  4,  /* 0x30 - 0x3F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x40 - 0x4F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x50 - 0x5F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x60 - 0x6F */
+				7,  7,  7,  7,  7,  7,  4,  7,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x70 - 0x7F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x80 - 0x8F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0x90 - 0x9F */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0xA0 - 0xAF */
+				4,  4,  4,  4,  4,  4,  7,  4,      4,  4,  4,  4,  4,  4,  7,  4,  /* 0xB0 - 0xBF */
+				5,  10, 10, 10, 10, 11, 7,  11,     5,  10, 10, 0,  10, 17, 7,  11, /* 0xC0 - 0xCF */
+				5,  10, 10, 11, 10, 11, 7,  11,     5,  4,  10, 11, 10, 0,  7,  11, /* 0xD0 - 0xDF */
+				5,  10, 10, 19, 10, 11, 7,  11,     5,  4,  10, 4,  10, 0,  7,  11, /* 0xE0 - 0xEF */
+				5,  10, 10, 4,  10, 11, 7,  11,     5,  6,  10, 4,  10, 0,  7,  11  /* 0xF0 - 0xFF */
+			};
+
+			public static readonly int[] PrefixED = new int[]
+			{
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x00 - 0x0F */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x10 - 0x1F */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x20 - 0x2F */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x30 - 0x3F */
+				12, 12, 15, 20, 8,  14, 8,  9,      12, 12, 15, 20, 8,  14, 8,  9,  /* 0x40 - 0x4F */
+				12, 12, 15, 20, 8,  14, 8,  9,      12, 12, 15, 20, 8,  14, 8,  9,  /* 0x50 - 0x5F */
+				12, 12, 15, 20, 8,  14, 8,  18,     12, 12, 15, 20, 8,  14, 8,  18, /* 0x60 - 0x6F */
+				12, 12, 15, 20, 8,  14, 8,  4,      12, 12, 15, 20, 8,  14, 8,  4,  /* 0x70 - 0x7F */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x80 - 0x8F */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0x90 - 0x9F */
+				16, 16, 16, 16, 8,  8,  8,  8,      16, 16, 16, 16, 8,  8,  8,  8,  /* 0xA0 - 0xAF */
+				16, 16, 16, 16, 8,  8,  8,  8,      16, 16, 16, 16, 8,  8,  8,  8,  /* 0xB0 - 0xBF */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0xC0 - 0xCF */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0xD0 - 0xDF */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8,  /* 0xE0 - 0xEF */
+				8,  8,  8,  8,  8,  8,  8,  8,      8,  8,  8,  8,  8,  8,  8,  8   /* 0xF0 - 0xFF */
+			};
+
+			public static readonly int[] PrefixCB = new int[]
+			{
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x00 - 0x0F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x10 - 0x1F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x20 - 0x2F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x30 - 0x3F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x40 - 0x4F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x50 - 0x5F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x60 - 0x6F */
+				8,  8,  8,  8,  8,  8,  12, 8,      8,  8,  8,  8,  8,  8,  12, 8,  /* 0x70 - 0x7F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x80 - 0x8F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0x90 - 0x9F */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0xA0 - 0xAF */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0xB0 - 0xBF */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0xC0 - 0xCF */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0xD0 - 0xDF */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8,  /* 0xE0 - 0xEF */
+				8,  8,  8,  8,  8,  8,  15, 8,      8,  8,  8,  8,  8,  8,  15, 8   /* 0xF0 - 0xFF */
+			};
+
+			public static readonly int[] PrefixDDFD = new int[]
+			{
+				0,  0,  0,  0,  0,  0,  0,  0,      0,  15, 0,  0,  0,  0,  0,  0,  /* 0x00 - 0x0F */
+				0,  0,  0,  0,  0,  0,  0,  0,      0,  15, 0,  0,  0,  0,  0,  0,  /* 0x10 - 0x1F */
+				0,  14, 20, 10, 0,  0,  0,  0,      0,  15, 20, 10, 0,  0,  0,  0,  /* 0x20 - 0x2F */
+				0,  0,  0,  0,  23, 23, 19, 0,      0,  15, 0,  0,  0,  0,  0,  0,  /* 0x30 - 0x3F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0x40 - 0x4F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0x50 - 0x5F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0x60 - 0x6F */
+				19, 19, 19, 19, 19, 19, 0,  19,     0,  0,  0,  0,  0,  0,  19, 0,  /* 0x70 - 0x7F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0x80 - 0x8F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0x90 - 0x9F */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0xA0 - 0xAF */
+				0,  0,  0,  0,  0,  0,  19, 0,      0,  0,  0,  0,  0,  0,  19, 0,  /* 0xB0 - 0xBF */
+				0,  0,  0,  0,  0,  0,  0,  0,      0,  0,  0,  0,  0,  0,  0,  0,  /* 0xC0 - 0xCF */
+				0,  0,  0,  0,  0,  0,  0,  0,      0,  0,  0,  0,  0,  0,  0,  0,  /* 0xD0 - 0xDF */
+				0,  14, 0,  23, 0,  15, 0,  0,      0,  8,  0,  0,  0,  0,  0,  0,  /* 0xE0 - 0xEF */
+				0,  0,  0,  0,  0,  0,  0,  0,      0,  10, 0,  0,  0,  0,  0,  0   /* 0xF0 - 0xFF */
+			};
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs.meta
new file mode 100644
index 0000000..a72f1fa
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.CycleCounts.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3c40dfcb38d98fe45a313cee0df21449
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs
new file mode 100644
index 0000000..e2c07de
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs
@@ -0,0 +1,489 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		// TODO: add mnemonics for undocumented opcodes
+
+		static readonly string[] opcodeMnemonicNoPrefix = new string[]
+		{
+			"NOP",                  "LD BC, 0x{0:X4}",      "LD (BC), A",           "INC BC",               "INC B",                "DEC B",                "LD B, 0x{0:X2}",       "RLCA",                 /* 0x00 */
+			"EX AF, AF'",           "ADD HL, BC",           "LD A, (BC)",           "DEC BC",               "INC C",                "DEC C",                "LD C, 0x{0:X2}",       "RRCA",                 /* 0x08 */
+			"DJNZ 0x{0:X2}",        "LD DE, 0x{0:X4}",      "LD (DE), A",           "INC DE",               "INC D",                "DEC D",                "LD D, 0x{0:X2}",       "RLA",                  /* 0x10 */
+			"JR 0x{0:X2}",          "ADD HL, DE",           "LD A, (DE)",           "DEC DE",               "INC E",                "DEC E",                "LD E, 0x{0:X2}",       "RRA",                  /* 0x18 */
+			"JR NZ, 0x{0:X2}",      "LD HL, 0x{0:X4}",      "LD (0x{0:X4}), HL",    "INC HL",               "INC H",                "DEC H",                "LD H, 0x{0:X2}",       "DAA",                  /* 0x20 */
+			"JR Z, 0x{0:X2}",       "ADD HL, HL",           "LD HL, (0x{0:X4})",    "DEC HL",               "INC L",                "DEC L",                "LD L, 0x{0:X2}",       "CPL",                  /* 0x28 */
+			"JR NC, 0x{0:X2}",      "LD SP, 0x{0:X4}",      "LD (0x{0:X4}), A",     "INC SP",               "INC (HL)",             "DEC (HL)",             "LD (HL), 0x{0:X2}",    "SCF",                  /* 0x30 */
+			"JR C, 0x{0:X2}",       "ADD HL, SP",           "LD A, (0x{0:X4})",     "DEC SP",               "INC A",                "DEC A",                "LD A, 0x{0:X2}",       "CCF",                  /* 0x38 */
+			"LD B, B",              "LD B, C",              "LD B, D",              "LD B, E",              "LD B, H",              "LD B, L",              "LD B, (HL)",           "LD B, A",              /* 0x40 */
+			"LD C, B",              "LD C, C",              "LD C, D",              "LD C, E",              "LD C, H",              "LD C, L",              "LD C, (HL)",           "LD C, A",              /* 0x48 */
+			"LD D, B",              "LD D, C",              "LD D, D",              "LD D, E",              "LD D, H",              "LD D, L",              "LD D, (HL)",           "LD D, A",              /* 0x50 */
+			"LD E, B",              "LD E, C",              "LD E, D",              "LD E, E",              "LD E, H",              "LD E, L",              "LD E, (HL)",           "LD E, A",              /* 0x58 */
+			"LD H, B",              "LD H, C",              "LD H, D",              "LD H, E",              "LD H, H",              "LD H, L",              "LD H, (HL)",           "LD H, A",              /* 0x60 */
+			"LD L, B",              "LD L, C",              "LD L, D",              "LD L, E",              "LD L, H",              "LD L, L",              "LD L, (HL)",           "LD L, A",              /* 0x68 */
+			"LD (HL), B",           "LD (HL), C",           "LD (HL), D",           "LD (HL), E",           "LD (HL), H",           "LD (HL), L",           "HALT",                 "LD (HL), A",           /* 0x70 */
+			"LD A, B",              "LD A, C",              "LD A, D",              "LD A, E",              "LD A, H",              "LD A, L",              "LD A, (HL)",           "LD A, A",              /* 0x78 */
+			"ADD A, B",             "ADD A, C",             "ADD A, D",             "ADD A, E",             "ADD A, H",             "ADD A, L",             "ADD A, (HL)",          "ADD A, A",             /* 0x80 */
+			"ADC A, B",             "ADC A, C",             "ADC A, D",             "ADC A, E",             "ADC A, H",             "ADC A, L",             "ADC A, (HL)",          "ADC A, A",             /* 0x88 */
+			"SUB B",                "SUB C",                "SUB D",                "SUB E",                "SUB H",                "SUB L",                "SUB (HL)",             "SUB A",                /* 0x90 */
+			"SBC B",                "SBC C",                "SBC D",                "SBC E",                "SBC H",                "SBC L",                "SBC (HL)",             "SBC A",                /* 0x98 */
+			"AND B",                "AND C",                "AND D",                "AND E",                "AND H",                "AND L",                "AND (HL)",             "AND A",                /* 0xA0 */
+			"XOR B",                "XOR C",                "XOR D",                "XOR E",                "XOR H",                "XOR L",                "XOR (HL)",             "XOR A",                /* 0xA8 */
+			"OR B",                 "OR C",                 "OR D",                 "OR E",                 "OR H",                 "OR L",                 "OR (HL)",              "OR A",                 /* 0xA0 */
+			"CP B",                 "CP C",                 "CP D",                 "CP E",                 "CP H",                 "CP L",                 "CP (HL)",              "CP A",                 /* 0xB8 */
+			"RET NZ",               "POP BC",               "JP NZ, 0x{0:X4}",      "JP 0x{0:X4}",          "CALL NZ, 0x{0:X4}",    "PUSH BC",              "ADD A, 0x{0:X2}",      "RST 00",               /* 0xC0 */
+			"RET Z",                "RET",                  "JP Z, 0x{0:X4}",       string.Empty,           "CALL Z, 0x{0:X4}",     "CALL 0x{0:X4}",        "ADC A, 0x{0:X2}",      "RST 08",               /* 0xC8 */
+			"RET NC",               "POP DE",               "JP NC, 0x{0:X4}",      "OUT 0x{0:X2}, A",      "CALL NC, 0x{0:X4}",    "PUSH DE",              "SUB 0x{0:X2}",         "RST 10",               /* 0xD0 */
+			"RET C",                "EXX",                  "JP C, 0x{0:X4}",       "IN A, 0x{0:X2}",       "CALL C, 0x{0:X4}",     string.Empty,           "SBC 0x{0:X2}",         "RST 18",               /* 0xD8 */
+			"RET PO",               "POP HL",               "JP PO, 0x{0:X4}",      "EX (SP), HL",          "CALL PO, 0x{0:X4}",    "PUSH HL",              "AND 0x{0:X2}",         "RST 20",               /* 0xE0 */
+			"RET PE",               "JP (HL)",              "JP PE, 0x{0:X4}",      "EX DE, HL",            "CALL PE, 0x{0:X4}",    string.Empty,           "XOR 0x{0:X2}",         "RST 28",               /* 0xE8 */
+			"RET P",                "POP AF",               "JP P, 0x{0:X4}",       "DI",                   "CALL P, 0x{0:X4}",     "PUSH AF",              "OR 0x{0:X2}",          "RST 30",               /* 0xF0 */
+			"RET M",                "LD SP, HL",            "JP M, 0x{0:X4}",       "EI",                   "CALL M, 0x{0:X4}",     string.Empty,           "CP 0x{0:X2}",          "RST 38"                /* 0xF0 */
+		};
+
+		static readonly int[] opcodeLengthNoPrefix = new int[]
+		{
+			1, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1,
+			2, 3, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 2, 1,
+			2, 3, 3, 1, 1, 1, 2, 1, 2, 1, 3, 1, 1, 1, 2, 1,
+			2, 3, 3, 1, 1, 1, 2, 1, 2, 1, 3, 1, 1, 1, 2, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+			1, 1, 3, 3, 3, 1, 2, 1, 1, 1, 3, -1, 3, 3, 2, 1,
+			1, 1, 3, 2, 3, 1, 2, 1, 1, 1, 3, 2, 3, -1, 2, 1,
+			1, 1, 3, 1, 3, 1, 2, 1, 1, 1, 3, 1, 3, -1, 2, 1,
+			1, 1, 3, 1, 3, 1, 2, 1, 1, 1, 3, 1, 3, -1, 2, 1,
+		};
+
+		static readonly string[] opcodeMnemonicPrefixED = new string[]
+		{
+			".DB 0xED, 0x00",       ".DB 0xED, 0x01",       ".DB 0xED, 0x02",       ".DB 0xED, 0x03",       ".DB 0xED, 0x04",       ".DB 0xED, 0x05",       ".DB 0xED, 0x06",       ".DB 0xED, 0x07",       /* 0x00 */
+			".DB 0xED, 0x08",       ".DB 0xED, 0x09",       ".DB 0xED, 0x0A",       ".DB 0xED, 0x0B",       ".DB 0xED, 0x0C",       ".DB 0xED, 0x0D",       ".DB 0xED, 0x0E",       ".DB 0xED, 0x0F",       /* 0x08 */
+			".DB 0xED, 0x10",       ".DB 0xED, 0x11",       ".DB 0xED, 0x12",       ".DB 0xED, 0x13",       ".DB 0xED, 0x14",       ".DB 0xED, 0x15",       ".DB 0xED, 0x16",       ".DB 0xED, 0x17",       /* 0x10 */
+			".DB 0xED, 0x18",       ".DB 0xED, 0x19",       ".DB 0xED, 0x1A",       ".DB 0xED, 0x1B",       ".DB 0xED, 0x1C",       ".DB 0xED, 0x1D",       ".DB 0xED, 0x1E",       ".DB 0xED, 0x1F",       /* 0x18 */
+			".DB 0xED, 0x20",       ".DB 0xED, 0x21",       ".DB 0xED, 0x22",       ".DB 0xED, 0x23",       ".DB 0xED, 0x24",       ".DB 0xED, 0x25",       ".DB 0xED, 0x26",       ".DB 0xED, 0x27",       /* 0x20 */
+			".DB 0xED, 0x28",       ".DB 0xED, 0x29",       ".DB 0xED, 0x2A",       ".DB 0xED, 0x2B",       ".DB 0xED, 0x2C",       ".DB 0xED, 0x2D",       ".DB 0xED, 0x2E",       ".DB 0xED, 0x2F",       /* 0x28 */
+			".DB 0xED, 0x30",       ".DB 0xED, 0x31",       ".DB 0xED, 0x32",       ".DB 0xED, 0x33",       ".DB 0xED, 0x34",       ".DB 0xED, 0x35",       ".DB 0xED, 0x36",       ".DB 0xED, 0x37",       /* 0x30 */
+			".DB 0xED, 0x38",       ".DB 0xED, 0x39",       ".DB 0xED, 0x3A",       ".DB 0xED, 0x3B",       ".DB 0xED, 0x3C",       ".DB 0xED, 0x3D",       ".DB 0xED, 0x3E",       ".DB 0xED, 0x3F",       /* 0x38 */
+			"IN B, (C)",            "OUT (C), B",           "SBC HL, BC",           "LD (0x{0:X4}), BC",    "NEG",                  "RETN",                 "IM 0",                 "LD I, A",              /* 0x40 */
+			"IN C, (C)",            "OUT (C), C",           "ADC HL, BC",           "LD BC, (0x{0:X4})",    ".DB 0xED, 0x4C",       "RETI",                 ".DB 0xED, 0x4E",       "LD R, A",              /* 0x48 */
+			"IN D, (C)",            "OUT (C), D",           "SBC HL, DE",           "LD (0x{0:X4}), DE",    ".DB 0xED, 0x54",       ".DB 0xED, 0x55",       "IM 1",                 "LD A, I",              /* 0x50 */
+			"IN E, (C)",            "OUT (C), E",           "ADC HL, DE",           "LD DE, (0x{0:X4})",    ".DB 0xED, 0x5C",       ".DB 0xED, 0x5D",       "IM 2",                 "LD A, R",              /* 0x58 */
+			"IN H, (C)",            "OUT (C), H",           "SBC HL, HL",           ".DB 0xED, 0x63",       ".DB 0xED, 0x64",       ".DB 0xED, 0x65",       ".DB 0xED, 0x66",       "RRD",                  /* 0x60 */
+			"IN L, (C)",            "OUT (C), L",           "ADC HL, HL",           ".DB 0xED, 0x6B",       ".DB 0xED, 0x6C",       ".DB 0xED, 0x6D",       ".DB 0xED, 0x6E",       "RLD",                  /* 0x68 */
+			".DB 0xED, 0x70",       ".DB 0xED, 0x71",       "SBC HL, SP",           "LD (0x{0:X4}), SP",    ".DB 0xED, 0x74",       ".DB 0xED, 0x75",       ".DB 0xED, 0x76",       ".DB 0xED, 0x77",       /* 0x70 */
+			"IN A, (C)",            "OUT (C), A",           "ADC HL, SP",           "LD SP, (0x{0:X4})",    ".DB 0xED, 0x7C",       ".DB 0xED, 0x7D",       ".DB 0xED, 0x7E",       ".DB 0xED, 0x7F",       /* 0x78 */
+			".DB 0xED, 0x80",       ".DB 0xED, 0x81",       ".DB 0xED, 0x82",       ".DB 0xED, 0x83",       ".DB 0xED, 0x84",       ".DB 0xED, 0x85",       ".DB 0xED, 0x86",       ".DB 0xED, 0x87",       /* 0x80 */
+			".DB 0xED, 0x88",       ".DB 0xED, 0x89",       ".DB 0xED, 0x8A",       ".DB 0xED, 0x8B",       ".DB 0xED, 0x8C",       ".DB 0xED, 0x8D",       ".DB 0xED, 0x8E",       ".DB 0xED, 0x8F",       /* 0x88 */
+			".DB 0xED, 0x90",       ".DB 0xED, 0x91",       ".DB 0xED, 0x92",       ".DB 0xED, 0x93",       ".DB 0xED, 0x94",       ".DB 0xED, 0x95",       ".DB 0xED, 0x96",       ".DB 0xED, 0x97",       /* 0x90 */
+			".DB 0xED, 0x98",       ".DB 0xED, 0x99",       ".DB 0xED, 0x9A",       ".DB 0xED, 0x9B",       ".DB 0xED, 0x9C",       ".DB 0xED, 0x9D",       ".DB 0xED, 0x9E",       ".DB 0xED, 0x9F",       /* 0x98 */
+			"LDI",                  "CPI",                  "INI",                  "OUTI",                 ".DB 0xED, 0xA4",       ".DB 0xED, 0xA5",       ".DB 0xED, 0xA6",       ".DB 0xED, 0xA7",       /* 0xA0 */
+			"LDD",                  "CPD",                  "IND",                  "OUTD",                 ".DB 0xED, 0xAC",       ".DB 0xED, 0xAD",       ".DB 0xED, 0xAE",       ".DB 0xED, 0xAF",       /* 0xA8 */
+			"LDIR",                 "CPIR",                 "INIR",                 "OTIR",                 ".DB 0xED, 0xB4",       ".DB 0xED, 0xB5",       ".DB 0xED, 0xB6",       ".DB 0xED, 0xB7",       /* 0xB0 */
+			"LDDR",                 "CPDR",                 "INDR",                 "OTDR",                 ".DB 0xED, 0xBC",       ".DB 0xED, 0xBD",       ".DB 0xED, 0xBE",       ".DB 0xED, 0xBF",       /* 0xB8 */
+			".DB 0xED, 0xC0",       ".DB 0xED, 0xC1",       ".DB 0xED, 0xC2",       ".DB 0xED, 0xC3",       ".DB 0xED, 0xC4",       ".DB 0xED, 0xC5",       ".DB 0xED, 0xC6",       ".DB 0xED, 0xC7",       /* 0xC0 */
+			".DB 0xED, 0xC8",       ".DB 0xED, 0xC9",       ".DB 0xED, 0xCA",       ".DB 0xED, 0xCB",       ".DB 0xED, 0xCC",       ".DB 0xED, 0xCD",       ".DB 0xED, 0xCE",       ".DB 0xED, 0xCF",       /* 0xC8 */
+			".DB 0xED, 0xD0",       ".DB 0xED, 0xD1",       ".DB 0xED, 0xD2",       ".DB 0xED, 0xD3",       ".DB 0xED, 0xD4",       ".DB 0xED, 0xD5",       ".DB 0xED, 0xD6",       ".DB 0xED, 0xD7",       /* 0xD0 */
+			".DB 0xED, 0xD8",       ".DB 0xED, 0xD9",       ".DB 0xED, 0xDA",       ".DB 0xED, 0xDB",       ".DB 0xED, 0xDC",       ".DB 0xED, 0xDD",       ".DB 0xED, 0xDE",       ".DB 0xED, 0xDF",       /* 0xD8 */
+			".DB 0xED, 0xE0",       ".DB 0xED, 0xE1",       ".DB 0xED, 0xE2",       ".DB 0xED, 0xE3",       ".DB 0xED, 0xE4",       ".DB 0xED, 0xE5",       ".DB 0xED, 0xE6",       ".DB 0xED, 0xE7",       /* 0xE0 */
+			".DB 0xED, 0xE8",       ".DB 0xED, 0xE9",       ".DB 0xED, 0xEA",       ".DB 0xED, 0xEB",       ".DB 0xED, 0xEC",       ".DB 0xED, 0xED",       ".DB 0xED, 0xEE",       ".DB 0xED, 0xEF",       /* 0xE8 */
+			".DB 0xED, 0xF0",       ".DB 0xED, 0xF1",       ".DB 0xED, 0xF2",       ".DB 0xED, 0xF3",       ".DB 0xED, 0xF4",       ".DB 0xED, 0xF5",       ".DB 0xED, 0xF6",       ".DB 0xED, 0xF7",       /* 0xF0 */
+			".DB 0xED, 0xF8",       ".DB 0xED, 0xF9",       ".DB 0xED, 0xFA",       ".DB 0xED, 0xFB",       ".DB 0xED, 0xFC",       ".DB 0xED, 0xFD",       ".DB 0xED, 0xFE",       ".DB 0xED, 0xFF"        /* 0xF8 */
+		};
+
+		static readonly int[] opcodeLengthPrefixED = new int[]
+		{
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2,
+			2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+		};
+
+		static readonly string[] opcodeMnemonicPrefixCB = new string[]
+		{
+			"RLC B",                "RLC C",                "RLC D",                "RLC E",                "RLC H",                "RLC L",                "RLC (HL)",             "RLC A",                /* 0x00 */
+			"RRC B",                "RRC C",                "RRC D",                "RRC E",                "RRC H",                "RRC L",                "RRC (HL)",             "RRC A",                /* 0x08 */
+			"RL B",                 "RL C",                 "RL D",                 "RL E",                 "RL H",                 "RL L",                 "RL (HL)",              "RL A",                 /* 0x10 */
+			"RR B",                 "RR C",                 "RR D",                 "RR E",                 "RR H",                 "RR L",                 "RR (HL)",              "RR A",                 /* 0x18 */
+			"SLA B",                "SLA C",                "SLA D",                "SLA E",                "SLA H",                "SLA L",                "SLA (HL)",             "SLA A",                /* 0x20 */
+			"SRA B",                "SRA C",                "SRA D",                "SRA E",                "SRA H",                "SRA L",                "SRA (HL)",             "SRA A",                /* 0x28 */
+			"SLL B",                "SLL C",                "SLL D",                "SLL E",                "SLL H",                "SLL L",                "SLL (HL)",             "SLL A",                /* 0x30 */
+			"SRL B",                "SRL C",                "SRL D",                "SRL E",                "SRL H",                "SRL L",                "SRL (HL)",             "SRL A",                /* 0x38 */
+			"BIT 0, B",             "BIT 0, C",             "BIT 0, D",             "BIT 0, E",             "BIT 0, H",             "BIT 0, L",             "BIT 0, (HL)",          "BIT 0, A",             /* 0x40 */
+			"BIT 1, B",             "BIT 1, C",             "BIT 1, D",             "BIT 1, E",             "BIT 1, H",             "BIT 1, L",             "BIT 1, (HL)",          "BIT 1, A",             /* 0x48 */
+			"BIT 2, B",             "BIT 2, C",             "BIT 2, D",             "BIT 2, E",             "BIT 2, H",             "BIT 2, L",             "BIT 2, (HL)",          "BIT 2, A",             /* 0x50 */
+			"BIT 3, B",             "BIT 3, C",             "BIT 3, D",             "BIT 3, E",             "BIT 3, H",             "BIT 3, L",             "BIT 3, (HL)",          "BIT 3, A",             /* 0x58 */
+			"BIT 4, B",             "BIT 4, C",             "BIT 4, D",             "BIT 4, E",             "BIT 4, H",             "BIT 4, L",             "BIT 4, (HL)",          "BIT 4, A",             /* 0x60 */
+			"BIT 5, B",             "BIT 5, C",             "BIT 5, D",             "BIT 5, E",             "BIT 5, H",             "BIT 5, L",             "BIT 5, (HL)",          "BIT 5, A",             /* 0x68 */
+			"BIT 6, B",             "BIT 6, C",             "BIT 6, D",             "BIT 6, E",             "BIT 6, H",             "BIT 6, L",             "BIT 6, (HL)",          "BIT 6, A",             /* 0x70 */
+			"BIT 7, B",             "BIT 7, C",             "BIT 7, D",             "BIT 7, E",             "BIT 7, H",             "BIT 7, L",             "BIT 7, (HL)",          "BIT 7, A",             /* 0x78 */
+			"RES 0, B",             "RES 0, C",             "RES 0, D",             "RES 0, E",             "RES 0, H",             "RES 0, L",             "RES 0, (HL)",          "RES 0, A",             /* 0x80 */
+			"RES 1, B",             "RES 1, C",             "RES 1, D",             "RES 1, E",             "RES 1, H",             "RES 1, L",             "RES 1, (HL)",          "RES 1, A",             /* 0x88 */
+			"RES 2, B",             "RES 2, C",             "RES 2, D",             "RES 2, E",             "RES 2, H",             "RES 2, L",             "RES 2, (HL)",          "RES 2, A",             /* 0x90 */
+			"RES 3, B",             "RES 3, C",             "RES 3, D",             "RES 3, E",             "RES 3, H",             "RES 3, L",             "RES 3, (HL)",          "RES 3, A",             /* 0x98 */
+			"RES 4, B",             "RES 4, C",             "RES 4, D",             "RES 4, E",             "RES 4, H",             "RES 4, L",             "RES 4, (HL)",          "RES 4, A",             /* 0xA0 */
+			"RES 5, B",             "RES 5, C",             "RES 5, D",             "RES 5, E",             "RES 5, H",             "RES 5, L",             "RES 5, (HL)",          "RES 5, A",             /* 0xA8 */
+			"RES 6, B",             "RES 6, C",             "RES 6, D",             "RES 6, E",             "RES 6, H",             "RES 6, L",             "RES 6, (HL)",          "RES 6, A",             /* 0xB0 */
+			"RES 7, B",             "RES 7, C",             "RES 7, D",             "RES 7, E",             "RES 7, H",             "RES 7, L",             "RES 7, (HL)",          "RES 7, A",             /* 0xB8 */
+			"SET 0, B",             "SET 0, C",             "SET 0, D",             "SET 0, E",             "SET 0, H",             "SET 0, L",             "SET 0, (HL)",          "SET 0, A",             /* 0xC0 */
+			"SET 1, B",             "SET 1, C",             "SET 1, D",             "SET 1, E",             "SET 1, H",             "SET 1, L",             "SET 1, (HL)",          "SET 1, A",             /* 0xC8 */
+			"SET 2, B",             "SET 2, C",             "SET 2, D",             "SET 2, E",             "SET 2, H",             "SET 2, L",             "SET 2, (HL)",          "SET 2, A",             /* 0xD0 */
+			"SET 3, B",             "SET 3, C",             "SET 3, D",             "SET 3, E",             "SET 3, H",             "SET 3, L",             "SET 3, (HL)",          "SET 3, A",             /* 0xD8 */
+			"SET 4, B",             "SET 4, C",             "SET 4, D",             "SET 4, E",             "SET 4, H",             "SET 4, L",             "SET 4, (HL)",          "SET 4, A",             /* 0xE0 */
+			"SET 5, B",             "SET 5, C",             "SET 5, D",             "SET 5, E",             "SET 5, H",             "SET 5, L",             "SET 5, (HL)",          "SET 5, A",             /* 0xE8 */
+			"SET 6, B",             "SET 6, C",             "SET 6, D",             "SET 6, E",             "SET 6, H",             "SET 6, L",             "SET 6, (HL)",          "SET 6, A",             /* 0xF0 */
+			"SET 7, B",             "SET 7, C",             "SET 7, D",             "SET 7, E",             "SET 7, H",             "SET 7, L",             "SET 7, (HL)",          "SET 7, A"              /* 0xF8 */
+		};
+
+		static readonly int[] opcodeLength_CB = new int[]
+		{
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+		};
+
+		static readonly string[] opcodeMnemonicPrefixDD = new string[]
+		{
+			".DB 0xDD, 0x00",       ".DB 0xDD, 0x01",       ".DB 0xDD, 0x02",       ".DB 0xDD, 0x03",       ".DB 0xDD, 0x04",       ".DB 0xDD, 0x05",       ".DB 0xDD, 0x06",       ".DB 0xDD, 0x07",       /* 0x00 */
+			".DB 0xDD, 0x08",       "ADD IX, BC",           ".DB 0xDD, 0x0A",       ".DB 0xDD, 0x0B",       ".DB 0xDD, 0x0C",       ".DB 0xDD, 0x0D",       ".DB 0xDD, 0x0E",       ".DB 0xDD, 0x0F",       /* 0x08 */
+			".DB 0xDD, 0x10",       ".DB 0xDD, 0x11",       ".DB 0xDD, 0x12",       ".DB 0xDD, 0x13",       ".DB 0xDD, 0x14",       ".DB 0xDD, 0x15",       ".DB 0xDD, 0x16",       ".DB 0xDD, 0x17",       /* 0x10 */
+			".DB 0xDD, 0x18",       "ADD IX, DE",           ".DB 0xDD, 0x1A",       ".DB 0xDD, 0x1B",       ".DB 0xDD, 0x1C",       ".DB 0xDD, 0x1D",       ".DB 0xDD, 0x1E",       ".DB 0xDD, 0x1F",       /* 0x18 */
+			".DB 0xDD, 0x20",       "LD IX, 0x{0:X4}",      "LD (0x{0:X4}), IX",    "INC IX",               "INC IXH",              "DEC IXH",              "LD IXH, 0x{0:X2}",     ".DB 0xDD, 0x27",       /* 0x20 */
+			".DB 0xDD, 0x28",       "ADD IX, IX",           "LD IX, (0x{0:X4})",    "DEC IX",               "INC IXL",              "DEC IXL",              "LD IXL, 0x{0:X2}",     ".DB 0xDD, 0x2F",       /* 0x28 */
+			".DB 0xDD, 0x30",       ".DB 0xDD, 0x31",       ".DB 0xDD, 0x32",       ".DB 0xDD, 0x33",       "INC (IX+0x{0:X2})",    "DEC (IX+0x{0:X2})",    "LD (IX+0x{0:X2}), 0x{0:X2}",   ".DB 0xDD, 0x37",   /* 0x30 */
+			".DB 0xDD, 0x38",       "ADD IX, SP",           ".DB 0xDD, 0x3A",       ".DB 0xDD, 0x3B",       ".DB 0xDD, 0x3C",       ".DB 0xDD, 0x3D",       ".DB 0xDD, 0x3E",       ".DB 0xDD, 0x3F",       /* 0x38 */
+			".DB 0xDD, 0x40",       ".DB 0xDD, 0x41",       ".DB 0xDD, 0x42",       ".DB 0xDD, 0x43",       ".DB 0xDD, 0x44",       ".DB 0xDD, 0x45",       "LD B, (IX+0x{0:X2})",  ".DB 0xDD, 0x47",       /* 0x40 */
+			".DB 0xDD, 0x48",       ".DB 0xDD, 0x49",       ".DB 0xDD, 0x4A",       ".DB 0xDD, 0x4B",       ".DB 0xDD, 0x4C",       ".DB 0xDD, 0x4D",       "LD C, (IX+0x{0:X2})",  ".DB 0xDD, 0x4F",       /* 0x48 */
+			".DB 0xDD, 0x50",       ".DB 0xDD, 0x51",       ".DB 0xDD, 0x52",       ".DB 0xDD, 0x53",       ".DB 0xDD, 0x54",       ".DB 0xDD, 0x55",       "LD D, (IX+0x{0:X2})",  ".DB 0xDD, 0x57",       /* 0x50 */
+			".DB 0xDD, 0x58",       ".DB 0xDD, 0x59",       ".DB 0xDD, 0x5A",       ".DB 0xDD, 0x5B",       ".DB 0xDD, 0x5C",       ".DB 0xDD, 0x5D",       "LD E, (IX+0x{0:X2})",  ".DB 0xDD, 0x5F",       /* 0x58 */
+			".DB 0xDD, 0x60",       ".DB 0xDD, 0x61",       ".DB 0xDD, 0x62",       ".DB 0xDD, 0x63",       ".DB 0xDD, 0x64",       ".DB 0xDD, 0x65",       "LD H, (IX+0x{0:X2})",  ".DB 0xDD, 0x67",       /* 0x60 */
+			".DB 0xDD, 0x68",       ".DB 0xDD, 0x69",       ".DB 0xDD, 0x6A",       ".DB 0xDD, 0x6B",       ".DB 0xDD, 0x6C",       ".DB 0xDD, 0x6D",       "LD L, (IX+0x{0:X2})",  ".DB 0xDD, 0x6F",       /* 0x68 */
+			"LD (IX+0x{0:X2}), B",  "LD (IX+0x{0:X2}), C",  "LD (IX+0x{0:X2}), D",  "LD (IX+0x{0:X2}), E",  "LD (IX+0x{0:X2}), H",  "LD (IX+0x{0:X2}), L",  ".DB 0xDD, 0x76",       "LD (IX+0x{0:X2}), A",  /* 0x70 */
+			".DB 0xDD, 0x78",       ".DB 0xDD, 0x79",       ".DB 0xDD, 0x7A",       ".DB 0xDD, 0x7B",       ".DB 0xDD, 0x7C",       ".DB 0xDD, 0x7D",       "LD A, (IX+0x{0:X2})",  ".DB 0xDD, 0x7F",       /* 0x78 */
+			".DB 0xDD, 0x80",       ".DB 0xDD, 0x81",       ".DB 0xDD, 0x82",       ".DB 0xDD, 0x83",       ".DB 0xDD, 0x84",       ".DB 0xDD, 0x85",       "ADD A, (IX+0x{0:X2})", ".DB 0xDD, 0x87",       /* 0x80 */
+			".DB 0xDD, 0x88",       ".DB 0xDD, 0x89",       ".DB 0xDD, 0x8A",       ".DB 0xDD, 0x8B",       ".DB 0xDD, 0x8C",       ".DB 0xDD, 0x8D",       "ADC A, (IX+0x{0:X2})", ".DB 0xDD, 0x8F",       /* 0x88 */
+			".DB 0xDD, 0x90",       ".DB 0xDD, 0x91",       ".DB 0xDD, 0x92",       ".DB 0xDD, 0x93",       ".DB 0xDD, 0x94",       ".DB 0xDD, 0x95",       "SUB (IX+0x{0:X2})",    ".DB 0xDD, 0x97",       /* 0x90 */
+			".DB 0xDD, 0x98",       ".DB 0xDD, 0x99",       ".DB 0xDD, 0x9A",       ".DB 0xDD, 0x9B",       ".DB 0xDD, 0x9C",       ".DB 0xDD, 0x9D",       "SBC (IX+0x{0:X2})",    ".DB 0xDD, 0x9F",       /* 0x98 */
+			".DB 0xDD, 0xA0",       ".DB 0xDD, 0xA1",       ".DB 0xDD, 0xA2",       ".DB 0xDD, 0xA3",       ".DB 0xDD, 0xA4",       ".DB 0xDD, 0xA5",       "AND (IX+0x{0:X2})",    ".DB 0xDD, 0xA7",       /* 0xA0 */
+			".DB 0xDD, 0xA8",       ".DB 0xDD, 0xA9",       ".DB 0xDD, 0xAA",       ".DB 0xDD, 0xAB",       ".DB 0xDD, 0xAC",       ".DB 0xDD, 0xAD",       "XOR (IX+0x{0:X2})",    ".DB 0xDD, 0xAF",       /* 0xA8 */
+			".DB 0xDD, 0xB0",       ".DB 0xDD, 0xB1",       ".DB 0xDD, 0xB2",       ".DB 0xDD, 0xB3",       ".DB 0xDD, 0xB4",       ".DB 0xDD, 0xB5",       "OR (IX+0x{0:X2})",     ".DB 0xDD, 0xB7",       /* 0xB0 */
+			".DB 0xDD, 0xB8",       ".DB 0xDD, 0xB9",       ".DB 0xDD, 0xBA",       ".DB 0xDD, 0xBB",       ".DB 0xDD, 0xBC",       ".DB 0xDD, 0xBD",       "CP (IX+0x{0:X2})",     ".DB 0xDD, 0xBF",       /* 0xB8 */
+			".DB 0xDD, 0xC0",       ".DB 0xDD, 0xC1",       ".DB 0xDD, 0xC2",       ".DB 0xDD, 0xC3",       ".DB 0xDD, 0xC4",       ".DB 0xDD, 0xC5",       ".DB 0xDD, 0xC6",       ".DB 0xDD, 0xC7",       /* 0xC0 */
+			".DB 0xDD, 0xC8",       ".DB 0xDD, 0xC9",       ".DB 0xDD, 0xCA",       string.Empty,           ".DB 0xDD, 0xCC",       ".DB 0xDD, 0xCD",       ".DB 0xDD, 0xCE",       ".DB 0xDD, 0xCF",       /* 0xC8 */
+			".DB 0xDD, 0xD0",       ".DB 0xDD, 0xD1",       ".DB 0xDD, 0xD2",       ".DB 0xDD, 0xD3",       ".DB 0xDD, 0xD4",       ".DB 0xDD, 0xD5",       ".DB 0xDD, 0xD6",       ".DB 0xDD, 0xD7",       /* 0xD0 */
+			".DB 0xDD, 0xD8",       ".DB 0xDD, 0xD9",       ".DB 0xDD, 0xDA",       ".DB 0xDD, 0xDB",       ".DB 0xDD, 0xDC",       ".DB 0xDD, 0xDD",       ".DB 0xDD, 0xDE",       ".DB 0xDD, 0xDF",       /* 0xD8 */
+			".DB 0xED, 0xE0",       "POP IX",               ".DB 0xED, 0xE2",       "EX (SP), IX",          ".DB 0xED, 0xE4",       "PUSH IX",              ".DB 0xED, 0xE6",       ".DB 0xED, 0xE7",       /* 0xE0 */
+			".DB 0xED, 0xE8",       "JP (IX)",              ".DB 0xED, 0xEA",       ".DB 0xED, 0xEB",       ".DB 0xED, 0xEC",       ".DB 0xED, 0xED",       ".DB 0xED, 0xEE",       ".DB 0xED, 0xEF",       /* 0xE8 */
+			".DB 0xFD, 0xF0",       ".DB 0xFD, 0xF1",       ".DB 0xFD, 0xF2",       ".DB 0xFD, 0xF3",       ".DB 0xFD, 0xF4",       ".DB 0xFD, 0xF5",       ".DB 0xFD, 0xF6",       ".DB 0xFD, 0xF7",       /* 0xF0 */
+			".DB 0xFD, 0xF8",       "LD SP, IX",            ".DB 0xFD, 0xFA",       ".DB 0xFD, 0xFB",       ".DB 0xFD, 0xFC",       ".DB 0xFD, 0xFD",       ".DB 0xFD, 0xFE",       ".DB 0xFD, 0xFF"        /* 0xF8 */
+		};
+
+		static readonly string[] opcodeMnemonicPrefixFD = new string[]
+		{
+			".DB 0xFD, 0x00",       ".DB 0xFD, 0x01",       ".DB 0xFD, 0x02",       ".DB 0xFD, 0x03",       ".DB 0xFD, 0x04",       ".DB 0xFD, 0x05",       ".DB 0xFD, 0x06",       ".DB 0xFD, 0x07",       /* 0x00 */
+			".DB 0xFD, 0x08",       "ADD IY, BC",           ".DB 0xFD, 0x0A",       ".DB 0xFD, 0x0B",       ".DB 0xFD, 0x0C",       ".DB 0xFD, 0x0D",       ".DB 0xFD, 0x0E",       ".DB 0xFD, 0x0F",       /* 0x08 */
+			".DB 0xFD, 0x10",       ".DB 0xFD, 0x11",       ".DB 0xFD, 0x12",       ".DB 0xFD, 0x13",       ".DB 0xFD, 0x14",       ".DB 0xFD, 0x15",       ".DB 0xFD, 0x16",       ".DB 0xFD, 0x17",       /* 0x10 */
+			".DB 0xFD, 0x18",       "ADD IY, DE",           ".DB 0xFD, 0x1A",       ".DB 0xFD, 0x1B",       ".DB 0xFD, 0x1C",       ".DB 0xFD, 0x1D",       ".DB 0xFD, 0x1E",       ".DB 0xFD, 0x1F",       /* 0x18 */
+			".DB 0xFD, 0x20",       "LD IY, 0x{0:X4}",      "LD (0x{0:X4}), IY",    "INC IY",               ".DB 0xFD, 0x24",       ".DB 0xFD, 0x25",       ".DB 0xFD, 0x26",       ".DB 0xFD, 0x27",       /* 0x20 */
+			".DB 0xFD, 0x28",       "ADD IY, IY",           "LD IY, (0x{0:X4})",    "DEC IY",               ".DB 0xFD, 0x2C",       ".DB 0xFD, 0x2D",       ".DB 0xFD, 0x2E",       ".DB 0xFD, 0x2F",       /* 0x28 */
+			".DB 0xFD, 0x30",       ".DB 0xFD, 0x31",       ".DB 0xFD, 0x32",       ".DB 0xFD, 0x33",       "INC (IY+0x{0:X2})",    "DEC (IY+0x{0:X2})",    "LD (IY+0x{0:X2}), 0x{0:X2}",   ".DB 0xFD, 0x37",   /* 0x30 */
+			".DB 0xFD, 0x38",       "ADD IY, SP",           ".DB 0xFD, 0x3A",       ".DB 0xFD, 0x3B",       ".DB 0xFD, 0x3C",       ".DB 0xFD, 0x3D",       ".DB 0xFD, 0x3E",       ".DB 0xFD, 0x3F",       /* 0x38 */
+			".DB 0xFD, 0x40",       ".DB 0xFD, 0x41",       ".DB 0xFD, 0x42",       ".DB 0xFD, 0x43",       ".DB 0xFD, 0x44",       ".DB 0xFD, 0x45",       "LD B, (IY+0x{0:X2})",  ".DB 0xFD, 0x47",       /* 0x40 */
+			".DB 0xFD, 0x48",       ".DB 0xFD, 0x49",       ".DB 0xFD, 0x4A",       ".DB 0xFD, 0x4B",       ".DB 0xFD, 0x4C",       ".DB 0xFD, 0x4D",       "LD C, (IY+0x{0:X2})",  ".DB 0xFD, 0x4F",       /* 0x48 */
+			".DB 0xFD, 0x50",       ".DB 0xFD, 0x51",       ".DB 0xFD, 0x52",       ".DB 0xFD, 0x53",       ".DB 0xFD, 0x54",       ".DB 0xFD, 0x55",       "LD D, (IY+0x{0:X2})",  ".DB 0xFD, 0x57",       /* 0x50 */
+			".DB 0xFD, 0x58",       ".DB 0xFD, 0x59",       ".DB 0xFD, 0x5A",       ".DB 0xFD, 0x5B",       ".DB 0xFD, 0x5C",       ".DB 0xFD, 0x5D",       "LD E, (IY+0x{0:X2})",  ".DB 0xFD, 0x5F",       /* 0x58 */
+			".DB 0xFD, 0x60",       ".DB 0xFD, 0x61",       ".DB 0xFD, 0x62",       ".DB 0xFD, 0x63",       ".DB 0xFD, 0x64",       ".DB 0xFD, 0x65",       "LD H, (IY+0x{0:X2})",  ".DB 0xFD, 0x67",       /* 0x60 */
+			".DB 0xFD, 0x68",       ".DB 0xFD, 0x69",       ".DB 0xFD, 0x6A",       ".DB 0xFD, 0x6B",       ".DB 0xFD, 0x6C",       ".DB 0xFD, 0x6D",       "LD L, (IY+0x{0:X2})",  ".DB 0xFD, 0x6F",       /* 0x68 */
+			"LD (IY+0x{0:X2}), B",  "LD (IY+0x{0:X2}), C",  "LD (IY+0x{0:X2}), D",  "LD (IY+0x{0:X2}), E",  "LD (IY+0x{0:X2}), H",  "LD (IY+0x{0:X2}), L",  ".DB 0xFD, 0x76",       "LD (IY+0x{0:X2}), A",  /* 0x70 */
+			".DB 0xFD, 0x78",       ".DB 0xFD, 0x79",       ".DB 0xFD, 0x7A",       ".DB 0xFD, 0x7B",       ".DB 0xFD, 0x7C",       ".DB 0xFD, 0x7D",       "LD A, (IY+0x{0:X2})",  ".DB 0xFD, 0x7F",       /* 0x78 */
+			".DB 0xFD, 0x80",       ".DB 0xFD, 0x81",       ".DB 0xFD, 0x82",       ".DB 0xFD, 0x83",       ".DB 0xFD, 0x84",       ".DB 0xFD, 0x85",       "ADD A, (IY+0x{0:X2})", ".DB 0xFD, 0x87",       /* 0x80 */
+			".DB 0xFD, 0x88",       ".DB 0xFD, 0x89",       ".DB 0xFD, 0x8A",       ".DB 0xFD, 0x8B",       ".DB 0xFD, 0x8C",       ".DB 0xFD, 0x8D",       "ADC A, (IY+0x{0:X2})", ".DB 0xFD, 0x8F",       /* 0x88 */
+			".DB 0xFD, 0x90",       ".DB 0xFD, 0x91",       ".DB 0xFD, 0x92",       ".DB 0xFD, 0x93",       ".DB 0xFD, 0x94",       ".DB 0xFD, 0x95",       "SUB (IY+0x{0:X2})",    ".DB 0xFD, 0x97",       /* 0x90 */
+			".DB 0xFD, 0x98",       ".DB 0xFD, 0x99",       ".DB 0xFD, 0x9A",       ".DB 0xFD, 0x9B",       ".DB 0xFD, 0x9C",       ".DB 0xFD, 0x9D",       "SBC (IY+0x{0:X2})",    ".DB 0xFD, 0x9F",       /* 0x98 */
+			".DB 0xFD, 0xA0",       ".DB 0xFD, 0xA1",       ".DB 0xFD, 0xA2",       ".DB 0xFD, 0xA3",       ".DB 0xFD, 0xA4",       ".DB 0xFD, 0xA5",       "AND (IY+0x{0:X2})",    ".DB 0xFD, 0xA7",       /* 0xA0 */
+			".DB 0xFD, 0xA8",       ".DB 0xFD, 0xA9",       ".DB 0xFD, 0xAA",       ".DB 0xFD, 0xAB",       ".DB 0xFD, 0xAC",       ".DB 0xFD, 0xAD",       "XOR (IY+0x{0:X2})",    ".DB 0xFD, 0xAF",       /* 0xA8 */
+			".DB 0xFD, 0xB0",       ".DB 0xFD, 0xB1",       ".DB 0xFD, 0xB2",       ".DB 0xFD, 0xB3",       ".DB 0xFD, 0xB4",       ".DB 0xFD, 0xB5",       "OR (IY+0x{0:X2})",     ".DB 0xFD, 0xB7",       /* 0xB0 */
+			".DB 0xFD, 0xB8",       ".DB 0xFD, 0xB9",       ".DB 0xFD, 0xBA",       ".DB 0xFD, 0xBB",       ".DB 0xFD, 0xBC",       ".DB 0xFD, 0xBD",       "CP (IY+0x{0:X2})",     ".DB 0xFD, 0xBF",       /* 0xB8 */
+			".DB 0xFD, 0xC0",       ".DB 0xFD, 0xC1",       ".DB 0xFD, 0xC2",       ".DB 0xFD, 0xC3",       ".DB 0xFD, 0xC4",       ".DB 0xFD, 0xC5",       ".DB 0xFD, 0xC6",       ".DB 0xFD, 0xC7",       /* 0xC0 */
+			".DB 0xFD, 0xC8",       ".DB 0xFD, 0xC9",       ".DB 0xFD, 0xCA",       string.Empty,           ".DB 0xFD, 0xCC",       ".DB 0xFD, 0xCD",       ".DB 0xFD, 0xCE",       ".DB 0xFD, 0xCF",       /* 0xC8 */
+			".DB 0xFD, 0xD0",       ".DB 0xFD, 0xD1",       ".DB 0xFD, 0xD2",       ".DB 0xFD, 0xD3",       ".DB 0xFD, 0xD4",       ".DB 0xFD, 0xD5",       ".DB 0xFD, 0xD6",       ".DB 0xFD, 0xD7",       /* 0xD0 */
+			".DB 0xFD, 0xD8",       ".DB 0xFD, 0xD9",       ".DB 0xFD, 0xDA",       ".DB 0xFD, 0xDB",       ".DB 0xFD, 0xDC",       ".DB 0xFD, 0xFD",       ".DB 0xFD, 0xDE",       ".DB 0xFD, 0xDF",       /* 0xD8 */
+			".DB 0xED, 0xE0",       "POP IY",               ".DB 0xED, 0xE2",       "EX (SP), IY",          ".DB 0xED, 0xE4",       "PUSH IY",              ".DB 0xED, 0xE6",       ".DB 0xED, 0xE7",       /* 0xE0 */
+			".DB 0xED, 0xE8",       "JP (IY)",              ".DB 0xED, 0xEA",       ".DB 0xED, 0xEB",       ".DB 0xED, 0xEC",       ".DB 0xED, 0xED",       ".DB 0xED, 0xEE",       ".DB 0xED, 0xEF",       /* 0xE8 */
+			".DB 0xFD, 0xF0",       ".DB 0xFD, 0xF1",       ".DB 0xFD, 0xF2",       ".DB 0xFD, 0xF3",       ".DB 0xFD, 0xF4",       ".DB 0xFD, 0xF5",       ".DB 0xFD, 0xF6",       ".DB 0xFD, 0xF7",       /* 0xF0 */
+			".DB 0xFD, 0xF8",       "LD SP, IY",            ".DB 0xFD, 0xFA",       ".DB 0xFD, 0xFB",       ".DB 0xFD, 0xFC",       ".DB 0xFD, 0xFD",       ".DB 0xFD, 0xFE",       ".DB 0xFD, 0xFF"        /* 0xF8 */
+		};
+
+		static readonly string[] opcodeMnemonicPrefixDDCB = new string[]
+		{
+			".DB 0xDD, 0xCB, 0x00", ".DB 0xDD, 0xCB, 0x01", ".DB 0xDD, 0xCB, 0x02", ".DB 0xDD, 0xCB, 0x03", ".DB 0xDD, 0xCB, 0x04", ".DB 0xDD, 0xCB, 0x05", "RLC (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x07", /* 0x00 */
+			".DB 0xDD, 0xCB, 0x08", ".DB 0xDD, 0xCB, 0x09", ".DB 0xDD, 0xCB, 0x0A", ".DB 0xDD, 0xCB, 0x0B", ".DB 0xDD, 0xCB, 0x0C", ".DB 0xDD, 0xCB, 0x0D", "RRC (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x0F", /* 0x08 */
+			".DB 0xDD, 0xCB, 0x10", ".DB 0xDD, 0xCB, 0x11", ".DB 0xDD, 0xCB, 0x12", ".DB 0xDD, 0xCB, 0x13", ".DB 0xDD, 0xCB, 0x14", ".DB 0xDD, 0xCB, 0x15", "RL (IX+0x{0:X2})",     ".DB 0xDD, 0xCB, 0x17", /* 0x10 */
+			".DB 0xDD, 0xCB, 0x18", ".DB 0xDD, 0xCB, 0x19", ".DB 0xDD, 0xCB, 0x1A", ".DB 0xDD, 0xCB, 0x1B", ".DB 0xDD, 0xCB, 0x1C", ".DB 0xDD, 0xCB, 0x1D", "RR (IX+0x{0:X2})",     ".DB 0xDD, 0xCB, 0x1F", /* 0x18 */
+			".DB 0xDD, 0xCB, 0x20", ".DB 0xDD, 0xCB, 0x21", ".DB 0xDD, 0xCB, 0x22", ".DB 0xDD, 0xCB, 0x23", ".DB 0xDD, 0xCB, 0x24", ".DB 0xDD, 0xCB, 0x25", "SLA (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x27", /* 0x20 */
+			".DB 0xDD, 0xCB, 0x28", ".DB 0xDD, 0xCB, 0x29", ".DB 0xDD, 0xCB, 0x2A", ".DB 0xDD, 0xCB, 0x2B", ".DB 0xDD, 0xCB, 0x2C", ".DB 0xDD, 0xCB, 0x2D", "SRA (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x2F", /* 0x28 */
+			".DB 0xDD, 0xCB, 0x30", ".DB 0xDD, 0xCB, 0x31", ".DB 0xDD, 0xCB, 0x32", ".DB 0xDD, 0xCB, 0x33", ".DB 0xDD, 0xCB, 0x34", ".DB 0xDD, 0xCB, 0x35", "SLL (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x37", /* 0x30 */
+			".DB 0xDD, 0xCB, 0x38", ".DB 0xDD, 0xCB, 0x39", ".DB 0xDD, 0xCB, 0x3A", ".DB 0xDD, 0xCB, 0x3B", ".DB 0xDD, 0xCB, 0x3C", ".DB 0xDD, 0xCB, 0x3D", "SRL (IX+0x{0:X2})",    ".DB 0xDD, 0xCB, 0x3F", /* 0x38 */
+			".DB 0xDD, 0xCB, 0x40", ".DB 0xDD, 0xCB, 0x41", ".DB 0xDD, 0xCB, 0x42", ".DB 0xDD, 0xCB, 0x43", ".DB 0xDD, 0xCB, 0x44", ".DB 0xDD, 0xCB, 0x45", "BIT 0, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x47", /* 0x40 */
+			".DB 0xDD, 0xCB, 0x48", ".DB 0xDD, 0xCB, 0x49", ".DB 0xDD, 0xCB, 0x4A", ".DB 0xDD, 0xCB, 0x4B", ".DB 0xDD, 0xCB, 0x4C", ".DB 0xDD, 0xCB, 0x4D", "BIT 1, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x4F", /* 0x48 */
+			".DB 0xDD, 0xCB, 0x50", ".DB 0xDD, 0xCB, 0x51", ".DB 0xDD, 0xCB, 0x52", ".DB 0xDD, 0xCB, 0x53", ".DB 0xDD, 0xCB, 0x54", ".DB 0xDD, 0xCB, 0x55", "BIT 2, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x57", /* 0x50 */
+			".DB 0xDD, 0xCB, 0x58", ".DB 0xDD, 0xCB, 0x59", ".DB 0xDD, 0xCB, 0x5A", ".DB 0xDD, 0xCB, 0x5B", ".DB 0xDD, 0xCB, 0x5C", ".DB 0xDD, 0xCB, 0x5D", "BIT 3, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x5F", /* 0x58 */
+			".DB 0xDD, 0xCB, 0x60", ".DB 0xDD, 0xCB, 0x61", ".DB 0xDD, 0xCB, 0x62", ".DB 0xDD, 0xCB, 0x63", ".DB 0xDD, 0xCB, 0x64", ".DB 0xDD, 0xCB, 0x65", "BIT 4, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x67", /* 0x60 */
+			".DB 0xDD, 0xCB, 0x68", ".DB 0xDD, 0xCB, 0x69", ".DB 0xDD, 0xCB, 0x6A", ".DB 0xDD, 0xCB, 0x6B", ".DB 0xDD, 0xCB, 0x6C", ".DB 0xDD, 0xCB, 0x6D", "BIT 5, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x6F", /* 0x68 */
+			".DB 0xDD, 0xCB, 0x70", ".DB 0xDD, 0xCB, 0x71", ".DB 0xDD, 0xCB, 0x72", ".DB 0xDD, 0xCB, 0x73", ".DB 0xDD, 0xCB, 0x74", ".DB 0xDD, 0xCB, 0x75", "BIT 6, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x77", /* 0x70 */
+			".DB 0xDD, 0xCB, 0x78", ".DB 0xDD, 0xCB, 0x79", ".DB 0xDD, 0xCB, 0x7A", ".DB 0xDD, 0xCB, 0x7B", ".DB 0xDD, 0xCB, 0x7C", ".DB 0xDD, 0xCB, 0x7D", "BIT 7, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x7F", /* 0x78 */
+			".DB 0xDD, 0xCB, 0x80", ".DB 0xDD, 0xCB, 0x81", ".DB 0xDD, 0xCB, 0x82", ".DB 0xDD, 0xCB, 0x83", ".DB 0xDD, 0xCB, 0x84", ".DB 0xDD, 0xCB, 0x85", "RES 0, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x87", /* 0x80 */
+			".DB 0xDD, 0xCB, 0x88", ".DB 0xDD, 0xCB, 0x89", ".DB 0xDD, 0xCB, 0x8A", ".DB 0xDD, 0xCB, 0x8B", ".DB 0xDD, 0xCB, 0x8C", ".DB 0xDD, 0xCB, 0x8D", "RES 1, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x8F", /* 0x88 */
+			".DB 0xDD, 0xCB, 0x90", ".DB 0xDD, 0xCB, 0x91", ".DB 0xDD, 0xCB, 0x92", ".DB 0xDD, 0xCB, 0x93", ".DB 0xDD, 0xCB, 0x94", ".DB 0xDD, 0xCB, 0x95", "RES 2, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x97", /* 0x90 */
+			".DB 0xDD, 0xCB, 0x98", ".DB 0xDD, 0xCB, 0x99", ".DB 0xDD, 0xCB, 0x9A", ".DB 0xDD, 0xCB, 0x9B", ".DB 0xDD, 0xCB, 0x9C", ".DB 0xDD, 0xCB, 0x9D", "RES 3, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0x9F", /* 0x98 */
+			".DB 0xDD, 0xCB, 0xA0", ".DB 0xDD, 0xCB, 0xA1", ".DB 0xDD, 0xCB, 0xA2", ".DB 0xDD, 0xCB, 0xA3", ".DB 0xDD, 0xCB, 0xA4", ".DB 0xDD, 0xCB, 0xA5", "RES 4, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xA7", /* 0xA0 */
+			".DB 0xDD, 0xCB, 0xA8", ".DB 0xDD, 0xCB, 0xA9", ".DB 0xDD, 0xCB, 0xAA", ".DB 0xDD, 0xCB, 0xAB", ".DB 0xDD, 0xCB, 0xAC", ".DB 0xDD, 0xCB, 0xAD", "RES 5, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xAF", /* 0xA8 */
+			".DB 0xDD, 0xCB, 0xB0", ".DB 0xDD, 0xCB, 0xB1", ".DB 0xDD, 0xCB, 0xB2", ".DB 0xDD, 0xCB, 0xB3", ".DB 0xDD, 0xCB, 0xB4", ".DB 0xDD, 0xCB, 0xB5", "RES 6, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xB7", /* 0xB0 */
+			".DB 0xDD, 0xCB, 0xB8", ".DB 0xDD, 0xCB, 0xB9", ".DB 0xDD, 0xCB, 0xBA", ".DB 0xDD, 0xCB, 0xBB", ".DB 0xDD, 0xCB, 0xBC", ".DB 0xDD, 0xCB, 0xBD", "RES 7, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xBF", /* 0xB8 */
+			".DB 0xDD, 0xCB, 0xC0", ".DB 0xDD, 0xCB, 0xC1", ".DB 0xDD, 0xCB, 0xC2", ".DB 0xDD, 0xCB, 0xC3", ".DB 0xDD, 0xCB, 0xC4", ".DB 0xDD, 0xCB, 0xC5", "SET 0, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xC7", /* 0xC0 */
+			".DB 0xDD, 0xCB, 0xC8", ".DB 0xDD, 0xCB, 0xC9", ".DB 0xDD, 0xCB, 0xCA", ".DB 0xDD, 0xCB, 0xCB", ".DB 0xDD, 0xCB, 0xCC", ".DB 0xDD, 0xCB, 0xCD", "SET 1, (IX+0x{0:X2})", ".DB 0xDD, 0xCB, 0xCF", /* 0xC8 */
+			".DB 0xDD, 0xDB, 0xD0", ".DB 0xDD, 0xDB, 0xD1", ".DB 0xDD, 0xDB, 0xD2", ".DB 0xDD, 0xDB, 0xD3", ".DB 0xDD, 0xDB, 0xD4", ".DB 0xDD, 0xDB, 0xD5", "SET 2, (IX+0x{0:X2})", ".DB 0xDD, 0xDB, 0xD7", /* 0xD0 */
+			".DB 0xDD, 0xDB, 0xD8", ".DB 0xDD, 0xDB, 0xD9", ".DB 0xDD, 0xDB, 0xDA", ".DB 0xDD, 0xDB, 0xDB", ".DB 0xDD, 0xDB, 0xDC", ".DB 0xDD, 0xDB, 0xDD", "SET 3, (IX+0x{0:X2})", ".DB 0xDD, 0xDB, 0xDF", /* 0xD8 */
+			".DB 0xDD, 0xEB, 0xE0", ".DB 0xDD, 0xEB, 0xE1", ".DB 0xDD, 0xEB, 0xE2", ".DB 0xDD, 0xEB, 0xE3", ".DB 0xDD, 0xEB, 0xE4", ".DB 0xDD, 0xEB, 0xE5", "SET 4, (IX+0x{0:X2})", ".DB 0xDD, 0xEB, 0xE7", /* 0xE0 */
+			".DB 0xDD, 0xEB, 0xE8", ".DB 0xDD, 0xEB, 0xE9", ".DB 0xDD, 0xEB, 0xEA", ".DB 0xDD, 0xEB, 0xEB", ".DB 0xDD, 0xEB, 0xEC", ".DB 0xDD, 0xEB, 0xED", "SET 5, (IX+0x{0:X2})", ".DB 0xDD, 0xEB, 0xEF", /* 0xE8 */
+			".DB 0xDD, 0xFB, 0xF0", ".DB 0xDD, 0xFB, 0xF1", ".DB 0xDD, 0xFB, 0xF2", ".DB 0xDD, 0xFB, 0xF3", ".DB 0xDD, 0xFB, 0xF4", ".DB 0xDD, 0xFB, 0xF5", "SET 6, (IX+0x{0:X2})", ".DB 0xDD, 0xFB, 0xF7", /* 0xF0 */
+			".DB 0xDD, 0xFB, 0xF8", ".DB 0xDD, 0xFB, 0xF9", ".DB 0xDD, 0xFB, 0xFA", ".DB 0xDD, 0xFB, 0xFB", ".DB 0xDD, 0xFB, 0xFC", ".DB 0xDD, 0xFB, 0xFD", "SET 7, (IX+0x{0:X2})", ".DB 0xDD, 0xFB, 0xFF"  /* 0xF8 */
+		};
+
+		static readonly string[] opcodeMnemonicPrefixFDCB = new string[]
+		{
+			".DB 0xFD, 0xCB, 0x00", ".DB 0xFD, 0xCB, 0x01", ".DB 0xFD, 0xCB, 0x02", ".DB 0xFD, 0xCB, 0x03", ".DB 0xFD, 0xCB, 0x04", ".DB 0xFD, 0xCB, 0x05", "RLC (IX+0x{0:X2})",    ".DB 0xFD, 0xCB, 0x07", /* 0x00 */
+			".DB 0xFD, 0xCB, 0x08", ".DB 0xFD, 0xCB, 0x09", ".DB 0xFD, 0xCB, 0x0A", ".DB 0xFD, 0xCB, 0x0B", ".DB 0xFD, 0xCB, 0x0C", ".DB 0xFD, 0xCB, 0x0D", "RRC (IX+0x{0:X2})",    ".DB 0xFD, 0xCB, 0x0F", /* 0x08 */
+			".DB 0xFD, 0xCB, 0x10", ".DB 0xFD, 0xCB, 0x11", ".DB 0xFD, 0xCB, 0x12", ".DB 0xFD, 0xCB, 0x13", ".DB 0xFD, 0xCB, 0x14", ".DB 0xFD, 0xCB, 0x15", "RL (IX+0x{0:X2})",     ".DB 0xFD, 0xCB, 0x17", /* 0x10 */
+			".DB 0xFD, 0xCB, 0x18", ".DB 0xFD, 0xCB, 0x19", ".DB 0xFD, 0xCB, 0x1A", ".DB 0xFD, 0xCB, 0x1B", ".DB 0xFD, 0xCB, 0x1C", ".DB 0xFD, 0xCB, 0x1D", "RR (IX+0x{0:X2})",     ".DB 0xFD, 0xCB, 0x1F", /* 0x18 */
+			".DB 0xFD, 0xCB, 0x20", ".DB 0xFD, 0xCB, 0x21", ".DB 0xFD, 0xCB, 0x22", ".DB 0xFD, 0xCB, 0x23", ".DB 0xFD, 0xCB, 0x24", ".DB 0xFD, 0xCB, 0x25", "SLA (IX+0x{0:X2})",    ".DB 0xFD, 0xCB, 0x27", /* 0x20 */
+			".DB 0xFD, 0xCB, 0x28", ".DB 0xFD, 0xCB, 0x29", ".DB 0xFD, 0xCB, 0x2A", ".DB 0xFD, 0xCB, 0x2B", ".DB 0xFD, 0xCB, 0x2C", ".DB 0xFD, 0xCB, 0x2D", "SRA (IX+0x{0:X2})",    ".DB 0xFD, 0xCB, 0x2F", /* 0x28 */
+			".DB 0xFD, 0xCB, 0x30", ".DB 0xFD, 0xCB, 0x31", ".DB 0xFD, 0xCB, 0x32", ".DB 0xFD, 0xCB, 0x33", ".DB 0xFD, 0xCB, 0x34", ".DB 0xFD, 0xCB, 0x35", ".DB 0xFD, 0xCB, 0x36", ".DB 0xFD, 0xCB, 0x37", /* 0x30 */
+			".DB 0xFD, 0xCB, 0x38", ".DB 0xFD, 0xCB, 0x39", ".DB 0xFD, 0xCB, 0x3A", ".DB 0xFD, 0xCB, 0x3B", ".DB 0xFD, 0xCB, 0x3C", ".DB 0xFD, 0xCB, 0x3D", "SRL (IX+0x{0:X2})",    ".DB 0xFD, 0xCB, 0x3F", /* 0x38 */
+			".DB 0xFD, 0xCB, 0x40", ".DB 0xFD, 0xCB, 0x41", ".DB 0xFD, 0xCB, 0x42", ".DB 0xFD, 0xCB, 0x43", ".DB 0xFD, 0xCB, 0x44", ".DB 0xFD, 0xCB, 0x45", "BIT 0, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x47", /* 0x40 */
+			".DB 0xFD, 0xCB, 0x48", ".DB 0xFD, 0xCB, 0x49", ".DB 0xFD, 0xCB, 0x4A", ".DB 0xFD, 0xCB, 0x4B", ".DB 0xFD, 0xCB, 0x4C", ".DB 0xFD, 0xCB, 0x4D", "BIT 1, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x4F", /* 0x48 */
+			".DB 0xFD, 0xCB, 0x50", ".DB 0xFD, 0xCB, 0x51", ".DB 0xFD, 0xCB, 0x52", ".DB 0xFD, 0xCB, 0x53", ".DB 0xFD, 0xCB, 0x54", ".DB 0xFD, 0xCB, 0x55", "BIT 2, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x57", /* 0x50 */
+			".DB 0xFD, 0xCB, 0x58", ".DB 0xFD, 0xCB, 0x59", ".DB 0xFD, 0xCB, 0x5A", ".DB 0xFD, 0xCB, 0x5B", ".DB 0xFD, 0xCB, 0x5C", ".DB 0xFD, 0xCB, 0x5D", "BIT 3, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x5F", /* 0x58 */
+			".DB 0xFD, 0xCB, 0x60", ".DB 0xFD, 0xCB, 0x61", ".DB 0xFD, 0xCB, 0x62", ".DB 0xFD, 0xCB, 0x63", ".DB 0xFD, 0xCB, 0x64", ".DB 0xFD, 0xCB, 0x65", "BIT 4, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x67", /* 0x60 */
+			".DB 0xFD, 0xCB, 0x68", ".DB 0xFD, 0xCB, 0x69", ".DB 0xFD, 0xCB, 0x6A", ".DB 0xFD, 0xCB, 0x6B", ".DB 0xFD, 0xCB, 0x6C", ".DB 0xFD, 0xCB, 0x6D", "BIT 5, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x6F", /* 0x68 */
+			".DB 0xFD, 0xCB, 0x70", ".DB 0xFD, 0xCB, 0x71", ".DB 0xFD, 0xCB, 0x72", ".DB 0xFD, 0xCB, 0x73", ".DB 0xFD, 0xCB, 0x74", ".DB 0xFD, 0xCB, 0x75", "BIT 6, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x77", /* 0x70 */
+			".DB 0xFD, 0xCB, 0x78", ".DB 0xFD, 0xCB, 0x79", ".DB 0xFD, 0xCB, 0x7A", ".DB 0xFD, 0xCB, 0x7B", ".DB 0xFD, 0xCB, 0x7C", ".DB 0xFD, 0xCB, 0x7D", "BIT 7, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x7F", /* 0x78 */
+			".DB 0xFD, 0xCB, 0x80", ".DB 0xFD, 0xCB, 0x81", ".DB 0xFD, 0xCB, 0x82", ".DB 0xFD, 0xCB, 0x83", ".DB 0xFD, 0xCB, 0x84", ".DB 0xFD, 0xCB, 0x85", "RES 0, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x87", /* 0x80 */
+			".DB 0xFD, 0xCB, 0x88", ".DB 0xFD, 0xCB, 0x89", ".DB 0xFD, 0xCB, 0x8A", ".DB 0xFD, 0xCB, 0x8B", ".DB 0xFD, 0xCB, 0x8C", ".DB 0xFD, 0xCB, 0x8D", "RES 1, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x8F", /* 0x88 */
+			".DB 0xFD, 0xCB, 0x90", ".DB 0xFD, 0xCB, 0x91", ".DB 0xFD, 0xCB, 0x92", ".DB 0xFD, 0xCB, 0x93", ".DB 0xFD, 0xCB, 0x94", ".DB 0xFD, 0xCB, 0x95", "RES 2, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x97", /* 0x90 */
+			".DB 0xFD, 0xCB, 0x98", ".DB 0xFD, 0xCB, 0x99", ".DB 0xFD, 0xCB, 0x9A", ".DB 0xFD, 0xCB, 0x9B", ".DB 0xFD, 0xCB, 0x9C", ".DB 0xFD, 0xCB, 0x9D", "RES 3, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0x9F", /* 0x98 */
+			".DB 0xFD, 0xCB, 0xA0", ".DB 0xFD, 0xCB, 0xA1", ".DB 0xFD, 0xCB, 0xA2", ".DB 0xFD, 0xCB, 0xA3", ".DB 0xFD, 0xCB, 0xA4", ".DB 0xFD, 0xCB, 0xA5", "RES 4, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xA7", /* 0xA0 */
+			".DB 0xFD, 0xCB, 0xA8", ".DB 0xFD, 0xCB, 0xA9", ".DB 0xFD, 0xCB, 0xAA", ".DB 0xFD, 0xCB, 0xAB", ".DB 0xFD, 0xCB, 0xAC", ".DB 0xFD, 0xCB, 0xAD", "RES 5, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xAF", /* 0xA8 */
+			".DB 0xFD, 0xCB, 0xB0", ".DB 0xFD, 0xCB, 0xB1", ".DB 0xFD, 0xCB, 0xB2", ".DB 0xFD, 0xCB, 0xB3", ".DB 0xFD, 0xCB, 0xB4", ".DB 0xFD, 0xCB, 0xB5", "RES 6, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xB7", /* 0xB0 */
+			".DB 0xFD, 0xCB, 0xB8", ".DB 0xFD, 0xCB, 0xB9", ".DB 0xFD, 0xCB, 0xBA", ".DB 0xFD, 0xCB, 0xBB", ".DB 0xFD, 0xCB, 0xBC", ".DB 0xFD, 0xCB, 0xBD", "RES 7, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xBF", /* 0xB8 */
+			".DB 0xFD, 0xCB, 0xC0", ".DB 0xFD, 0xCB, 0xC1", ".DB 0xFD, 0xCB, 0xC2", ".DB 0xFD, 0xCB, 0xC3", ".DB 0xFD, 0xCB, 0xC4", ".DB 0xFD, 0xCB, 0xC5", "SET 0, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xC7", /* 0xC0 */
+			".DB 0xFD, 0xCB, 0xC8", ".DB 0xFD, 0xCB, 0xC9", ".DB 0xFD, 0xCB, 0xCA", ".DB 0xFD, 0xCB, 0xCB", ".DB 0xFD, 0xCB, 0xCC", ".DB 0xFD, 0xCB, 0xCD", "SET 1, (IX+0x{0:X2})", ".DB 0xFD, 0xCB, 0xCF", /* 0xC8 */
+			".DB 0xFD, 0xDB, 0xD0", ".DB 0xFD, 0xDB, 0xD1", ".DB 0xFD, 0xDB, 0xD2", ".DB 0xFD, 0xDB, 0xD3", ".DB 0xFD, 0xDB, 0xD4", ".DB 0xFD, 0xDB, 0xD5", "SET 2, (IX+0x{0:X2})", ".DB 0xFD, 0xDB, 0xD7", /* 0xD0 */
+			".DB 0xFD, 0xDB, 0xD8", ".DB 0xFD, 0xDB, 0xD9", ".DB 0xFD, 0xDB, 0xDA", ".DB 0xFD, 0xDB, 0xDB", ".DB 0xFD, 0xDB, 0xDC", ".DB 0xFD, 0xDB, 0xFD", "SET 3, (IX+0x{0:X2})", ".DB 0xFD, 0xDB, 0xDF", /* 0xD8 */
+			".DB 0xFD, 0xEB, 0xE0", ".DB 0xFD, 0xEB, 0xE1", ".DB 0xFD, 0xEB, 0xE2", ".DB 0xFD, 0xEB, 0xE3", ".DB 0xFD, 0xEB, 0xE4", ".DB 0xFD, 0xEB, 0xE5", "SET 4, (IX+0x{0:X2})", ".DB 0xFD, 0xEB, 0xE7", /* 0xE0 */
+			".DB 0xFD, 0xEB, 0xE8", ".DB 0xFD, 0xEB, 0xE9", ".DB 0xFD, 0xEB, 0xEA", ".DB 0xFD, 0xEB, 0xEB", ".DB 0xFD, 0xEB, 0xEC", ".DB 0xFD, 0xEB, 0xED", "SET 5, (IX+0x{0:X2})", ".DB 0xFD, 0xEB, 0xEF", /* 0xE8 */
+			".DB 0xFD, 0xFB, 0xF0", ".DB 0xFD, 0xFB, 0xF1", ".DB 0xFD, 0xFB, 0xF2", ".DB 0xFD, 0xFB, 0xF3", ".DB 0xFD, 0xFB, 0xF4", ".DB 0xFD, 0xFB, 0xF5", "SET 6, (IX+0x{0:X2})", ".DB 0xFD, 0xFB, 0xF7", /* 0xF0 */
+			".DB 0xFD, 0xFB, 0xF8", ".DB 0xFD, 0xFB, 0xF9", ".DB 0xFD, 0xFB, 0xFA", ".DB 0xFD, 0xFB, 0xFB", ".DB 0xFD, 0xFB, 0xFC", ".DB 0xFD, 0xFB, 0xFD", "SET 7, (IX+0x{0:X2})", ".DB 0xFD, 0xFB, 0xFF"  /* 0xF8 */
+		};
+
+		static readonly int[] opcodeLengthPrefixDDFD = new int[]
+		{
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 4, 4, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 3, 3, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			3, 3, 3, 3, 3, 3, 2, 3, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 3, 2, 2, 2, 2, 2, 2, 2, 3, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, -1, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+			2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
+		};
+
+		static readonly int[] opcodeLengthPrefixDDFDCB = new int[]
+		{
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+			4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
+		};
+
+		public static string PrintRegisters(Z80A cpu)
+		{
+			return string.Format("AF:{0:X4} BC:{1:X4} DE:{2:X4} HL:{3:X4} IX:{4:X4} IY:{5:X4} SP:{6:X4}", cpu.af.Word, cpu.bc.Word, cpu.de.Word, cpu.hl.Word, cpu.ix.Word, cpu.iy.Word, cpu.sp);
+		}
+
+		public static string PrintFlags(Z80A cpu)
+		{
+			return string.Format("[{7}{6}{5}{4}{3}{2}{1}{0}]",
+				cpu.IsFlagSet(Flags.Carry) ? "C" : "-",
+				cpu.IsFlagSet(Flags.Subtract) ? "N" : "-",
+				cpu.IsFlagSet(Flags.ParityOrOverflow) ? "P" : "-",
+				cpu.IsFlagSet(Flags.UnusedBitX) ? "X" : "-",
+				cpu.IsFlagSet(Flags.HalfCarry) ? "H" : "-",
+				cpu.IsFlagSet(Flags.UnusedBitY) ? "Y" : "-",
+				cpu.IsFlagSet(Flags.Zero) ? "Z" : "-",
+				cpu.IsFlagSet(Flags.Sign) ? "S" : "-");
+		}
+
+		public static string PrintInterrupt(Z80A cpu)
+		{
+			return string.Format("[IM{0} {1} {2} {3}]", cpu.im, (cpu.iff1 ? "EI" : "DI"), (cpu.halt ? "HLT" : "---"), (cpu.intState == InterruptState.Assert ? "ASR" : "---"));
+		}
+
+		public static string DisassembleOpcode(Z80A cpu, ushort address)
+		{
+			byte[] opcode = DisassembleGetOpcodeBytes(cpu, address);
+			return string.Format("0x{0:X4} | {1} | {2}", address, DisassembleMakeByteString(cpu, opcode).PadRight(15), DisassembleMakeMnemonicString(cpu, opcode));
+		}
+
+		public static byte[] DisassembleGetOpcodeBytes(Z80A cpu, ushort address)
+		{
+			byte[] opcode = new byte[5];
+			for (int i = 0; i < opcode.Length; i++)
+				opcode[i] = (address + i <= 0xFFFF ? cpu.ReadMemory8((ushort)(address + i)) : (byte)0);
+			return opcode;
+		}
+
+		public static int DisassembleGetOpcodeLen(Z80A cpu, byte[] opcode)
+		{
+			switch (opcode[0])
+			{
+				case 0xCB: return opcodeLength_CB[opcode[1]];
+				case 0xED: return opcodeLengthPrefixED[opcode[1]];
+
+				case 0xDD:
+				case 0xFD:
+					if (opcode[1] == 0xCB)
+						return opcodeLengthPrefixDDFDCB[opcode[3]];
+					else
+						return opcodeLengthPrefixDDFD[opcode[1]];
+
+				default: return opcodeLengthNoPrefix[opcode[0]];
+			}
+		}
+
+		public static string DisassembleMakeByteString(Z80A cpu, byte[] opcode)
+		{
+			return string.Join(" ", opcode.Select(x => string.Format("{0:X2}", x)).Take(DisassembleGetOpcodeLen(cpu, opcode)));
+		}
+
+		public static string DisassembleMakeMnemonicString(Z80A cpu, byte[] opcode)
+		{
+			int len = DisassembleGetOpcodeLen(cpu, opcode);
+
+			int start = 0;
+			string[] mnemonics = opcodeMnemonicNoPrefix;
+			bool isDDFDCB = false;
+
+			switch (opcode[0])
+			{
+				case 0xCB: start = 1; mnemonics = opcodeMnemonicPrefixCB; break;
+				case 0xED: start = 1; mnemonics = opcodeMnemonicPrefixED; break;
+
+				case 0xDD:
+					if (opcode[1] == 0xCB)
+					{
+						mnemonics = opcodeMnemonicPrefixDDCB;
+						isDDFDCB = true;
+					}
+					else
+					{
+						start = 1;
+						mnemonics = opcodeMnemonicPrefixDD;
+					}
+					break;
+
+				case 0xFD:
+					if (opcode[1] == 0xCB)
+					{
+						mnemonics = opcodeMnemonicPrefixFDCB;
+						isDDFDCB = true;
+					}
+					else
+					{
+						start = 1;
+						mnemonics = opcodeMnemonicPrefixFD;
+					}
+					break;
+			}
+
+			if (mnemonics == null) return "(unimplemented)";
+
+			if (!isDDFDCB)
+			{
+				switch (len - start)
+				{
+					case 1: return mnemonics[opcode[start]];
+					case 2: return string.Format(mnemonics[opcode[start]], opcode[start + 1]);
+					case 3: return string.Format(mnemonics[opcode[start]], (opcode[start + 2] << 8 | opcode[start + 1]));
+					default: return string.Empty;
+				}
+			}
+			else
+			{
+				return string.Format(mnemonics[opcode[3]], opcode[2]);
+			}
+		}
+
+		private string MakeUnimplementedOpcodeString(string prefix, ushort address)
+		{
+			byte[] opcode = DisassembleGetOpcodeBytes(this, address);
+			return string.Format("Unimplemented {0}opcode {1} ({2})", (prefix != string.Empty ? prefix + " " : prefix), DisassembleMakeByteString(this, opcode), DisassembleMakeMnemonicString(this, opcode));
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs.meta
new file mode 100644
index 0000000..b8e260b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Disassembly.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9fa152db63418e74ab7f359175172290
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs
new file mode 100644
index 0000000..3b65608
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs
@@ -0,0 +1,289 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		static SimpleOpcodeDelegate[] opcodesNoPrefix = new SimpleOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new SimpleOpcodeDelegate((c) => { /* NOP */ }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.bc.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftAccumulatorCircular(); }),
+			new SimpleOpcodeDelegate((c) => { c.ExchangeRegisters16(ref c.af, ref c.af_); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.bc.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterFromMemory8(ref c.af.High, c.bc.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.bc.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightAccumulatorCircular(); }),
+			/* 0x10 */
+			new SimpleOpcodeDelegate((c) => { c.DecrementJumpNonZero(); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.de.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftAccumulator(); }),
+			new SimpleOpcodeDelegate((c) => { c.Jump8(); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.de.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterFromMemory8(ref c.af.High, c.de.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.de.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightAccumulator(); }),
+			/* 0x20 */
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory16(c.ReadMemory16(c.pc), c.hl.Word); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.DecimalAdjustAccumulator(); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.hl.Word, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister16(ref c.hl.Word, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High ^= 0xFF; c.SetFlag(Flags.Subtract | Flags.HalfCarry); c.SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(c.af.High, 5)); c.SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(c.af.High, 3)); }),
+			/* 0x30 */
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.ReadMemory16(c.pc), c.af.High); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Increment16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.IncrementMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.DecrementMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.SetFlag(Flags.Carry); c.ClearFlag(Flags.Subtract | Flags.HalfCarry); c.SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(c.af.High, 5)); c.SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(c.af.High, 3)); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional8(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.sp, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterFromMemory8(ref c.af.High, c.ReadMemory16(c.pc), false); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement16(ref c.sp); }),
+			new SimpleOpcodeDelegate((c) => { c.Increment8(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Decrement8(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegisterImmediate8(ref c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.SetClearFlagConditional(Flags.HalfCarry, c.IsFlagSet(Flags.Carry)); c.SetClearFlagConditional(Flags.Carry, !c.IsFlagSet(Flags.Carry)); c.ClearFlag(Flags.Subtract); c.SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(c.af.High, 5)); c.SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(c.af.High, 3)); }),
+			/* 0x40 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.bc.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.bc.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.bc.Low, c.af.High, false); }),
+			/* 0x50 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.de.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.de.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.de.Low, c.af.High, false); }),
+			/* 0x60 */
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.hl.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.High, c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.hl.Low = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.hl.Low, c.af.High, false); }),
+			/* 0x70 */
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.EnterHaltState(); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory8(c.hl.Word, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadMemory8(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.af.High, false); }),
+			/* 0x80 */
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.hl.Word), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.bc.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.de.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.hl.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.hl.Word), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.af.High, true); }),
+			/* 0x90 */
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.Low, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.hl.Word), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.af.High, false); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.bc.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.de.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.High, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.hl.Low, true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.hl.Word), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.af.High, true); }),
+			/* 0xA0 */
+			new SimpleOpcodeDelegate((c) => { c.And8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.af.High); }),
+			/* 0xB0 */
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.ReadMemory8(c.hl.Word)); }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.af.High); }),
+			/* 0xC0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.bc); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(true); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.bc); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.pc++), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0000); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { /* CB - handled elsewhere */ }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new SimpleOpcodeDelegate((c) => { c.Call16(); }),
+			new SimpleOpcodeDelegate((c) => { c.Add8(c.ReadMemory8(c.pc++), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0008); }),
+			/* 0xD0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.de); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.ReadMemory8(c.pc++), c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.de); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.pc++), false); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0010); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.ExchangeRegisters16(ref c.bc, ref c.bc_); c.ExchangeRegisters16(ref c.de, ref c.de_); c.ExchangeRegisters16(ref c.hl, ref c.hl_); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { c.af.High = c.ReadPort(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new SimpleOpcodeDelegate((c) => { /* DD - handled elsewhere */ }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract8(c.ReadMemory8(c.pc++), true); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0018); }),
+			/* 0xE0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { c.ExchangeStackRegister16(ref c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.And8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0020); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { c.pc = c.hl.Word; }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { c.ExchangeRegisters16(ref c.de, ref c.hl); }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new SimpleOpcodeDelegate((c) => { /* ED - handled elsewhere */ }),
+			new SimpleOpcodeDelegate((c) => { c.Xor8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0028); }),
+			/* 0xF0 */
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(!c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { c.Pop(ref c.af); }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(!c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2 = false; }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(!c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { c.Push(c.af); }),
+			new SimpleOpcodeDelegate((c) => { c.Or8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0030); }),
+			new SimpleOpcodeDelegate((c) => { c.ReturnConditional(c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { c.sp = c.hl.Word; }),
+			new SimpleOpcodeDelegate((c) => { c.JumpConditional16(c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { c.eiDelay = true; }),
+			new SimpleOpcodeDelegate((c) => { c.CallConditional16(c.IsFlagSet(Flags.Sign)); }),
+			new SimpleOpcodeDelegate((c) => { /* FD - handled elsewhere */ }),
+			new SimpleOpcodeDelegate((c) => { c.Cp8(c.ReadMemory8(c.pc++)); }),
+			new SimpleOpcodeDelegate((c) => { c.Restart(0x0038); })
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs.meta
new file mode 100644
index 0000000..5088845
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesNoPrefix.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a8c690c6cf2d024418c1412af53e453f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs
new file mode 100644
index 0000000..f55b5d9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		static SimpleOpcodeDelegate[] opcodesPrefixCB = new SimpleOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeftCircular(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRightCircular(ref c.af.High); }),
+			/* 0x10 */
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight(ref c.af.High); }),
+			/* 0x20 */
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftArithmetic(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightArithmetic(ref c.af.High); }),
+			/* 0x30 */
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftLeftLogical(ref c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(c.hl.Word); }),
+			new SimpleOpcodeDelegate((c) => { c.ShiftRightLogical(ref c.af.High); }),
+			/* 0x40 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 1); }),
+			/* 0x50 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 3); }),
+			/* 0x60 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 5); }),
+			/* 0x70 */
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.TestBit(c.af.High, 7); }),
+			/* 0x80 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 1); }),
+			/* 0x90 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 3); }),
+			/* 0xA0 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 5); }),
+			/* 0xB0 */
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.ResetBit(ref c.af.High, 7); }),
+			/* 0xC0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 0); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 1); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 1); }),
+			/* 0xD0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 2); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 3); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 3); }),
+			/* 0xE0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 4); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 5); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 5); }),
+			/* 0xF0 */
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 6); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.bc.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.de.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.High, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.hl.Low, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(c.hl.Word, 7); }),
+			new SimpleOpcodeDelegate((c) => { c.SetBit(ref c.af.High, 7); }),
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs.meta
new file mode 100644
index 0000000..8da79c8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixCB.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1eaf728772256b548a5dd0f76aac95a8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs
new file mode 100644
index 0000000..a8c2efd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		static DDFDOpcodeDelegate[] opcodesPrefixDDFD = new DDFDOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { /* NOP */ }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate16(ref c.bc.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.bc.Word, c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment16(ref c.bc.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate8(ref c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.RotateLeftAccumulatorCircular(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ExchangeRegisters16(ref c.af, ref c.af_); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add16(ref r, c.bc.Word, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterFromMemory8(ref c.af.High, c.bc.Word, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement16(ref c.bc.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate8(ref c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.RotateRightAccumulatorCircular(); }),
+			/* 0x10 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.DecrementJumpNonZero(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate16(ref c.de.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.de.Word, c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment16(ref c.de.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate8(ref c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.RotateLeftAccumulator(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Jump8(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add16(ref r, c.de.Word, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterFromMemory8(ref c.af.High, c.de.Word, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement16(ref c.de.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate8(ref c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.RotateRightAccumulator(); }),
+			/* 0x20 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional8(!c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate16(ref r.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory16(c.ReadMemory16(c.pc), r.Word); c.pc += 2; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment16(ref r.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { r.High = c.ReadMemory8(c.pc++); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.DecimalAdjustAccumulator(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional8(c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add16(ref r, r.Word, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister16(ref r.Word, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement16(ref r.Word); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { r.Low = c.ReadMemory8(c.pc++); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.af.High ^= 0xFF; c.SetFlag(Flags.Subtract | Flags.HalfCarry); }),
+			/* 0x30 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional8(!c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate16(ref c.sp); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.ReadMemory16(c.pc), c.af.High); c.pc += 2; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment16(ref c.sp); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.IncrementMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.DecrementMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.SetFlag(Flags.Carry); c.ClearFlag(Flags.Subtract | Flags.HalfCarry); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional8(c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add16(ref r, c.sp, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterFromMemory8(ref c.af.High, c.ReadMemory16(c.pc), false); c.pc += 2; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement16(ref c.sp); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Increment8(ref c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Decrement8(ref c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegisterImmediate8(ref c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ClearFlag(Flags.Subtract); c.SetClearFlagConditional(Flags.Carry, !c.IsFlagSet(Flags.Carry)); }),
+			/* 0x40 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.bc.High = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.High, c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.bc.Low = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.bc.Low, c.af.High, false); }),
+			/* 0x50 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.de.High = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.High, c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.de.Low = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.de.Low, c.af.High, false); }),
+			/* 0x60 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.hl.High = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.High, c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.hl.Low = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref r.Low, c.af.High, false); }),
+			/* 0x70 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.hl.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.hl.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.EnterHaltState(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadMemory8(c.CalculateIXIYAddress(r), c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.af.High = c.ReadMemory8(c.CalculateIXIYAddress(r)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.LoadRegister8(ref c.af.High, c.af.High, false); }),
+			/* 0x80 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.ReadMemory8(c.CalculateIXIYAddress(r)), false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.bc.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.bc.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.de.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.de.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(r.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(r.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.ReadMemory8(c.CalculateIXIYAddress(r)), true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.af.High, true); }),
+			/* 0x90 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.bc.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.bc.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.de.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.de.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(r.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(r.Low, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.ReadMemory8(c.CalculateIXIYAddress(r)), false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.af.High, false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.bc.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.bc.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.de.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.de.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(r.High, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(r.Low, true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.ReadMemory8(c.CalculateIXIYAddress(r)), true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.af.High, true); }),
+			/* 0xA0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.ReadMemory8(c.CalculateIXIYAddress(r))); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.ReadMemory8(c.CalculateIXIYAddress(r))); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.af.High); }),
+			/* 0xB0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.ReadMemory8(c.CalculateIXIYAddress(r))); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.bc.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.bc.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.de.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.de.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(r.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(r.Low); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.ReadMemory8(c.CalculateIXIYAddress(r))); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.af.High); }),
+			/* 0xC0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(!c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Pop(ref c.bc); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(!c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Push(c.bc); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.ReadMemory8(c.pc++), false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0000); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Return(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ExecuteOpDDFDCB(c.ReadMemory8((ushort)(c.pc + 1)), ref r); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(c.IsFlagSet(Flags.Zero)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Call16(); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Add8(c.ReadMemory8(c.pc++), true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0008); }),
+			/* 0xD0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(!c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Pop(ref c.de); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.WritePort(c.ReadMemory8(c.pc++), c.af.High); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(!c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Push(c.de); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.ReadMemory8(c.pc++), false); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0010); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ExchangeRegisters16(ref c.bc, ref c.bc_); c.ExchangeRegisters16(ref c.de, ref c.de_); c.ExchangeRegisters16(ref c.hl, ref c.hl_); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.af.High = c.ReadPort(c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(c.IsFlagSet(Flags.Carry)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { /* DD - treat as NOP */ }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Subtract8(c.ReadMemory8(c.pc++), true); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0018); }),
+			/* 0xE0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Pop(ref r); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ExchangeStackRegister16(ref r); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(!c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Push(r); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.And8(c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0020); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.pc = r.Word; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ExchangeRegisters16(ref c.de, ref c.hl); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(c.IsFlagSet(Flags.ParityOrOverflow)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { /* ED - treat as NOP */ }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Xor8(c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0028); }),
+			/* 0xF0 */
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(!c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Pop(ref c.af); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(!c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.iff1 = c.iff2 = false; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(!c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Push(c.af); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Or8(c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0030); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.ReturnConditional(c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.sp = r.Word; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.JumpConditional16(c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.eiDelay = true; }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.CallConditional16(c.IsFlagSet(Flags.Sign)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { /* FD - treat as NOP */ }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Cp8(c.ReadMemory8(c.pc++)); }),
+			new DDFDOpcodeDelegate((Z80A c, ref Register r) => { c.Restart(0x0038); })
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs.meta
new file mode 100644
index 0000000..6aa7faf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFD.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 59243a6d2ecd14d41816ff23fd5204cb
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs
new file mode 100644
index 0000000..7bcc066
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		static DDFDCBOpcodeDelegate[] opcodesPrefixDDFDCB = new DDFDCBOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.RotateLeftCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.RotateRightCircular(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.RotateRightCircular(address); }),
+			/* 0x10 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.RotateLeft(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.RotateRight(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.RotateRight(address); }),
+			/* 0x20 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ShiftLeftArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ShiftRightArithmetic(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ShiftRightArithmetic(address); }),
+			/* 0x30 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ShiftLeftLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ShiftRightLogical(address); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ShiftRightLogical(address); }),
+			/* 0x40 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 1); }),
+			/* 0x50 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 3); }),
+			/* 0x60 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 5); }),
+			/* 0x70 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.TestBit(address, 7); }),
+			/* 0x80 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 1); }),
+			/* 0x90 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 3); }),
+			/* 0xA0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 5); }),
+			/* 0xB0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.ResetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.ResetBit(address, 7); }),
+			/* 0xC0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 0); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 1); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 1); }),
+			/* 0xD0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 2); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 3); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 3); }),
+			/* 0xE0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 4); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 5); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 5); }),
+			/* 0xF0 */
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 6); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.High = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.bc.Low = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.High = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.de.Low = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.High = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.hl.Low = c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.SetBit(address, 7); }),
+			new DDFDCBOpcodeDelegate((Z80A c, ref Register r, ushort address) => { c.af.High = c.SetBit(address, 7); }),
+		};
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs.meta
new file mode 100644
index 0000000..7e9f34d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixDDFDCB.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9f1c07a0773a3a94486e1d50ed7552cb
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs
new file mode 100644
index 0000000..787c8a8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs
@@ -0,0 +1,287 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		static SimpleOpcodeDelegate[] opcodesPrefixED = new SimpleOpcodeDelegate[]
+		{
+			/* 0x00 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0x10 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0x20 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0x30 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0x40 */
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.bc.High, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.bc.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract16(ref c.hl, c.bc.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory16(c.ReadMemory16(c.pc), c.bc.Word); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 0; }),
+			new SimpleOpcodeDelegate((c) => { c.i = c.af.High; }),
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.bc.Low, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.bc.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister16(ref c.bc.Word, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.Return(); c.iff1 = c.iff2; }),
+			new SimpleOpcodeDelegate((c) => { c.im = 0; }),
+			new SimpleOpcodeDelegate((c) => { c.r = c.af.High; }),
+			/* 0x50 */
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.de.High, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.de.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract16(ref c.hl, c.de.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory16(c.ReadMemory16(c.pc), c.de.Word); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 1; }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.i, true); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.de.Low, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.de.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.de.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister16(ref c.de.Word, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 2; }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister8(ref c.af.High, c.r, true); }),
+			/* 0x60 */
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.hl.High, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.hl.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract16(ref c.hl, c.hl.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory16(c.ReadMemory16(c.pc), c.hl.Word); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 0; }),
+			new SimpleOpcodeDelegate((c) => { c.RotateRight4B(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.hl.Low, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.hl.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.hl.Word, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister16(ref c.hl.Word, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 0; }),
+			new SimpleOpcodeDelegate((c) => { c.RotateLeft4B(); }),
+			/* 0x70 */
+			new SimpleOpcodeDelegate((c) => { c.PortInputFlagsOnly(c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, 0x00); }),
+			new SimpleOpcodeDelegate((c) => { c.Subtract16(ref c.hl, c.sp, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadMemory16(c.ReadMemory16(c.pc), c.sp); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 1; }),
+			new SimpleOpcodeDelegate((c) => { /* NOP */ }),
+			new SimpleOpcodeDelegate((c) => { c.PortInput(ref c.af.High, c.bc.Low); }),
+			new SimpleOpcodeDelegate((c) => { c.WritePort(c.bc.Low, c.af.High); }),
+			new SimpleOpcodeDelegate((c) => { c.Add16(ref c.hl, c.sp, true); }),
+			new SimpleOpcodeDelegate((c) => { c.LoadRegister16(ref c.sp, c.ReadMemory16(c.ReadMemory16(c.pc))); c.pc += 2; }),
+			new SimpleOpcodeDelegate((c) => { c.Negate(); }),
+			new SimpleOpcodeDelegate((c) => { c.iff1 = c.iff2; c.Return(); }),
+			new SimpleOpcodeDelegate((c) => { c.im = 2; }),
+			new SimpleOpcodeDelegate((c) => { /* NOP */ }),
+			/* 0x80 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0x90 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0xA0 */
+			new SimpleOpcodeDelegate((c) => { c.LoadIncrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.CompareIncrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInputIncrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortOutputIncrement(); }),
+			new SimpleOpcodeDelegate((c) => { /* A4 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* A5 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* A6 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* A7 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { c.LoadDecrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.CompareDecrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInputDecrement(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortOutputDecrement(); }),
+			new SimpleOpcodeDelegate((c) => { /* AC - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* AD - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* AE - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* AF - nothing */ }),
+			/* 0xB0 */
+			new SimpleOpcodeDelegate((c) => { c.LoadIncrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.CompareIncrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInputIncrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortOutputIncrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { /* B4 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* B5 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* B6 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* B7 - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { c.LoadDecrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.CompareDecrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortInputDecrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { c.PortOutputDecrementRepeat(); }),
+			new SimpleOpcodeDelegate((c) => { /* BC - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* BD - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* BE - nothing */ }),
+			new SimpleOpcodeDelegate((c) => { /* BF - nothing */ }),
+			/* 0xC0 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0xD0 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0xE0 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			/* 0xF0 */
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ }),
+			new SimpleOpcodeDelegate((c) => { /* NOP (2x) */ })
+	  };
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs.meta
new file mode 100644
index 0000000..d58a23f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.OpcodesPrefixED.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5a8dae171d8e67546b93ab06aafbcb53
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs
new file mode 100644
index 0000000..e82820c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A
+	{
+		[DebuggerDisplay("{Word}")]
+		[StructLayout(LayoutKind.Explicit)]
+		[Serializable]
+		public struct Register
+		{
+			[NonSerialized]
+			[FieldOffset(0)]
+			public byte Low;
+			[NonSerialized]
+			[FieldOffset(1)]
+			public byte High;
+
+			[FieldOffset(0)]
+			public ushort Word;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs.meta
new file mode 100644
index 0000000..37eb995
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.Register.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 09764bf46e9451741a9fa2016022c5ea
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs
new file mode 100644
index 0000000..4e00d1d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs
@@ -0,0 +1,1504 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.CPU
+{
+	public partial class Z80A : ICPU
+	{
+		[Flags]
+		enum Flags : byte
+		{
+			Carry = (1 << 0),               /* C */
+			Subtract = (1 << 1),            /* N */
+			ParityOrOverflow = (1 << 2),    /* P */
+			UnusedBitX = (1 << 3),          /* (X) */
+			HalfCarry = (1 << 4),           /* H */
+			UnusedBitY = (1 << 5),          /* (Y) */
+			Zero = (1 << 6),                /* Z */
+			Sign = (1 << 7)                 /* S */
+		}
+
+		public delegate byte MemoryReadDelegate(ushort address);
+		public delegate void MemoryWriteDelegate(ushort address, byte value);
+		public delegate byte PortReadDelegate(byte port);
+		public delegate void PortWriteDelegate(byte port, byte value);
+
+		delegate void SimpleOpcodeDelegate(Z80A c);
+		delegate void DDFDOpcodeDelegate(Z80A c, ref Register register);
+		delegate void DDFDCBOpcodeDelegate(Z80A c, ref Register register, ushort address);
+
+		MemoryReadDelegate memoryReadDelegate;
+		MemoryWriteDelegate memoryWriteDelegate;
+		PortReadDelegate portReadDelegate;
+		PortWriteDelegate portWriteDelegate;
+
+		[StateRequired]
+		protected Register af, bc, de, hl;
+		[StateRequired]
+		protected Register af_, bc_, de_, hl_;
+		[StateRequired]
+		protected Register ix, iy;
+		[StateRequired]
+		protected byte i, r;
+		[StateRequired]
+		protected ushort sp, pc;
+
+		[StateRequired]
+		protected bool iff1, iff2, eiDelay, halt;
+		[StateRequired]
+		protected byte im;
+
+		[StateRequired]
+		protected byte op;
+
+		[StateRequired]
+		InterruptState intState, nmiState;
+
+		[StateRequired]
+		int currentCycles;
+
+		public Z80A(MemoryReadDelegate memoryRead, MemoryWriteDelegate memoryWrite, PortReadDelegate portRead, PortWriteDelegate portWrite)
+		{
+			af = bc = de = hl = new Register();
+			af_ = bc_ = de_ = hl_ = new Register();
+			ix = iy = new Register();
+
+			memoryReadDelegate = memoryRead;
+			memoryWriteDelegate = memoryWrite;
+			portReadDelegate = portRead;
+			portWriteDelegate = portWrite;
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			if (memoryReadDelegate == null) throw new EmulationException("Z80A: Memory read method is null");
+			if (memoryWriteDelegate == null) throw new EmulationException("Z80A: Memory write method is null");
+			if (portReadDelegate == null) throw new EmulationException("Z80A: Port read method is null");
+			if (portWriteDelegate == null) throw new EmulationException("Z80A: Port write method is null");
+		}
+
+		public virtual void Shutdown()
+		{
+			//
+		}
+
+		public virtual void Reset()
+		{
+			af.Word = bc.Word = de.Word = hl.Word = 0;
+			af_.Word = bc_.Word = de_.Word = hl_.Word = 0;
+			ix.Word = iy.Word = 0;
+			i = r = 0;
+			pc = 0;
+			sp = 0;
+
+			iff1 = iff2 = eiDelay = halt = false;
+			im = 0;
+
+			intState = nmiState = InterruptState.Clear;
+
+			currentCycles = 0;
+		}
+
+		public int Step()
+		{
+			currentCycles = 0;
+
+			/* Handle delayed interrupt enable */
+			if (eiDelay)
+			{
+				eiDelay = false;
+				iff1 = iff2 = true;
+			}
+			else
+			{
+				/* Check INT line */
+				if (intState == InterruptState.Assert)
+				{
+					ServiceInterrupt();
+				}
+
+				/* Check NMI line */
+				if (nmiState == InterruptState.Assert)
+				{
+					nmiState = InterruptState.Clear;
+					ServiceNonMaskableInterrupt();
+				}
+			}
+
+
+			if (AppEnvironment.EnableSuperSlowCPULogger)
+			{
+				string disasm = string.Format("{0} | {1} | {2} | {3}\n", DisassembleOpcode(this, pc).PadRight(48), PrintRegisters(this), PrintFlags(this), PrintInterrupt(this));
+				System.IO.File.AppendAllText(@"D:\Temp\Essgee\log.txt", disasm);
+			}
+
+			/* Fetch and execute opcode */
+			op = ReadMemory8(pc++);
+			switch (op)
+			{
+				case 0xCB: ExecuteOpCB(); break;
+				case 0xDD: ExecuteOpDD(); break;
+				case 0xED: ExecuteOpED(); break;
+				case 0xFD: ExecuteOpFD(); break;
+				default: ExecuteOpcodeNoPrefix(op); break;
+			}
+
+			return currentCycles;
+		}
+
+		#region Opcode Execution and Cycle Management
+
+		private void ExecuteOpcodeNoPrefix(byte op)
+		{
+			IncrementRefresh();
+			opcodesNoPrefix[op](this);
+
+			currentCycles += CycleCounts.NoPrefix[op];
+		}
+
+		private void ExecuteOpED()
+		{
+			IncrementRefresh();
+			byte edOp = ReadMemory8(pc++);
+
+			IncrementRefresh();
+			opcodesPrefixED[edOp](this);
+
+			currentCycles += CycleCounts.PrefixED[edOp];
+		}
+
+		private void ExecuteOpCB()
+		{
+			IncrementRefresh();
+			byte cbOp = ReadMemory8(pc++);
+
+			IncrementRefresh();
+			opcodesPrefixCB[cbOp](this);
+
+			currentCycles += CycleCounts.PrefixCB[cbOp];
+		}
+
+		private void ExecuteOpDD()
+		{
+			IncrementRefresh();
+			byte ddOp = ReadMemory8(pc++);
+
+			if (ddOp != 0xDD)
+			{
+				IncrementRefresh();
+				opcodesPrefixDDFD[ddOp](this, ref ix);
+			}
+
+			currentCycles += (CycleCounts.PrefixDDFD[ddOp] != 0 ? CycleCounts.PrefixDDFD[ddOp] : CycleCounts.NoPrefix[ddOp] + CycleCounts.AdditionalDDFDOps);
+		}
+
+		private void ExecuteOpFD()
+		{
+			IncrementRefresh();
+			byte fdOp = ReadMemory8(pc++);
+
+			if (fdOp != 0xFD)
+			{
+				IncrementRefresh();
+				opcodesPrefixDDFD[fdOp](this, ref iy);
+			}
+
+			currentCycles += (CycleCounts.PrefixDDFD[fdOp] != 0 ? CycleCounts.PrefixDDFD[fdOp] : CycleCounts.NoPrefix[fdOp] + CycleCounts.AdditionalDDFDOps);
+		}
+
+		private void ExecuteOpDDFDCB(byte op, ref Register register)
+		{
+			IncrementRefresh();
+			sbyte operand = (sbyte)ReadMemory8(pc);
+			ushort address = (ushort)(register.Word + operand);
+			pc += 2;
+
+			IncrementRefresh();
+			opcodesPrefixDDFDCB[op](this, ref register, address);
+
+			currentCycles += (CycleCounts.PrefixCB[op] + CycleCounts.AdditionalDDFDCBOps);
+		}
+
+		#endregion
+
+		#region Helpers (Refresh Register, Flags, etc.)
+
+		public void SetStackPointer(ushort value)
+		{
+			sp = value;
+		}
+
+		public void SetProgramCounter(ushort value)
+		{
+			pc = value;
+		}
+
+		private void IncrementRefresh()
+		{
+			r = (byte)(((r + 1) & 0x7F) | (r & 0x80));
+		}
+
+		private void SetFlag(Flags flags)
+		{
+			af.Low |= (byte)flags;
+		}
+
+		private void ClearFlag(Flags flags)
+		{
+			af.Low &= (byte)~flags;
+		}
+
+		private void SetClearFlagConditional(Flags flags, bool condition)
+		{
+			if (condition)
+				af.Low |= (byte)flags;
+			else
+				af.Low &= (byte)~flags;
+		}
+
+		private bool IsFlagSet(Flags flags)
+		{
+			return (((Flags)af.Low & flags) == flags);
+		}
+
+		private void CalculateAndSetParity(byte value)
+		{
+			int bitsSet = 0;
+			while (value != 0) { bitsSet += (value & 0x01); value >>= 1; }
+			SetClearFlagConditional(Flags.ParityOrOverflow, (bitsSet == 0 || (bitsSet % 2) == 0));
+		}
+
+		private ushort CalculateIXIYAddress(Register register)
+		{
+			return (ushort)(register.Word + (sbyte)ReadMemory8(pc++));
+		}
+
+		#endregion
+
+		#region Interrupt and Halt State Handling
+
+		public void SetInterruptLine(InterruptType type, InterruptState state)
+		{
+			switch (type)
+			{
+				case InterruptType.Maskable:
+					intState = state;
+					break;
+
+				case InterruptType.NonMaskable:
+					nmiState = state;
+					break;
+
+				default: throw new EmulationException("Z80A: Unknown interrupt type");
+			}
+		}
+
+		private void ServiceInterrupt()
+		{
+			if (!iff1) return;
+
+			LeaveHaltState();
+			iff1 = iff2 = false;
+
+			switch (im)
+			{
+				case 0x00:
+					/* Execute opcode(s) from data bus */
+					/* TODO: no real data bus emulation, just execute opcode 0xFF instead (Xenon 2 SMS, http://www.smspower.org/forums/1172-EmulatingInterrupts#5395) */
+					ExecuteOpcodeNoPrefix(0xFF);
+					currentCycles += 30;
+					break;
+
+				case 0x01:
+					/* Restart to location 0x0038, same as opcode 0xFF */
+					ExecuteOpcodeNoPrefix(0xFF);
+					currentCycles += 30;
+					break;
+
+				case 0x02:
+					/* Indirect call via I register */
+					/* TODO: unsupported at the moment, not needed in currently emulated systems */
+					IncrementRefresh();
+					break;
+			}
+		}
+
+		private void ServiceNonMaskableInterrupt()
+		{
+			IncrementRefresh();
+			Restart(0x0066);
+
+			iff2 = iff1;
+			iff1 = halt = false;
+
+			currentCycles += 11;
+		}
+
+		private void EnterHaltState()
+		{
+			halt = true;
+			pc--;
+		}
+
+		private void LeaveHaltState()
+		{
+			if (halt)
+			{
+				halt = false;
+				pc++;
+			}
+		}
+
+		#endregion
+
+		#region Memory and Port Access Functions
+
+		private byte ReadMemory8(ushort address)
+		{
+			return memoryReadDelegate(address);
+		}
+
+		private void WriteMemory8(ushort address, byte value)
+		{
+			memoryWriteDelegate(address, value);
+		}
+
+		private ushort ReadMemory16(ushort address)
+		{
+			return (ushort)((memoryReadDelegate((ushort)(address + 1)) << 8) | memoryReadDelegate(address));
+		}
+
+		private void WriteMemory16(ushort address, ushort value)
+		{
+			memoryWriteDelegate(address, (byte)(value & 0xFF));
+			memoryWriteDelegate((ushort)(address + 1), (byte)(value >> 8));
+		}
+
+		private byte ReadPort(byte port)
+		{
+			return portReadDelegate(port);
+		}
+
+		private void WritePort(byte port, byte value)
+		{
+			portWriteDelegate(port, value);
+		}
+
+		#endregion
+
+		#region Opcodes: 8-Bit Load Group
+
+		protected void LoadRegisterFromMemory8(ref byte register, ushort address, bool specialRegs)
+		{
+			LoadRegister8(ref register, ReadMemory8(address), specialRegs);
+		}
+
+		protected void LoadRegisterImmediate8(ref byte register, bool specialRegs)
+		{
+			LoadRegister8(ref register, ReadMemory8(pc++), specialRegs);
+		}
+
+		protected void LoadRegister8(ref byte register, byte value, bool specialRegs)
+		{
+			register = value;
+
+			// Register is I or R?
+			if (specialRegs)
+			{
+				SetClearFlagConditional(Flags.Sign, IsBitSet(register, 7));
+				SetClearFlagConditional(Flags.Zero, (register == 0x00));
+				ClearFlag(Flags.HalfCarry);
+				SetClearFlagConditional(Flags.ParityOrOverflow, (iff2));
+				ClearFlag(Flags.Subtract);
+				// C
+			}
+		}
+
+		protected void LoadMemory8(ushort address, byte value)
+		{
+			WriteMemory8(address, value);
+		}
+
+		#endregion
+
+		#region Opcodes: 16-Bit Load Group
+
+		protected void LoadRegisterImmediate16(ref ushort register)
+		{
+			LoadRegister16(ref register, ReadMemory16(pc));
+			pc += 2;
+		}
+
+		protected void LoadRegister16(ref ushort register, ushort value)
+		{
+			register = value;
+		}
+
+		protected void LoadMemory16(ushort address, ushort value)
+		{
+			WriteMemory16(address, value);
+		}
+
+		protected void Push(Register register)
+		{
+			WriteMemory8(--sp, register.High);
+			WriteMemory8(--sp, register.Low);
+		}
+
+		protected void Pop(ref Register register)
+		{
+			register.Low = ReadMemory8(sp++);
+			register.High = ReadMemory8(sp++);
+		}
+
+		#endregion
+
+		#region Opcodes: Exchange, Block Transfer and Search Group
+
+		protected void ExchangeRegisters16(ref Register reg1, ref Register reg2)
+		{
+			ushort tmp = reg1.Word;
+			reg1.Word = reg2.Word;
+			reg2.Word = tmp;
+		}
+
+		protected void ExchangeStackRegister16(ref Register reg)
+		{
+			byte sl = ReadMemory8(sp);
+			byte sh = ReadMemory8((ushort)(sp + 1));
+
+			WriteMemory8(sp, reg.Low);
+			WriteMemory8((ushort)(sp + 1), reg.High);
+
+			reg.Low = sl;
+			reg.High = sh;
+		}
+
+		protected void LoadIncrement()
+		{
+			byte hlValue = ReadMemory8(hl.Word);
+			WriteMemory8(de.Word, hlValue);
+			Increment16(ref de.Word);
+			Increment16(ref hl.Word);
+			Decrement16(ref bc.Word);
+
+			byte n = (byte)(hlValue + af.High);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(n, 1));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(n, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (bc.Word != 0));
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void LoadIncrementRepeat()
+		{
+			LoadIncrement();
+
+			if (bc.Word != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+		}
+
+		protected void LoadDecrement()
+		{
+			byte hlValue = ReadMemory8(hl.Word);
+			WriteMemory8(de.Word, hlValue);
+			Decrement16(ref de.Word);
+			Decrement16(ref hl.Word);
+			Decrement16(ref bc.Word);
+
+			byte n = (byte)(hlValue + af.High);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(n, 1));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(n, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (bc.Word != 0));
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void LoadDecrementRepeat()
+		{
+			LoadDecrement();
+
+			if (bc.Word != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+		}
+
+		protected void CompareIncrement()
+		{
+			byte operand = ReadMemory8(hl.Word);
+			int result = (af.High - (sbyte)operand);
+
+			hl.Word++;
+			bc.Word--;
+
+			bool halfCarry = (((af.High ^ result ^ operand) & 0x10) != 0);
+			byte n = (byte)(result - (halfCarry ? 1 : 0));
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, (af.High == operand));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(n, 1));
+			SetClearFlagConditional(Flags.HalfCarry, halfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(n, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (bc.Word != 0));
+			SetFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void CompareIncrementRepeat()
+		{
+			CompareIncrement();
+
+			if (bc.Word != 0 && !IsFlagSet(Flags.Zero))
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+		}
+
+		protected void CompareDecrement()
+		{
+			byte operand = ReadMemory8(hl.Word);
+			int result = (af.High - (sbyte)operand);
+
+			hl.Word--;
+			bc.Word--;
+
+			bool halfCarry = (((af.High ^ result ^ operand) & 0x10) != 0);
+			byte n = (byte)(result - (halfCarry ? 1 : 0));
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, (af.High == operand));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(n, 1));
+			SetClearFlagConditional(Flags.HalfCarry, halfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(n, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (bc.Word != 0));
+			SetFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void CompareDecrementRepeat()
+		{
+			CompareDecrement();
+
+			if (bc.Word != 0 && !IsFlagSet(Flags.Zero))
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+		}
+
+		#endregion
+
+		#region Opcodes: 8-Bit Arithmetic Group
+
+		protected void Add8(byte operand, bool withCarry)
+		{
+			int operandWithCarry = (operand + (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+			int result = (af.High + operandWithCarry);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)result, 5));
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)result, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (((operand ^ af.High ^ 0x80) & (af.High ^ result) & 0x80) != 0));
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, (result > 0xFF));
+
+			af.High = (byte)result;
+		}
+
+		protected void Subtract8(byte operand, bool withCarry)
+		{
+			int operandWithCarry = (operand + (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+			int result = (af.High - operandWithCarry);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)result, 5));
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)result, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (((operand ^ af.High) & (af.High ^ result) & 0x80) != 0));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, (af.High < operandWithCarry));
+
+			af.High = (byte)result;
+		}
+
+		protected void And8(byte operand)
+		{
+			int result = (af.High & operand);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)result, 5));
+			SetFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)result, 3));
+			CalculateAndSetParity((byte)result);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Or8(byte operand)
+		{
+			int result = (af.High | operand);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)result, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)result, 3));
+			CalculateAndSetParity((byte)result);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Xor8(byte operand)
+		{
+			int result = (af.High ^ operand);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)result, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)result, 3));
+			CalculateAndSetParity((byte)result);
+			ClearFlag(Flags.Subtract);
+			ClearFlag(Flags.Carry);
+
+			af.High = (byte)result;
+		}
+
+		protected void Cp8(byte operand)
+		{
+			int result = (af.High - operand);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet((byte)result, 7));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(operand, 5));
+			SetClearFlagConditional(Flags.HalfCarry, (((af.High ^ result ^ operand) & 0x10) != 0));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(operand, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (((operand ^ af.High) & (af.High ^ result) & 0x80) != 0));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, (af.High < operand));
+		}
+
+		protected void Increment8(ref byte register)
+		{
+			byte result = (byte)(register + 1);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(result, 7));
+			SetClearFlagConditional(Flags.Zero, (result == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(result, 5));
+			SetClearFlagConditional(Flags.HalfCarry, ((register & 0x0F) == 0x0F));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(result, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (register == 0x7F));
+			ClearFlag(Flags.Subtract);
+			// C
+
+			register = result;
+		}
+
+		protected void IncrementMemory8(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			Increment8(ref value);
+			WriteMemory8(address, value);
+		}
+
+		protected void Decrement8(ref byte register)
+		{
+			byte result = (byte)(register - 1);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(result, 7));
+			SetClearFlagConditional(Flags.Zero, (result == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(result, 5));
+			SetClearFlagConditional(Flags.HalfCarry, ((register & 0x0F) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(result, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (register == 0x80));
+			SetFlag(Flags.Subtract);
+			// C
+
+			register = result;
+		}
+
+		protected void DecrementMemory8(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			Decrement8(ref value);
+			WriteMemory8(address, value);
+		}
+
+		#endregion
+
+		#region Opcodes: General-Purpose Arithmetic and CPU Control Group
+
+		protected void DecimalAdjustAccumulator()
+		{
+			/* "The Undocumented Z80 Documented" by Sean Young, chapter 4.7, http://www.z80.info/zip/z80-documented.pdf */
+
+			byte before = af.High, diff = 0x00, result;
+			bool carry = IsFlagSet(Flags.Carry), halfCarry = IsFlagSet(Flags.HalfCarry);
+			byte highNibble = (byte)((before & 0xF0) >> 4), lowNibble = (byte)(before & 0x0F);
+
+			if (carry)
+			{
+				diff |= 0x60;
+				if ((halfCarry && lowNibble <= 0x09) || lowNibble >= 0x0A)
+					diff |= 0x06;
+			}
+			else
+			{
+				if (lowNibble >= 0x0A && lowNibble <= 0x0F)
+				{
+					diff |= 0x06;
+					if (highNibble >= 0x09 && highNibble <= 0x0F)
+						diff |= 0x60;
+				}
+				else
+				{
+					if (highNibble >= 0x0A && highNibble <= 0x0F)
+						diff |= 0x60;
+					if (halfCarry)
+						diff |= 0x06;
+				}
+
+				SetClearFlagConditional(Flags.Carry, (
+					((highNibble >= 0x09 && highNibble <= 0x0F) && (lowNibble >= 0x0A && lowNibble <= 0x0F)) ||
+					((highNibble >= 0x0A && highNibble <= 0x0F) && (lowNibble >= 0x00 && lowNibble <= 0x09))));
+			}
+
+			if (!IsFlagSet(Flags.Subtract))
+				SetClearFlagConditional(Flags.HalfCarry, (lowNibble >= 0x0A && lowNibble <= 0x0F));
+			else
+				SetClearFlagConditional(Flags.HalfCarry, (halfCarry && (lowNibble >= 0x00 && lowNibble <= 0x05)));
+
+			if (!IsFlagSet(Flags.Subtract))
+				result = (byte)(before + diff);
+			else
+				result = (byte)(before - diff);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(result, 7));
+			SetClearFlagConditional(Flags.Zero, (result == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(result, 5));
+			// H (set above)
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(result, 3));
+			CalculateAndSetParity(result);
+			// N
+			// C (set above)
+
+			af.High = result;
+		}
+
+		protected void Negate()
+		{
+			int result = (0 - af.High);
+
+			SetClearFlagConditional(Flags.Sign, ((result & 0xFF) >= 0x80));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFF) == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)(result & 0xFF), 5));
+			SetClearFlagConditional(Flags.HalfCarry, ((0 - (af.High & 0x0F)) < 0));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)(result & 0xFF), 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (af.High == 0x80));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, (af.High != 0x00));
+
+			af.High = (byte)result;
+		}
+
+		#endregion
+
+		#region Opcodes: 16-Bit Arithmetic Group
+
+		protected void Add16(ref Register dest, ushort operand, bool withCarry)
+		{
+			int operandWithCarry = ((short)operand + (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+			int result = (dest.Word + operandWithCarry);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)(result >> 8), 5));
+			SetClearFlagConditional(Flags.HalfCarry, (((dest.Word & 0x0FFF) + (operandWithCarry & 0x0FFF)) > 0x0FFF));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)(result >> 8), 3));
+			// PV
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, (((dest.Word & 0xFFFF) + (operandWithCarry & 0xFFFF)) > 0xFFFF));
+
+			if (withCarry)
+			{
+				SetClearFlagConditional(Flags.Sign, ((result & 0x8000) != 0x0000));
+				SetClearFlagConditional(Flags.Zero, ((result & 0xFFFF) == 0x0000));
+				SetClearFlagConditional(Flags.ParityOrOverflow, (((dest.Word ^ operandWithCarry) & 0x8000) == 0 && ((dest.Word ^ (result & 0xFFFF)) & 0x8000) != 0));
+			}
+
+			dest.Word = (ushort)result;
+		}
+
+		protected void Subtract16(ref Register dest, ushort operand, bool withCarry)
+		{
+			int result = (dest.Word - operand - (withCarry && IsFlagSet(Flags.Carry) ? 1 : 0));
+
+			SetClearFlagConditional(Flags.Sign, ((result & 0x8000) != 0x0000));
+			SetClearFlagConditional(Flags.Zero, ((result & 0xFFFF) == 0x0000));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)(result >> 8), 5));
+			SetClearFlagConditional(Flags.HalfCarry, ((((dest.Word ^ result ^ operand) >> 8) & 0x10) != 0));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)(result >> 8), 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, (((operand ^ dest.Word) & (dest.Word ^ result) & 0x8000) != 0));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, ((result & 0x10000) != 0));
+
+			dest.Word = (ushort)result;
+		}
+
+		protected void Increment16(ref ushort register)
+		{
+			register++;
+		}
+
+		protected void Decrement16(ref ushort register)
+		{
+			register--;
+		}
+
+		#endregion
+
+		#region Opcodes: Rotate and Shift Group
+
+		protected byte RotateLeft(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateLeft(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateLeft(ref byte value)
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+			if (isCarrySet) SetBit(ref value, 0);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte RotateLeftCircular(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateLeftCircular(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateLeftCircular(ref byte value)
+		{
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+			if (isMsbSet) SetBit(ref value, 0);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte RotateRight(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateRight(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateRight(ref byte value)
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+			if (isCarrySet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected byte RotateRightCircular(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			RotateRightCircular(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void RotateRightCircular(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+			if (isLsbSet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected void RotateLeftAccumulator()
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isMsbSet = IsBitSet(af.High, 7);
+			af.High <<= 1;
+			if (isCarrySet) SetBit(ref af.High, 0);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			// PV
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected void RotateLeftAccumulatorCircular()
+		{
+			bool isMsbSet = IsBitSet(af.High, 7);
+			af.High <<= 1;
+			if (isMsbSet) SetBit(ref af.High, 0);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			// PV
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected void RotateRightAccumulator()
+		{
+			bool isCarrySet = IsFlagSet(Flags.Carry);
+			bool isLsbSet = IsBitSet(af.High, 0);
+			af.High >>= 1;
+			if (isCarrySet) SetBit(ref af.High, 7);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			// PV
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected void RotateRightAccumulatorCircular()
+		{
+			bool isLsbSet = IsBitSet(af.High, 0);
+			af.High >>= 1;
+			if (isLsbSet) SetBit(ref af.High, 7);
+
+			// S
+			// Z
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			// PV
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected void RotateRight4B()
+		{
+			byte hlValue = ReadMemory8(hl.Word);
+
+			// A=WX  (HL)=YZ
+			// A=WZ  (HL)=XY
+			byte a1 = (byte)(af.High >> 4);     //W
+			byte a2 = (byte)(af.High & 0xF);    //X
+			byte hl1 = (byte)(hlValue >> 4);    //Y
+			byte hl2 = (byte)(hlValue & 0xF);   //Z
+
+			af.High = (byte)((a1 << 4) | hl2);
+			hlValue = (byte)((a2 << 4) | hl1);
+
+			WriteMemory8(hl.Word, hlValue);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(af.High, 7));
+			SetClearFlagConditional(Flags.Zero, (af.High == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			CalculateAndSetParity(af.High);
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void RotateLeft4B()
+		{
+			byte hlValue = ReadMemory8(hl.Word);
+
+			// A=WX  (HL)=YZ
+			// A=WY  (HL)=ZX
+			byte a1 = (byte)(af.High >> 4);     //W
+			byte a2 = (byte)(af.High & 0xF);    //X
+			byte hl1 = (byte)(hlValue >> 4);    //Y
+			byte hl2 = (byte)(hlValue & 0xF);   //Z
+
+			af.High = (byte)((a1 << 4) | hl1);
+			hlValue = (byte)((hl2 << 4) | a2);
+
+			WriteMemory8(hl.Word, hlValue);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(af.High, 7));
+			SetClearFlagConditional(Flags.Zero, (af.High == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(af.High, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(af.High, 3));
+			CalculateAndSetParity(af.High);
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		protected byte ShiftLeftArithmetic(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftLeftArithmetic(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftLeftArithmetic(ref byte value)
+		{
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte ShiftRightArithmetic(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftRightArithmetic(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftRightArithmetic(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			bool isMsbSet = IsBitSet(value, 7);
+			value >>= 1;
+			if (isMsbSet) SetBit(ref value, 7);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		protected byte ShiftLeftLogical(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftLeftLogical(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftLeftLogical(ref byte value)
+		{
+			bool isMsbSet = IsBitSet(value, 7);
+			value <<= 1;
+			value |= 0x01;
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isMsbSet);
+		}
+
+		protected byte ShiftRightLogical(ushort address)
+		{
+			byte value = ReadMemory8(address);
+			ShiftRightLogical(ref value);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ShiftRightLogical(ref byte value)
+		{
+			bool isLsbSet = IsBitSet(value, 0);
+			value >>= 1;
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(value, 7));
+			SetClearFlagConditional(Flags.Zero, (value == 0x00));
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			ClearFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			CalculateAndSetParity(value);
+			ClearFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, isLsbSet);
+		}
+
+		#endregion
+
+		#region Opcodes: Bit Set, Reset and Test Group
+
+		protected byte SetBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+			SetBit(ref value, bit);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void SetBit(ref byte value, int bit)
+		{
+			value |= (byte)(1 << bit);
+		}
+
+		protected byte ResetBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+			ResetBit(ref value, bit);
+			WriteMemory8(address, value);
+			return value;
+		}
+
+		protected void ResetBit(ref byte value, int bit)
+		{
+			value &= (byte)~(1 << bit);
+		}
+
+		protected void TestBit(ushort address, int bit)
+		{
+			byte value = ReadMemory8(address);
+
+			TestBit(value, bit);
+
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet((byte)(address >> 8), 5));
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet((byte)(address >> 8), 3));
+		}
+
+		protected void TestBit(byte value, int bit)
+		{
+			bool isBitSet = ((value & (1 << bit)) != 0);
+
+			SetClearFlagConditional(Flags.Sign, (bit == 7 && isBitSet));
+			SetClearFlagConditional(Flags.Zero, !isBitSet);
+			SetClearFlagConditional(Flags.UnusedBitY, IsBitSet(value, 5));
+			SetFlag(Flags.HalfCarry);
+			SetClearFlagConditional(Flags.UnusedBitX, IsBitSet(value, 3));
+			SetClearFlagConditional(Flags.ParityOrOverflow, !isBitSet);
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		#endregion
+
+		#region Opcodes: Jump Group
+
+		protected void DecrementJumpNonZero()
+		{
+			bc.High--;
+			JumpConditional8(bc.High != 0);
+		}
+
+		protected void Jump8()
+		{
+			pc += (ushort)(((sbyte)ReadMemory8(pc)) + 1);
+		}
+
+		protected void JumpConditional8(bool condition)
+		{
+			if (condition)
+			{
+				Jump8();
+				currentCycles += CycleCounts.AdditionalJumpCond8Taken;
+			}
+			else
+				pc++;
+		}
+
+		protected void JumpConditional16(bool condition)
+		{
+			if (condition)
+				pc = ReadMemory16(pc);
+			else
+				pc += 2;
+		}
+
+		#endregion
+
+		#region Opcodes: Call and Return Group
+
+		protected void Call16()
+		{
+			WriteMemory8(--sp, (byte)((pc + 2) >> 8));
+			WriteMemory8(--sp, (byte)((pc + 2) & 0xFF));
+			pc = ReadMemory16(pc);
+		}
+
+		protected void CallConditional16(bool condition)
+		{
+			if (condition)
+			{
+				Call16();
+				currentCycles += CycleCounts.AdditionalCallCondTaken;
+			}
+			else
+				pc += 2;
+		}
+
+		protected void Return()
+		{
+			pc = ReadMemory16(sp);
+			sp += 2;
+		}
+
+		protected void ReturnConditional(bool condition)
+		{
+			if (condition)
+			{
+				Return();
+				currentCycles += CycleCounts.AdditionalRetCondTaken;
+			}
+		}
+
+		protected void Restart(ushort address)
+		{
+			WriteMemory8(--sp, (byte)(pc >> 8));
+			WriteMemory8(--sp, (byte)(pc & 0xFF));
+			pc = address;
+		}
+
+		#endregion
+
+		#region Opcodes: Input and Output Group
+
+		protected void PortInput(ref byte dest, byte port)
+		{
+			dest = ReadPort(port);
+
+			SetClearFlagConditional(Flags.Sign, IsBitSet(dest, 7));
+			SetClearFlagConditional(Flags.Zero, (dest == 0x00));
+			ClearFlag(Flags.HalfCarry);
+			CalculateAndSetParity(dest);
+			ClearFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void PortInputFlagsOnly(byte port)
+		{
+			byte temp = 0;
+
+			PortInput(ref temp, port);
+		}
+
+		protected void PortInputIncrement()
+		{
+			WriteMemory8(hl.Word, ReadPort(bc.Low));
+			Increment16(ref hl.Word);
+			Decrement8(ref bc.High);
+
+			// S
+			SetClearFlagConditional(Flags.Zero, (bc.High == 0x00));
+			// H
+			// PV
+			SetFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void PortInputIncrementRepeat()
+		{
+			PortInputIncrement();
+
+			if (bc.High != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+			else
+			{
+				// S
+				SetFlag(Flags.Zero);
+				// H
+				// PV
+				SetFlag(Flags.Subtract);
+				// C
+			}
+		}
+
+		protected void PortInputDecrement()
+		{
+			WriteMemory8(hl.Word, ReadPort(bc.Low));
+			Decrement16(ref hl.Word);
+			Decrement8(ref bc.High);
+
+			// S
+			SetClearFlagConditional(Flags.Zero, (bc.High == 0x00));
+			// H
+			// PV
+			SetFlag(Flags.Subtract);
+			// C
+		}
+
+		protected void PortInputDecrementRepeat()
+		{
+			PortInputDecrement();
+
+			if (bc.High != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+			else
+			{
+				// S
+				SetFlag(Flags.Zero);
+				// H
+				// PV
+				SetFlag(Flags.Subtract);
+				// C
+			}
+		}
+
+		protected void PortOutputIncrement()
+		{
+			byte value = ReadMemory8(hl.Word);
+			WritePort(bc.Low, value);
+			Increment16(ref hl.Word);
+			Decrement8(ref bc.High);
+
+			bool setHC = ((value + hl.Low) > 255);
+
+			// S
+			SetClearFlagConditional(Flags.Zero, (bc.High == 0x00));
+			SetClearFlagConditional(Flags.HalfCarry, setHC);
+			CalculateAndSetParity((byte)(((value + hl.Low) & 0x07) ^ bc.High));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, setHC);
+		}
+
+		protected void PortOutputIncrementRepeat()
+		{
+			PortOutputIncrement();
+
+			if (bc.High != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+			else
+			{
+				// S
+				SetFlag(Flags.Zero);
+				// H
+				// PV
+				SetFlag(Flags.Subtract);
+				// C
+			}
+		}
+
+		protected void PortOutputDecrement()
+		{
+			byte value = ReadMemory8(hl.Word);
+			WritePort(bc.Low, value);
+			Decrement16(ref hl.Word);
+			Decrement8(ref bc.High);
+
+			bool setHC = ((value + hl.Low) > 255);
+
+			// S
+			SetClearFlagConditional(Flags.Zero, (bc.High == 0x00));
+			SetClearFlagConditional(Flags.HalfCarry, setHC);
+			CalculateAndSetParity((byte)(((value + hl.Low) & 0x07) ^ bc.High));
+			SetFlag(Flags.Subtract);
+			SetClearFlagConditional(Flags.Carry, setHC);
+		}
+
+		protected void PortOutputDecrementRepeat()
+		{
+			PortOutputDecrement();
+
+			if (bc.High != 0)
+			{
+				currentCycles += CycleCounts.AdditionalRepeatByteOps;
+				pc -= 2;
+			}
+			else
+			{
+				// S
+				SetFlag(Flags.Zero);
+				// H
+				// PV
+				SetFlag(Flags.Subtract);
+				// C
+			}
+		}
+
+		#endregion
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs.meta b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs.meta
new file mode 100644
index 0000000..caf0cd9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CPU/Z80A.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 02ea1ee0691d94f4ba12b23ecea75623
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs b/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs
new file mode 100644
index 0000000..b1ae198
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.IO;
+using System.IO.Compression;
+
+using Essgee.Emulation.Machines;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation
+{
+	public static class CartridgeLoader
+	{
+		static Dictionary<string, Type> fileExtensionSystemDictionary;
+
+		static CartridgeLoader()
+		{
+			fileExtensionSystemDictionary = new Dictionary<string, Type>();
+			foreach (var machineType in Assembly.GetExecutingAssembly().GetTypes().Where(x => typeof(IMachine).IsAssignableFrom(x) && !x.IsInterface).OrderBy(x => x.GetCustomAttribute<MachineIndexAttribute>()?.Index))
+			{
+				if (machineType == null) continue;
+
+				var instance = (IMachine)Activator.CreateInstance(machineType);
+				foreach (var extension in instance.FileFilter.Extension.Split(';'))
+					fileExtensionSystemDictionary.Add(extension, machineType);
+			}
+		}
+
+		public static (Type, byte[]) Load(string fileName, string fileType)
+		{
+			Type machineType = null;
+			byte[] romData = null;
+
+			if (!File.Exists(fileName))
+				throw new CartridgeLoaderException($"{fileType} file not found.");
+
+			try
+			{
+				var fileExtension = Path.GetExtension(fileName);
+				if (fileExtension == ".zip")
+				{
+					using (var zip = ZipFile.Open(fileName, ZipArchiveMode.Read))
+					{
+						foreach (var entry in zip.Entries)
+						{
+							var entryExtension = Path.GetExtension(entry.Name);
+							if (fileExtensionSystemDictionary.ContainsKey(entryExtension))
+							{
+								machineType = fileExtensionSystemDictionary[entryExtension];
+								using (var stream = entry.Open())
+								{
+									romData = new byte[entry.Length];
+									stream.Read(romData, 0, romData.Length);
+								}
+								break;
+							}
+						}
+					}
+				}
+				else if (fileExtensionSystemDictionary.ContainsKey(fileExtension))
+				{
+					machineType = fileExtensionSystemDictionary[fileExtension];
+					romData = File.ReadAllBytes(fileName);
+				}
+			}
+			catch (Exception ex) when (!AppEnvironment.DebugMode)
+			{
+				throw new CartridgeLoaderException("File load error", ex);
+			}
+
+			if (machineType == null)
+				throw new CartridgeLoaderException($"File could not be recognized as {fileType}.");
+
+			return (machineType, romData);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs.meta b/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs.meta
new file mode 100644
index 0000000..7f7ea3c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/CartridgeLoader.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f1b72196f7ea1bf41b9b580316878200
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges.meta b/Assets/Plugins/Essgee/Emulation/Cartridges.meta
new file mode 100644
index 0000000..6b93e80
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: fc75a07f5083eef408264c31457c5131
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco.meta
new file mode 100644
index 0000000..173df53
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1185c3d60bd11204ead23f542a5c6427
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs
new file mode 100644
index 0000000..e90f519
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+
+namespace Essgee.Emulation.Cartridges.Coleco
+{
+	public class ColecoCartridge : ICartridge
+	{
+		// TODO: http://atariage.com/forums/topic/210168-colecovision-bank-switching/ ?
+
+		byte[] romData;
+
+		public ColecoCartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			//
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return null;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return (ushort)(romData.Length - 1);
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address <= 0x1FFF)
+			{
+				/* BIOS */
+				return romData[address & 0x1FFF];
+			}
+			else
+			{
+				/* Cartridge */
+				address -= 0x8000;
+				if (address >= romData.Length) address -= (ushort)romData.Length;
+				return romData[address];
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			/* Cannot write to cartridge */
+			return;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs.meta
new file mode 100644
index 0000000..d0a208b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Coleco/ColecoCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9d70811b8aea410499364dc0485491e3
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs
new file mode 100644
index 0000000..11b8e71
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Cartridges
+{
+	public interface ICartridge
+	{
+		void LoadRom(byte[] data);
+		void LoadRam(byte[] data);
+
+		byte[] GetRomData();
+		byte[] GetRamData();
+		bool IsRamSaveNeeded();
+
+		ushort GetLowerBound();
+		ushort GetUpperBound();
+
+		void Step(int clockCyclesInStep);
+
+		byte Read(ushort address);
+		void Write(ushort address, byte value);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs.meta
new file mode 100644
index 0000000..99393f8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/ICartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 69d4af746d98c1b4ab29d5be6b2d9f2d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo.meta
new file mode 100644
index 0000000..3ecdcd2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: eea6a30841878a34286f04f99571c050
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs
new file mode 100644
index 0000000..942afc4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs
@@ -0,0 +1,425 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+using System.Drawing;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	/* Image processing, etc. based on https://github.com/AntonioND/gbcam-rev-engineer/blob/master/doc/gb_camera_doc_v1_1_1.pdf */
+
+	public class GBCameraCartridge : IGameBoyCartridge
+	{
+		const int camSensorExtraLines = 8;
+		const int camSensorWidth = 128;
+		const int camSensorHeight = 112 + camSensorExtraLines;
+
+		const int camWidth = 128;
+		const int camHeight = 112;
+
+		static readonly float[] edgeRatioLookUpTable = new float[] { 0.50f, 0.75f, 1.00f, 1.25f, 2.00f, 3.00f, 4.00f, 5.00f };
+
+		public enum ImageSources
+		{
+			[Description("Random Noise")]
+			Noise,
+			[Description("Image File")]
+			File
+		}
+
+		ImageSources imageSourceType;
+		//Bitmap scaledImage;
+
+		readonly int[,] webcamOutput, camRetinaOutput;
+		readonly byte[,,] tileBuffer;
+
+		byte[] romData, ramData;
+		bool hasBattery;
+
+		byte romBank, ramBank;
+		bool ramEnable;
+
+		readonly byte[] camRegisters;
+		bool camSelected;
+
+		int cameraCycles, camClocksLeft;
+
+		public GBCameraCartridge(int romSize, int ramSize)
+		{
+			imageSourceType = ImageSources.Noise;
+			//scaledImage = new Bitmap(camSensorWidth, camSensorHeight);
+
+			webcamOutput = new int[camSensorWidth, camSensorHeight];
+			camRetinaOutput = new int[camSensorWidth, camSensorHeight];
+			tileBuffer = new byte[14, 16, 16];
+
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBank = 1;
+			ramBank = 0;
+
+			ramEnable = false;
+
+			camRegisters = new byte[0x80];  // 0x36 used
+			camSelected = false;
+
+			hasBattery = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+		}
+
+		public void SetImageSource(ImageSources source, string filename)
+		{
+			imageSourceType = source;
+
+			if (imageSourceType == ImageSources.File)
+			{
+                //TODO  imageSourceType == ImageSources.File
+
+                //using (var tempImage = new Bitmap(filename))
+                //{
+                //	using (var g = System.Drawing.Graphics.FromImage(scaledImage))
+                //	{
+                //		var ratio = Math.Min(tempImage.Width / (float)camSensorWidth, tempImage.Height / (float)camSensorHeight);
+                //		var srcWidth = (int)(camSensorWidth * ratio);
+                //		var srcHeight = (int)(camSensorHeight * ratio);
+                //		var srcX = (tempImage.Width - srcWidth) / 2;
+                //		var srcY = (tempImage.Height - srcHeight) / 2;
+                //		var scaledRect = new Rectangle(0, 0, camSensorWidth, camSensorHeight);
+
+                //		g.FillRectangle(Brushes.White, scaledRect);
+                //		g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
+                //		g.DrawImage(tempImage, scaledRect, new Rectangle(srcX, srcY, srcWidth, srcHeight), GraphicsUnit.Pixel);
+                //	}
+                //}
+
+                //for (var x = 0; x < camSensorWidth; x++)
+                //	for (var y = 0; y < camSensorHeight; y++)
+                //		webcamOutput[x, y] = (int)(scaledImage.GetPixel(x, y).GetBrightness() * 255);
+            }
+        }
+
+		public void Step(int clockCyclesInStep)
+		{
+			cameraCycles += clockCyclesInStep;
+			if (cameraCycles >= camClocksLeft)
+			{
+				camRegisters[0x00] &= 0xFE;
+				cameraCycles = 0;
+			}
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x3FFF)
+			{
+				return romData[address & 0x3FFF];
+			}
+			else if (address >= 0x4000 && address <= 0x7FFF)
+			{
+				return romData[(romBank << 14) | (address & 0x3FFF)];
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (!camSelected)
+				{
+					if ((camRegisters[0x00] & 0b1) == 0)
+						return ramData[(ramBank << 13) | (address & 0x1FFF)];
+					else
+						return 0xFF;
+				}
+				else
+				{
+					var reg = (byte)(address & 0x7F);
+					if (reg == 0x00)
+						return (byte)(camRegisters[reg] & 0x07);
+					else
+						return 0xFF;
+				}
+			}
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				ramEnable = (value & 0x0F) == 0x0A;
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				romBank = (byte)((romBank & 0xC0) | (value & 0x3F));
+				romBank &= (byte)((romData.Length >> 14) - 1);
+				if ((romBank & 0x3F) == 0x00) romBank |= 0x01;
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				if ((value & 0x10) != 0)
+				{
+					camSelected = true;
+				}
+				else
+				{
+					camSelected = false;
+					ramBank = (byte)(value & 0x0F);
+				}
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (!camSelected)
+				{
+					if (ramEnable)
+						ramData[(ramBank << 13) | (address & 0x1FFF)] = value;
+				}
+				else
+				{
+					var reg = (byte)(address & 0x7F);
+					if (reg == 0x00 && (value & 0b1) != 0)
+						GenerateImage();
+
+					camRegisters[reg] = value;
+				}
+			}
+		}
+
+		private int Clamp(int value, int min, int max)
+		{
+			if (value < min) value = min;
+			else if (value > max) value = max;
+			return value;
+		}
+
+		private void GenerateImage()
+		{
+			/* Get configuration -- register 0 */
+			var pBits = 0;
+			var mBits = 0;
+
+			switch ((camRegisters[0x00] >> 1) & 0b11)
+			{
+				case 0: pBits = 0x00; mBits = 0x01; break;
+				case 1: pBits = 0x01; mBits = 0x00; break;
+				case 2:
+				case 3: pBits = 0x01; mBits = 0x02; break;
+			}
+
+			/* Register 1 */
+			var nBit = ((camRegisters[0x01] >> 7) & 0b1) != 0;
+			var vhBits = (camRegisters[0x01] >> 5) & 0b11;
+
+			/* Registers 2 and 3 */
+			var exposureBits = camRegisters[0x02] << 8 | camRegisters[0x03];
+
+			/* Register 4 */
+			var edgeAlpha = edgeRatioLookUpTable[(camRegisters[0x04] >> 4) & 0b111];
+			var e3Bit = ((camRegisters[0x04] >> 7) & 0b1) != 0;
+			var iBit = ((camRegisters[0x04] >> 3) & 0b1) != 0;
+
+			/* Calculate timings */
+			camClocksLeft = 4 * (32446 + (nBit ? 0 : 512) + 16 * exposureBits);
+
+			/* Clear tile buffer */
+			for (var j = 0; j < 14; j++)
+				for (var i = 0; i < 16; i++)
+					for (var k = 0; k < 16; k++)
+						tileBuffer[j, i, k] = 0x00;
+
+			/* Sensor handling */
+			/* Copy webcam buffer to sensor buffer, apply color correction & exposure time */
+			for (var i = 0; i < camSensorWidth; i++)
+			{
+				for (var j = 0; j < camSensorHeight; j++)
+				{
+					var value = 0;
+					switch (imageSourceType)
+					{
+						case ImageSources.File: value = webcamOutput[i, j]; break;
+						case ImageSources.Noise: value = StandInfo.Random.Next(255); break;
+					}
+
+					value = (value * exposureBits) / 0x0300;
+					value = 128 + (((value - 128) * 1) / 8);
+					camRetinaOutput[i, j] = Clamp(value, 0, 255);
+
+					/* Invert */
+					if (iBit)
+						camRetinaOutput[i, j] = 255 - camRetinaOutput[i, j];
+
+					/* Make signed */
+					camRetinaOutput[i, j] = camRetinaOutput[i, j] - 128;
+				}
+			}
+
+			var tempBuffer = new int[camSensorWidth, camSensorHeight];
+			var filteringMode = (nBit ? 8 : 0) | (vhBits << 1) | (e3Bit ? 1 : 0);
+			switch (filteringMode)
+			{
+				case 0x00:
+					/* 1-D filtering */
+					for (var i = 0; i < camSensorWidth; i++)
+						for (var j = 0; j < camSensorHeight; j++)
+							tempBuffer[i, j] = camRetinaOutput[i, j];
+
+					for (var i = 0; i < camSensorWidth; i++)
+					{
+						for (var j = 0; j < camSensorHeight; j++)
+						{
+							var ms = tempBuffer[i, Math.Min(j + 1, camSensorHeight - 1)];
+							var px = tempBuffer[i, j];
+
+							var value = 0;
+							if ((pBits & 0b01) != 0) value += px;
+							if ((pBits & 0b10) != 0) value += ms;
+							if ((mBits & 0b01) != 0) value -= px;
+							if ((mBits & 0b10) != 0) value -= ms;
+
+							camRetinaOutput[i, j] = Clamp(value, -128, 127);
+						}
+					}
+					break;
+
+				case 0x02:
+					/* 1-D filtering + Horiz. enhancement : P + {2P-(MW+ME)} * alpha */
+					for (var i = 0; i < camSensorWidth; i++)
+					{
+						for (var j = 0; j < camSensorHeight; j++)
+						{
+							var mw = camRetinaOutput[Math.Max(0, i - 1), j];
+							var me = camRetinaOutput[Math.Min(i + 1, camSensorWidth - 1), j];
+							var px = camRetinaOutput[i, j];
+
+							tempBuffer[i, j] = Clamp((int)(px + ((2 * px - mw - me) * edgeAlpha)), 0, 255);
+						}
+					}
+
+					for (var i = 0; i < camSensorWidth; i++)
+					{
+						for (var j = 0; j < camSensorHeight; j++)
+						{
+							var ms = tempBuffer[i, Math.Min(j + 1, camSensorHeight - 1)];
+							var px = tempBuffer[i, j];
+
+							var value = 0;
+							if ((pBits & 0b01) != 0) value += px;
+							if ((pBits & 0b10) != 0) value += ms;
+							if ((mBits & 0b01) != 0) value -= px;
+							if ((mBits & 0b10) != 0) value -= ms;
+
+							camRetinaOutput[i, j] = Clamp(value, -128, 127);
+						}
+					}
+					break;
+
+				case 0x0E:
+					/* 2D enhancement : P + {4P-(MN+MS+ME+MW)} * alpha */
+					for (var i = 0; i < camSensorWidth; i++)
+					{
+						for (var j = 0; j < camSensorHeight; j++)
+						{
+							var ms = camRetinaOutput[i, Math.Min(j + 1, camSensorHeight - 1)];
+							var mn = camRetinaOutput[i, Math.Max(0, j - 1)];
+							var mw = camRetinaOutput[Math.Max(0, i - 1), j];
+							var me = camRetinaOutput[Math.Min(i + 1, camSensorWidth - 1), j];
+							var px = camRetinaOutput[i, j];
+
+							tempBuffer[i, j] = Clamp((int)(px + ((4 * px - mw - me - mn - ms) * edgeAlpha)), -128, 127);
+						}
+					}
+
+					for (var i = 0; i < camSensorWidth; i++)
+						for (var j = 0; j < camSensorHeight; j++)
+							camRetinaOutput[i, j] = tempBuffer[i, j];
+					break;
+
+				case 0x01:
+					/* Unknown, always same color; sensor datasheet does not document this, maybe a bug? */
+					for (var i = 0; i < camSensorWidth; i++)
+						for (var j = 0; j < camSensorHeight; j++)
+							camRetinaOutput[i, j] = 0;
+					break;
+
+				default:
+					/* Unknown; write to log if enabled */
+					if (AppEnvironment.EnableLogger)
+					{
+						EssgeeLogger.WriteLine($"Unsupported GB Camera mode 0x{filteringMode:X2}");
+						EssgeeLogger.WriteLine(string.Join(" ", camRegisters.Take(6).Select(x => $"0x{x:X2}")));
+					}
+					break;
+			}
+
+			/* Make unsigned */
+			for (var i = 0; i < camSensorWidth; i++)
+				for (var j = 0; j < camSensorHeight; j++)
+					camRetinaOutput[i, j] = camRetinaOutput[i, j] + 128;
+
+			/* Convert output to GB tiles */
+			for (var i = 0; i < camWidth; i++)
+			{
+				for (var j = 0; j < camHeight; j++)
+				{
+					var sensorValue = camRetinaOutput[i, j + (camSensorExtraLines / 2)];
+					var matrixOffset = 0x06 + ((j % 4) * 12) + ((i % 4) * 3);
+
+					var c = (byte)0;
+					if (sensorValue < camRegisters[matrixOffset + 0]) c = 3;
+					else if (sensorValue < camRegisters[matrixOffset + 1]) c = 2;
+					else if (sensorValue < camRegisters[matrixOffset + 2]) c = 1;
+					else c = 0;
+
+					if ((c & 1) != 0) tileBuffer[j >> 3, i >> 3, ((j & 7) * 2) + 0] |= (byte)(1 << (7 - (7 & i)));
+					if ((c & 2) != 0) tileBuffer[j >> 3, i >> 3, ((j & 7) * 2) + 1] |= (byte)(1 << (7 - (7 & i)));
+				}
+			}
+
+			/* Copy tiles to cartridge RAM */
+			int outputOffset = 0x100;
+			for (var j = 0; j < 14; j++)
+				for (var i = 0; i < 16; i++)
+					for (var k = 0; k < 16; k++)
+						ramData[outputOffset++] = tileBuffer[j, i, k];
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs.meta
new file mode 100644
index 0000000..e6b4a2c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/GBCameraCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3887c3ff437835449bdf694c3e812afa
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs
new file mode 100644
index 0000000..0195d20
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public interface IGameBoyCartridge : ICartridge
+	{
+		void SetCartridgeConfig(bool battery, bool rtc, bool rumble);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs.meta
new file mode 100644
index 0000000..9fee577
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/IGameBoyCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8f215e1ba8582ab42b21a0dae809cb6e
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs
new file mode 100644
index 0000000..2b7d5d6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public class MBC1Cartridge : IGameBoyCartridge
+	{
+		byte[] romData, ramData;
+		bool hasBattery;
+
+		byte romBank, ramBank;
+		bool ramEnable;
+
+		byte bankingMode;
+
+		public MBC1Cartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBank = 1;
+			ramBank = 0;
+
+			ramEnable = false;
+
+			bankingMode = 0;
+
+			hasBattery = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x3FFF)
+			{
+				return romData[address & 0x3FFF];
+			}
+			else if (address >= 0x4000 && address <= 0x7FFF)
+			{
+				return romData[(romBank << 14) | (address & 0x3FFF)];
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (ramEnable && ramData.Length != 0)
+					return ramData[(ramBank << 13) | (address & 0x1FFF)];
+				else
+					return 0xFF;
+			}
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				ramEnable = (value & 0x0F) == 0x0A;
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				romBank = (byte)((romBank & 0xE0) | (value & 0x1F));
+				romBank &= (byte)((romData.Length >> 14) - 1);
+				if ((romBank & 0x1F) == 0x00) romBank |= 0x01;
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				if (bankingMode == 0)
+				{
+					romBank = (byte)((romBank & 0x9F) | ((value & 0x03) << 5));
+					romBank &= (byte)((romData.Length >> 14) - 1);
+					if ((romBank & 0x1F) == 0x00) romBank |= 0x01;
+				}
+				else
+				{
+					ramBank = (byte)(value & 0x03);
+				}
+			}
+			else if (address >= 0x6000 && address <= 0x7FFF)
+			{
+				bankingMode = (byte)(value & 0b1);
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (ramEnable && ramData.Length != 0)
+					ramData[(ramBank << 13) | (address & 0x1FFF)] = value;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs.meta
new file mode 100644
index 0000000..975cb25
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC1Cartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9e0eda098544f8e4aa894031f92b2b57
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs
new file mode 100644
index 0000000..b8272ae
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public class MBC2Cartridge : IGameBoyCartridge
+	{
+		byte[] romData, ramData;
+		bool hasBattery;
+
+		byte romBank;
+		bool ramEnable;
+
+		public MBC2Cartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBank = 1;
+
+			ramEnable = false;
+
+			hasBattery = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x3FFF)
+			{
+				return romData[address & 0x3FFF];
+			}
+			else if (address >= 0x4000 && address <= 0x7FFF)
+			{
+				return romData[(romBank << 14) | (address & 0x3FFF)];
+			}
+			else if (address >= 0xA000 && address <= 0xA1FF)
+			{
+				if (ramEnable)
+				{
+					var ramOffset = (address >> 1) & 0x00FF;
+					var valueShift = (address & 0x01) << 2;
+					return (byte)((ramData[ramOffset] >> valueShift) & 0x0F);
+				}
+				else
+					return 0xFF;
+			}
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				if ((address & 0x0100) == 0)
+					ramEnable = (value & 0x0F) == 0x0A;
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				if ((address & 0x0100) != 0)
+				{
+					romBank = (byte)((romBank & 0xF0) | (value & 0x0F));
+					romBank &= (byte)((romData.Length >> 14) - 1);
+					if ((romBank & 0x0F) == 0x00) romBank |= 0x01;
+				}
+			}
+			else if (address >= 0xA000 && address <= 0xA1FF)
+			{
+				if (ramEnable)
+				{
+					var ramOffset = (address >> 1) & 0x00FF;
+					var valueShift = (address & 0x01) << 2;
+
+					ramData[ramOffset] = (byte)((ramData[ramOffset] & (0x0F << (valueShift ^ 0x04))) | (value << valueShift));
+				}
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs.meta
new file mode 100644
index 0000000..1b7cd17
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC2Cartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b5795dc3ee1b33746a64186a778e2bd6
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs
new file mode 100644
index 0000000..bff75c8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs
@@ -0,0 +1,267 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public class MBC3Cartridge : IGameBoyCartridge
+	{
+		// https://thomas.spurden.name/gameboy/#mbc3-real-time-clock-rtc
+
+		public class RTC
+		{
+			public const int NumRegisters = 0x05;
+
+			public byte[] BaseRegisters { get; private set; }
+			public byte[] LatchedRegisters { get; private set; }
+
+			public DateTime BaseTime { get; set; }
+
+			public bool IsSelected { get; set; }
+			public byte SelectedRegister { get; set; }
+			public bool IsLatched { get; set; }
+
+			public RTC()
+			{
+				BaseRegisters = new byte[NumRegisters];
+				LatchedRegisters = new byte[NumRegisters];
+
+				BaseTime = DateTime.Now;
+
+				IsSelected = false;
+				SelectedRegister = 0;
+				IsLatched = false;
+			}
+
+			public void FromSaveData(byte[] ramData)
+			{
+				var rtcOffset = ramData.Length - 0x30;
+
+				// Time
+				BaseRegisters[0x00] = ramData[rtcOffset + 0];
+				BaseRegisters[0x01] = ramData[rtcOffset + 4];
+				BaseRegisters[0x02] = ramData[rtcOffset + 8];
+				BaseRegisters[0x03] = ramData[rtcOffset + 12];
+				BaseRegisters[0x04] = ramData[rtcOffset + 16];
+
+				// Latched time
+				LatchedRegisters[0x00] = ramData[rtcOffset + 20];
+				LatchedRegisters[0x01] = ramData[rtcOffset + 24];
+				LatchedRegisters[0x02] = ramData[rtcOffset + 28];
+				LatchedRegisters[0x03] = ramData[rtcOffset + 32];
+				LatchedRegisters[0x04] = ramData[rtcOffset + 36];
+
+				// Timestamp
+				BaseTime = DateTimeOffset.FromUnixTimeSeconds((long)BitConverter.ToUInt64(ramData, rtcOffset + 40)).UtcDateTime;
+			}
+
+			public byte[] ToSaveData()
+			{
+				var appendData = new byte[0x30];
+
+				// Time
+				appendData[0] = BaseRegisters[0x00];
+				appendData[4] = BaseRegisters[0x01];
+				appendData[8] = BaseRegisters[0x02];
+				appendData[12] = BaseRegisters[0x03];
+				appendData[16] = BaseRegisters[0x04];
+
+				// Latched time
+				appendData[20] = LatchedRegisters[0x00];
+				appendData[24] = LatchedRegisters[0x01];
+				appendData[28] = LatchedRegisters[0x02];
+				appendData[32] = LatchedRegisters[0x03];
+				appendData[36] = LatchedRegisters[0x04];
+
+				// Timestamp
+				var timestamp = BitConverter.GetBytes(((DateTimeOffset)BaseTime).ToUnixTimeSeconds());
+				for (var i = 0; i < timestamp.Length; i++) appendData[40 + i] = timestamp[i];
+
+				return appendData;
+			}
+
+			public void Update()
+			{
+				// GOLD,38695,3000 == 00931
+
+
+				var currentTime = DateTime.Now;
+				var newTime = currentTime;
+
+				if (((BaseRegisters[0x04] >> 6) & 0b1) == 0 && currentTime > BaseTime)
+					newTime.Add(currentTime - BaseTime);
+
+				newTime.AddSeconds(BaseRegisters[0x00]);
+				newTime.AddMinutes(BaseRegisters[0x01]);
+				newTime.AddHours(BaseRegisters[0x02]);
+				newTime.AddDays(BaseRegisters[0x03]);
+				newTime.AddDays((BaseRegisters[0x04] & 0b1) << 8);
+
+				BaseRegisters[0x00] = (byte)newTime.Second;
+				BaseRegisters[0x01] = (byte)newTime.Minute;
+				BaseRegisters[0x02] = (byte)newTime.Hour;
+				BaseRegisters[0x03] = (byte)(newTime.Day & 0xFF);
+				BaseRegisters[0x04] = (byte)((BaseRegisters[0x04] & 0xFE) | ((newTime.Day >> 8) & 0b1) | ((newTime.Day >> 8) & 0b1) << 7);
+
+				BaseTime = currentTime;
+			}
+		}
+
+		byte[] romData, ramData;
+		bool hasBattery, hasRTC;
+
+		byte romBank, ramBank;
+		bool ramEnable;
+
+		RTC rtc;
+
+		public MBC3Cartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			hasBattery = false;
+			hasRTC = false;
+
+			romBank = 1;
+			ramBank = 0;
+			ramEnable = false;
+
+			rtc = new RTC();
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			/* Has appended RTC state data? */
+			if ((data.Length & 0x30) == 0x30) rtc.FromSaveData(data);
+
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			if (hasRTC)
+				return ramData.Concat(rtc.ToSaveData()).ToArray();
+			else
+				return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+			hasRTC = rtc;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x3FFF)
+			{
+				return romData[address & 0x3FFF];
+			}
+			else if (address >= 0x4000 && address <= 0x7FFF)
+			{
+				return romData[(romBank << 14) | (address & 0x3FFF)];
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (rtc.IsSelected)
+				{
+					if (rtc.IsLatched)
+						return rtc.LatchedRegisters[rtc.SelectedRegister];
+					else
+						return rtc.BaseRegisters[rtc.SelectedRegister];
+				}
+				else if (ramEnable)
+					return ramData[(ramBank << 13) | (address & 0x1FFF)];
+				else
+					return 0xFF;
+			}
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				ramEnable = (value & 0x0F) == 0x0A;
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				romBank = (byte)((romBank & 0x80) | (value & 0x7F));
+				romBank &= (byte)((romData.Length >> 14) - 1);
+				if (romBank == 0x00) romBank = 0x01;
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				if (value >= 0x00 && value <= 0x07)
+				{
+					rtc.IsSelected = false;
+					ramBank = (byte)(value & 0x07);
+				}
+				else if (value >= 0x08 && value <= 0x0C)
+				{
+					rtc.IsSelected = true;
+					rtc.SelectedRegister = (byte)(value - 0x08);
+				}
+			}
+			else if (address >= 0x6000 && address <= 0x7FFF)
+			{
+				if (value == 0x00 && rtc.IsLatched)
+					rtc.IsLatched = false;
+				else if (value == 0x01 && !rtc.IsLatched)
+				{
+					rtc.Update();
+					for (var i = 0; i < RTC.NumRegisters; i++)
+						rtc.LatchedRegisters[i] = rtc.BaseRegisters[i];
+					rtc.IsLatched = true;
+				}
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (rtc.IsSelected)
+				{
+					rtc.Update();
+					rtc.BaseRegisters[rtc.SelectedRegister] = value;
+				}
+				else if (ramEnable)
+					ramData[(ramBank << 13) | (address & 0x1FFF)] = value;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs.meta
new file mode 100644
index 0000000..123814f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC3Cartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 739832bbd2b7ce54e8658790d0d1b4ae
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs
new file mode 100644
index 0000000..e6f52b9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	// TODO: rumble?
+
+	public class MBC5Cartridge : IGameBoyCartridge
+	{
+		public event EventHandler<EventArgs> EnableRumble;
+		protected virtual void OnEnableRumble(EventArgs e) { EnableRumble?.Invoke(this, EventArgs.Empty); }
+
+		byte[] romData, ramData;
+		bool hasBattery, hasRumble;
+
+		ushort romBank;
+		byte ramBank;
+		bool ramEnable;
+
+		public MBC5Cartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBank = 1;
+			ramBank = 0;
+
+			ramEnable = false;
+
+			hasBattery = false;
+			hasRumble = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+			hasRumble = rumble;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x3FFF)
+			{
+				return romData[address & 0x3FFF];
+			}
+			else if (address >= 0x4000 && address <= 0x7FFF)
+			{
+				return romData[(romBank << 14) | (address & 0x3FFF)];
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (ramEnable && ramData.Length != 0)
+					return ramData[(ramBank << 13) | (address & 0x1FFF)];
+				else
+					return 0xFF;
+			}
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				ramEnable = (value & 0x0F) == 0x0A;
+			}
+			else if (address >= 0x2000 && address <= 0x2FFF)
+			{
+				romBank = (ushort)((romBank & 0x0100) | value);
+				romBank &= (ushort)((romData.Length >> 14) - 1);
+			}
+			else if (address >= 0x3000 && address <= 0x3FFF)
+			{
+				romBank = (ushort)((romBank & 0x00FF) | ((value & 0x01) << 8));
+				romBank &= (ushort)((romData.Length >> 14) - 1);
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				if (hasRumble)
+				{
+					if ((value & 0x08) == 0x08) OnEnableRumble(EventArgs.Empty);
+					ramBank = (byte)(value & 0x07);
+					ramBank %= (byte)(ramData.Length >> 13);
+				}
+				else
+				{
+					ramBank = (byte)(value & 0x0F);
+					ramBank %= (byte)(ramData.Length >> 13);
+				}
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				if (ramEnable && ramData.Length != 0)
+					ramData[(ramBank << 13) | (address & 0x1FFF)] = value;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs.meta
new file mode 100644
index 0000000..5427293
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/MBC5Cartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f083c0cfa9c1cf54ba90f19320c53c16
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs
new file mode 100644
index 0000000..e2ddb08
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public class NoMapperCartridge : IGameBoyCartridge
+	{
+		byte[] romData, ramData;
+		bool hasBattery;
+
+		public NoMapperCartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasBattery;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0x7FFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public void SetCartridgeConfig(bool battery, bool rtc, bool rumble)
+		{
+			hasBattery = battery;
+		}
+
+		public byte Read(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x7FFF)
+				return romData[address & 0x7FFF];
+			else if (address >= 0xA000 && address <= 0xBFFF)
+				return ramData[address & 0x1FFF];
+			else
+				return 0xFF;
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0xA000 && address <= 0xBFFF)
+				ramData[address & 0x1FFF] = value;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs.meta
new file mode 100644
index 0000000..e426369
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/NoMapperCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 90768449a6b38a440b08bb61118b37b7
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs
new file mode 100644
index 0000000..908d075
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+
+namespace Essgee.Emulation.Cartridges.Nintendo
+{
+	public static class SpecializedLoader
+	{
+		public static IGameBoyCartridge CreateCartridgeInstance(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			var romSize = -1;
+			switch (romData[0x0148])
+			{
+				case 0x00: romSize = 32 * 1024; break;
+				case 0x01: romSize = 64 * 1024; break;
+				case 0x02: romSize = 128 * 1024; break;
+				case 0x03: romSize = 256 * 1024; break;
+				case 0x04: romSize = 512 * 1024; break;
+				case 0x05: romSize = 1024 * 1024; break;
+				case 0x06: romSize = 2048 * 1024; break;
+				case 0x07: romSize = 4096 * 1024; break;
+				case 0x08: romSize = 8192 * 1024; break;
+				case 0x52: romSize = 1152 * 1024; break;
+				case 0x53: romSize = 1280 * 1024; break;
+				case 0x54: romSize = 1536 * 1024; break;
+
+				default: romSize = romData.Length; break;
+			}
+
+			var ramSize = -1;
+			switch (romData[0x0149])
+			{
+				case 0x00: ramSize = 0 * 1024; break;
+				case 0x01: ramSize = 2 * 1024; break;
+				case 0x02: ramSize = 8 * 1024; break;
+				case 0x03: ramSize = 32 * 1024; break;
+				case 0x04: ramSize = 128 * 1024; break;
+				case 0x05: ramSize = 64 * 1024; break;
+
+				default: ramSize = 0; break;
+			}
+
+			/* NOTES:
+			 *  MBC2 internal RAM is not given in header, 512*4b == 256 bytes 
+			 *  GB Camera internal RAM ~seems~ to not be given in header? 128 kbytes
+			 */
+
+			var mapperTypeFromHeader = typeof(NoMapperCartridge);
+			var hasBattery = false;
+			var hasRtc = false;
+			var hasRumble = false;
+			switch (romData[0x0147])
+			{
+				case 0x00: mapperType = typeof(NoMapperCartridge); break;
+				case 0x01: mapperType = typeof(MBC1Cartridge); break;
+				case 0x02: mapperType = typeof(MBC1Cartridge); break;
+				case 0x03: mapperType = typeof(MBC1Cartridge); hasBattery = true; break;
+				case 0x05: mapperType = typeof(MBC2Cartridge); ramSize = 0x100; break;
+				case 0x06: mapperType = typeof(MBC2Cartridge); ramSize = 0x100; hasBattery = true; break;
+				case 0x08: mapperType = typeof(NoMapperCartridge); break;
+				case 0x09: mapperType = typeof(NoMapperCartridge); hasBattery = true; break;
+				// 0B-0D, MMM01
+				case 0x0F: mapperType = typeof(MBC3Cartridge); hasBattery = true; hasRtc = true; break;
+				case 0x10: mapperType = typeof(MBC3Cartridge); hasBattery = true; hasRtc = true; break;
+				case 0x11: mapperType = typeof(MBC3Cartridge); break;
+				case 0x12: mapperType = typeof(MBC3Cartridge); break;
+				case 0x13: mapperType = typeof(MBC3Cartridge); hasBattery = true; break;
+				case 0x19: mapperType = typeof(MBC5Cartridge); break;
+				case 0x1A: mapperType = typeof(MBC5Cartridge); break;
+				case 0x1B: mapperType = typeof(MBC5Cartridge); hasBattery = true; break;
+				case 0x1C: mapperType = typeof(MBC5Cartridge); hasRumble = true; break;
+				case 0x1D: mapperType = typeof(MBC5Cartridge); hasRumble = true; break;
+				case 0x1E: mapperType = typeof(MBC5Cartridge); hasBattery = true; hasRumble = true; break;
+				// 20, MBC6
+				// 22, MBC7
+				case 0xFC: mapperType = typeof(GBCameraCartridge); ramSize = 128 * 1024; break;
+				// FD, BANDAI TAMA5
+				// FE, HuC3
+				// FF, HuC1
+
+				default: throw new EmulationException($"Unimplemented cartridge type 0x{romData[0x0147]:X2}");
+			}
+
+			if (mapperType == null)
+				mapperType = mapperTypeFromHeader;
+
+			if (romSize != romData.Length)
+			{
+				var romSizePadded = 1;
+				while (romSizePadded < romData.Length) romSizePadded <<= 1;
+				romSize = Math.Max(romSizePadded, romData.Length);
+			}
+
+			var cartridge = (IGameBoyCartridge)Activator.CreateInstance(mapperType, new object[] { romSize, ramSize });
+			cartridge.LoadRom(romData);
+			cartridge.LoadRam(ramData);
+			cartridge.SetCartridgeConfig(hasBattery, hasRtc, hasRumble);
+
+			return cartridge;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs.meta
new file mode 100644
index 0000000..93599c1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Nintendo/SpecializedLoader.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 40e53d5096e06264a829b803422f047e
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega.meta
new file mode 100644
index 0000000..45cd0cd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9dbf1dc8385034e4082468ce1f21dca0
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs
new file mode 100644
index 0000000..e4bc7eb
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	public class CodemastersCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		byte[] ramData;
+
+		[StateRequired]
+		readonly byte[] pagingRegisters;
+		[StateRequired]
+		readonly byte bankMask;
+
+		[StateRequired]
+		bool isRamEnabled;
+
+		public CodemastersCartridge(int romSize, int ramSize)
+		{
+			pagingRegisters = new byte[3];
+			pagingRegisters[0] = 0x00;
+			pagingRegisters[1] = 0x01;
+			pagingRegisters[2] = 0x02;
+
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			bankMask = (byte)((romData.Length >> 14) - 1);
+
+			isRamEnabled = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xC000)
+			{
+				case 0x0000:
+					return romData[((pagingRegisters[0] << 14) | (address & 0x3FFF))];
+
+				case 0x4000:
+					return romData[((pagingRegisters[1] << 14) | (address & 0x3FFF))];
+
+				case 0x8000:
+					if (isRamEnabled && (address >= 0xA000 && address <= 0xBFFF))
+						return ramData[address & 0x1FFF];
+					else
+						return romData[((pagingRegisters[2] << 14) | (address & 0x3FFF))];
+
+				default:
+					throw new EmulationException(string.Format("Codemasters mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			switch (address)
+			{
+				case 0x0000:
+					pagingRegisters[0] = (byte)(value & bankMask);
+					break;
+
+				case 0x4000:
+					pagingRegisters[1] = (byte)(value & bankMask);
+					isRamEnabled = ((value & 0x80) == 0x80);
+					break;
+
+				case 0x8000:
+					pagingRegisters[2] = (byte)(value & bankMask);
+					break;
+			}
+
+			if (isRamEnabled && ((address & 0xF000) == 0xA000 || (address & 0xF000) == 0xB000))
+				ramData[address & 0x1FFF] = value;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs.meta
new file mode 100644
index 0000000..465846d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/CodemastersCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f7cca9db8813d3c45a1ce0cfb7792b71
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs
new file mode 100644
index 0000000..219609f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	public class KoreanMSX8kMapperCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		readonly byte[] pagingRegisters;
+
+		[StateRequired]
+		byte bankMask;
+
+		public KoreanMSX8kMapperCartridge(int romSize, int ramSize)
+		{
+			pagingRegisters = new byte[4];
+
+			romData = new byte[romSize];
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+
+			var romSizeRounded = 1;
+			while (romSizeRounded < romData.Length) romSizeRounded <<= 1;
+
+			bankMask = (byte)((romSizeRounded >> 13) - 1);
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			//
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return null;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xE000)
+			{
+				case 0x0000: return romData[(0x00 << 13) | (address & 0x1FFF)];
+				case 0x2000: return romData[(0x01 << 13) | (address & 0x1FFF)];
+				case 0x4000: return romData[(pagingRegisters[2] << 13) | (address & 0x1FFF)];
+				case 0x6000: return romData[(pagingRegisters[3] << 13) | (address & 0x1FFF)];
+				case 0x8000: return romData[(pagingRegisters[0] << 13) | (address & 0x1FFF)];
+				case 0xA000: return romData[(pagingRegisters[1] << 13) | (address & 0x1FFF)];
+				default: throw new EmulationException(string.Format("Korean MSX 8k mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x0003)
+			{
+				pagingRegisters[address & 0x0003] = (byte)(value & bankMask);
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs.meta
new file mode 100644
index 0000000..fa5e9fc
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMSX8kMapperCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ec1ba102183679945bca01f524bb1868
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs
new file mode 100644
index 0000000..55204c4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	public class KoreanMapperCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		byte bankMask, pagingRegister;
+
+		public KoreanMapperCartridge(int romSize, int ramSize)
+		{
+			pagingRegister = 0x02;
+
+			romData = new byte[romSize];
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+
+			var romSizeRounded = 1;
+			while (romSizeRounded < romData.Length) romSizeRounded <<= 1;
+
+			bankMask = (byte)((romSizeRounded >> 14) - 1);
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			//
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return null;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xC000)
+			{
+				case 0x0000:
+					return romData[address & 0x3FFF];
+
+				case 0x4000:
+					return romData[(0x01 << 14) | (address & 0x3FFF)];
+
+				case 0x8000:
+					return romData[((pagingRegister << 14) | (address & 0x3FFF))];
+
+				default:
+					throw new EmulationException(string.Format("Korean mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			switch (address)
+			{
+				case 0xA000:
+					pagingRegister = (byte)(value & bankMask);
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs.meta
new file mode 100644
index 0000000..221eb0d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanMapperCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 510ae156eb8cf4e4fb7c5fa573062f07
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs
new file mode 100644
index 0000000..5b09e17
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs
@@ -0,0 +1,181 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	/* Mostly standard Sega mapper, but with bit-reversing functionality to flip sprites
+	 * 
+	 * Mapper writes: https://github.com/ocornut/meka/blob/0f1bf8f876a99cb23c440043d2aadfd683c5c812/meka/srcs/mappers.cpp#L571
+	 * Bit-reversing logic: https://stackoverflow.com/a/3590938 */
+
+	public class KoreanSpriteMapperCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		byte[] ramData;
+
+		[StateRequired]
+		readonly byte[] pagingRegisters;
+
+		[StateRequired]
+		byte romBankMask;
+		[StateRequired]
+		bool hasCartRam;
+
+		bool isRamEnabled { get { return IsBitSet(pagingRegisters[0], 3); } }
+		bool isRomWriteEnable { get { return IsBitSet(pagingRegisters[0], 7); } }
+		int ramBank { get { return ((pagingRegisters[0] >> 2) & 0x01); } }
+		int romBank0 { get { return pagingRegisters[1]; } }
+		int romBank1 { get { return pagingRegisters[2]; } }
+		int romBank2 { get { return pagingRegisters[3]; } }
+
+		[StateRequired]
+		bool isBitReverseBank1, isBitReverseBank2;
+
+		public KoreanSpriteMapperCartridge(int romSize, int ramSize)
+		{
+			pagingRegisters = new byte[0x04];
+			pagingRegisters[0] = 0x00;  /* Mapper control */
+			pagingRegisters[1] = 0x00;  /* Page 0 ROM bank */
+			pagingRegisters[2] = 0x01;  /* Page 1 ROM bank */
+			pagingRegisters[3] = 0x02;  /* Page 2 ROM bank */
+
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBankMask = 0xFF;
+			hasCartRam = false;
+
+			isBitReverseBank1 = isBitReverseBank2 = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+
+			var romSizeRounded = 1;
+			while (romSizeRounded < romData.Length) romSizeRounded <<= 1;
+
+			romBankMask = (byte)((romSizeRounded >> 14) - 1);
+
+			/* Ensure startup banks are within ROM size */
+			pagingRegisters[1] &= romBankMask;
+			pagingRegisters[2] &= romBankMask;
+			pagingRegisters[3] &= romBankMask;
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasCartRam;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xC000)
+			{
+				case 0x0000:
+					if (address < 0x400)
+						/* First 1kb is constant to preserve interrupt vectors */
+						return romData[address];
+					else
+						return romData[((romBank0 << 14) | (address & 0x3FFF))];
+
+				case 0x4000:
+					{
+						/* If requested, reverse bits before return */
+						var romAddress = ((romBank1 << 14) | (address & 0x3FFF));
+						if (!isBitReverseBank1)
+							return romData[romAddress];
+						else
+							return (byte)(((romData[romAddress] * 0x80200802ul) & 0x0884422110ul) * 0x0101010101ul >> 32);
+					}
+
+				case 0x8000:
+					if (isRamEnabled)
+						return ramData[((ramBank << 14) | (address & 0x3FFF))];
+					else
+					{
+						/* If requested, reverse bits before return */
+						var romAddress = ((romBank2 << 14) | (address & 0x3FFF));
+						if (!isBitReverseBank2)
+							return romData[romAddress];
+						else
+							return (byte)(((romData[romAddress] * 0x80200802ul) & 0x0884422110ul) * 0x0101010101ul >> 32);
+					}
+
+				default:
+					throw new EmulationException(string.Format("Korean sprite-flip mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0xFFFC && address <= 0xFFFF)
+			{
+				/* Check for bit-reverse flags */
+				if ((address & 0x0003) == 0x02)
+					isBitReverseBank1 = ((value & 0x40) == 0x40);
+				else if ((address & 0x0003) == 0x03)
+					isBitReverseBank2 = ((value & 0x40) == 0x40);
+
+				/* Write to paging register */
+				if ((address & 0x0003) != 0x00) value &= romBankMask;
+				pagingRegisters[address & 0x0003] = value;
+
+				/* Check if RAM ever gets enabled; if it is, indicate that we'll need to save the RAM */
+				if (!hasCartRam && isRamEnabled && (address & 0x0003) == 0x0000)
+					hasCartRam = true;
+			}
+			if (isRamEnabled && (address & 0xC000) == 0x8000)
+			{
+				/* Cartridge RAM */
+				ramData[((ramBank << 14) | (address & 0x3FFF))] = value;
+			}
+			else if (isRomWriteEnable)
+			{
+				/* ROM write enabled...? */
+			}
+
+			/* Otherwise ignore writes to ROM, as some games seem to be doing that? (ex. Gunstar Heroes GG to 0000) */
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs.meta
new file mode 100644
index 0000000..d27b63e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/KoreanSpriteMapperCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9ebf387a119ca924bac38d2244e917ec
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs
new file mode 100644
index 0000000..2ec46f5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs
@@ -0,0 +1,105 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	/* http://www.smspower.org/forums/post69724#69724 */
+
+	public class Multicart4PakAllActionCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		readonly int romMask;
+
+		[StateRequired]
+		int romBank0, romBank1, romBank2;
+
+		public Multicart4PakAllActionCartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+
+			romMask = 1;
+			while (romMask < romSize) romMask <<= 1;
+			romMask -= 1;
+
+			romBank0 = romBank1 = romBank2 = 0;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			//
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return null;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xC000)
+			{
+				case 0x0000:
+					return romData[((romBank0 << 14) | (address & 0x3FFF))];
+
+				case 0x4000:
+					return romData[((romBank1 << 14) | (address & 0x3FFF))];
+
+				case 0x8000:
+					return romData[((((romBank0 & 0x30) + romBank2) << 14) | (address & 0x3FFF))];
+
+				default:
+					throw new EmulationException(string.Format("4 Pak mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			// TODO: really just these addresses? no mirroring?
+			if (address == 0x3FFE)
+				romBank0 = value;
+			else if (address == 0x7FFF)
+				romBank1 = value;
+			else if (address == 0xBFFF)
+				romBank2 = value;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs.meta
new file mode 100644
index 0000000..4a3a9e8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/Multicart4PakAllActionCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 92b6b8db8e7eac6469ed11bfe02b5428
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs
new file mode 100644
index 0000000..40b4a19
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	public class SegaMapperCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		byte[] ramData;
+
+		[StateRequired]
+		readonly byte[] pagingRegisters;
+
+		[StateRequired]
+		byte romBankMask;
+		[StateRequired]
+		bool hasCartRam;
+
+		bool isRamEnabled { get { return IsBitSet(pagingRegisters[0], 3); } }
+		bool isRomWriteEnable { get { return IsBitSet(pagingRegisters[0], 7); } }
+		int ramBank { get { return ((pagingRegisters[0] >> 2) & 0x01); } }
+		int romBank0 { get { return pagingRegisters[1]; } }
+		int romBank1 { get { return pagingRegisters[2]; } }
+		int romBank2 { get { return pagingRegisters[3]; } }
+
+		public SegaMapperCartridge(int romSize, int ramSize)
+		{
+			pagingRegisters = new byte[0x04];
+			pagingRegisters[0] = 0x00;  /* Mapper control */
+			pagingRegisters[1] = 0x00;  /* Page 0 ROM bank */
+			pagingRegisters[2] = 0x01;  /* Page 1 ROM bank */
+			pagingRegisters[3] = 0x02;  /* Page 2 ROM bank */
+
+			romSize = Math.Max(romSize, 0xC000);
+
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romBankMask = 0xFF;
+			hasCartRam = false;
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+
+			var romSizeRounded = 1;
+			while (romSizeRounded < romData.Length) romSizeRounded <<= 1;
+
+			romBankMask = (byte)((romSizeRounded >> 14) - 1);
+
+			/* Ensure startup banks are within ROM size */
+			pagingRegisters[1] &= romBankMask;
+			pagingRegisters[2] &= romBankMask;
+			pagingRegisters[3] &= romBankMask;
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return hasCartRam;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return 0xBFFF;
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			switch (address & 0xC000)
+			{
+				case 0x0000:
+					if (address < 0x400)
+						/* First 1kb is constant to preserve interrupt vectors */
+						return romData[address];
+					else
+						return romData[((romBank0 << 14) | (address & 0x3FFF))];
+
+				case 0x4000:
+					return romData[((romBank1 << 14) | (address & 0x3FFF))];
+
+				case 0x8000:
+					if (isRamEnabled)
+						return ramData[((ramBank << 14) | (address & 0x3FFF))];
+					else
+						return romData[((romBank2 << 14) | (address & 0x3FFF))];
+
+				default:
+					throw new EmulationException(string.Format("Sega mapper: Cannot read from cartridge address 0x{0:X4}", address));
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (address >= 0xFFFC && address <= 0xFFFF)
+			{
+				/* Write to paging register */
+				if ((address & 0x0003) != 0x00) value &= romBankMask;
+				pagingRegisters[address & 0x0003] = value;
+
+				/* Check if RAM ever gets enabled; if it is, indicate that we'll need to save the RAM */
+				if (!hasCartRam && isRamEnabled && (address & 0x0003) == 0x0000)
+					hasCartRam = true;
+			}
+			if (isRamEnabled && (address & 0xC000) == 0x8000)
+			{
+				/* Cartridge RAM */
+				ramData[((ramBank << 14) | (address & 0x3FFF))] = value;
+			}
+			else if (isRomWriteEnable)
+			{
+				/* ROM write enabled...? */
+			}
+
+			/* Otherwise ignore writes to ROM, as some games seem to be doing that? (ex. Gunstar Heroes GG to 0000) */
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs.meta
new file mode 100644
index 0000000..51d6ef2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaMapperCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d86b15c129c010f4f911b9aba4e5670a
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs
new file mode 100644
index 0000000..4c3c742
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Cartridges.Sega
+{
+	public class SegaSGCartridge : ICartridge
+	{
+		byte[] romData;
+
+		[StateRequired]
+		byte[] ramData;
+
+		[StateRequired]
+		readonly int romMask, ramMask;
+
+		public SegaSGCartridge(int romSize, int ramSize)
+		{
+			romData = new byte[romSize];
+			ramData = new byte[ramSize];
+
+			romMask = 1;
+			while (romMask < romSize) romMask <<= 1;
+			romMask -= 1;
+
+			ramMask = (ramSize - 1);
+		}
+
+		public void LoadRom(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, romData, 0, Math.Min(data.Length, romData.Length));
+		}
+
+		public void LoadRam(byte[] data)
+		{
+			Buffer.BlockCopy(data, 0, ramData, 0, Math.Min(data.Length, ramData.Length));
+		}
+
+		public byte[] GetRomData()
+		{
+			return romData;
+		}
+
+		public byte[] GetRamData()
+		{
+			return ramData;
+		}
+
+		public bool IsRamSaveNeeded()
+		{
+			return false;
+		}
+
+		public ushort GetLowerBound()
+		{
+			return 0x0000;
+		}
+
+		public ushort GetUpperBound()
+		{
+			return (ushort)((romData.Length + ramData.Length) - 1);
+		}
+
+		public void Step(int clockCyclesInStep)
+		{
+			/* Nothing to do */
+		}
+
+		public byte Read(ushort address)
+		{
+			if (ramData.Length > 0)
+			{
+				if (address < (romMask + 1))
+					return romData[address & romMask];
+				else
+					return ramData[address & ramMask];
+			}
+			else
+			{
+				return romData[address & romMask];
+			}
+		}
+
+		public void Write(ushort address, byte value)
+		{
+			if (ramData.Length > 0 && address >= (romMask + 1))
+				ramData[address & ramMask] = value;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs.meta b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs.meta
new file mode 100644
index 0000000..cbd2155
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Cartridges/Sega/SegaSGCartridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a9ece9b21b502a2468df0428e956bbba
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration.meta b/Assets/Plugins/Essgee/Emulation/Configuration.meta
new file mode 100644
index 0000000..211444d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9359d9b379638da42a314af0f095fe0c
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs b/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs
new file mode 100644
index 0000000..16b9067
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs
@@ -0,0 +1,95 @@
+using Essgee.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Essgee.Emulation.Configuration
+{
+    ////todo Unity [ElementPriorityAttribute(4)]
+	public class ColecoVision : IConfiguration
+	{
+		[IsBootstrapRomPath]
+		//todo Unity [FileBrowserControl("General", "BIOS Path", "ColecoVision BIOS ROM (*.col;*.zip)|*.col;*.zip")]
+		public string BiosRom { get; set; }
+
+		//todo Unity [DropDownControl("Controls", "Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsUp { get; set; }
+		//todo Unity [DropDownControl("Controls", "Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsDown { get; set; }
+		//todo Unity [DropDownControl("Controls", "Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsLeft { get; set; }
+		//todo Unity [DropDownControl("Controls", "Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsRight { get; set; }
+		//todo Unity [DropDownControl("Controls", "Left Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsButtonLeft { get; set; }
+		//todo Unity [DropDownControl("Controls", "Right Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsButtonRight { get; set; }
+
+		//todo Unity [DropDownControl("Controls", "Keypad 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad1 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad2 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 3", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad3 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 4", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad4 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 5", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad5 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 6", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad6 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 7", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad7 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 8", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad8 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 9", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad9 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad 0", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypad0 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad *", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypadStar { get; set; }
+		//todo Unity [DropDownControl("Controls", "Keypad #", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsKeypadNumberSign { get; set; }
+
+		public ColecoVision()
+		{
+			BiosRom = string.Empty;
+
+			ControlsUp = Keys.Up;
+			ControlsDown = Keys.Down;
+			ControlsLeft = Keys.Left;
+			ControlsRight = Keys.Right;
+			ControlsButtonLeft = Keys.A;
+			ControlsButtonRight = Keys.S;
+
+			ControlsKeypad1 = Keys.NumPad1;
+			ControlsKeypad2 = Keys.NumPad2;
+			ControlsKeypad3 = Keys.NumPad3;
+			ControlsKeypad4 = Keys.NumPad4;
+			ControlsKeypad5 = Keys.NumPad5;
+			ControlsKeypad6 = Keys.NumPad6;
+			ControlsKeypad7 = Keys.NumPad7;
+			ControlsKeypad8 = Keys.NumPad8;
+			ControlsKeypad9 = Keys.NumPad9;
+			ControlsKeypad0 = Keys.NumPad0;
+			ControlsKeypadStar = Keys.Multiply;
+			ControlsKeypadNumberSign = Keys.Divide;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs.meta
new file mode 100644
index 0000000..ba580cf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/ColecoVision.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4ae1eb77c5770f141917f1d7cb917ca1
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs
new file mode 100644
index 0000000..596a2e3
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs
@@ -0,0 +1,72 @@
+using Essgee.Emulation.Cartridges.Nintendo;
+using Essgee.Emulation.ExtDevices.Nintendo;
+using Essgee.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using System;
+
+namespace Essgee.Emulation.Configuration
+{
+    //todo Unity [ElementPriority(5)]
+    public class GameBoy : IConfiguration
+	{
+		//todo Unity [CheckBoxControl("General", "Use Bootstrap ROM")]
+		public bool UseBootstrap { get; set; }
+		[IsBootstrapRomPath]
+		//todo Unity [FileBrowserControl("General", "Bootstrap Path", "Game Boy Bootstrap ROM (*.gb;*.bin;*.zip)|*.gb;*.bin;*.zip")]
+		public string BootstrapRom { get; set; }
+		//todo Unity [DropDownControl("General", "Serial Device", typeof(ISerialDevice))]
+		[JsonConverter(typeof(TypeNameJsonConverter), "Essgee.Emulation.ExtDevices.Nintendo")]
+		public Type SerialDevice { get; set; }
+
+		//todo Unity [DropDownControl("GB Camera", "Camera Source", typeof(GBCameraCartridge.ImageSources))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public GBCameraCartridge.ImageSources CameraSource { get; set; }
+		//todo Unity [FileBrowserControl("GB Camera", "Camera Image", "Image Files (*.png;*.bmp)|*.png;*.bmp")]
+		public string CameraImageFile { get; set; }
+
+		//todo Unity [DropDownControl("Controls", "Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsUp { get; set; }
+		//todo Unity [DropDownControl("Controls", "Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsDown { get; set; }
+		//todo Unity [DropDownControl("Controls", "Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsLeft { get; set; }
+		//todo Unity [DropDownControl("Controls", "Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsRight { get; set; }
+		//todo Unity [DropDownControl("Controls", "A", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsA { get; set; }
+		//todo Unity [DropDownControl("Controls", "B", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsB { get; set; }
+		//todo Unity [DropDownControl("Controls", "Select", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsSelect { get; set; }
+		//todo Unity [DropDownControl("Controls", "Start", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsStart { get; set; }
+
+		public GameBoy()
+		{
+			UseBootstrap = false;
+			BootstrapRom = string.Empty;
+
+			SerialDevice = typeof(DummyDevice);
+			CameraSource = GBCameraCartridge.ImageSources.Noise;
+			CameraImageFile = string.Empty;
+
+			ControlsUp = Keys.Up;
+			ControlsDown = Keys.Down;
+			ControlsLeft = Keys.Left;
+			ControlsRight = Keys.Right;
+			ControlsA = Keys.S;
+			ControlsB = Keys.A;
+			ControlsSelect = Keys.Space;
+			ControlsStart = Keys.Enter;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs.meta
new file mode 100644
index 0000000..cfee7f8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0f330bdde80f4154a97f9d8e34402812
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs
new file mode 100644
index 0000000..f0963ef
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs
@@ -0,0 +1,85 @@
+using Essgee.Emulation.Cartridges.Nintendo;
+using Essgee.Emulation.ExtDevices.Nintendo;
+using Essgee.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using System;
+
+namespace Essgee.Emulation.Configuration
+{
+    //todo Unity [ElementPriority(6)]
+	public class GameBoyColor : IConfiguration
+	{
+		//todo Unity [CheckBoxControl("General", "Use Bootstrap ROM")]
+		public bool UseBootstrap { get; set; }
+		[IsBootstrapRomPath]
+		//todo Unity [FileBrowserControl("General", "Bootstrap Path", "Game Boy Color Bootstrap ROM (*.gbc;*.bin;*.zip)|*.gbc;*.bin;*.zip")]
+		public string BootstrapRom { get; set; }
+		//todo Unity [DropDownControl("General", "Serial Device", typeof(ISerialDevice))]
+		[JsonConverter(typeof(TypeNameJsonConverter), "Essgee.Emulation.ExtDevices.Nintendo")]
+		public Type SerialDevice { get; set; }
+
+		//todo Unity [DropDownControl("GB Camera", "Camera Source", typeof(GBCameraCartridge.ImageSources))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public GBCameraCartridge.ImageSources CameraSource { get; set; }
+		//todo Unity [FileBrowserControl("GB Camera", "Camera Image", "Image Files (*.png;*.bmp)|*.png;*.bmp")]
+		public string CameraImageFile { get; set; }
+
+		//todo Unity [DropDownControl("Infrared", "Infrared Source", typeof(Machines.GameBoyColor.InfraredSources))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Machines.GameBoyColor.InfraredSources InfraredSource { get; set; }
+		//todo Unity [FileBrowserControl("Infrared", "Pokemon Pikachu DB", "Database Binary (*.bin)|*.bin")]
+		public string InfraredDatabasePikachu { get; set; }
+
+		//todo Unity [DropDownControl("Controls", "Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsUp { get; set; }
+		//todo Unity [DropDownControl("Controls", "Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsDown { get; set; }
+		//todo Unity [DropDownControl("Controls", "Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsLeft { get; set; }
+		//todo Unity [DropDownControl("Controls", "Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsRight { get; set; }
+		//todo Unity [DropDownControl("Controls", "A", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsA { get; set; }
+		//todo Unity [DropDownControl("Controls", "B", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsB { get; set; }
+		//todo Unity [DropDownControl("Controls", "Select", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsSelect { get; set; }
+		//todo Unity [DropDownControl("Controls", "Start", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsStart { get; set; }
+		//todo Unity [DropDownControl("Controls", "Send IR Signal", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsSendIR { get; set; }
+
+		public GameBoyColor()
+		{
+			UseBootstrap = false;
+			BootstrapRom = string.Empty;
+
+			SerialDevice = typeof(DummyDevice);
+			CameraSource = GBCameraCartridge.ImageSources.Noise;
+			CameraImageFile = string.Empty;
+
+			InfraredSource = Machines.GameBoyColor.InfraredSources.None;
+			InfraredDatabasePikachu = string.Empty;
+
+			ControlsUp = Keys.Up;
+			ControlsDown = Keys.Down;
+			ControlsLeft = Keys.Left;
+			ControlsRight = Keys.Right;
+			ControlsA = Keys.S;
+			ControlsB = Keys.A;
+			ControlsSelect = Keys.Space;
+			ControlsStart = Keys.Enter;
+			ControlsSendIR = Keys.Back;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs.meta
new file mode 100644
index 0000000..0af47d0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameBoyColor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4e72130cc49f0324fa58c96d9aaa4771
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs b/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs
new file mode 100644
index 0000000..e59e623
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs
@@ -0,0 +1,61 @@
+using Essgee.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Essgee.Emulation.Configuration
+{
+    //todo Unity [ElementPriority(3)]
+    public class GameGear : IConfiguration
+	{
+		//todo Unity [DropDownControl("General", "Region", typeof(Region))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Region Region { get; set; }
+
+		//todo Unity [CheckBoxControl("General", "Use Bootstrap ROM")]
+		public bool UseBootstrap { get; set; }
+		[IsBootstrapRomPath]
+		//todo Unity [FileBrowserControl("General", "Bootstrap Path", "Game Gear Bootstrap ROM (*.gg;*.zip)|*.gg;*.zip")]
+		public string BootstrapRom { get; set; }
+
+		//todo Unity [DropDownControl("Controls", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsUp { get; set; }
+		//todo Unity [DropDownControl("Controls", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsDown { get; set; }
+		//todo Unity [DropDownControl("Controls", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsLeft { get; set; }
+		//todo Unity [DropDownControl("Controls", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsRight { get; set; }
+		//todo Unity [DropDownControl("Controls", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsButton1 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsButton2 { get; set; }
+		//todo Unity [DropDownControl("Controls", "Start", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys ControlsStart { get; set; }
+
+		public bool AllowMemoryControl { get; set; }
+
+		public GameGear()
+		{
+			BootstrapRom = string.Empty;
+
+			Region = Region.Export;
+
+			ControlsUp = Keys.Up;
+			ControlsDown = Keys.Down;
+			ControlsLeft = Keys.Left;
+			ControlsRight = Keys.Right;
+			ControlsButton1 = Keys.A;
+			ControlsButton2 = Keys.S;
+			ControlsStart = Keys.Return;
+
+			AllowMemoryControl = true;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs.meta
new file mode 100644
index 0000000..c6c0a6a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/GameGear.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f8c8deb0a7eac484e8b82758c20c92cd
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs b/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs
new file mode 100644
index 0000000..0c21fc6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Configuration
+{
+	public interface IConfiguration { }
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs.meta
new file mode 100644
index 0000000..700b26a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/IConfiguration.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 12f32f85f1566d740bd12985ea4a9834
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs b/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs
new file mode 100644
index 0000000..a76468c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs
@@ -0,0 +1,109 @@
+using Essgee.Utilities;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Essgee.Emulation.Configuration
+{
+    ////todo Unity [ElementPriority(2)]
+	public class MasterSystem : IConfiguration
+	{
+		//todo Unity [DropDownControl("General", "TV Standard", typeof(TVStandard))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public TVStandard TVStandard { get; set; }
+		//todo Unity [DropDownControl("General", "Region", typeof(Region))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Region Region { get; set; }
+		//todo Unity [DropDownControl("General", "Model", typeof(VDPTypes), Tooltip = "Selects which type of VDP chip is emulated. This is used by some software to detect which console model it is running on.")]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public VDPTypes VDPType { get; set; }
+
+		//todo Unity [CheckBoxControl("General", "Use Bootstrap ROM")]
+		public bool UseBootstrap { get; set; }
+		[IsBootstrapRomPath]
+		//todo Unity [FileBrowserControl("General", "Bootstrap Path", "SMS Bootstrap ROM (*.sms;*.zip)|*.sms;*.zip")]
+		public string BootstrapRom { get; set; }
+
+		//todo Unity [DropDownControl("General", "Pause Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputPause { get; set; }
+		//todo Unity [DropDownControl("General", "Reset Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputReset { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 1", "Device Type", typeof(InputDevice))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public InputDevice Joypad1DeviceType { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button2 { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 2", "Device Type", typeof(InputDevice))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public InputDevice Joypad2DeviceType { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button2 { get; set; }
+
+		public bool AllowMemoryControl { get; set; }
+
+		public MasterSystem()
+		{
+			BootstrapRom = string.Empty;
+
+			TVStandard = TVStandard.NTSC;
+			Region = Region.Export;
+			VDPType = VDPTypes.SMS2GG;
+
+			InputPause = Keys.Space;
+			InputReset = Keys.Back;
+
+			Joypad1DeviceType = InputDevice.Controller;
+			Joypad1Up = Keys.Up;
+			Joypad1Down = Keys.Down;
+			Joypad1Left = Keys.Left;
+			Joypad1Right = Keys.Right;
+			Joypad1Button1 = Keys.A;
+			Joypad1Button2 = Keys.S;
+
+			Joypad2DeviceType = InputDevice.Controller;
+			Joypad2Up = Keys.NumPad8;
+			Joypad2Down = Keys.NumPad2;
+			Joypad2Left = Keys.NumPad4;
+			Joypad2Right = Keys.NumPad6;
+			Joypad2Button1 = Keys.NumPad1;
+			Joypad2Button2 = Keys.NumPad3;
+
+			AllowMemoryControl = true;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs.meta
new file mode 100644
index 0000000..5262134
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/MasterSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 28c56adc13b701a4580ddd74b71da8f6
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs b/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs
new file mode 100644
index 0000000..e8fdbc2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs
@@ -0,0 +1,86 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Essgee.Emulation.Configuration
+{
+    //todo Unity [ElementPriority(1)]
+    public class SC3000 : IConfiguration
+	{
+		//todo Unity [DropDownControl("General", "TV Standard", typeof(TVStandard))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public TVStandard TVStandard { get; set; }
+
+		//todo Unity [DropDownControl("General", "Reset Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputReset { get; set; }
+
+		//todo Unity [DropDownControl("General", "Change Input Mode", typeof(Keys), Keys.F11, Tooltip = "Selects which PC keyboard key is used to switch between SC-3000 keyboard and controller input.")]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputChangeMode { get; set; }
+
+		//todo Unity [DropDownControl("General", "Play Tape", typeof(Keys), Keys.F11, Tooltip = "Note that tape emulation is currently non-functional.")]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputPlayTape { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button2 { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button2 { get; set; }
+
+		public SC3000()
+		{
+			TVStandard = TVStandard.NTSC;
+
+			InputReset = Keys.F12;
+			InputChangeMode = Keys.F1;
+			InputPlayTape = Keys.F2;
+
+			Joypad1Up = Keys.Up;
+			Joypad1Down = Keys.Down;
+			Joypad1Left = Keys.Left;
+			Joypad1Right = Keys.Right;
+			Joypad1Button1 = Keys.A;
+			Joypad1Button2 = Keys.S;
+
+			Joypad2Up = Keys.NumPad8;
+			Joypad2Down = Keys.NumPad2;
+			Joypad2Left = Keys.NumPad4;
+			Joypad2Right = Keys.NumPad6;
+			Joypad2Button1 = Keys.NumPad1;
+			Joypad2Button2 = Keys.NumPad3;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs.meta
new file mode 100644
index 0000000..d6603af
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/SC3000.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 727a7522f4818ff44a2728f98cda2b66
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs b/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs
new file mode 100644
index 0000000..d729d4b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs
@@ -0,0 +1,76 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Essgee.Emulation.Configuration
+{
+    //todo Unity [ElementPriority(0)]
+    public class SG1000 : IConfiguration
+	{
+		//todo Unity [DropDownControl("General", "TV Standard", typeof(TVStandard))]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public TVStandard TVStandard { get; set; }
+
+		//todo Unity [DropDownControl("General", "Pause Button", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys InputPause { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 1", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad1Button2 { get; set; }
+
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Up", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Up { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Down", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Down { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Left", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Left { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "D-Pad Right", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Right { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 1", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button1 { get; set; }
+		//todo Unity [DropDownControl("Controller Port 2", "Button 2", typeof(Keys), Keys.F11)]
+		[JsonConverter(typeof(StringEnumConverter))]
+		public Keys Joypad2Button2 { get; set; }
+
+		public SG1000()
+		{
+			TVStandard = TVStandard.NTSC;
+
+			InputPause = Keys.Space;
+
+			Joypad1Up = Keys.Up;
+			Joypad1Down = Keys.Down;
+			Joypad1Left = Keys.Left;
+			Joypad1Right = Keys.Right;
+			Joypad1Button1 = Keys.A;
+			Joypad1Button2 = Keys.S;
+
+			Joypad2Up = Keys.NumPad8;
+			Joypad2Down = Keys.NumPad2;
+			Joypad2Left = Keys.NumPad4;
+			Joypad2Right = Keys.NumPad6;
+			Joypad2Button1 = Keys.NumPad1;
+			Joypad2Button2 = Keys.NumPad3;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs.meta b/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs.meta
new file mode 100644
index 0000000..75083d0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Configuration/SG1000.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ff8803776ef43a6489bbe3909015e4af
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs b/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs
new file mode 100644
index 0000000..f8d3d86
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs
@@ -0,0 +1,339 @@
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.Machines;
+using Essgee.EventArguments;
+using Essgee.Metadata;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+
+namespace Essgee.Emulation
+{
+    public class EmulatorHandler
+	{
+		readonly Action<Exception> exceptionHandler;
+
+		IMachine emulator;
+
+		Thread emulationThread;
+		volatile bool emulationThreadRunning;
+
+		volatile bool limitFps;
+		volatile bool emulationThreadPaused;
+
+		volatile bool configChangeRequested = false;
+		volatile IConfiguration newConfiguration = null;
+
+		volatile bool stateLoadRequested = false, stateSaveRequested = false;
+		volatile int stateNumber = -1;
+
+		volatile Queue<bool> pauseStateChangesRequested = new Queue<bool>();
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage
+		{
+			add { emulator.SendLogMessage += value; }
+			remove { emulator.SendLogMessage -= value; }
+		}
+
+		public event EventHandler<EventArgs> EmulationReset
+		{
+			add { emulator.EmulationReset += value; }
+			remove { emulator.EmulationReset -= value; }
+		}
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { emulator.RenderScreen += value; }
+			remove { emulator.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { emulator.SizeScreen += value; }
+			remove { emulator.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport
+		{
+			add { emulator.ChangeViewport += value; }
+			remove { emulator.ChangeViewport -= value; }
+		}
+
+		public event EventHandler<PollInputEventArgs> PollInput
+		{
+			add { emulator.PollInput += value; }
+			remove { emulator.PollInput -= value; }
+		}
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { emulator.EnqueueSamples += value; }
+			remove { emulator.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData
+		{
+			add { emulator.SaveExtraData += value; }
+			remove { emulator.SaveExtraData -= value; }
+		}
+
+		public event EventHandler<EventArgs> EnableRumble
+		{
+			add { emulator.EnableRumble += value; }
+			remove { emulator.EnableRumble -= value; }
+		}
+
+		public event EventHandler<EventArgs> PauseChanged;
+
+		GameMetadata currentGameMetadata;
+
+		public bool IsCartridgeLoaded { get; private set; }
+
+		public bool IsRunning => emulationThreadRunning;
+		public bool IsPaused => emulationThreadPaused;
+
+		public bool IsHandlingSaveState => (stateLoadRequested || stateSaveRequested);
+
+		public (string Manufacturer, string Model, string DatFileName, double RefreshRate, double PixelAspectRatio, (string Name, string Description)[] RuntimeOptions) Information =>
+			(emulator.ManufacturerName, emulator.ModelName, emulator.DatFilename, emulator.RefreshRate, emulator.PixelAspectRatio, emulator.RuntimeOptions);
+
+		public EmulatorHandler(Type type, Action<Exception> exceptionHandler = null)
+		{
+			this.exceptionHandler = exceptionHandler;
+
+			emulator = (IMachine)Activator.CreateInstance(type);
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			if (emulationThreadRunning)
+			{
+				configChangeRequested = true;
+				newConfiguration = config;
+			}
+			else
+				emulator.SetConfiguration(config);
+		}
+
+		public void Initialize()
+		{
+			emulator.Initialize();
+		}
+
+		public void Startup()
+		{
+			emulationThreadRunning = true;
+			emulationThreadPaused = false;
+
+			emulator.Startup();
+			emulator.Reset();
+
+			emulationThread = new Thread(ThreadMainLoop) { Name = "EssgeeEmulation", Priority = ThreadPriority.Normal };
+			emulationThread.Start();
+		}
+
+		public void Reset()
+		{
+			emulator.Reset();
+		}
+
+		public void Shutdown()
+		{
+			emulationThreadRunning = false;
+
+			emulationThread?.Join();
+
+			emulator.Shutdown();
+		}
+
+		public void Pause(bool pauseState)
+		{
+			pauseStateChangesRequested.Enqueue(pauseState);
+		}
+
+		public string GetSaveStateFilename(int number)
+		{
+			return Path.Combine(StandInfo.SaveStatePath, $"{Path.GetFileNameWithoutExtension(currentGameMetadata.FileName)} (State {number:D2}).est");
+		}
+
+		public void LoadState(int number)
+		{
+			stateLoadRequested = true;
+			stateNumber = number;
+		}
+
+		public void SaveState(int number)
+		{
+			stateSaveRequested = true;
+			stateNumber = number;
+		}
+
+		public void LoadCartridge(byte[] romData, GameMetadata gameMetadata)
+		{
+			currentGameMetadata = gameMetadata;
+
+			byte[] ramData = new byte[currentGameMetadata.RamSize];
+
+			var savePath = Path.Combine(StandInfo.SaveDataPath, Path.ChangeExtension(currentGameMetadata.FileName, "sav"));
+			if (File.Exists(savePath))
+				ramData = File.ReadAllBytes(savePath);
+
+			emulator.Load(romData, ramData, currentGameMetadata.MapperType);
+
+			IsCartridgeLoaded = true;
+		}
+
+		public void SaveCartridge()
+		{
+			if (currentGameMetadata == null) return;
+
+			var cartRamSaveNeeded = emulator.IsCartridgeRamSaveNeeded();
+			if ((cartRamSaveNeeded && currentGameMetadata.MapperType != null && currentGameMetadata.HasNonVolatileRam) ||
+				cartRamSaveNeeded)
+			{
+				var ramData = emulator.GetCartridgeRam();
+				var savePath = Path.Combine(StandInfo.SaveDataPath, Path.ChangeExtension(currentGameMetadata.FileName, "sav"));
+				File.WriteAllBytes(savePath, ramData);
+			}
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			return emulator.GetDebugInformation();
+		}
+
+		public Type GetMachineType()
+		{
+			return emulator.GetType();
+		}
+
+		public void SetFpsLimiting(bool value)
+		{
+			limitFps = value;
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			return emulator.GetRuntimeOption(name);
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			emulator.SetRuntimeOption(name, value);
+		}
+
+		public int FramesPerSecond { get; private set; }
+
+		private void ThreadMainLoop()
+		{
+			// TODO: rework fps limiter/counter - AGAIN - because the counter is inaccurate at sampleTimespan=0.25 and the limiter CAN cause sound crackling at sampleTimespan>0.25
+			// try this maybe? https://stackoverflow.com/a/34839411
+
+			var stopWatch = Stopwatch.StartNew();
+
+			TimeSpan accumulatedTime = TimeSpan.Zero, lastStartTime = TimeSpan.Zero, lastEndTime = TimeSpan.Zero;
+
+			var frameCounter = 0;
+			var sampleTimespan = TimeSpan.FromSeconds(0.5);
+
+			try
+			{
+				while (true)
+				{
+					if (!emulationThreadRunning)
+						break;
+
+					if (stateLoadRequested && stateNumber != -1)
+					{
+						var statePath = GetSaveStateFilename(stateNumber);
+						if (File.Exists(statePath))
+						{
+							using (var stream = new FileStream(statePath, FileMode.Open))
+							{
+								emulator.SetState(SaveStateHandler.Load(stream, emulator.GetType().Name));
+							}
+						}
+
+						stateLoadRequested = false;
+						stateNumber = -1;
+					}
+
+					var refreshRate = emulator.RefreshRate;
+					var targetElapsedTime = TimeSpan.FromTicks((long)Math.Round(TimeSpan.TicksPerSecond / refreshRate));
+
+					var startTime = stopWatch.Elapsed;
+
+					while (pauseStateChangesRequested.Count > 0)
+					{
+						var newPauseState = pauseStateChangesRequested.Dequeue();
+						emulationThreadPaused = newPauseState;
+
+						PauseChanged?.Invoke(this, EventArgs.Empty);
+					}
+
+					if (!emulationThreadPaused)
+					{
+						if (limitFps)
+						{
+							var elapsedTime = (startTime - lastStartTime);
+							lastStartTime = startTime;
+
+							if (elapsedTime < targetElapsedTime)
+							{
+								accumulatedTime += elapsedTime;
+
+								while (accumulatedTime >= targetElapsedTime)
+								{
+									emulator.RunFrame();
+									frameCounter++;
+
+									accumulatedTime -= targetElapsedTime;
+								}
+							}
+						}
+						else
+						{
+							emulator.RunFrame();
+							frameCounter++;
+						}
+
+						if ((stopWatch.Elapsed - lastEndTime) >= sampleTimespan)
+						{
+							FramesPerSecond = (int)((frameCounter * 1000.0) / sampleTimespan.TotalMilliseconds);
+							frameCounter = 0;
+							lastEndTime = stopWatch.Elapsed;
+						}
+					}
+					else
+					{
+						lastEndTime = stopWatch.Elapsed;
+					}
+
+					if (configChangeRequested)
+					{
+						emulator.SetConfiguration(newConfiguration);
+						configChangeRequested = false;
+					}
+
+					if (stateSaveRequested && stateNumber != -1)
+					{
+						var statePath = GetSaveStateFilename(stateNumber);
+						using (var stream = new FileStream(statePath, FileMode.OpenOrCreate))
+						{
+							SaveStateHandler.Save(stream, emulator.GetType().Name, emulator.GetState());
+						}
+
+						stateSaveRequested = false;
+						stateNumber = -1;
+					}
+				}
+			}
+			catch (Exception ex) when (!AppEnvironment.DebugMode)
+			{
+				ex.Data.Add("Thread", Thread.CurrentThread.Name);
+				exceptionHandler(ex);
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs.meta b/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs.meta
new file mode 100644
index 0000000..21be897
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/EmulatorHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2a3dbee1ff1b11c4f82f85a480959bee
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Enumerations.cs b/Assets/Plugins/Essgee/Emulation/Enumerations.cs
new file mode 100644
index 0000000..4adc8fa
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Enumerations.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+
+namespace Essgee.Emulation
+{
+	// TODO change all these b/c gameboy is a thing now
+
+	public enum TVStandard
+	{
+		//todo unity [ValueIgnored(true)]
+		Auto = -1,
+		[Description("NTSC (60 Hz)")]
+		NTSC = 0,
+		[Description("PAL (50 Hz)")]
+		PAL
+	}
+	public enum Region
+	{
+		//todo unity [ValueIgnored(true)]
+		Auto = -1,
+		[Description("Domestic (Japan)")]
+		Domestic = 0,
+		[Description("Export")]
+		Export
+	}
+
+	public enum InputDevice
+	{
+		[Description("None")]
+		None = 0,
+		[Description("Standard Controller")]
+		Controller,
+		[Description("Light Phaser")]
+		Lightgun
+	}
+
+	public enum VDPTypes
+	{
+		[Description("Mark III / Master System")]
+		Mk3SMS1 = 0,
+		[Description("Master System II / Game Gear")]
+		SMS2GG = 1
+	}
+
+	public enum InterruptType
+	{
+		Maskable,
+		NonMaskable
+	}
+
+	public enum InterruptState
+	{
+		Clear,
+		Assert
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Enumerations.cs.meta b/Assets/Plugins/Essgee/Emulation/Enumerations.cs.meta
new file mode 100644
index 0000000..b057d8b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Enumerations.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3dcbfdb676ca9d943a2c4e7049a71487
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices.meta
new file mode 100644
index 0000000..58e1490
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 32a6b746a89ac5242be599fa472bab1b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo.meta
new file mode 100644
index 0000000..a256439
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a09bb4257e29ab646b70aba3720660e1
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs
new file mode 100644
index 0000000..0704c75
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.ExtDevices.Nintendo
+{
+	[Description("None")]
+	//todo Unity [ElementPriority(0)]
+	public class DummyDevice : ISerialDevice
+	{
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public void Initialize() { }
+		public void Shutdown() { }
+		public byte ExchangeBit(int left, byte data) { return 0b1; }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs.meta
new file mode 100644
index 0000000..1c152dc
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/DummyDevice.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0ac549cbc5bf99c48933ba2369c6838e
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs
new file mode 100644
index 0000000..6dac4db
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs
@@ -0,0 +1,428 @@
+using Essgee.EventArguments;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Linq;
+
+namespace Essgee.Emulation.ExtDevices.Nintendo
+{
+    [Description("Game Boy Printer")]
+	//todo Unity [ElementPriority(2)]
+	public class GBPrinter : ISerialDevice
+	{
+		readonly Color[] defaultPalette = new Color[]
+		{
+			Color.FromArgb(0xF8, 0xF8, 0xF8),
+			Color.FromArgb(0x9B, 0x9B, 0x9B),
+			Color.FromArgb(0x3E, 0x3E, 0x3E),
+			Color.FromArgb(0x1F, 0x1F, 0x1F)
+		};
+
+		enum PrinterPacketBytes
+		{
+			MagicLSB,
+			MagicMSB,
+			CommandByte,
+			CompressionFlag,
+			DataLengthLSB,
+			DataLengthMSB,
+			DataByte,
+			ChecksumLSB,
+			ChecksumMSB,
+			Execute
+		}
+
+		enum PrinterCommands : byte
+		{
+			Initialize = 0x01,
+			StartPrinting = 0x02,
+			Unknown = 0x03,
+			ImageTransfer = 0x04,
+			ReadStatus = 0x0F
+		}
+
+		[Flags]
+		enum PrinterPresenceBits : byte
+		{
+			Unknown = (1 << 0),
+			Present = (1 << 7),
+		}
+
+		[Flags]
+		enum PrinterStatusBits : byte
+		{
+			BadChecksum = (1 << 0),
+			PrintInProgress = (1 << 1),
+			PrintRequested = (1 << 2),
+			ReadyToPrint = (1 << 3),
+			LowVoltage = (1 << 4),
+			Unknown = (1 << 5),
+			PaperJam = (1 << 6),
+			ThermalProblem = (1 << 7)
+		};
+
+		(ushort magic, PrinterCommands command, bool isCompressed, ushort dataLen, byte[] data, ushort checksum) packet;
+		PrinterPacketBytes nextPacketByte;
+		int dataBytesLeft;
+
+		PrinterStatusBits status;
+		PrinterPresenceBits presence;
+
+		List<byte> imageData;
+		byte marginBefore, marginAfter, palette, exposure;
+
+		int imageHeight;
+		int printDelay;
+
+		byte serialData;
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public GBPrinter()
+		{
+			imageData = new List<byte>();
+		}
+
+		public void Initialize()
+		{
+			ResetPacket();
+
+			nextPacketByte = PrinterPacketBytes.MagicLSB;
+			dataBytesLeft = 0;
+
+			status = 0;
+			presence = 0;
+
+			marginBefore = marginAfter = 0;
+			palette = exposure = 0;
+
+			imageHeight = 0;
+			printDelay = 0;
+
+			serialData = 0;
+		}
+
+		public void Shutdown()
+		{
+			//
+		}
+
+		private void ResetPacket()
+		{
+			packet = (0, 0, false, 0, new byte[0], 0);
+		}
+
+		public byte ExchangeBit(int left, byte data)
+		{
+			var bitToSend = (byte)((serialData >> 7) & 0b1);
+			serialData = (byte)((serialData << 1) | (data & 0b1));
+			if (left == 0) serialData = ProcessReceivedByte(serialData);
+			return bitToSend;
+		}
+
+		public byte ProcessReceivedByte(byte data)
+		{
+			byte ret = 0;
+
+			switch (nextPacketByte)
+			{
+				case PrinterPacketBytes.MagicLSB:
+					/* Received: First magic byte
+					 * Action:   Reset packet
+					 * Send:     Nothing
+					 */
+					if (data == 0x88)
+					{
+						ResetPacket();
+						packet.magic |= (ushort)(data << 8);
+						nextPacketByte = PrinterPacketBytes.MagicMSB;
+					}
+					break;
+
+				case PrinterPacketBytes.MagicMSB:
+					/* Received: Second magic byte
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					if (data == 0x33)
+					{
+						packet.magic |= data;
+						nextPacketByte = PrinterPacketBytes.CommandByte;
+					}
+					break;
+
+				case PrinterPacketBytes.CommandByte:
+					/* Received: Command byte
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					packet.command = (PrinterCommands)data;
+					nextPacketByte = PrinterPacketBytes.CompressionFlag;
+					break;
+
+				case PrinterPacketBytes.CompressionFlag:
+					/* Received: Compression flag
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					packet.isCompressed = (data & 0x01) != 0;
+					nextPacketByte = PrinterPacketBytes.DataLengthLSB;
+					break;
+
+				case PrinterPacketBytes.DataLengthLSB:
+					/* Received: Data length LSB
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					packet.dataLen |= data;
+					nextPacketByte = PrinterPacketBytes.DataLengthMSB;
+					break;
+
+				case PrinterPacketBytes.DataLengthMSB:
+					/* Received: Data length MSB
+					 * Action:   Prepare to receive data
+					 * Send:     Nothing
+					 */
+					packet.dataLen |= (ushort)(data << 8);
+					packet.data = new byte[packet.dataLen];
+					dataBytesLeft = packet.dataLen;
+					if (dataBytesLeft > 0)
+						nextPacketByte = PrinterPacketBytes.DataByte;
+					else
+						nextPacketByte = PrinterPacketBytes.ChecksumLSB;
+					break;
+
+				case PrinterPacketBytes.DataByte:
+					/* Received: Data byte
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					if (dataBytesLeft > 0)
+					{
+						packet.data[--dataBytesLeft] = data;
+						if (dataBytesLeft == 0)
+							nextPacketByte = PrinterPacketBytes.ChecksumLSB;
+					}
+					break;
+
+				case PrinterPacketBytes.ChecksumLSB:
+					/* Received: Checksum LSB
+					 * Action:   Nothing
+					 * Send:     Nothing
+					 */
+					packet.checksum |= data;
+					nextPacketByte = PrinterPacketBytes.ChecksumMSB;
+					break;
+
+				case PrinterPacketBytes.ChecksumMSB:
+					/* Received: Checksum MSB
+					 * Action:   Nothing
+					 * Send:     Printer presence
+					 */
+					packet.checksum |= (ushort)(data << 8);
+					presence = PrinterPresenceBits.Present | PrinterPresenceBits.Unknown;
+					ret = (byte)presence;
+					nextPacketByte = PrinterPacketBytes.Execute;
+					break;
+
+				case PrinterPacketBytes.Execute:
+					/* Received: Execute command
+					 * Action:   Nothing
+					 * Send:     Printer status
+					 */
+
+					/* First, we're done with the packet, so check what we need to do now */
+					packet.data = packet.data.Reverse().ToArray();
+					switch (packet.command)
+					{
+						case PrinterCommands.Initialize:
+							/* Reset some data */
+							status = 0;
+							imageData.Clear();
+							imageHeight = 0;
+							printDelay = 0;
+							break;
+
+						case PrinterCommands.ImageTransfer:
+							/* Copy packet data for drawing, increase image height & tell GB we're ready to print */
+							if (packet.data.Length > 0)
+							{
+								if (packet.isCompressed)
+								{
+									/* Decompress RLE first! */
+									List<byte> decomp = new List<byte>();
+									int ofs = 0, numbytes = 0;
+									while (ofs < packet.dataLen)
+									{
+										if ((packet.data[ofs] & 0x80) != 0)
+										{
+											/* Compressed */
+											numbytes = (packet.data[ofs] & 0x7F) + 2;
+											for (int i = 0; i < numbytes; i++) decomp.Add(packet.data[ofs + 1]);
+											ofs += 2;
+										}
+										else
+										{
+											/* Uncompressed */
+											numbytes = (packet.data[ofs] & 0x7F) + 1;
+											for (int i = 0; i < numbytes; i++) decomp.Add(packet.data[ofs + 1 + i]);
+											ofs += (numbytes + 1);
+										}
+									}
+									packet.data = decomp.ToArray();
+									packet.dataLen = (ushort)decomp.Count;
+								}
+
+								imageData.AddRange(packet.data);
+								imageHeight += (packet.data.Length / 0x28);
+
+								status |= PrinterStatusBits.ReadyToPrint;
+							}
+							break;
+
+						case PrinterCommands.StartPrinting:
+							/* Fetch parameters from packet, tell GB that we're about to print & perform printing */
+							marginBefore = (byte)((packet.data[1] >> 4) & 0x0F);
+							marginAfter = (byte)(packet.data[1] & 0x0F);
+							palette = packet.data[2];
+							exposure = (byte)(packet.data[3] & 0x7F);
+
+							status &= ~PrinterStatusBits.ReadyToPrint;
+							status |= PrinterStatusBits.PrintRequested;
+							PerformPrint();
+							break;
+
+						case PrinterCommands.ReadStatus:
+							if ((status & PrinterStatusBits.PrintRequested) != 0)
+							{
+								/* If we said printing has been requested, tell the GB it's in progress now */
+								status &= ~PrinterStatusBits.PrintRequested;
+								status |= PrinterStatusBits.PrintInProgress;
+							}
+							else if ((status & PrinterStatusBits.PrintInProgress) != 0)
+							{
+								/* Delay the process a bit... */
+								printDelay++;
+								if (printDelay >= 16)   // TODO: figure out actual print duration/timing?
+								{
+									/* If we said printing is in progress, tell the GB we're finished with it */
+									status &= ~PrinterStatusBits.PrintInProgress;
+									printDelay = 0;
+								}
+							}
+							break;
+					}
+
+					/* End of packet */
+					DumpPacket();
+
+					ret = (byte)status;
+
+					nextPacketByte = PrinterPacketBytes.MagicLSB;
+					break;
+			}
+
+			return ret;
+		}
+
+		public byte DoMasterTransfer(byte data)
+		{
+			/* Not used */
+			return 0xFF;
+		}
+
+		private void PerformPrint()
+		{
+			if (imageHeight == 0) return;
+
+			//TODO 需要实现
+
+
+
+			///* Create bitmap for "printing" */
+			//using (var image = new Bitmap(160, imageHeight))
+			//{
+			//	/* Convert image tiles to pixels */
+			//	for (var y = 0; y < image.Height; y += 8)
+			//	{
+			//		for (var x = 0; x < image.Width; x += 8)
+			//		{
+			//			var tileAddress = ((y / 8) * 0x140) + ((x / 8) * 0x10);
+
+			//			for (var py = 0; py < 8; py++)
+			//			{
+			//				for (var px = 0; px < 8; px++)
+			//				{
+			//					var ba = (imageData[tileAddress + 0] >> (7 - (px % 8))) & 0b1;
+			//					var bb = (imageData[tileAddress + 1] >> (7 - (px % 8))) & 0b1;
+			//					var c = (byte)((bb << 1) | ba);
+			//					image.SetPixel(x + px, y + py, defaultPalette[(byte)((palette >> (c << 1)) & 0x03)]);
+			//				}
+
+			//				tileAddress += 2;
+			//			}
+			//		}
+			//	}
+
+			//	/* Apply approximate exposure (i.e. mess with the brightness a bit) */
+			//	using (var adjustedImage = new Bitmap(image.Width, image.Height))
+			//	{
+			//		using (var g = System.Drawing.Graphics.FromImage(adjustedImage))
+			//		{
+			//			var scale = ((128 - exposure) / 128.0f) + 0.5f;
+			//			var matrix = new float[][]
+			//			{
+			//				new float[] { scale, 0.0f, 0.0f, 0.0f, 0.0f },
+			//				new float[] { 0.0f, scale, 0.0f, 0.0f, 0.0f },
+			//				new float[] { 0.0f, 0.0f, scale, 0.0f, 0.0f },
+			//				new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
+			//				new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
+			//			};
+
+			//			var imageAttribs = new ImageAttributes();
+			//			imageAttribs.ClearColorMatrix();
+			//			imageAttribs.SetColorMatrix(new ColorMatrix(matrix), ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
+			//			g.DrawImage(image, new Rectangle(0, 0, adjustedImage.Width, adjustedImage.Height), 0, 0, image.Width, image.Height, GraphicsUnit.Pixel, imageAttribs);
+
+			//			/* Save the image */
+			//			OnSaveExtraData(new SaveExtraDataEventArgs(ExtraDataTypes.Image, ExtraDataOptions.IncludeDateTime, "Printout", adjustedImage));
+			//		}
+			//	}
+			//}
+
+
+		}
+
+		private void DumpPacket()
+		{
+			if (AppEnvironment.EnableLogger)
+			{
+                EssgeeLogger.WriteLine("[Received GB Printer Packet]");
+				EssgeeLogger.WriteLine("- Magic bytes: 0x" + packet.magic.ToString("X4"));
+				EssgeeLogger.WriteLine("- Command: " + packet.command.ToString());
+				EssgeeLogger.WriteLine("- Is data compressed? " + packet.isCompressed.ToString());
+				EssgeeLogger.WriteLine("- Data length: 0x" + packet.dataLen.ToString("X4"));
+				if (packet.dataLen != 0)
+				{
+					EssgeeLogger.WriteLine("- Data (UNCOMPRESSED):");
+					for (int line = 0; line < ((packet.dataLen / 16) == 0 ? 1 : (packet.dataLen / 16)); line++)
+					{
+						string msg = "";
+						msg+=(" - 0x" + (line * 16).ToString("X4") + ": ");
+						for (int byteno = 0; byteno < ((packet.dataLen % 16) == 0 ? 0x10 : (packet.dataLen % 16)); byteno++)
+                            msg += (packet.data[(line * 16) + byteno].ToString("X2") + " ");
+						EssgeeLogger.WriteLine(msg);
+                        EssgeeLogger.WriteLine();
+                    }
+				}
+				EssgeeLogger.WriteLine("- Checksum: 0x" + packet.checksum.ToString("X4"));
+				EssgeeLogger.WriteLine("[Status Returned]");
+				EssgeeLogger.WriteLine("- Presence: " + presence.ToString());
+				EssgeeLogger.WriteLine("- Status: " + status.ToString());
+				EssgeeLogger.WriteLine();
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs.meta
new file mode 100644
index 0000000..6770122
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GBPrinter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 46eed2246cd4ff74792eeedfbe26b3e6
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs
new file mode 100644
index 0000000..56352de
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.ExtDevices.Nintendo
+{
+	[Description("Game Boy (Link Cable)")]
+	//todo Unity [ElementPriority(1)]
+	public class GameBoyIPC : ISerialDevice
+	{
+		// TODO: ensure correct mmf/accessor disposal?
+
+		const string ipcName = "EssgeeGBLink";
+		const int ipcLength = 16;
+
+		const int ipcBaseOffsetSerialData = 0;
+		//
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		MemoryMappedFile mmf;
+		MemoryMappedViewAccessor accessor;
+
+		bool ipcConnectionExists;
+		int ipcOffsetSelf, ipcOffsetRemote;
+
+		public GameBoyIPC()
+		{
+			mmf = null;
+			accessor = null;
+
+			ipcConnectionExists = false;
+			ipcOffsetSelf = ipcOffsetRemote = 0;
+		}
+
+		public void Initialize()
+		{
+			//
+		}
+
+		public void Shutdown()
+		{
+			if (ipcConnectionExists)
+			{
+				accessor.Flush();
+				accessor.Dispose();
+				mmf.Dispose();
+
+				ipcConnectionExists = false;
+			}
+		}
+
+		private void EstablishIPCConnection()
+		{
+			if (ipcConnectionExists) return;
+
+			try
+			{
+				// Try to open existing mapped file; if it exists, assume other instance is first machine
+				mmf = MemoryMappedFile.OpenExisting(ipcName);
+				ipcOffsetSelf = ipcBaseOffsetSerialData + 1;
+				ipcOffsetRemote = ipcBaseOffsetSerialData + 0;
+			}
+			catch (FileNotFoundException)
+			{
+				// Mapped file does not yet exist, create file and assume this instance is first machine
+				mmf = MemoryMappedFile.CreateOrOpen(ipcName, ipcLength);
+				ipcOffsetSelf = ipcBaseOffsetSerialData + 0;
+				ipcOffsetRemote = ipcBaseOffsetSerialData + 1;
+			}
+			accessor = mmf.CreateViewAccessor(0, ipcLength, MemoryMappedFileAccess.ReadWrite);
+
+			ipcConnectionExists = true;
+		}
+
+		public byte ExchangeBit(int left, byte data)
+		{
+			if (!ipcConnectionExists) EstablishIPCConnection();
+
+			accessor.Write(ipcOffsetSelf, data);
+			return accessor.ReadByte(ipcOffsetRemote);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs.meta
new file mode 100644
index 0000000..66382cf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/GameBoyIPC.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ffe92e23b6dc04e4a9827a21262ccfe3
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs
new file mode 100644
index 0000000..a95a9c2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.ExtDevices.Nintendo
+{
+	public interface ISerialDevice
+	{
+		event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+
+		void Initialize();
+		void Shutdown();
+
+		byte ExchangeBit(int left, byte data);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs.meta b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs.meta
new file mode 100644
index 0000000..6f7fce5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/ExtDevices/Nintendo/ISerialDevice.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b2db0a79b5ac7344990fd1e3711a8a4a
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines.meta b/Assets/Plugins/Essgee/Emulation/Machines.meta
new file mode 100644
index 0000000..70899f1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e77364449233a0e4b8f13621ff8ee825
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs b/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs
new file mode 100644
index 0000000..6f4d42e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs
@@ -0,0 +1,443 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges;
+using Essgee.Emulation.Cartridges.Coleco;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	[MachineIndex(4)]
+	public class ColecoVision : IMachine
+	{
+		// TODO: accuracy, bugfixes, PAL machines??
+
+		const double masterClock = 10738635;
+		const double refreshRate = 59.922743;
+
+		const int ramSize = 1 * 1024;
+
+		double vdpClock, psgClock;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { vdp.RenderScreen += value; }
+			remove { vdp.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { vdp.SizeScreen += value; }
+			remove { vdp.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { psg.EnqueueSamples += value; }
+			remove { psg.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
+
+		public string ManufacturerName => "Coleco";
+		public string ModelName => "ColecoVision";
+		public string DatFilename => "Coleco - ColecoVision.dat";
+		public (string Extension, string Description) FileFilter => (".col", "ColecoVision ROMs");
+		public bool HasBootstrap => true;
+		public double RefreshRate => refreshRate;
+		public double PixelAspectRatio => 8.0 / 7.0;
+		public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
+
+		ICartridge bios, cartridge;
+		byte[] wram;
+		Z80A cpu;
+		TMS99xxA vdp;
+		SN76489 psg;
+
+		[Flags]
+		enum KeyJoyButtons : ushort
+		{
+			None = 0x0000,
+			KeyNumber6 = 0x0001,
+			KeyNumber1 = 0x0002,
+			KeyNumber3 = 0x0003,
+			KeyNumber9 = 0x0004,
+			KeyNumber0 = 0x0005,
+			KeyStarKey = 0x0006,
+			KeyInvalid8 = 0x0007,
+			KeyNumber2 = 0x0008,
+			KeyNumberSignKey = 0x0009,
+			KeyNumber7 = 0x000A,
+			KeyInvalid4 = 0x000B,
+			KeyNumber5 = 0x000C,
+			KeyNumber4 = 0x000D,
+			KeyNumber8 = 0x000E,
+			KeyMask = 0x000F,
+			JoyRightButton = 0x0040,
+			JoyUp = 0x0100,
+			JoyRight = 0x0200,
+			JoyDown = 0x0400,
+			JoyLeft = 0x0800,
+			JoyLeftButton = 0x4000,
+			JoyMask = 0x0F40,
+		}
+
+		ushort portControls1, portControls2;
+		byte controlsReadMode;
+
+		bool isNmi, isNmiPending;
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.ColecoVision configuration;
+
+		public ColecoVision() { }
+
+		public void Initialize()
+		{
+			bios = null;
+			cartridge = null;
+
+			wram = new byte[ramSize];
+			cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
+			vdp = new TMS99xxA();
+			psg = new SN76489();
+
+			vdp.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.ColecoVision)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return vdp.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return psg.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				vdp.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				psg.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			vdpClock = (masterClock / 1.0);
+			psgClock = (masterClock / 3.0);
+
+			vdp?.SetClockRate(vdpClock);
+			vdp?.SetRefreshRate(refreshRate);
+			vdp?.SetRevision(0);
+
+			psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			psg?.SetOutputChannels(2);
+			psg?.SetClockRate(psgClock);
+			psg?.SetRefreshRate(refreshRate);
+
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / refreshRate);
+
+			OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
+		}
+
+		private void LoadBios()
+		{
+			var (type, bootstrapRomData) = CartridgeLoader.Load(configuration.BiosRom, "ColecoVision BIOS");
+			bios = new ColecoCartridge(bootstrapRomData.Length, 0);
+			bios.LoadRom(bootstrapRomData);
+		}
+
+		public void Startup()
+		{
+			LoadBios();
+
+			cpu.Startup();
+			cpu.SetStackPointer(0xFFFF);
+			vdp.Startup();
+			psg.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			vdp.Reset();
+			psg.Reset();
+
+			portControls1 = portControls2 = 0xFFFF;
+			controlsReadMode = 0x00;
+
+			isNmi = isNmiPending = false;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			cpu?.Shutdown();
+			vdp?.Shutdown();
+			psg?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			SaveStateHandler.PerformSetState(cartridge, state[nameof(cartridge)]);
+			wram = state[nameof(wram)];
+			SaveStateHandler.PerformSetState(cpu, state[nameof(cpu)]);
+			SaveStateHandler.PerformSetState(vdp, state[nameof(vdp)]);
+			SaveStateHandler.PerformSetState(psg, state[nameof(psg)]);
+
+			portControls1 = state[nameof(portControls1)];
+			portControls2 = state[nameof(portControls2)];
+			controlsReadMode = state[nameof(controlsReadMode)];
+			isNmi = state[nameof(isNmi)];
+			isNmiPending = state[nameof(isNmiPending)];
+
+			ReconfigureSystem();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			return new Dictionary<string, dynamic>
+			{
+				[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
+				[nameof(wram)] = wram,
+				[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
+				[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
+				[nameof(psg)] = SaveStateHandler.PerformGetState(psg),
+
+				[nameof(portControls1)] = portControls1,
+				[nameof(portControls2)] = portControls2,
+				[nameof(controlsReadMode)] = controlsReadMode,
+				[nameof(isNmi)] = isNmi,
+				[nameof(isNmiPending)] = isNmiPending
+			};
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			if (mapperType == null)
+				mapperType = typeof(ColecoCartridge);
+
+			cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, 0 });
+			cartridge.LoadRom(romData);
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge?.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			if (cartridge == null) return false;
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
+		}
+
+		public void RunStep()
+		{
+			double currentCpuClockCycles = 0.0;
+			currentCpuClockCycles += cpu.Step();
+
+			double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
+
+			vdp.Step((int)Math.Round(currentMasterClockCycles));
+
+			if (vdp.InterruptLine == InterruptState.Assert && !isNmi) isNmiPending = true;
+			isNmi = (vdp.InterruptLine == InterruptState.Assert);
+			if (isNmiPending)
+			{
+				isNmiPending = false;
+				cpu.SetInterruptLine(InterruptType.NonMaskable, InterruptState.Assert);
+			}
+
+			psg.Step((int)Math.Round(currentCpuClockCycles));
+
+			cartridge?.Step((int)Math.Round(currentCpuClockCycles));
+
+			currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			var keysDown = eventArgs.Keyboard;
+
+			ushort dataCtrl1 = 0x0000;
+
+			if (keysDown.Contains(configuration.ControlsKeypad1)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber1;
+			if (keysDown.Contains(configuration.ControlsKeypad2)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber2;
+			if (keysDown.Contains(configuration.ControlsKeypad3)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber3;
+			if (keysDown.Contains(configuration.ControlsKeypad4)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber4;
+			if (keysDown.Contains(configuration.ControlsKeypad5)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber5;
+			if (keysDown.Contains(configuration.ControlsKeypad6)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber6;
+			if (keysDown.Contains(configuration.ControlsKeypad7)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber7;
+			if (keysDown.Contains(configuration.ControlsKeypad8)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber8;
+			if (keysDown.Contains(configuration.ControlsKeypad9)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber9;
+			if (keysDown.Contains(configuration.ControlsKeypad0)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumber0;
+			if (keysDown.Contains(configuration.ControlsKeypadStar)) dataCtrl1 = (ushort)KeyJoyButtons.KeyStarKey;
+			if (keysDown.Contains(configuration.ControlsKeypadNumberSign)) dataCtrl1 = (ushort)KeyJoyButtons.KeyNumberSignKey;
+
+			if (keysDown.Contains(configuration.ControlsButtonRight)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyRightButton;
+
+			if (keysDown.Contains(configuration.ControlsUp)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyUp;
+			if (keysDown.Contains(configuration.ControlsDown)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyDown;
+			if (keysDown.Contains(configuration.ControlsLeft)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyLeft;
+			if (keysDown.Contains(configuration.ControlsRight)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyRight;
+			if (keysDown.Contains(configuration.ControlsButtonLeft)) dataCtrl1 |= (ushort)KeyJoyButtons.JoyLeftButton;
+
+			portControls1 = (ushort)~dataCtrl1;
+
+			// TODO: controller 2
+
+			portControls2 = 0xFFFF;
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				return (bios?.Read(address) ?? 0x00);
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				/* Expansion port */
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				/* Expansion port */
+			}
+			else if (address >= 0x6000 && address <= 0x7FFF)
+			{
+				return wram[address & (ramSize - 1)];
+			}
+			else if (address >= 0x8000 && address <= 0xFFFF)
+			{
+				return (cartridge != null ? cartridge.Read(address) : (byte)0x00);
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x1FFF)
+			{
+				/* Can't write to BIOS */
+			}
+			else if (address >= 0x2000 && address <= 0x3FFF)
+			{
+				/* Expansion port */
+			}
+			else if (address >= 0x4000 && address <= 0x5FFF)
+			{
+				/* Expansion port */
+			}
+			else if (address >= 0x6000 && address <= 0x7FFF)
+			{
+				wram[address & (ramSize - 1)] = value;
+			}
+			else if (address >= 0x8000 && address <= 0xFFFF)
+			{
+				cartridge?.Write(address, value);
+			}
+		}
+
+		private byte ReadPort(byte port)
+		{
+			switch (port & 0xE0)
+			{
+				case 0xA0:                          /* VDP ports */
+					return vdp.ReadPort(port);
+
+				case 0xE0:                          /* Controls */
+					if ((port & 0x01) == 0)
+						return (controlsReadMode == 0x00 ? (byte)(portControls1 & 0xFF) : (byte)((portControls1 >> 8) & 0xFF));
+					else
+						return (controlsReadMode == 0x00 ? (byte)(portControls2 & 0xFF) : (byte)((portControls2 >> 8) & 0xFF));
+
+				default:
+					return 0xFF;                    /* Not connected */
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			switch (port & 0xE0)
+			{
+				case 0x80:                          /* Control mode (keypad/right buttons) */
+					controlsReadMode = 0x00;
+					break;
+
+				case 0xA0:                          /* VDP */
+					vdp.WritePort(port, value);
+					break;
+
+				case 0xC0:                          /* Control mode (joystick/left buttons) */
+					controlsReadMode = 0x01;
+					break;
+
+				case 0xE0:                          /* PSG */
+					psg.WritePort(port, value);
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs.meta
new file mode 100644
index 0000000..f3f14c5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/ColecoVision.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6861fcd381d9ac84c9ec16b4d8eb2bce
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs b/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs
new file mode 100644
index 0000000..5c39580
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs
@@ -0,0 +1,704 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video.Nintendo;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges.Nintendo;
+using Essgee.Emulation.ExtDevices.Nintendo;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	[MachineIndex(5)]
+	public class GameBoy : IMachine
+	{
+		const double masterClock = 4194304;
+		const double refreshRate = 59.727500569606;
+
+		const int wramSize = 8 * 1024;
+		const int hramSize = 0x7F;
+
+		const int serialCycleCount = 512;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { video.RenderScreen += value; }
+			remove { video.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { video.SizeScreen += value; }
+			remove { video.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { audio.EnqueueSamples += value; }
+			remove { audio.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble;
+		protected virtual void OnEnableRumble(EventArgs e) { EnableRumble?.Invoke(this, EventArgs.Empty); }
+
+		public string ManufacturerName => "Nintendo";
+		public string ModelName => "Game Boy";
+		public string DatFilename => "Nintendo - Game Boy.dat";
+		public (string Extension, string Description) FileFilter => (".gb", "Game Boy ROMs");
+		public bool HasBootstrap => true;
+		public double RefreshRate => refreshRate;
+		public double PixelAspectRatio => 1.0;
+		public (string Name, string Description)[] RuntimeOptions => video.RuntimeOptions.Concat(audio.RuntimeOptions).ToArray();
+
+		byte[] bootstrap;
+		IGameBoyCartridge cartridge;
+		byte[] wram, hram;
+		byte ie;
+		SM83 cpu;
+		DMGVideo video;
+		DMGAudio audio;
+		ISerialDevice serialDevice;
+
+		// FF00 - P1/JOYP
+		byte joypadRegister;
+
+		// FF01 - SB
+		byte serialData;
+		// FF02 - SC
+		bool serialUseInternalClock, serialTransferInProgress;
+
+		// FF04 - DIV
+		byte divider;
+		// FF05 - TIMA
+		byte timerCounter;
+		//
+		ushort clockCycleCount;
+
+		// FF06 - TMA
+		byte timerModulo;
+
+		// FF07 - TAC
+		bool timerRunning;
+		byte timerInputClock;
+		//
+		bool timerOverflow, timerLoading;
+
+		// FF0F - IF
+		bool irqVBlank, irqLCDCStatus, irqTimerOverflow, irqSerialIO, irqKeypad;
+
+		// FF50
+		bool bootstrapDisabled;
+
+		[Flags]
+		enum JoypadInputs : byte
+		{
+			Right = (1 << 0),
+			Left = (1 << 1),
+			Up = (1 << 2),
+			Down = (1 << 3),
+			A = (1 << 4),
+			B = (1 << 5),
+			Select = (1 << 6),
+			Start = (1 << 7)
+		}
+
+		JoypadInputs inputsPressed;
+
+		int serialBitsCounter, serialCycles;
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.GameBoy configuration;
+
+		public GameBoy() { }
+
+		public void Initialize()
+		{
+			bootstrap = null;
+			cartridge = null;
+
+			wram = new byte[wramSize];
+			hram = new byte[hramSize];
+			cpu = new SM83(ReadMemory, WriteMemory);
+			video = new DMGVideo(ReadMemory, cpu.RequestInterrupt);
+			audio = new DMGAudio();
+
+			video.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.GameBoy)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return video.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return audio.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				video.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				audio.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			/* Video */
+			video?.SetClockRate(masterClock);
+			video?.SetRefreshRate(refreshRate);
+			video?.SetRevision(0);
+
+			/* Audio */
+			audio?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			audio?.SetOutputChannels(2);
+			audio?.SetClockRate(masterClock);
+			audio?.SetRefreshRate(refreshRate);
+
+			/* Cartridge */
+			if (cartridge is GBCameraCartridge camCartridge)
+				camCartridge.SetImageSource(configuration.CameraSource, configuration.CameraImageFile);
+
+			/* Serial */
+			if (serialDevice != null)
+			{
+				serialDevice.SaveExtraData -= SaveExtraData;
+				serialDevice.Shutdown();
+			}
+
+			serialDevice = (ISerialDevice)Activator.CreateInstance(configuration.SerialDevice);
+			serialDevice.SaveExtraData += SaveExtraData;
+			serialDevice.Initialize();
+
+			/* Misc timing */
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / refreshRate);
+
+			/* Announce viewport */
+			OnChangeViewport(new ChangeViewportEventArgs(video.Viewport));
+		}
+
+		private void LoadBootstrap()
+		{
+			if (configuration.UseBootstrap)
+			{
+				var (type, bootstrapRomData) = CartridgeLoader.Load(configuration.BootstrapRom, "Game Boy Bootstrap");
+				bootstrap = new byte[bootstrapRomData.Length];
+				Buffer.BlockCopy(bootstrapRomData, 0, bootstrap, 0, bootstrap.Length);
+			}
+		}
+
+		public void Startup()
+		{
+			LoadBootstrap();
+
+			cpu.Startup();
+			video.Startup();
+			audio.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			video.Reset();
+			audio.Reset();
+
+			if (configuration.UseBootstrap)
+			{
+				cpu.SetProgramCounter(0x0000);
+				cpu.SetStackPointer(0x0000);
+			}
+			else
+			{
+				cpu.SetProgramCounter(0x0100);
+				cpu.SetStackPointer(0xFFFE);
+				cpu.SetRegisterAF(0x01B0);
+				cpu.SetRegisterBC(0x0013);
+				cpu.SetRegisterDE(0x00D8);
+				cpu.SetRegisterHL(0x014D);
+
+				video.WritePort(0x40, 0x91);
+				video.WritePort(0x42, 0x00);
+				video.WritePort(0x43, 0x00);
+				video.WritePort(0x45, 0x00);
+				video.WritePort(0x47, 0xFC);
+				video.WritePort(0x48, 0xFF);
+				video.WritePort(0x49, 0xFF);
+				video.WritePort(0x4A, 0x00);
+				video.WritePort(0x4B, 0x00);
+			}
+
+			joypadRegister = 0x0F;
+
+			serialData = 0xFF;
+			serialUseInternalClock = serialTransferInProgress = false;
+
+			timerCounter = 0;
+			clockCycleCount = 0;
+
+			timerModulo = 0;
+
+			timerRunning = false;
+			timerInputClock = 0;
+
+			timerOverflow = timerLoading = false;
+
+			irqVBlank = irqLCDCStatus = irqTimerOverflow = irqSerialIO = irqKeypad = false;
+
+			bootstrapDisabled = !configuration.UseBootstrap;
+
+			inputsPressed = 0;
+
+			serialBitsCounter = serialCycles = 0;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			if (serialDevice != null)
+			{
+				serialDevice.SaveExtraData -= SaveExtraData;
+				serialDevice.Shutdown();
+			}
+
+			if (cartridge is MBC5Cartridge mbc5Cartridge)
+				mbc5Cartridge.EnableRumble -= EnableRumble;
+
+			cpu?.Shutdown();
+			video?.Shutdown();
+			audio?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			throw new NotImplementedException();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			throw new NotImplementedException();
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			cartridge = SpecializedLoader.CreateCartridgeInstance(romData, ramData, mapperType);
+
+			if (cartridge is GBCameraCartridge camCartridge)
+				camCartridge.SetImageSource(configuration.CameraSource, configuration.CameraImageFile);
+
+			if (cartridge is MBC5Cartridge mbc5Cartridge)
+				mbc5Cartridge.EnableRumble += EnableRumble;
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge?.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			if (cartridge == null) return false;
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame = 0;
+		}
+
+		public void RunStep()
+		{
+			var clockCyclesInStep = cpu.Step();
+
+			for (var s = 0; s < clockCyclesInStep / 4; s++)
+			{
+				HandleTimerOverflow();
+				UpdateCycleCounter((ushort)(clockCycleCount + 4));
+
+				HandleSerialIO(4);
+
+				video.Step(4);
+				audio.Step(4);
+				cartridge?.Step(4);
+
+				currentMasterClockCyclesInFrame += 4;
+			}
+		}
+
+		private void IncrementTimer()
+		{
+			timerCounter++;
+			if (timerCounter == 0) timerOverflow = true;
+		}
+
+		private bool GetTimerBit(byte value, ushort cycles)
+		{
+			switch (value & 0b11)
+			{
+				case 0: return (cycles & (1 << 9)) != 0;
+				case 1: return (cycles & (1 << 3)) != 0;
+				case 2: return (cycles & (1 << 5)) != 0;
+				case 3: return (cycles & (1 << 7)) != 0;
+				default: throw new EmulationException("Unhandled timer state");
+			}
+		}
+
+		private void UpdateCycleCounter(ushort value)
+		{
+			if (timerRunning)
+			{
+				if (!GetTimerBit(timerInputClock, value) && GetTimerBit(timerInputClock, clockCycleCount))
+					IncrementTimer();
+			}
+
+			clockCycleCount = value;
+			divider = (byte)(clockCycleCount >> 8);
+		}
+
+		private void HandleTimerOverflow()
+		{
+			timerLoading = false;
+
+			if (timerOverflow)
+			{
+				cpu.RequestInterrupt(SM83.InterruptSource.TimerOverflow);
+				timerOverflow = false;
+
+				timerCounter = timerModulo;
+				timerLoading = true;
+			}
+		}
+
+		private void HandleSerialIO(int clockCyclesInStep)
+		{
+			if (serialTransferInProgress)
+			{
+				if (serialUseInternalClock)
+				{
+					for (var c = 0; c < clockCyclesInStep; c++)
+					{
+						serialCycles++;
+						if (serialCycles == serialCycleCount)
+						{
+							serialCycles = 0;
+
+							serialBitsCounter--;
+
+							var bitToSend = (byte)((serialData >> 7) & 0b1);
+							var bitReceived = serialDevice.ExchangeBit(serialBitsCounter, bitToSend);
+							serialData = (byte)((serialData << 1) | (bitReceived & 0b1));
+
+							if (serialBitsCounter == 0)
+							{
+								cpu.RequestInterrupt(SM83.InterruptSource.SerialIO);
+								serialTransferInProgress = false;
+							}
+						}
+					}
+				}
+			}
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			inputsPressed = 0;
+
+			/* Keyboard */
+			if (eventArgs.Keyboard.Contains(configuration.ControlsRight) && !eventArgs.Keyboard.Contains(configuration.ControlsLeft)) inputsPressed |= JoypadInputs.Right;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsLeft) && !eventArgs.Keyboard.Contains(configuration.ControlsRight)) inputsPressed |= JoypadInputs.Left;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsUp) && !eventArgs.Keyboard.Contains(configuration.ControlsDown)) inputsPressed |= JoypadInputs.Up;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsDown) && !eventArgs.Keyboard.Contains(configuration.ControlsUp)) inputsPressed |= JoypadInputs.Down;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsA)) inputsPressed |= JoypadInputs.A;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsB)) inputsPressed |= JoypadInputs.B;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsSelect)) inputsPressed |= JoypadInputs.Select;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsStart)) inputsPressed |= JoypadInputs.Start;
+
+			/* XInput controller */
+			if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) inputsPressed |= JoypadInputs.Right;
+			if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) inputsPressed |= JoypadInputs.Left;
+			if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) inputsPressed |= JoypadInputs.Up;
+			if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) inputsPressed |= JoypadInputs.Down;
+			if (eventArgs.ControllerState.IsAPressed()) inputsPressed |= JoypadInputs.A;
+			if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) inputsPressed |= JoypadInputs.B;
+			if (eventArgs.ControllerState.IsBackPressed()) inputsPressed |= JoypadInputs.Select;
+			if (eventArgs.ControllerState.IsStartPressed()) inputsPressed |= JoypadInputs.Start;
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x7FFF)
+			{
+				if (configuration.UseBootstrap && address < 0x0100 && !bootstrapDisabled)
+					return bootstrap[address & 0x00FF];
+				else
+					return (cartridge != null ? cartridge.Read(address) : (byte)0xFF);
+			}
+			else if (address >= 0x8000 && address <= 0x9FFF)
+			{
+				return video.ReadVram(address);
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				return (cartridge != null ? cartridge.Read(address) : (byte)0xFF);
+			}
+			else if (address >= 0xC000 && address <= 0xFDFF)
+			{
+				return wram[address & (wramSize - 1)];
+			}
+			else if (address >= 0xFE00 && address <= 0xFE9F)
+			{
+				return video.ReadOam(address);
+			}
+			else if (address >= 0xFF00 && address <= 0xFF7F)
+			{
+				return ReadIo(address);
+			}
+			else if (address >= 0xFF80 && address <= 0xFFFE)
+			{
+				return hram[address - 0xFF80];
+			}
+			else if (address == 0xFFFF)
+			{
+				return ie;
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private byte ReadIo(ushort address)
+		{
+			if ((address & 0xFFF0) == 0xFF40)
+				return video.ReadPort((byte)(address & 0xFF));
+			else if ((address & 0xFFF0) == 0xFF10 || (address & 0xFFF0) == 0xFF20 || (address & 0xFFF0) == 0xFF30)
+				return audio.ReadPort((byte)(address & 0xFF));
+			else
+			{
+				switch (address)
+				{
+					case 0xFF00:
+						// P1/JOYP
+						return joypadRegister;
+
+					case 0xFF01:
+						// SB
+						return serialData;
+
+					case 0xFF02:
+						// SC
+						return (byte)(
+							0x7E |
+							(serialUseInternalClock ? (1 << 0) : 0) |
+							(serialTransferInProgress ? (1 << 7) : 0));
+
+					case 0xFF04:
+						// DIV
+						return divider;
+
+					case 0xFF05:
+						// TIMA
+						return timerCounter;
+
+					case 0xFF06:
+						// TMA
+						return timerModulo;
+
+					case 0xFF07:
+						// TAC
+						return (byte)(
+							0xF8 |
+							(timerRunning ? (1 << 2) : 0) |
+							(timerInputClock & 0b11));
+
+					case 0xFF0F:
+						// IF
+						return (byte)(
+							0xE0 |
+							(irqVBlank ? (1 << 0) : 0) |
+							(irqLCDCStatus ? (1 << 1) : 0) |
+							(irqTimerOverflow ? (1 << 2) : 0) |
+							(irqSerialIO ? (1 << 3) : 0) |
+							(irqKeypad ? (1 << 4) : 0));
+
+					case 0xFF50:
+						// Bootstrap disable
+						return (byte)(
+							0xFE |
+							(bootstrapDisabled ? (1 << 0) : 0));
+
+					default:
+						return 0xFF;// throw new NotImplementedException();
+				}
+			}
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x7FFF)
+			{
+				cartridge?.Write(address, value);
+			}
+			else if (address >= 0x8000 && address <= 0x9FFF)
+			{
+				video.WriteVram(address, value);
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				cartridge?.Write(address, value);
+			}
+			else if (address >= 0xC000 && address <= 0xFDFF)
+			{
+				wram[address & (wramSize - 1)] = value;
+			}
+			else if (address >= 0xFE00 && address <= 0xFE9F)
+			{
+				video.WriteOam(address, value);
+			}
+			else if (address >= 0xFF00 && address <= 0xFF7F)
+			{
+				WriteIo(address, value);
+			}
+			else if (address >= 0xFF80 && address <= 0xFFFE)
+			{
+				hram[address - 0xFF80] = value;
+			}
+			else if (address == 0xFFFF)
+			{
+				ie = value;
+			}
+		}
+
+		private void WriteIo(ushort address, byte value)
+		{
+			if ((address & 0xFFF0) == 0xFF40)
+				video.WritePort((byte)(address & 0xFF), value);
+			else if ((address & 0xFFF0) == 0xFF10 || (address & 0xFFF0) == 0xFF20 || (address & 0xFFF0) == 0xFF30)
+				audio.WritePort((byte)(address & 0xFF), value);
+			else
+			{
+				switch (address)
+				{
+					case 0xFF00:
+						joypadRegister = (byte)((joypadRegister & 0xC0) | (value & 0x30));
+						if ((joypadRegister & 0x30) == 0x20)
+							joypadRegister |= (byte)(((byte)inputsPressed & 0x0F) ^ 0x0F);
+						else if ((joypadRegister & 0x30) == 0x10)
+							joypadRegister |= (byte)((((byte)inputsPressed & 0xF0) >> 4) ^ 0x0F);
+						else
+							joypadRegister = 0xFF;
+						break;
+
+					case 0xFF01:
+						serialData = value;
+						break;
+
+					case 0xFF02:
+						serialUseInternalClock = (value & (1 << 0)) != 0;
+						serialTransferInProgress = (value & (1 << 7)) != 0;
+
+						if (serialTransferInProgress) serialCycles = 0;
+						serialBitsCounter = 8;
+						break;
+
+					case 0xFF04:
+						UpdateCycleCounter(0);
+						break;
+
+					case 0xFF05:
+						if (!timerLoading)
+						{
+							timerCounter = value;
+							timerOverflow = false;
+						}
+						break;
+
+					case 0xFF06:
+						timerModulo = value;
+						if (timerLoading)
+							timerCounter = value;
+						break;
+
+					case 0xFF07:
+						{
+							var newTimerRunning = (value & (1 << 2)) != 0;
+							var newTimerInputClock = (byte)(value & 0b11);
+
+							var oldBit = timerRunning && GetTimerBit(timerInputClock, clockCycleCount);
+							var newBit = newTimerRunning && GetTimerBit(newTimerInputClock, clockCycleCount);
+
+							if (oldBit && !newBit)
+								IncrementTimer();
+
+							timerRunning = newTimerRunning;
+							timerInputClock = newTimerInputClock;
+						}
+						break;
+
+					case 0xFF0F:
+						irqVBlank = (value & (1 << 0)) != 0;
+						irqLCDCStatus = (value & (1 << 1)) != 0;
+						irqTimerOverflow = (value & (1 << 2)) != 0;
+						irqSerialIO = (value & (1 << 3)) != 0;
+						irqKeypad = (value & (1 << 4)) != 0;
+						break;
+
+					case 0xFF50:
+						if (!bootstrapDisabled)
+							bootstrapDisabled = (value & (1 << 0)) != 0;
+						break;
+				}
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs.meta
new file mode 100644
index 0000000..9399769
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameBoy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d24bc8f5b7f44b2439d6860b8d024aee
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs b/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs
new file mode 100644
index 0000000..fee10c5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs
@@ -0,0 +1,877 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.ComponentModel;
+using System.IO;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video.Nintendo;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges.Nintendo;
+using Essgee.Emulation.ExtDevices.Nintendo;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	[MachineIndex(6)]
+	public class GameBoyColor : IMachine
+	{
+		const double masterClock = 4194304;
+		const double refreshRate = 59.727500569606;
+
+		const int wramSize = 8 * 1024;
+		const int hramSize = 0x7F;
+
+		const int serialCycleCountNormal = 512;
+		const int serialCycleCountFast = 16;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { video.RenderScreen += value; }
+			remove { video.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { video.SizeScreen += value; }
+			remove { video.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { audio.EnqueueSamples += value; }
+			remove { audio.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble;
+		protected virtual void OnEnableRumble(EventArgs e) { EnableRumble?.Invoke(this, EventArgs.Empty); }
+
+		public string ManufacturerName => "Nintendo";
+		public string ModelName => "Game Boy Color";
+		public string DatFilename => "Nintendo - Game Boy Color.dat";
+		public (string Extension, string Description) FileFilter => (".gbc", "Game Boy Color ROMs");
+		public bool HasBootstrap => true;
+		public double RefreshRate => refreshRate;
+		public double PixelAspectRatio => 1.0;
+		public (string Name, string Description)[] RuntimeOptions => video.RuntimeOptions.Concat(audio.RuntimeOptions).ToArray();
+
+		byte[] bootstrap;
+		IGameBoyCartridge cartridge;
+		byte[,] wram;
+		byte[] hram;
+		byte ie;
+		SM83CGB cpu;
+		CGBVideo video;
+		CGBAudio audio;
+		ISerialDevice serialDevice;
+
+		// FF00 - P1/JOYP
+		byte joypadRegister;
+
+		// FF01 - SB
+		byte serialData;
+		// FF02 - SC
+		bool serialUseInternalClock, serialFastClockSpeed, serialTransferInProgress;
+
+		// FF04 - DIV
+		byte divider;
+		// FF05 - TIMA
+		byte timerCounter;
+		//
+		ushort clockCycleCount;
+
+		// FF06 - TMA
+		byte timerModulo;
+
+		// FF07 - TAC
+		bool timerRunning;
+		byte timerInputClock;
+		//
+		bool timerOverflow, timerLoading;
+
+		// FF0F - IF
+		bool irqVBlank, irqLCDCStatus, irqTimerOverflow, irqSerialIO, irqKeypad;
+
+		// FF4C
+
+		// FF4D - KEY1
+		bool speedIsDouble, speedSwitchPending;
+
+		// FF50
+		bool bootstrapDisabled;
+
+		// FF56 - RP
+		bool irSendingSignal, irNotReceivingSignal, irReadEnableA, irReadEnableB;
+
+		// FF70 - SVBK
+		byte wramBank;
+
+		public enum InfraredSources
+		{
+			[Description("None")]
+			None,
+			[Description("Random")]
+			Random,
+			[Description("Constant Light (Lamp)")]
+			ConstantOn,
+			[Description("Pocket Pikachu Color")]
+			PocketPikachuColor
+		}
+		ushort[] irDatabase;
+		int irDatabaseBaseIndex, irDatabaseStep;
+		int irDatabaseCurrentIndex, irCycles;
+		bool irExternalTransferActive;
+
+		[Flags]
+		enum JoypadInputs : byte
+		{
+			Right = (1 << 0),
+			Left = (1 << 1),
+			Up = (1 << 2),
+			Down = (1 << 3),
+			A = (1 << 4),
+			B = (1 << 5),
+			Select = (1 << 6),
+			Start = (1 << 7)
+		}
+
+		JoypadInputs inputsPressed;
+
+		int serialBitsCounter, serialCycles, clockCyclesPerSerialBit;
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.GameBoyColor configuration;
+
+		public GameBoyColor() { }
+
+		public void Initialize()
+		{
+			bootstrap = null;
+			cartridge = null;
+
+			wram = new byte[8, wramSize];
+			hram = new byte[hramSize];
+			cpu = new SM83CGB(ReadMemory, WriteMemory);
+			video = new CGBVideo(ReadMemory, cpu.RequestInterrupt);
+			audio = new CGBAudio();
+			serialDevice = null;
+
+			video.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.GameBoyColor)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return video.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return audio.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				video.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				audio.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			/* Video */
+			video?.SetClockRate(masterClock);
+			video?.SetRefreshRate(refreshRate);
+			video?.SetRevision(0);
+
+			/* Audio */
+			audio?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			audio?.SetOutputChannels(2);
+			audio?.SetClockRate(masterClock);
+			audio?.SetRefreshRate(refreshRate);
+
+			/* Cartridge */
+			if (cartridge is GBCameraCartridge camCartridge)
+				camCartridge.SetImageSource(configuration.CameraSource, configuration.CameraImageFile);
+
+			/* Serial */
+			if (serialDevice != null)
+			{
+				serialDevice.SaveExtraData -= SaveExtraData;
+				serialDevice.Shutdown();
+			}
+
+			serialDevice = (ISerialDevice)Activator.CreateInstance(configuration.SerialDevice);
+			serialDevice.SaveExtraData += SaveExtraData;
+			serialDevice.Initialize();
+
+			/* Infrared */
+			irDatabaseBaseIndex = 0;
+			irDatabaseStep = 0;
+			irDatabaseCurrentIndex = irCycles = 0;
+			irExternalTransferActive = false;
+
+			if (configuration.InfraredSource == InfraredSources.PocketPikachuColor && File.Exists(configuration.InfraredDatabasePikachu))
+			{
+				using (var reader = new BinaryReader(new FileStream(configuration.InfraredDatabasePikachu, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite)))
+				{
+					irDatabase = new ushort[reader.BaseStream.Length / 2];
+					for (var i = 0; i < irDatabase.Length; i++)
+						irDatabase[i] = reader.ReadUInt16();
+
+					irDatabaseStep = 2007;
+					if ((irDatabaseBaseIndex < 0) || (irDatabaseBaseIndex * irDatabaseStep >= irDatabase.Length))
+						irDatabaseBaseIndex = 0;
+				}
+			}
+
+			/* Misc timing */
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / refreshRate);
+
+			/* Announce viewport */
+			OnChangeViewport(new ChangeViewportEventArgs(video.Viewport));
+		}
+
+		private void LoadBootstrap()
+		{
+			if (configuration.UseBootstrap)
+			{
+				var (type, bootstrapRomData) = CartridgeLoader.Load(configuration.BootstrapRom, "Game Boy Color Bootstrap");
+				bootstrap = new byte[bootstrapRomData.Length];
+				Buffer.BlockCopy(bootstrapRomData, 0, bootstrap, 0, bootstrap.Length);
+			}
+		}
+
+		public void Startup()
+		{
+			LoadBootstrap();
+
+			cpu.Startup();
+			video.Startup();
+			audio.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			video.Reset();
+			audio.Reset();
+
+			if (configuration.UseBootstrap)
+			{
+				cpu.SetProgramCounter(0x0000);
+				cpu.SetStackPointer(0x0000);
+			}
+			else
+			{
+				cpu.SetProgramCounter(0x0100);
+				cpu.SetStackPointer(0xFFFE);
+				cpu.SetRegisterAF(0x11B0);
+				cpu.SetRegisterBC(0x0000);
+				cpu.SetRegisterDE(0xFF56);
+				cpu.SetRegisterHL(0x000D);
+
+				video.WritePort(0x40, 0x91);
+				video.WritePort(0x42, 0x00);
+				video.WritePort(0x43, 0x00);
+				video.WritePort(0x45, 0x00);
+				video.WritePort(0x47, 0xFC);
+				video.WritePort(0x48, 0xFF);
+				video.WritePort(0x49, 0xFF);
+				video.WritePort(0x4A, 0x00);
+				video.WritePort(0x4B, 0x00);
+			}
+
+			joypadRegister = 0x0F;
+
+			serialData = 0xFF;
+			serialUseInternalClock = serialFastClockSpeed = serialTransferInProgress = false;
+
+			timerCounter = 0;
+			clockCycleCount = 0;
+
+			timerModulo = 0;
+
+			timerRunning = false;
+			timerInputClock = 0;
+
+			timerOverflow = timerLoading = false;
+
+			irqVBlank = irqLCDCStatus = irqTimerOverflow = irqSerialIO = irqKeypad = false;
+
+			bootstrapDisabled = !configuration.UseBootstrap;
+
+			irSendingSignal = irReadEnableA = irReadEnableB = false;
+			irNotReceivingSignal = true;
+
+			wramBank = 0x01;
+
+			inputsPressed = 0;
+
+			serialBitsCounter = serialCycles = clockCyclesPerSerialBit = 0;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			if (serialDevice != null)
+			{
+				serialDevice.SaveExtraData -= SaveExtraData;
+				serialDevice.Shutdown();
+			}
+
+			if (cartridge is MBC5Cartridge mbc5Cartridge)
+				mbc5Cartridge.EnableRumble -= EnableRumble;
+
+			cpu?.Shutdown();
+			video?.Shutdown();
+			audio?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			throw new NotImplementedException();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			throw new NotImplementedException();
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			cartridge = SpecializedLoader.CreateCartridgeInstance(romData, ramData, mapperType);
+
+			if (cartridge is GBCameraCartridge camCartridge)
+				camCartridge.SetImageSource(configuration.CameraSource, configuration.CameraImageFile);
+
+			if (cartridge is MBC5Cartridge mbc5Cartridge)
+				mbc5Cartridge.EnableRumble += EnableRumble;
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge?.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			if (cartridge == null) return false;
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame = 0;
+		}
+
+		public void RunStep()
+		{
+			// TODO: verify if current handling of CGB double speed mode is correct! seems to work but is probably wrong??
+			// NOTES:
+			//  https://github.com/LIJI32/GBVideoPlayer/blob/master/How%20It%20Works.md#hblank-and-sub-hblank-tricks
+			//  https://gbdev.io/pandocs/#ff4d-key1-cgb-mode-only-prepare-speed-switch
+
+			var clockCyclesInStep = RunCpuStep();
+			var cycleLength = cpu.IsDoubleSpeed ? 2 : 4;
+
+			video.IsDoubleSpeed = cpu.IsDoubleSpeed;
+
+			for (var s = 0; s < clockCyclesInStep / 4; s++)
+			{
+				HandleTimerOverflow();
+				UpdateCycleCounter((ushort)(clockCycleCount + 4));
+
+				HandleSerialIO(cycleLength);
+
+				HandleIRCommunication(cycleLength);
+
+				video.Step(cycleLength);
+				audio.Step(cycleLength);
+				cartridge?.Step(cycleLength);
+
+				currentMasterClockCyclesInFrame += cycleLength;
+			}
+		}
+
+		private int RunCpuStep()
+		{
+			if (video.GDMAWaitCycles > 0)
+			{
+				var cycleLength = cpu.IsDoubleSpeed ? 2 : 4;
+				video.GDMAWaitCycles -= cycleLength;
+				return cycleLength;
+			}
+			else
+				return cpu.Step();
+		}
+
+		private void IncrementTimer()
+		{
+			timerCounter++;
+			if (timerCounter == 0) timerOverflow = true;
+		}
+
+		private bool GetTimerBit(byte value, ushort cycles)
+		{
+			switch (value & 0b11)
+			{
+				case 0: return (cycles & (1 << 9)) != 0;
+				case 1: return (cycles & (1 << 3)) != 0;
+				case 2: return (cycles & (1 << 5)) != 0;
+				case 3: return (cycles & (1 << 7)) != 0;
+				default: throw new EmulationException("Unhandled timer state");
+			}
+		}
+
+		private void UpdateCycleCounter(ushort value)
+		{
+			if (timerRunning)
+			{
+				if (!GetTimerBit(timerInputClock, value) && GetTimerBit(timerInputClock, clockCycleCount))
+					IncrementTimer();
+			}
+
+			clockCycleCount = value;
+			divider = (byte)(clockCycleCount >> 8);
+		}
+
+		private void HandleTimerOverflow()
+		{
+			timerLoading = false;
+
+			if (timerOverflow)
+			{
+				cpu.RequestInterrupt(SM83.InterruptSource.TimerOverflow);
+				timerOverflow = false;
+
+				timerCounter = timerModulo;
+				timerLoading = true;
+			}
+		}
+
+		private void HandleSerialIO(int clockCyclesInStep)
+		{
+			if (serialTransferInProgress)
+			{
+				for (var c = 0; c < clockCyclesInStep; c++)
+				{
+					serialCycles++;
+					if (serialCycles == clockCyclesPerSerialBit)
+					{
+						serialCycles = 0;
+
+						serialBitsCounter--;
+
+						var bitToSend = (byte)((serialData >> 7) & 0b1);
+						var bitReceived = serialDevice.ExchangeBit(serialBitsCounter, bitToSend);
+						serialData = (byte)((serialData << 1) | (bitReceived & 0b1));
+
+						if (serialBitsCounter == 0)
+						{
+							cpu.RequestInterrupt(SM83.InterruptSource.SerialIO);
+							serialTransferInProgress = false;
+						}
+					}
+				}
+			}
+		}
+
+		private void HandleIRCommunication(int clockCyclesInStep)
+		{
+			switch (configuration.InfraredSource)
+			{
+				case InfraredSources.None:
+					irNotReceivingSignal = true;
+					break;
+
+				case InfraredSources.Random:
+					irNotReceivingSignal = (StandInfo.Random.Next(256) % 2) == 0;
+					break;
+
+				case InfraredSources.ConstantOn:
+					irNotReceivingSignal = false;
+					break;
+
+				case InfraredSources.PocketPikachuColor:
+					if (irExternalTransferActive)
+					{
+						for (var c = 0; c < clockCyclesInStep; c++)
+						{
+							irCycles++;
+							if (irCycles == irDatabase[(irDatabaseBaseIndex * irDatabaseStep) + irDatabaseCurrentIndex])
+							{
+								irCycles = 0;
+
+								irNotReceivingSignal = !irNotReceivingSignal;
+
+								irDatabaseCurrentIndex++;
+								if (irDatabaseCurrentIndex >= irDatabaseStep)
+								{
+									irDatabaseCurrentIndex = 0;
+									irExternalTransferActive = false;
+									irNotReceivingSignal = true;
+								}
+							}
+						}
+					}
+					break;
+			}
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			inputsPressed = 0;
+
+			/* Keyboard */
+			if (eventArgs.Keyboard.Contains(configuration.ControlsRight) && !eventArgs.Keyboard.Contains(configuration.ControlsLeft)) inputsPressed |= JoypadInputs.Right;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsLeft) && !eventArgs.Keyboard.Contains(configuration.ControlsRight)) inputsPressed |= JoypadInputs.Left;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsUp) && !eventArgs.Keyboard.Contains(configuration.ControlsDown)) inputsPressed |= JoypadInputs.Up;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsDown) && !eventArgs.Keyboard.Contains(configuration.ControlsUp)) inputsPressed |= JoypadInputs.Down;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsA)) inputsPressed |= JoypadInputs.A;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsB)) inputsPressed |= JoypadInputs.B;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsSelect)) inputsPressed |= JoypadInputs.Select;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsStart)) inputsPressed |= JoypadInputs.Start;
+
+			if (eventArgs.Keyboard.Contains(configuration.ControlsSendIR))
+			{
+				irExternalTransferActive = true;
+				irDatabaseCurrentIndex = 0;
+				irCycles = 0;
+
+				irNotReceivingSignal = false;
+			}
+
+			/* XInput controller */
+			if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) inputsPressed |= JoypadInputs.Right;
+			if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) inputsPressed |= JoypadInputs.Left;
+			if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) inputsPressed |= JoypadInputs.Up;
+			if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) inputsPressed |= JoypadInputs.Down;
+			if (eventArgs.ControllerState.IsAPressed()) inputsPressed |= JoypadInputs.A;
+			if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) inputsPressed |= JoypadInputs.B;
+			if (eventArgs.ControllerState.IsBackPressed()) inputsPressed |= JoypadInputs.Select;
+			if (eventArgs.ControllerState.IsStartPressed()) inputsPressed |= JoypadInputs.Start;
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0x7FFF)
+			{
+				if (configuration.UseBootstrap && (address <= 0x00FF || (address >= 0x0200 && address <= 0x08FF)) && !bootstrapDisabled)
+					return bootstrap[address];
+				else
+					return (cartridge != null ? cartridge.Read(address) : (byte)0xFF);
+			}
+			else if (address >= 0x8000 && address <= 0x9FFF)
+			{
+				return video.ReadVram(address);
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				return (cartridge != null ? cartridge.Read(address) : (byte)0xFF);
+			}
+			else if (address >= 0xC000 && address <= 0xFDFF)
+			{
+				if ((address & 0x1000) == 0)
+					return wram[0, address & 0x0FFF];
+				else
+					return wram[wramBank, address & 0x0FFF];
+			}
+			else if (address >= 0xFE00 && address <= 0xFE9F)
+			{
+				return video.ReadOam(address);
+			}
+			else if (address >= 0xFF00 && address <= 0xFF7F)
+			{
+				return ReadIo(address);
+			}
+			else if (address >= 0xFF80 && address <= 0xFFFE)
+			{
+				return hram[address - 0xFF80];
+			}
+			else if (address == 0xFFFF)
+			{
+				return ie;
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private byte ReadIo(ushort address)
+		{
+			if (((address & 0xFFF0) == 0xFF40 && address != 0xFF4C && address != 0xFF4D) || (address >= 0xFF51 && address <= 0xFF55) || (address >= 0xFF68 && address <= 0xFF6B))
+				return video.ReadPort((byte)(address & 0xFF));
+			else if ((address & 0xFFF0) == 0xFF10 || (address & 0xFFF0) == 0xFF20 || (address & 0xFFF0) == 0xFF30)
+				return audio.ReadPort((byte)(address & 0xFF));
+			else
+			{
+				switch (address)
+				{
+					case 0xFF00:
+						// P1/JOYP
+						return joypadRegister;
+
+					case 0xFF01:
+						// SB
+						return serialData;
+
+					case 0xFF02:
+						// SC
+						return (byte)(
+							0x7C |
+							(serialUseInternalClock ? (1 << 0) : 0) |
+							(serialFastClockSpeed ? (1 << 1) : 0) |
+							(serialTransferInProgress ? (1 << 7) : 0));
+
+					case 0xFF04:
+						// DIV
+						return divider;
+
+					case 0xFF05:
+						// TIMA
+						return timerCounter;
+
+					case 0xFF06:
+						// TMA
+						return timerModulo;
+
+					case 0xFF07:
+						// TAC
+						return (byte)(
+							0xF8 |
+							(timerRunning ? (1 << 2) : 0) |
+							(timerInputClock & 0b11));
+
+					case 0xFF0F:
+						// IF
+						return (byte)(
+							0xE0 |
+							(irqVBlank ? (1 << 0) : 0) |
+							(irqLCDCStatus ? (1 << 1) : 0) |
+							(irqTimerOverflow ? (1 << 2) : 0) |
+							(irqSerialIO ? (1 << 3) : 0) |
+							(irqKeypad ? (1 << 4) : 0));
+
+					case 0xFF4D:
+						// KEY1
+						return (byte)(
+							0x7E |
+							(speedSwitchPending ? (1 << 0) : 0) |
+							((speedIsDouble = cpu.IsDoubleSpeed) ? (1 << 7) : 0));
+
+					case 0xFF50:
+						// Bootstrap disable
+						return (byte)(
+							0xFE |
+							(bootstrapDisabled ? (1 << 0) : 0));
+
+					case 0xFF56:
+						// RP
+						return (byte)(
+							0x3C |
+							(irSendingSignal ? (1 << 0) : 0) |
+							(!irReadEnableA || !irReadEnableB || irNotReceivingSignal ? (1 << 1) : 0) |
+							(irReadEnableA ? (1 << 6) : 0) |
+							(irReadEnableB ? (1 << 7) : 0));
+
+					case 0xFF70:
+						// SVBK
+						return (byte)(
+							0xF8 |
+							(wramBank & 0b111));
+
+					default:
+						return 0xFF;// throw new NotImplementedException();
+				}
+			}
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0x7FFF)
+			{
+				cartridge?.Write(address, value);
+			}
+			else if (address >= 0x8000 && address <= 0x9FFF)
+			{
+				video.WriteVram(address, value);
+			}
+			else if (address >= 0xA000 && address <= 0xBFFF)
+			{
+				cartridge?.Write(address, value);
+			}
+			else if (address >= 0xC000 && address <= 0xFDFF)
+			{
+				if ((address & 0x1000) == 0)
+					wram[0, address & 0x0FFF] = value;
+				else
+					wram[wramBank, address & 0x0FFF] = value;
+			}
+			else if (address >= 0xFE00 && address <= 0xFE9F)
+			{
+				video.WriteOam(address, value);
+			}
+			else if (address >= 0xFF00 && address <= 0xFF7F)
+			{
+				WriteIo(address, value);
+			}
+			else if (address >= 0xFF80 && address <= 0xFFFE)
+			{
+				hram[address - 0xFF80] = value;
+			}
+			else if (address == 0xFFFF)
+			{
+				ie = value;
+			}
+		}
+
+		private void WriteIo(ushort address, byte value)
+		{
+			if (((address & 0xFFF0) == 0xFF40 && address != 0xFF4C && address != 0xFF4D) || (address >= 0xFF51 && address <= 0xFF55) || (address >= 0xFF68 && address <= 0xFF6B))
+				video.WritePort((byte)(address & 0xFF), value);
+			else if ((address & 0xFFF0) == 0xFF10 || (address & 0xFFF0) == 0xFF20 || (address & 0xFFF0) == 0xFF30)
+				audio.WritePort((byte)(address & 0xFF), value);
+			else
+			{
+				switch (address)
+				{
+					case 0xFF00:
+						joypadRegister = (byte)((joypadRegister & 0xC0) | (value & 0x30));
+						if ((joypadRegister & 0x30) == 0x20)
+							joypadRegister |= (byte)(((byte)inputsPressed & 0x0F) ^ 0x0F);
+						else if ((joypadRegister & 0x30) == 0x10)
+							joypadRegister |= (byte)((((byte)inputsPressed & 0xF0) >> 4) ^ 0x0F);
+						else
+							joypadRegister = 0xFF;
+						break;
+
+					case 0xFF01:
+						serialData = value;
+						break;
+
+					case 0xFF02:
+						serialUseInternalClock = (value & (1 << 0)) != 0;
+						serialFastClockSpeed = (value & (1 << 1)) != 0;
+						serialTransferInProgress = (value & (1 << 7)) != 0;
+
+						clockCyclesPerSerialBit = (serialFastClockSpeed ? serialCycleCountFast : serialCycleCountNormal) >> (cpu.IsDoubleSpeed ? 1 : 0);
+
+						if (serialTransferInProgress) serialCycles = 0;
+						serialBitsCounter = 8;
+						break;
+
+					case 0xFF04:
+						UpdateCycleCounter(0);
+						break;
+
+					case 0xFF05:
+						if (!timerLoading)
+						{
+							timerCounter = value;
+							timerOverflow = false;
+						}
+						break;
+
+					case 0xFF06:
+						timerModulo = value;
+						if (timerLoading)
+							timerCounter = value;
+						break;
+
+					case 0xFF07:
+						{
+							var newTimerRunning = (value & (1 << 2)) != 0;
+							var newTimerInputClock = (byte)(value & 0b11);
+
+							var oldBit = timerRunning && GetTimerBit(timerInputClock, clockCycleCount);
+							var newBit = newTimerRunning && GetTimerBit(newTimerInputClock, clockCycleCount);
+
+							if (oldBit && !newBit)
+								IncrementTimer();
+
+							timerRunning = newTimerRunning;
+							timerInputClock = newTimerInputClock;
+						}
+						break;
+
+					case 0xFF0F:
+						irqVBlank = (value & (1 << 0)) != 0;
+						irqLCDCStatus = (value & (1 << 1)) != 0;
+						irqTimerOverflow = (value & (1 << 2)) != 0;
+						irqSerialIO = (value & (1 << 3)) != 0;
+						irqKeypad = (value & (1 << 4)) != 0;
+						break;
+
+					case 0xFF4D:
+						speedSwitchPending = (value & (1 << 0)) != 0;
+						break;
+
+					case 0xFF50:
+						if (!bootstrapDisabled)
+							bootstrapDisabled = (value & (1 << 0)) != 0;
+						break;
+
+					case 0xFF56:
+						irSendingSignal = (value & (1 << 0)) != 0;
+						irReadEnableA = (value & (1 << 6)) != 0;
+						irReadEnableB = (value & (1 << 7)) != 0;
+						break;
+
+					case 0xFF70:
+						wramBank = (byte)(value & 0b111);
+						if (wramBank == 0x00) wramBank = 0x01;
+						break;
+				}
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs.meta
new file mode 100644
index 0000000..39e580c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameBoyColor.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4840cc971763acf498a4729ba103aef2
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs b/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs
new file mode 100644
index 0000000..d50a7c2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs
@@ -0,0 +1,514 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges;
+using Essgee.Emulation.Cartridges.Sega;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	[MachineIndex(3)]
+	public class GameGear : IMachine
+	{
+		const double masterClock = 10738635;
+		const double refreshRate = 59.922743;
+
+		const int ramSize = 1 * 8192;
+
+		double vdpClock, psgClock;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { vdp.RenderScreen += value; }
+			remove { vdp.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { vdp.SizeScreen += value; }
+			remove { vdp.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { psg.EnqueueSamples += value; }
+			remove { psg.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
+
+		public string ManufacturerName => "Sega";
+		public string ModelName => "Game Gear";
+		public string DatFilename => "Sega - Game Gear.dat";
+		public (string Extension, string Description) FileFilter => (".gg", "Game Gear ROMs");
+		public bool HasBootstrap => true;
+		public double RefreshRate => refreshRate;
+		public double PixelAspectRatio => 8.0 / 7.0;
+		public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
+
+		ICartridge bootstrap, cartridge;
+		byte[] wram;
+		Z80A cpu;
+		SegaGGVDP vdp;
+		SegaGGPSG psg;
+
+		[Flags]
+		enum IOPortABInputs : byte
+		{
+			PortAUp = (1 << 0),
+			PortADown = (1 << 1),
+			PortALeft = (1 << 2),
+			PortARight = (1 << 3),
+			PortATL = (1 << 4),
+			PortATR = (1 << 5),
+			PortBUp = (1 << 6),
+			PortBDown = (1 << 7)
+		}
+
+		[Flags]
+		enum IOPortBMiscInputs : byte
+		{
+			PortBLeft = (1 << 0),
+			PortBRight = (1 << 1),
+			PortBTL = (1 << 2),
+			PortBTR = (1 << 3),
+			Reset = (1 << 4),
+			CartSlotCONTPin = (1 << 5),
+			PortATH = (1 << 6),
+			PortBTH = (1 << 7)
+		}
+
+		[Flags]
+		enum IOPortCInputs : byte
+		{
+			Start = (1 << 7)
+		}
+
+		IOPortABInputs portAInputsPressed;
+		IOPortBMiscInputs portBInputsPressed;
+		IOPortCInputs portCInputsPressed;
+
+		byte portMemoryControl, portIoControl, hCounterLatched, portIoAB, portIoBMisc;
+		byte portIoC, portParallelData, portDataDirNMI, portTxBuffer, portRxBuffer, portSerialControl;
+
+		bool isWorkRamEnabled { get { return !IsBitSet(portMemoryControl, 4); } }
+		bool isBootstrapRomEnabled { get { return !IsBitSet(portMemoryControl, 3); } }
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.GameGear configuration;
+
+		public GameGear() { }
+
+		public void Initialize()
+		{
+			bootstrap = null;
+			cartridge = null;
+
+			wram = new byte[ramSize];
+			cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
+			vdp = new SegaGGVDP();
+			psg = new SegaGGPSG();
+
+			vdp.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.GameGear)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return vdp.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return psg.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				vdp.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				psg.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			vdpClock = (masterClock / 1.0);
+			psgClock = (masterClock / 3.0);
+
+			vdp?.SetClockRate(vdpClock);
+			vdp?.SetRefreshRate(refreshRate);
+			vdp?.SetRevision(1);
+
+			psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			psg?.SetOutputChannels(2);
+			psg?.SetClockRate(psgClock);
+			psg?.SetRefreshRate(refreshRate);
+
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / refreshRate);
+
+			OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
+		}
+
+		private void LoadBootstrap()
+		{
+			if (configuration.UseBootstrap)
+			{
+				var (type, bootstrapRomData) = CartridgeLoader.Load(configuration.BootstrapRom, "GameGear Bootstrap");
+				bootstrap = new SegaMapperCartridge(bootstrapRomData.Length, 0);
+				bootstrap.LoadRom(bootstrapRomData);
+			}
+		}
+
+		public void Startup()
+		{
+			LoadBootstrap();
+
+			cpu.Startup();
+			vdp.Startup();
+			psg.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			cpu.SetStackPointer(0xDFF0);
+			vdp.Reset();
+			psg.Reset();
+
+			portAInputsPressed = 0;
+			portBInputsPressed = 0;
+			portCInputsPressed = 0;
+
+			portMemoryControl = (byte)(bootstrap != null ? 0xA3 : 0x00);
+			portIoControl = 0x0F;
+			hCounterLatched = 0x00;
+			portIoAB = portIoBMisc = 0xFF;
+
+			portIoC = (byte)(0x80 | (configuration.Region == Region.Export ? 0x40 : 0x00));
+			portParallelData = 0x00;
+			portDataDirNMI = 0xFF;
+			portTxBuffer = 0x00;
+			portRxBuffer = 0xFF;
+			portSerialControl = 0x00;
+			psg.WritePort(SegaGGPSG.PortStereoControl, 0xFF);
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			cpu?.Shutdown();
+			vdp?.Shutdown();
+			psg?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			configuration.Region = state[nameof(configuration.Region)];
+
+			SaveStateHandler.PerformSetState(bootstrap, state[nameof(bootstrap)]);
+			SaveStateHandler.PerformSetState(cartridge, state[nameof(cartridge)]);
+			wram = state[nameof(wram)];
+			SaveStateHandler.PerformSetState(cpu, state[nameof(cpu)]);
+			SaveStateHandler.PerformSetState(vdp, state[nameof(vdp)]);
+			SaveStateHandler.PerformSetState(psg, state[nameof(psg)]);
+
+			portMemoryControl = state[nameof(portMemoryControl)];
+			portIoControl = state[nameof(portIoControl)];
+			hCounterLatched = state[nameof(hCounterLatched)];
+			portIoAB = state[nameof(portIoAB)];
+			portIoBMisc = state[nameof(portIoBMisc)];
+
+			portIoC = state[nameof(portIoC)];
+			portParallelData = state[nameof(portParallelData)];
+			portDataDirNMI = state[nameof(portDataDirNMI)];
+			portTxBuffer = state[nameof(portTxBuffer)];
+			portRxBuffer = state[nameof(portRxBuffer)];
+			portSerialControl = state[nameof(portSerialControl)];
+
+			ReconfigureSystem();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			return new Dictionary<string, dynamic>
+			{
+				[nameof(configuration.Region)] = configuration.Region,
+
+				[nameof(bootstrap)] = SaveStateHandler.PerformGetState(bootstrap),
+				[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
+				[nameof(wram)] = wram,
+				[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
+				[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
+				[nameof(psg)] = SaveStateHandler.PerformGetState(psg),
+
+				[nameof(portMemoryControl)] = portMemoryControl,
+				[nameof(portIoControl)] = portIoControl,
+				[nameof(hCounterLatched)] = hCounterLatched,
+				[nameof(portIoAB)] = portIoAB,
+				[nameof(portIoBMisc)] = portIoBMisc,
+
+				[nameof(portIoC)] = portIoC,
+				[nameof(portParallelData)] = portParallelData,
+				[nameof(portDataDirNMI)] = portDataDirNMI,
+				[nameof(portTxBuffer)] = portTxBuffer,
+				[nameof(portRxBuffer)] = portRxBuffer,
+				[nameof(portSerialControl)] = portSerialControl
+			};
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			if (mapperType == null)
+				mapperType = typeof(SegaMapperCartridge);
+			if (ramData.Length == 0)
+				ramData = new byte[32768];
+
+			cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, ramData.Length });
+			cartridge.LoadRom(romData);
+			cartridge.LoadRam(ramData);
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge?.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			if (cartridge == null) return false;
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
+		}
+
+		public void RunStep()
+		{
+			double currentCpuClockCycles = 0.0;
+			currentCpuClockCycles += cpu.Step();
+
+			double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
+
+			vdp.Step((int)Math.Round(currentMasterClockCycles));
+
+			cpu.SetInterruptLine(InterruptType.Maskable, vdp.InterruptLine);
+
+			psg.Step((int)Math.Round(currentCpuClockCycles));
+
+			cartridge?.Step((int)Math.Round(currentCpuClockCycles));
+
+			currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			portAInputsPressed = 0;
+			portBInputsPressed = 0;
+			portCInputsPressed = 0;
+
+			/* Keyboard */
+			if (eventArgs.Keyboard.Contains(configuration.ControlsUp)) portAInputsPressed |= IOPortABInputs.PortAUp;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsDown)) portAInputsPressed |= IOPortABInputs.PortADown;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsLeft)) portAInputsPressed |= IOPortABInputs.PortALeft;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsRight)) portAInputsPressed |= IOPortABInputs.PortARight;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsButton1)) portAInputsPressed |= IOPortABInputs.PortATL;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsButton2)) portAInputsPressed |= IOPortABInputs.PortATR;
+			if (eventArgs.Keyboard.Contains(configuration.ControlsStart)) portCInputsPressed |= IOPortCInputs.Start;
+
+			/* XInput controller */
+			if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) portAInputsPressed |= IOPortABInputs.PortAUp;
+			if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) portAInputsPressed |= IOPortABInputs.PortADown;
+			if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) portAInputsPressed |= IOPortABInputs.PortALeft;
+			if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) portAInputsPressed |= IOPortABInputs.PortARight;
+			if (eventArgs.ControllerState.IsAPressed()) portAInputsPressed |= IOPortABInputs.PortATL;
+			if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) portAInputsPressed |= IOPortABInputs.PortATR;
+			if (eventArgs.ControllerState.IsStartPressed()) portCInputsPressed |= IOPortCInputs.Start;
+
+			portIoAB |= (byte)(IOPortABInputs.PortAUp | IOPortABInputs.PortADown | IOPortABInputs.PortALeft | IOPortABInputs.PortARight | IOPortABInputs.PortATL | IOPortABInputs.PortATR | IOPortABInputs.PortBUp | IOPortABInputs.PortBDown);
+			portIoBMisc |= (byte)(IOPortBMiscInputs.PortBLeft | IOPortBMiscInputs.PortBRight | IOPortBMiscInputs.PortBTL | IOPortBMiscInputs.PortBTR | IOPortBMiscInputs.Reset | IOPortBMiscInputs.CartSlotCONTPin | IOPortBMiscInputs.PortATH | IOPortBMiscInputs.PortBTH);
+			portIoC |= (byte)IOPortCInputs.Start;
+
+			portIoAB &= (byte)~portAInputsPressed;
+			portIoBMisc &= (byte)~portBInputsPressed;
+			portIoC &= (byte)~portCInputsPressed;
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0xBFFF)
+			{
+				if (address <= 0x0400 && isBootstrapRomEnabled && bootstrap != null)
+					return bootstrap.Read(address);
+
+				if (cartridge != null)
+					return cartridge.Read(address);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				if (isWorkRamEnabled)
+					return wram[address & (ramSize - 1)];
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (isBootstrapRomEnabled) bootstrap?.Write(address, value);
+			cartridge?.Write(address, value);
+
+			if (isWorkRamEnabled && address >= 0xC000 && address <= 0xFFFF)
+				wram[address & (ramSize - 1)] = value;
+		}
+
+		private byte ReadPort(byte port)
+		{
+			var maskedPort = (byte)(port & 0xC1);
+
+			switch (maskedPort & 0xF0)
+			{
+				case 0x00:
+					/* GG-specific ports */
+					switch (port)
+					{
+						case 0x00: return (byte)((portIoC & 0xBF) | (configuration.Region == Region.Export ? 0x40 : 0x00));
+						case 0x01: return portParallelData;
+						case 0x02: return portDataDirNMI;
+						case 0x03: return portTxBuffer;
+						case 0x04: return portRxBuffer;
+						case 0x05: return portSerialControl;
+						case 0x06: return 0xFF;
+					}
+					return 0xFF;
+
+				case 0x40:                                  /* Counters */
+				case 0x80:                                  /* VDP ports */
+					return vdp.ReadPort(maskedPort);
+
+				case 0xC0:
+					if (port == 0xC0 || port == 0xDC)
+						return portIoAB;                    /* IO port A/B register */
+					else if (port == 0xC1 || port == 0xDD)
+						return portIoBMisc;                 /* IO port B/misc register */
+					else
+						return 0xFF;
+
+				default:
+					// TODO: handle properly
+					return 0x00;
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			var maskedPort = (byte)(port & 0xC1);
+
+			switch (maskedPort & 0xF0)
+			{
+				case 0x00:
+					switch (port)
+					{
+						case 0x00: /* Read-only */ break;
+						case 0x01: portParallelData = value; break;
+						case 0x02: portDataDirNMI = value; break;
+						case 0x03: portTxBuffer = value; break;
+						case 0x04: /* Read-only? */; break;
+						case 0x05: portSerialControl = (byte)(value & 0xF8); break;
+						case 0x06: psg.WritePort(port, value); break;
+						default:
+							/* System stuff */
+							if ((maskedPort & 0x01) == 0)
+							{
+								/* Memory control */
+								if (configuration.AllowMemoryControl)
+									portMemoryControl = value;
+							}
+							else
+							{
+								/* I/O control */
+								if ((portIoControl & 0x0A) == 0x00 && ((value & 0x02) == 0x02 || (value & 0x08) == 0x08))
+									hCounterLatched = vdp.ReadPort(SegaSMSVDP.PortHCounter);
+								portIoControl = value;
+							}
+							break;
+					}
+					break;
+
+				case 0x40:
+					/* PSG */
+					psg.WritePort(maskedPort, value);
+					break;
+
+				case 0x80:
+					/* VDP */
+					vdp.WritePort(maskedPort, value);
+					break;
+
+				case 0xC0:
+					/* No effect */
+					break;
+
+				default:
+					// TODO: handle properly
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs.meta
new file mode 100644
index 0000000..9925a15
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/GameGear.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: de9201bc81725c14e8ce6987146f89d8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs b/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs
new file mode 100644
index 0000000..bd6f806
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.EventArguments;
+using Essgee.Emulation.Configuration;
+
+namespace Essgee.Emulation.Machines
+{
+	public interface IMachine
+	{
+		event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		event EventHandler<EventArgs> EmulationReset;
+		event EventHandler<RenderScreenEventArgs> RenderScreen;
+		event EventHandler<SizeScreenEventArgs> SizeScreen;
+		event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		event EventHandler<PollInputEventArgs> PollInput;
+		event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples;
+		event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		event EventHandler<EventArgs> EnableRumble;
+
+		string ManufacturerName { get; }
+		string ModelName { get; }
+		string DatFilename { get; }
+		(string Extension, string Description) FileFilter { get; }
+		bool HasBootstrap { get; }
+		double RefreshRate { get; }
+		double PixelAspectRatio { get; }
+		(string Name, string Description)[] RuntimeOptions { get; }
+
+		Dictionary<string, dynamic> GetDebugInformation();
+
+		void SetConfiguration(IConfiguration config);
+
+		object GetRuntimeOption(string name);
+		void SetRuntimeOption(string name, object value);
+
+		void Initialize();
+		void Startup();
+		void Reset();
+		void Shutdown();
+
+		void SetState(Dictionary<string, dynamic> state);
+		Dictionary<string, dynamic> GetState();
+
+		void Load(byte[] romData, byte[] ramData, Type mapperType);
+		byte[] GetCartridgeRam();
+		bool IsCartridgeRamSaveNeeded();
+
+		void RunFrame();
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs.meta
new file mode 100644
index 0000000..29f53d9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/IMachine.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e3358260543f04a48b308b485fdc8380
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs b/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs
new file mode 100644
index 0000000..22d676c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs
@@ -0,0 +1,632 @@
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges;
+using Essgee.Emulation.Cartridges.Sega;
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video;
+using Essgee.EventArguments;
+using Essgee.Utilities;
+using Essgee.Utilities.XInput;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+    [MachineIndex(2)]
+	public class MasterSystem : IMachine
+	{
+		const double masterClockNtsc = 10738635;
+		const double masterClockPal = 10640684;
+		const double refreshRateNtsc = 59.922743;
+		const double refreshRatePal = 49.701459;
+
+		const int ramSize = 1 * 8192;
+
+		double masterClock;
+		double vdpClock, psgClock;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { vdp.RenderScreen += value; }
+			remove { vdp.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { vdp.SizeScreen += value; }
+			remove { vdp.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { psg.EnqueueSamples += value; }
+			remove { psg.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
+
+		public string ManufacturerName => "Sega";
+		public string ModelName => "Master System";
+		public string DatFilename => "Sega - Master System - Mark III.dat";
+		public (string Extension, string Description) FileFilter => (".sms", "Master System ROMs");
+		public bool HasBootstrap => true;
+		public double RefreshRate { get; private set; }
+		public double PixelAspectRatio => 8.0 / 7.0;
+		public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
+
+		ICartridge bootstrap, cartridge;
+		byte[] wram;
+		Z80A cpu;
+		SegaSMSVDP vdp;
+		SegaSMSPSG psg;
+
+		InputDevice[] inputDevices;
+
+		[Flags]
+		enum ControllerInputs : byte
+		{
+			Up = 0b00000001,
+			Down = 0b00000010,
+			Left = 0b00000100,
+			Right = 0b00001000,
+			TL = 0b00010000,
+			TR = 0b00100000,
+			TH = 0b01000000
+		}
+		const byte inputResetButton = 0x10;
+
+		bool lightgunLatched;
+
+		const string pauseInputName = "Pause";
+		bool pauseButtonPressed, pauseButtonToggle;
+
+		byte portMemoryControl, portIoControl, hCounterLatched;
+
+		bool isExpansionSlotEnabled { get { return !IsBitSet(portMemoryControl, 7); } }
+		bool isCartridgeSlotEnabled { get { return !IsBitSet(portMemoryControl, 6); } }
+		bool isCardSlotEnabled { get { return !IsBitSet(portMemoryControl, 5); } }
+		bool isWorkRamEnabled { get { return !IsBitSet(portMemoryControl, 4); } }
+		bool isBootstrapRomEnabled { get { return !IsBitSet(portMemoryControl, 3); } }
+		bool isIoChipEnabled { get { return !IsBitSet(portMemoryControl, 2); } }
+
+		enum IOControlDirection { Output = 0, Input = 1 }
+		enum IOControlOutputLevel { Low = 0, High = 1 }
+		enum IOControlPort { A = 0, B = 1 }
+		enum IOControlPin { TR = 0, TH = 1 };
+
+		IOControlDirection portAPinTRDirection { get { return (IOControlDirection)((portIoControl >> 0) & 0x01); } }
+		IOControlDirection portAPinTHDirection { get { return (IOControlDirection)((portIoControl >> 1) & 0x01); } }
+		IOControlDirection portBPinTRDirection { get { return (IOControlDirection)((portIoControl >> 2) & 0x01); } }
+		IOControlDirection portBPinTHDirection { get { return (IOControlDirection)((portIoControl >> 3) & 0x01); } }
+		IOControlOutputLevel portAPinTROutputLevel { get { return (IOControlOutputLevel)((portIoControl >> 4) & 0x01); } }
+		IOControlOutputLevel portAPinTHOutputLevel { get { return (IOControlOutputLevel)((portIoControl >> 5) & 0x01); } }
+		IOControlOutputLevel portBPinTROutputLevel { get { return (IOControlOutputLevel)((portIoControl >> 6) & 0x01); } }
+		IOControlOutputLevel portBPinTHOutputLevel { get { return (IOControlOutputLevel)((portIoControl >> 7) & 0x01); } }
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.MasterSystem configuration;
+
+		IEnumerable<Keys> lastKeysDown;
+		ControllerState lastControllerState;
+		MouseButtons lastMouseButtons;
+		(int x, int y) lastMousePosition;
+
+		public MasterSystem() { }
+
+		public void Initialize()
+		{
+			bootstrap = null;
+			cartridge = null;
+
+			wram = new byte[ramSize];
+			cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
+			vdp = new SegaSMSVDP();
+			psg = new SegaSMSPSG();
+
+			inputDevices = new InputDevice[2];
+			inputDevices[0] = InputDevice.None;
+			inputDevices[1] = InputDevice.None;
+
+			lastKeysDown = new List<Keys>();
+			lastControllerState = new ControllerState();
+
+			vdp.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+
+				lastKeysDown = pollInputEventArgs.Keyboard;
+				lastControllerState = pollInputEventArgs.ControllerState;
+				lastMouseButtons = pollInputEventArgs.MouseButtons;
+				lastMousePosition = pollInputEventArgs.MousePosition;
+
+				HandlePauseButton();
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.MasterSystem)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return vdp.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return psg.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				vdp.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				psg.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			if (configuration.TVStandard == TVStandard.NTSC)
+			{
+				masterClock = masterClockNtsc;
+				RefreshRate = refreshRateNtsc;
+			}
+			else
+			{
+				masterClock = masterClockPal;
+				RefreshRate = refreshRatePal;
+			}
+
+			vdpClock = (masterClock / 1.0);
+			psgClock = (masterClock / 3.0);
+
+			vdp?.SetClockRate(vdpClock);
+			vdp?.SetRefreshRate(RefreshRate);
+			vdp?.SetRevision((int)configuration.VDPType);
+
+			psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			psg?.SetOutputChannels(2);
+			psg?.SetClockRate(psgClock);
+			psg?.SetRefreshRate(RefreshRate);
+
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / RefreshRate);
+
+			OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
+
+			inputDevices[0] = configuration.Joypad1DeviceType;
+			inputDevices[1] = configuration.Joypad2DeviceType;
+		}
+
+		private void LoadBootstrap()
+		{
+			if (configuration.UseBootstrap)
+			{
+				var (type, bootstrapRomData) = CartridgeLoader.Load(configuration.BootstrapRom, "Master System Bootstrap");
+				bootstrap = new SegaMapperCartridge(bootstrapRomData.Length, 0);
+				bootstrap.LoadRom(bootstrapRomData);
+			}
+		}
+
+		public void Startup()
+		{
+			LoadBootstrap();
+
+			cpu.Startup();
+			vdp.Startup();
+			psg.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			cpu.SetStackPointer(0xDFF0);
+			vdp.Reset();
+			psg.Reset();
+
+			pauseButtonPressed = pauseButtonToggle = false;
+
+			portMemoryControl = (byte)(bootstrap != null ? 0xE3 : 0x00);
+			portIoControl = 0x0F;
+			hCounterLatched = 0x00;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			cpu?.Shutdown();
+			vdp?.Shutdown();
+			psg?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			configuration.TVStandard = state[nameof(configuration.TVStandard)];
+			configuration.Region = state[nameof(configuration.Region)];
+
+			SaveStateHandler.PerformSetState(bootstrap, state[nameof(bootstrap)]);
+			SaveStateHandler.PerformSetState(cartridge, state[nameof(cartridge)]);
+			wram = state[nameof(wram)];
+			SaveStateHandler.PerformSetState(cpu, state[nameof(cpu)]);
+			SaveStateHandler.PerformSetState(vdp, state[nameof(vdp)]);
+			SaveStateHandler.PerformSetState(psg, state[nameof(psg)]);
+
+			inputDevices = state[nameof(inputDevices)];
+			lightgunLatched = state[nameof(lightgunLatched)];
+
+			portMemoryControl = state[nameof(portMemoryControl)];
+			portIoControl = state[nameof(portIoControl)];
+			hCounterLatched = state[nameof(hCounterLatched)];
+
+			ReconfigureSystem();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			return new Dictionary<string, dynamic>
+			{
+				[nameof(configuration.TVStandard)] = configuration.TVStandard,
+				[nameof(configuration.Region)] = configuration.Region,
+
+				[nameof(bootstrap)] = SaveStateHandler.PerformGetState(bootstrap),
+				[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
+				[nameof(wram)] = wram,
+				[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
+				[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
+				[nameof(psg)] = SaveStateHandler.PerformGetState(psg),
+
+				[nameof(inputDevices)] = inputDevices,
+				[nameof(lightgunLatched)] = lightgunLatched,
+
+				[nameof(portMemoryControl)] = portMemoryControl,
+				[nameof(portIoControl)] = portIoControl,
+				[nameof(hCounterLatched)] = hCounterLatched
+			};
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			if (mapperType == null)
+				mapperType = typeof(SegaMapperCartridge);
+			if (ramData.Length == 0)
+				ramData = new byte[32768];
+
+			cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, ramData.Length });
+			cartridge.LoadRom(romData);
+			cartridge.LoadRam(ramData);
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge?.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			if (cartridge == null) return false;
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
+		}
+
+		public void RunStep()
+		{
+			double currentCpuClockCycles = 0.0;
+			currentCpuClockCycles += cpu.Step();
+
+			double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
+
+			vdp.Step((int)Math.Round(currentMasterClockCycles));
+
+			if (pauseButtonPressed)
+			{
+				pauseButtonPressed = false;
+				cpu.SetInterruptLine(InterruptType.NonMaskable, InterruptState.Assert);
+			}
+
+			cpu.SetInterruptLine(InterruptType.Maskable, vdp.InterruptLine);
+
+			psg.Step((int)Math.Round(currentCpuClockCycles));
+
+			cartridge?.Step((int)Math.Round(currentCpuClockCycles));
+
+			currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
+		}
+
+		private void HandlePauseButton()
+		{
+			var pausePressed = lastKeysDown.Contains(configuration.InputPause) || lastControllerState.IsStartPressed();
+			var pauseButtonHeld = pauseButtonToggle && pausePressed;
+			if (pausePressed)
+			{
+				if (!pauseButtonHeld) pauseButtonPressed = true;
+				pauseButtonToggle = true;
+			}
+			else if (pauseButtonToggle)
+				pauseButtonToggle = false;
+		}
+
+		private byte ReadInput(int port)
+		{
+			var state = (byte)0xFF;
+
+			switch (inputDevices[port])
+			{
+				case InputDevice.None:
+					/* Do nothing */
+					break;
+
+				case InputDevice.Controller:
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Up : configuration.Joypad2Up) || (port == 0 && lastControllerState.IsAnyUpDirectionPressed() && !lastControllerState.IsAnyDownDirectionPressed()))
+						state &= (byte)~ControllerInputs.Up;
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Down : configuration.Joypad2Down) || (port == 0 && lastControllerState.IsAnyDownDirectionPressed() && !lastControllerState.IsAnyUpDirectionPressed()))
+						state &= (byte)~ControllerInputs.Down;
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Left : configuration.Joypad2Left) || (port == 0 && lastControllerState.IsAnyLeftDirectionPressed() && !lastControllerState.IsAnyRightDirectionPressed()))
+						state &= (byte)~ControllerInputs.Left;
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Right : configuration.Joypad2Right) || (port == 0 && lastControllerState.IsAnyRightDirectionPressed() && !lastControllerState.IsAnyLeftDirectionPressed()))
+						state &= (byte)~ControllerInputs.Right;
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Button1 : configuration.Joypad2Button1) || (port == 0 && lastControllerState.IsAPressed()))
+						state &= (byte)~ControllerInputs.TL;
+					if (lastKeysDown.Contains(port == 0 ? configuration.Joypad1Button2 : configuration.Joypad2Button2) || (port == 0 && (lastControllerState.IsXPressed() || lastControllerState.IsBPressed())))
+						state &= (byte)~ControllerInputs.TR;
+					break;
+
+				case InputDevice.Lightgun:
+					if (GetIOControlDirection(port == 0 ? IOControlPort.A : IOControlPort.B, IOControlPin.TH, portIoControl) == IOControlDirection.Input)
+					{
+						var diffX = Math.Abs(lastMousePosition.x - (vdp.ReadPort(SegaSMSVDP.PortHCounter) << 1));
+						var diffY = Math.Abs(lastMousePosition.y - vdp.ReadPort(SegaSMSVDP.PortVCounter));
+
+						if ((diffY <= 5) && (diffX <= 60))
+						{
+							state &= (byte)~ControllerInputs.TH;
+							if (!lightgunLatched)
+							{
+								hCounterLatched = (byte)(lastMousePosition.x >> 1);
+								lightgunLatched = true;
+							}
+						}
+						else
+							lightgunLatched = false;
+					}
+
+					var mouseButton = port == 0 ? MouseButtons.Left : MouseButtons.Right;
+					if ((lastMouseButtons & mouseButton) == mouseButton)
+						state &= (byte)~ControllerInputs.TL;
+					break;
+			}
+
+			return state;
+		}
+
+		private byte ReadResetButton()
+		{
+			return (!lastKeysDown.Contains(configuration.InputReset) ? inputResetButton : (byte)0x00);
+		}
+
+		private IOControlDirection GetIOControlDirection(IOControlPort port, IOControlPin pin, byte data)
+		{
+			return (IOControlDirection)((data >> (0 | ((byte)port << 1) | (byte)pin)) & 0x01);
+		}
+
+		private IOControlOutputLevel GetIOControlOutputLevel(IOControlPort port, IOControlPin pin, byte data)
+		{
+			return (IOControlOutputLevel)((data >> (4 | ((byte)port << 1) | (byte)pin)) & 0x01);
+		}
+
+		private byte ReadIoPort(byte port)
+		{
+			if ((port & 0x01) == 0)
+			{
+				/* IO port A/B register */
+				var inputCtrlA = ReadInput(0);                              /* Read controller port A */
+				var inputCtrlB = ReadInput(1);                              /* Read controller port B */
+
+				if (configuration.Region == Region.Export)
+				{
+					/* Adjust TR according to direction/level */
+					if (GetIOControlDirection(IOControlPort.A, IOControlPin.TR, portIoControl) == IOControlDirection.Output)
+					{
+						inputCtrlA &= (byte)~ControllerInputs.TR;
+						if (GetIOControlOutputLevel(IOControlPort.A, IOControlPin.TR, portIoControl) == IOControlOutputLevel.High)
+							inputCtrlA |= (byte)ControllerInputs.TR;
+					}
+				}
+
+				var portState = (byte)(inputCtrlA & 0x3F);                  /* Controller port A (bits 0-5, into bits 0-5) */
+				portState |= (byte)((inputCtrlB & 0x03) << 6);              /* Controller port B (bits 0-1, into bits 6-7) */
+
+				return portState;
+			}
+			else
+			{
+				/* IO port B/misc register */
+				var inputCtrlA = ReadInput(0);                              /* Read controller port A */
+				var inputCtrlB = ReadInput(1);                              /* Read controller port B */
+
+				if (configuration.Region == Region.Export)
+				{
+					/* Adjust TR and THx according to direction/level */
+					if (GetIOControlDirection(IOControlPort.B, IOControlPin.TR, portIoControl) == IOControlDirection.Output)
+					{
+						inputCtrlB &= (byte)~ControllerInputs.TR;
+						if (GetIOControlOutputLevel(IOControlPort.B, IOControlPin.TR, portIoControl) == IOControlOutputLevel.High)
+							inputCtrlB |= (byte)ControllerInputs.TR;
+					}
+					if (GetIOControlDirection(IOControlPort.A, IOControlPin.TH, portIoControl) == IOControlDirection.Output)
+					{
+						inputCtrlA &= (byte)~ControllerInputs.TH;
+						if (GetIOControlOutputLevel(IOControlPort.A, IOControlPin.TH, portIoControl) == IOControlOutputLevel.High)
+							inputCtrlA |= (byte)ControllerInputs.TH;
+					}
+					if (GetIOControlDirection(IOControlPort.B, IOControlPin.TH, portIoControl) == IOControlDirection.Output)
+					{
+						inputCtrlB &= (byte)~ControllerInputs.TH;
+						if (GetIOControlOutputLevel(IOControlPort.B, IOControlPin.TH, portIoControl) == IOControlOutputLevel.High)
+							inputCtrlB |= (byte)ControllerInputs.TH;
+					}
+				}
+
+				var portState = (byte)((inputCtrlB & 0x3F) >> 2);           /* Controller port B (bits 2-5, into bits 0-3) */
+				portState |= ReadResetButton();                             /* Reset button (bit 4, into bit 4) */
+				portState |= 0b00100000;                                    /* Cartridge slot CONT pin (bit 5, into bit 5) */
+				portState |= (byte)(((inputCtrlA >> 6) & 0x01) << 6);       /* Controller port A TH pin (bit 6, into bit 6) */
+				portState |= (byte)(((inputCtrlB >> 6) & 0x01) << 7);       /* Controller port B TH pin (bit 6, into bit 7) */
+
+				return portState;
+			}
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0xBFFF)
+			{
+				if (isBootstrapRomEnabled && bootstrap != null)
+					return bootstrap.Read(address);
+
+				if (isCartridgeSlotEnabled && cartridge != null)
+					return cartridge.Read(address);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				if (isWorkRamEnabled)
+					return wram[address & (ramSize - 1)];
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (isBootstrapRomEnabled) bootstrap?.Write(address, value);
+			if (isCartridgeSlotEnabled) cartridge?.Write(address, value);
+
+			if (isWorkRamEnabled && address >= 0xC000 && address <= 0xFFFF)
+				wram[address & (ramSize - 1)] = value;
+		}
+
+		private byte ReadPort(byte port)
+		{
+			port = (byte)(port & 0xC1);
+
+			switch (port & 0xF0)
+			{
+				case 0x00:
+					/* Behave like SMS2 */
+					return 0xFF;
+
+				case 0x40:
+					/* Counters */
+					if ((port & 0x01) == 0)
+						return vdp.ReadPort(port);      /* V counter */
+					else
+						return hCounterLatched;         /* H counter */
+
+				case 0x80:
+					return vdp.ReadPort(port);          /* VDP ports */
+
+				case 0xC0:
+					return ReadIoPort(port);            /* IO ports */
+
+				default:
+					// TODO: handle properly
+					return 0x00;
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			port = (byte)(port & 0xC1);
+
+			switch (port & 0xF0)
+			{
+				case 0x00:
+					/* System stuff */
+					if ((port & 0x01) == 0)
+					{
+						/* Memory control */
+
+						// NOTE: Sonic Chaos June 30 prototype writes 0xFF to port 0x06; mirroring causes write to memory control, which causes the game to disable all memory access
+						if (configuration.AllowMemoryControl)
+							portMemoryControl = value;
+					}
+					else
+					{
+						/* I/O control */
+						if ((GetIOControlDirection(IOControlPort.A, IOControlPin.TH, value) == IOControlDirection.Input &&
+							GetIOControlOutputLevel(IOControlPort.A, IOControlPin.TH, value) == IOControlOutputLevel.High &&
+							GetIOControlOutputLevel(IOControlPort.A, IOControlPin.TH, portIoControl) == IOControlOutputLevel.Low) ||
+							(GetIOControlDirection(IOControlPort.B, IOControlPin.TH, value) == IOControlDirection.Input &&
+							GetIOControlOutputLevel(IOControlPort.B, IOControlPin.TH, value) == IOControlOutputLevel.High &&
+							GetIOControlOutputLevel(IOControlPort.B, IOControlPin.TH, portIoControl) == IOControlOutputLevel.Low))
+						{
+							/* TH is input and transition is Low->High, latch HCounter */
+							hCounterLatched = vdp.ReadPort(SegaSMSVDP.PortHCounter);
+						}
+
+						portIoControl = value;
+					}
+					break;
+
+				case 0x40:
+					/* PSG */
+					psg.WritePort(port, value);
+					break;
+
+				case 0x80:
+					/* VDP */
+					vdp.WritePort(port, value);
+					break;
+
+				case 0xC0:
+					/* No effect */
+					break;
+
+				default:
+					// TODO: handle properly
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs.meta
new file mode 100644
index 0000000..f4de8b9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/MasterSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3c4298b2c8d87b948bca1a380584a60f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs b/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs
new file mode 100644
index 0000000..53917b2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs
@@ -0,0 +1,613 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges;
+using Essgee.Emulation.Cartridges.Sega;
+using Essgee.Emulation.Peripherals;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	/* TODO: verify everything, the SC-3000 isn't that well-documented...
+	 * 
+	 * Tape cassette notes: PPI port B bit 7 == input from cassette, port C bit 4 == output to cassette
+	 */
+
+	[MachineIndex(1)]
+	public class SC3000 : IMachine
+	{
+		const double masterClockNtsc = 10738635;
+		const double masterClockPal = 10640684;
+		const double refreshRateNtsc = 59.922743;
+		const double refreshRatePal = 49.701459;
+
+		const int ramSize = 1 * 2048;
+
+		double masterClock;
+		double vdpClock, psgClock;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { vdp.RenderScreen += value; }
+			remove { vdp.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { vdp.SizeScreen += value; }
+			remove { vdp.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { psg.EnqueueSamples += value; }
+			remove { psg.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
+
+		public string ManufacturerName => "Sega";
+		public string ModelName => "SC-3000";
+		public string DatFilename => "Sega - SG-1000.dat";      // TODO: SC-3000 .dat does not exist?
+		public (string Extension, string Description) FileFilter => (".sc", "SC-3000 ROMs");
+		public bool HasBootstrap => false;
+		public double RefreshRate { get; private set; }
+		public double PixelAspectRatio => 8.0 / 7.0;
+		public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
+
+		ICartridge cartridge;
+		byte[] wram;
+		Z80A cpu;
+		TMS99xxA vdp;
+		SN76489 psg;
+		Intel8255 ppi;
+		bool[,] keyboard;
+
+		[Flags]
+		enum PortAInputs : byte
+		{
+			P1Up = (1 << 0),
+			P1Down = (1 << 1),
+			P1Left = (1 << 2),
+			P1Right = (1 << 3),
+			P1Button1 = (1 << 4),
+			P1Button2 = (1 << 5),
+			P2Up = (1 << 6),
+			P2Down = (1 << 7),
+		}
+
+		[Flags]
+		enum PortBInputs : byte
+		{
+			P2Left = (1 << 0),
+			P2Right = (1 << 1),
+			P2Button1 = (1 << 2),
+			P2Button2 = (1 << 3),
+		}
+
+		PortAInputs portAInputsPressed;
+		PortBInputs portBInputsPressed;
+
+		bool resetButtonPressed, resetButtonToggle;
+
+		bool keyboardMode;
+		bool changeInputButtonPressed;
+
+		enum TapeUpdateModes
+		{
+			Reading,
+			Writing
+		}
+
+		bool isTapePlaying;
+		bool tapePlayButtonPressed;
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.SC3000 configuration;
+
+		public SC3000() { }
+
+		public void Initialize()
+		{
+			cartridge = null;
+			wram = new byte[ramSize];
+			cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
+			vdp = new TMS99xxA();
+			psg = new SN76489();
+			ppi = new Intel8255();
+			keyboard = new bool[12, 8];
+
+			vdp.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.SC3000)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return vdp.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return psg.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				vdp.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				psg.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			if (configuration.TVStandard == TVStandard.NTSC)
+			{
+				masterClock = masterClockNtsc;
+				RefreshRate = refreshRateNtsc;
+			}
+			else
+			{
+				masterClock = masterClockPal;
+				RefreshRate = refreshRatePal;
+			}
+
+			vdpClock = (masterClock / 1.0);
+			psgClock = (masterClock / 3.0);
+
+			vdp?.SetClockRate(vdpClock);
+			vdp?.SetRefreshRate(RefreshRate);
+			vdp?.SetRevision(0);
+
+			psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			psg?.SetOutputChannels(2);
+			psg?.SetClockRate(psgClock);
+			psg?.SetRefreshRate(RefreshRate);
+
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / RefreshRate);
+
+			OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
+		}
+
+		public void Startup()
+		{
+			cpu.Startup();
+			vdp.Startup();
+			psg.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			cpu.SetStackPointer(0xDFF0);
+			vdp.Reset();
+			psg.Reset();
+			ppi.Reset();
+
+			for (int i = 0; i < keyboard.GetLength(0); i++)
+				for (int j = 0; j < keyboard.GetLength(1); j++)
+					keyboard[i, j] = false;
+
+			portAInputsPressed = 0;
+			portBInputsPressed = 0;
+
+			resetButtonPressed = resetButtonToggle = false;
+
+			keyboardMode = true;
+			changeInputButtonPressed = false;
+
+			isTapePlaying = false;
+			tapePlayButtonPressed = false;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			cpu?.Shutdown();
+			vdp?.Shutdown();
+			psg?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			configuration.TVStandard = state[nameof(configuration.TVStandard)];
+
+			SaveStateHandler.PerformSetState(cartridge, state[nameof(cartridge)]);
+			wram = state[nameof(wram)];
+			SaveStateHandler.PerformSetState(cpu, state[nameof(cpu)]);
+			SaveStateHandler.PerformSetState(vdp, state[nameof(vdp)]);
+			SaveStateHandler.PerformSetState(psg, state[nameof(psg)]);
+			SaveStateHandler.PerformSetState(ppi, state[nameof(ppi)]);
+			keyboard = state[nameof(keyboard)];
+
+			ReconfigureSystem();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			return new Dictionary<string, dynamic>
+			{
+				[nameof(configuration.TVStandard)] = configuration.TVStandard,
+
+				[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
+				[nameof(wram)] = wram,
+				[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
+				[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
+				[nameof(psg)] = SaveStateHandler.PerformGetState(psg),
+				[nameof(ppi)] = SaveStateHandler.PerformGetState(ppi),
+				[nameof(keyboard)] = keyboard
+			};
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			if (mapperType == null)
+				mapperType = typeof(SegaSGCartridge);
+
+			cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, ramData.Length });
+			cartridge.LoadRom(romData);
+			cartridge.LoadRam(ramData);
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
+		}
+
+		public void RunStep()
+		{
+			double currentCpuClockCycles = 0.0;
+			currentCpuClockCycles += cpu.Step();
+
+			double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
+
+			vdp.Step((int)Math.Round(currentMasterClockCycles));
+
+			if (resetButtonPressed)
+			{
+				resetButtonPressed = false;
+				cpu.SetInterruptLine(InterruptType.NonMaskable, InterruptState.Assert);
+			}
+
+			cpu.SetInterruptLine(InterruptType.Maskable, vdp.InterruptLine);
+
+			psg.Step((int)Math.Round(currentCpuClockCycles));
+
+			cartridge?.Step((int)Math.Round(currentCpuClockCycles));
+
+			currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			/* Get variables */
+			var keysDown = eventArgs.Keyboard;
+
+			/* Handle reset button */
+			var resetPressed = keysDown.Contains(configuration.InputReset);
+			var resetButtonHeld = (resetButtonToggle && resetPressed);
+			if (resetPressed)
+			{
+				if (!resetButtonHeld) resetButtonPressed = true;
+				resetButtonToggle = true;
+			}
+			else if (resetButtonToggle)
+				resetButtonToggle = false;
+
+			/* Toggle input mode (keyboard or controllers) */
+			if (keysDown.Contains(configuration.InputChangeMode) && !changeInputButtonPressed)
+			{
+				keyboardMode = !keyboardMode;
+				var modeString = (keyboardMode ? "keyboard" : "controller");
+				SendLogMessage(this, new SendLogMessageEventArgs($"Selected {modeString} mode."));
+			}
+			changeInputButtonPressed = keysDown.Contains(configuration.InputChangeMode);
+
+			/* Toggle tape playback */
+			if (keysDown.Contains(configuration.InputPlayTape) && !tapePlayButtonPressed)
+			{
+				isTapePlaying = !isTapePlaying;
+				var playString = (isTapePlaying ? "playing" : "stopped");
+				SendLogMessage(this, new SendLogMessageEventArgs($"Tape is {playString}."));
+			}
+			tapePlayButtonPressed = keysDown.Contains(configuration.InputPlayTape);
+
+			if (keyboardMode)
+			{
+				/* Handle keyboard */
+
+				// TODO: Replace hardcoded English layout w/ user-configurable settings
+				keyboard[0, 0] = keysDown.Contains(Keys.D1);
+				keyboard[0, 1] = keysDown.Contains(Keys.D2);
+				keyboard[0, 2] = keysDown.Contains(Keys.D3);
+				keyboard[0, 3] = keysDown.Contains(Keys.D4);
+				keyboard[0, 4] = keysDown.Contains(Keys.D5);
+				keyboard[0, 5] = keysDown.Contains(Keys.D6);
+				keyboard[0, 6] = keysDown.Contains(Keys.D7);
+
+				keyboard[1, 0] = keysDown.Contains(Keys.Q);
+				keyboard[1, 1] = keysDown.Contains(Keys.W);
+				keyboard[1, 2] = keysDown.Contains(Keys.E);
+				keyboard[1, 3] = keysDown.Contains(Keys.R);
+				keyboard[1, 4] = keysDown.Contains(Keys.T);
+				keyboard[1, 5] = keysDown.Contains(Keys.Y);
+				keyboard[1, 6] = keysDown.Contains(Keys.U);
+
+				keyboard[2, 0] = keysDown.Contains(Keys.A);
+				keyboard[2, 1] = keysDown.Contains(Keys.S);
+				keyboard[2, 2] = keysDown.Contains(Keys.D);
+				keyboard[2, 3] = keysDown.Contains(Keys.F);
+				keyboard[2, 4] = keysDown.Contains(Keys.G);
+				keyboard[2, 5] = keysDown.Contains(Keys.H);
+				keyboard[2, 6] = keysDown.Contains(Keys.J);
+
+				keyboard[3, 0] = keysDown.Contains(Keys.Z);
+				keyboard[3, 1] = keysDown.Contains(Keys.X);
+				keyboard[3, 2] = keysDown.Contains(Keys.C);
+				keyboard[3, 3] = keysDown.Contains(Keys.V);
+				keyboard[3, 4] = keysDown.Contains(Keys.B);
+				keyboard[3, 5] = keysDown.Contains(Keys.N);
+				keyboard[3, 6] = keysDown.Contains(Keys.M);
+
+				keyboard[4, 0] = keysDown.Contains(Keys.None);             // Alphanumerics, Eng Dier's
+				keyboard[4, 1] = keysDown.Contains(Keys.Space);
+				keyboard[4, 2] = keysDown.Contains(Keys.Home);             // Clr, Home
+				keyboard[4, 3] = keysDown.Contains(Keys.Back);             // Del, Ins
+				keyboard[4, 4] = keysDown.Contains(Keys.None);             // Not on English keyboard?
+				keyboard[4, 5] = keysDown.Contains(Keys.None);             // ""
+				keyboard[4, 6] = keysDown.Contains(Keys.None);             // ""
+
+				keyboard[5, 0] = keysDown.Contains(Keys.Oemcomma);
+				keyboard[5, 1] = keysDown.Contains(Keys.OemPeriod);
+				keyboard[5, 2] = keysDown.Contains(Keys.OemQuestion);      // Forward slash
+				keyboard[5, 3] = keysDown.Contains(Keys.None);             // Pi
+				keyboard[5, 4] = keysDown.Contains(Keys.Down);
+				keyboard[5, 5] = keysDown.Contains(Keys.Left);
+				keyboard[5, 6] = keysDown.Contains(Keys.Right);
+
+				keyboard[6, 0] = keysDown.Contains(Keys.K);
+				keyboard[6, 1] = keysDown.Contains(Keys.L);
+				keyboard[6, 2] = keysDown.Contains(Keys.Oemplus);          // Semicolon
+				keyboard[6, 3] = keysDown.Contains(Keys.OemSemicolon);     // Colon
+				keyboard[6, 4] = keysDown.Contains(Keys.OemCloseBrackets);
+				keyboard[6, 5] = keysDown.Contains(Keys.Enter);
+				keyboard[6, 6] = keysDown.Contains(Keys.Up);
+
+				keyboard[7, 0] = keysDown.Contains(Keys.I);
+				keyboard[7, 1] = keysDown.Contains(Keys.O);
+				keyboard[7, 2] = keysDown.Contains(Keys.P);
+				keyboard[7, 3] = keysDown.Contains(Keys.PageUp);           // @
+				keyboard[7, 4] = keysDown.Contains(Keys.OemOpenBrackets);
+				keyboard[7, 5] = keysDown.Contains(Keys.None);             // Not on English keyboard?
+				keyboard[7, 6] = keysDown.Contains(Keys.None);             // ""
+
+				keyboard[8, 0] = keysDown.Contains(Keys.D8);
+				keyboard[8, 1] = keysDown.Contains(Keys.D9);
+				keyboard[8, 2] = keysDown.Contains(Keys.D0);
+				keyboard[8, 3] = keysDown.Contains(Keys.OemMinus);
+				keyboard[8, 4] = keysDown.Contains(Keys.Oemtilde);         // ^, ~
+				keyboard[8, 5] = keysDown.Contains(Keys.OemPipe);          // Yen, Pipe, Pound?
+				keyboard[8, 6] = keysDown.Contains(Keys.PageDown);         // Break
+
+				keyboard[9, 6] = keysDown.Contains(Keys.RControlKey);      // Graph
+
+				keyboard[10, 6] = keysDown.Contains(Keys.LControlKey);     // Ctrl
+
+				keyboard[11, 5] = keysDown.Contains(Keys.Tab);             // Func
+				keyboard[11, 6] = keysDown.Contains(Keys.ShiftKey);        // Shift
+			}
+			else
+			{
+				/* Handle controllers */
+				portAInputsPressed = 0;
+				portBInputsPressed = 0;
+
+				/* Keyboard */
+				if (keysDown.Contains(configuration.Joypad1Up)) portAInputsPressed |= PortAInputs.P1Up;
+				if (keysDown.Contains(configuration.Joypad1Down)) portAInputsPressed |= PortAInputs.P1Down;
+				if (keysDown.Contains(configuration.Joypad1Left)) portAInputsPressed |= PortAInputs.P1Left;
+				if (keysDown.Contains(configuration.Joypad1Right)) portAInputsPressed |= PortAInputs.P1Right;
+				if (keysDown.Contains(configuration.Joypad1Button1)) portAInputsPressed |= PortAInputs.P1Button1;
+				if (keysDown.Contains(configuration.Joypad1Button2)) portAInputsPressed |= PortAInputs.P1Button2;
+				if (keysDown.Contains(configuration.Joypad2Up)) portAInputsPressed |= PortAInputs.P2Up;
+				if (keysDown.Contains(configuration.Joypad2Down)) portAInputsPressed |= PortAInputs.P2Down;
+				if (keysDown.Contains(configuration.Joypad2Left)) portBInputsPressed |= PortBInputs.P2Left;
+				if (keysDown.Contains(configuration.Joypad2Right)) portBInputsPressed |= PortBInputs.P2Right;
+				if (keysDown.Contains(configuration.Joypad2Button1)) portBInputsPressed |= PortBInputs.P2Button1;
+				if (keysDown.Contains(configuration.Joypad2Button2)) portBInputsPressed |= PortBInputs.P2Button2;
+
+				/* XInput controller */
+				if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) portAInputsPressed |= PortAInputs.P1Up;
+				if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) portAInputsPressed |= PortAInputs.P1Down;
+				if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) portAInputsPressed |= PortAInputs.P1Left;
+				if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) portAInputsPressed |= PortAInputs.P1Right;
+				if (eventArgs.ControllerState.IsAPressed()) portAInputsPressed |= PortAInputs.P1Button1;
+				if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) portAInputsPressed |= PortAInputs.P1Button2;
+			}
+		}
+
+		private void UpdateInput()
+		{
+			byte portA = 0xFF, portB = 0xFF;
+			byte row = (byte)(ppi.PortCOutput & 0x07);
+
+			if (row == 0x07)
+			{
+				/* Controller ports */
+				portA &= (byte)~portAInputsPressed;
+				portB &= (byte)~portBInputsPressed;
+			}
+			else
+			{
+				/* Keyboard matrix */
+				for (int i = 0; i < 8; i++)
+					if (keyboard[i, row]) portA &= (byte)~(1 << i);
+
+				for (int i = 0; i < 4; i++)
+					if (keyboard[8 + i, row]) portB &= (byte)~(1 << i);
+			}
+			ppi.PortAInput = portA;
+			ppi.PortBInput = (byte)((ppi.PortBInput & 0xF0) | (portB & 0x0F));
+		}
+
+		private void UpdateTape(TapeUpdateModes updateMode)
+		{
+			if (!isTapePlaying) return;
+
+			// TODO: errr, try to actually emulate this? so far just seems to write repeating bit patterns, no ex. recognizable basic program data...
+
+			switch (updateMode)
+			{
+				case TapeUpdateModes.Reading:
+					var read = ((ppi.PortBInput >> 7) & 0b1);   // TODO: correct?
+
+					//
+					break;
+
+				case TapeUpdateModes.Writing:
+					var write = ((ppi.PortCOutput >> 4) & 0b1); // TODO: correct?
+
+					//
+					break;
+			}
+		}
+
+		/* Basic memory maps (via SC-3000 Service Manual, chp 2-8)
+		 *
+		 *      IIa     IIb     IIIa    IIIb
+		 * 8000 --      CartRAM CartRAM CartRAM
+		 * 8800 --      --      CartRAM CartRAM
+		 * C000 WRAM    WRAM    WRAM    CartRAM
+		 * C800 --      --      --      CartRAM
+		 */
+
+		private byte ReadMemory(ushort address)
+		{
+			if (cartridge != null && address >= cartridge.GetLowerBound() && address <= cartridge.GetUpperBound())
+			{
+				return cartridge.Read(address);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				return wram[address & (ramSize - 1)];
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (cartridge != null && address >= cartridge.GetLowerBound() && address <= cartridge.GetUpperBound())
+			{
+				cartridge.Write(address, value);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				wram[address & (ramSize - 1)] = value;
+			}
+		}
+
+		private byte ReadPort(byte port)
+		{
+			switch (port & 0xC0)
+			{
+				case 0x80:
+					return vdp.ReadPort(port);
+
+				case 0xC0:
+					UpdateTape(TapeUpdateModes.Reading);
+					UpdateInput();
+					return ppi.ReadPort(port);
+
+				default:
+					// TODO: handle properly
+					return 0x00;
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			switch (port & 0xC0)
+			{
+				case 0x40:
+					psg.WritePort(port, value);
+					break;
+
+				case 0x80:
+					vdp.WritePort(port, value);
+					break;
+
+				case 0xC0:
+					ppi.WritePort(port, value);
+					UpdateTape(TapeUpdateModes.Writing);
+					break;
+
+				default:
+					// TODO: handle properly
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs.meta
new file mode 100644
index 0000000..2208159
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/SC3000.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2df7595ac4e8eff45a21dab6630ec56c
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs b/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs
new file mode 100644
index 0000000..5e83208
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs
@@ -0,0 +1,414 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Emulation.Configuration;
+using Essgee.Emulation.CPU;
+using Essgee.Emulation.Video;
+using Essgee.Emulation.Audio;
+using Essgee.Emulation.Cartridges;
+using Essgee.Emulation.Cartridges.Sega;
+using Essgee.EventArguments;
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Machines
+{
+	[MachineIndex(0)]
+	public class SG1000 : IMachine
+	{
+		// TODO: verify port 0xC0-0xFF behavior wrt the lack of a PPI and the SG-1000 Test Cartridge Extension Port test?
+
+		const double masterClockNtsc = 10738635;
+		const double masterClockPal = 10640684;
+		const double refreshRateNtsc = 59.922743;
+		const double refreshRatePal = 49.701459;
+
+		const int ramSize = 1 * 1024;
+
+		double masterClock;
+		double vdpClock, psgClock;
+
+		public event EventHandler<SendLogMessageEventArgs> SendLogMessage;
+		protected virtual void OnSendLogMessage(SendLogMessageEventArgs e) { SendLogMessage?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EmulationReset;
+		protected virtual void OnEmulationReset(EventArgs e) { EmulationReset?.Invoke(this, e); }
+
+		public event EventHandler<RenderScreenEventArgs> RenderScreen
+		{
+			add { vdp.RenderScreen += value; }
+			remove { vdp.RenderScreen -= value; }
+		}
+
+		public event EventHandler<SizeScreenEventArgs> SizeScreen
+		{
+			add { vdp.SizeScreen += value; }
+			remove { vdp.SizeScreen -= value; }
+		}
+
+		public event EventHandler<ChangeViewportEventArgs> ChangeViewport;
+		protected virtual void OnChangeViewport(ChangeViewportEventArgs e) { ChangeViewport?.Invoke(this, e); }
+
+		public event EventHandler<PollInputEventArgs> PollInput;
+		protected virtual void OnPollInput(PollInputEventArgs e) { PollInput?.Invoke(this, e); }
+
+		public event EventHandler<EnqueueSamplesEventArgs> EnqueueSamples
+		{
+			add { psg.EnqueueSamples += value; }
+			remove { psg.EnqueueSamples -= value; }
+		}
+
+		public event EventHandler<SaveExtraDataEventArgs> SaveExtraData;
+		protected virtual void OnSaveExtraData(SaveExtraDataEventArgs e) { SaveExtraData?.Invoke(this, e); }
+
+		public event EventHandler<EventArgs> EnableRumble { add { } remove { } }
+
+		public string ManufacturerName => "Sega";
+		public string ModelName => "SG-1000";
+		public string DatFilename => "Sega - SG-1000.dat";
+		public (string Extension, string Description) FileFilter => (".sg", "SG-1000 ROMs");
+		public bool HasBootstrap => false;
+		public double RefreshRate { get; private set; }
+		public double PixelAspectRatio => 8.0 / 7.0;
+		public (string Name, string Description)[] RuntimeOptions => vdp.RuntimeOptions.Concat(psg.RuntimeOptions).ToArray();
+
+		ICartridge cartridge;
+		byte[] wram;
+		Z80A cpu;
+		TMS99xxA vdp;
+		SN76489 psg;
+
+		[Flags]
+		enum PortIoABValues : byte
+		{
+			P1Up = 0b00000001,
+			P1Down = 0b00000010,
+			P1Left = 0b00000100,
+			P1Right = 0b00001000,
+			P1Button1 = 0b00010000,
+			P1Button2 = 0b00100000,
+			P2Up = 0b01000000,
+			P2Down = 0b10000000,
+			Mask = 0b11111111
+		}
+
+		[Flags]
+		enum PortIoBMiscValues : byte
+		{
+			P2Left = 0b00000001,
+			P2Right = 0b00000010,
+			P2Button1 = 0b00000100,
+			P2Button2 = 0b00001000,
+			CON = 0b00010000,
+			IC21Pin6 = 0b00100000,
+			IC21Pin10 = 0b01000000,
+			IC21Pin13 = 0b10000000,
+			Mask = 0b11111111
+		}
+
+		PortIoABValues portIoABPressed;
+		PortIoBMiscValues portIoBMiscPressed;
+
+		bool pauseButtonPressed, pauseButtonToggle;
+
+		int currentMasterClockCyclesInFrame, totalMasterClockCyclesInFrame;
+
+		Configuration.SG1000 configuration;
+
+		public SG1000() { }
+
+		public void Initialize()
+		{
+			cartridge = null;
+			wram = new byte[ramSize];
+			cpu = new Z80A(ReadMemory, WriteMemory, ReadPort, WritePort);
+			vdp = new TMS99xxA();
+			psg = new SN76489();
+
+			vdp.EndOfScanline += (s, e) =>
+			{
+				PollInputEventArgs pollInputEventArgs = new PollInputEventArgs();
+				OnPollInput(pollInputEventArgs);
+				ParseInput(pollInputEventArgs);
+			};
+		}
+
+		public void SetConfiguration(IConfiguration config)
+		{
+			configuration = (Configuration.SG1000)config;
+
+			ReconfigureSystem();
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			if (name.StartsWith("Graphics"))
+				return vdp.GetRuntimeOption(name);
+			else if (name.StartsWith("Audio"))
+				return psg.GetRuntimeOption(name);
+			else
+				return null;
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			if (name.StartsWith("Graphics"))
+				vdp.SetRuntimeOption(name, value);
+			else if (name.StartsWith("Audio"))
+				psg.SetRuntimeOption(name, value);
+		}
+
+		private void ReconfigureSystem()
+		{
+			if (configuration.TVStandard == TVStandard.NTSC)
+			{
+				masterClock = masterClockNtsc;
+				RefreshRate = refreshRateNtsc;
+			}
+			else
+			{
+				masterClock = masterClockPal;
+				RefreshRate = refreshRatePal;
+			}
+
+			vdpClock = (masterClock / 1.0);
+			psgClock = (masterClock / 3.0);
+
+			vdp?.SetClockRate(vdpClock);
+			vdp?.SetRefreshRate(RefreshRate);
+			vdp?.SetRevision(0);
+
+			psg?.SetSampleRate(StandInfo.Configuration.SampleRate);
+			psg?.SetOutputChannels(2);
+			psg?.SetClockRate(psgClock);
+			psg?.SetRefreshRate(RefreshRate);
+
+			currentMasterClockCyclesInFrame = 0;
+			totalMasterClockCyclesInFrame = (int)Math.Round(masterClock / RefreshRate);
+
+			OnChangeViewport(new ChangeViewportEventArgs(vdp.Viewport));
+		}
+
+		public void Startup()
+		{
+			cpu.Startup();
+			vdp.Startup();
+			psg.Startup();
+		}
+
+		public void Reset()
+		{
+			cpu.Reset();
+			cpu.SetStackPointer(0xDFF0);
+			vdp.Reset();
+			psg.Reset();
+
+			portIoABPressed = 0;
+			portIoBMiscPressed = 0;
+
+			pauseButtonPressed = pauseButtonToggle = false;
+
+			OnEmulationReset(EventArgs.Empty);
+		}
+
+		public void Shutdown()
+		{
+			cpu?.Shutdown();
+			vdp?.Shutdown();
+			psg?.Shutdown();
+		}
+
+		public void SetState(Dictionary<string, dynamic> state)
+		{
+			configuration.TVStandard = state[nameof(configuration.TVStandard)];
+
+			SaveStateHandler.PerformSetState(cartridge, state[nameof(cartridge)]);
+			wram = state[nameof(wram)];
+			SaveStateHandler.PerformSetState(cpu, state[nameof(cpu)]);
+			SaveStateHandler.PerformSetState(vdp, state[nameof(vdp)]);
+			SaveStateHandler.PerformSetState(psg, state[nameof(psg)]);
+
+			ReconfigureSystem();
+		}
+
+		public Dictionary<string, dynamic> GetState()
+		{
+			return new Dictionary<string, dynamic>
+			{
+				[nameof(configuration.TVStandard)] = configuration.TVStandard,
+
+				[nameof(cartridge)] = SaveStateHandler.PerformGetState(cartridge),
+				[nameof(wram)] = wram,
+				[nameof(cpu)] = SaveStateHandler.PerformGetState(cpu),
+				[nameof(vdp)] = SaveStateHandler.PerformGetState(vdp),
+				[nameof(psg)] = SaveStateHandler.PerformGetState(psg)
+			};
+		}
+
+		public Dictionary<string, dynamic> GetDebugInformation()
+		{
+			var dict = new Dictionary<string, dynamic>
+			{
+				{ "CyclesInFrame", currentMasterClockCyclesInFrame },
+			};
+
+			return dict;
+		}
+
+		public void Load(byte[] romData, byte[] ramData, Type mapperType)
+		{
+			if (mapperType == null)
+				mapperType = typeof(SegaSGCartridge);
+
+			cartridge = (ICartridge)Activator.CreateInstance(mapperType, new object[] { romData.Length, ramData.Length });
+			cartridge.LoadRom(romData);
+			cartridge.LoadRam(ramData);
+		}
+
+		public byte[] GetCartridgeRam()
+		{
+			return cartridge.GetRamData();
+		}
+
+		public bool IsCartridgeRamSaveNeeded()
+		{
+			return cartridge.IsRamSaveNeeded();
+		}
+
+		public virtual void RunFrame()
+		{
+			while (currentMasterClockCyclesInFrame < totalMasterClockCyclesInFrame)
+				RunStep();
+
+			currentMasterClockCyclesInFrame -= totalMasterClockCyclesInFrame;
+		}
+
+		public void RunStep()
+		{
+			double currentCpuClockCycles = 0.0;
+			currentCpuClockCycles += cpu.Step();
+
+			double currentMasterClockCycles = (currentCpuClockCycles * 3.0);
+
+			vdp.Step((int)Math.Round(currentMasterClockCycles));
+
+			if (pauseButtonPressed)
+			{
+				pauseButtonPressed = false;
+				cpu.SetInterruptLine(InterruptType.NonMaskable, InterruptState.Assert);
+			}
+
+			cpu.SetInterruptLine(InterruptType.Maskable, vdp.InterruptLine);
+
+			psg.Step((int)Math.Round(currentCpuClockCycles));
+
+			cartridge?.Step((int)Math.Round(currentCpuClockCycles));
+
+			currentMasterClockCyclesInFrame += (int)Math.Round(currentMasterClockCycles);
+		}
+
+		private void ParseInput(PollInputEventArgs eventArgs)
+		{
+			/* Get variables */
+			var keysDown = eventArgs.Keyboard;
+
+			/* Handle Pause button */
+			var pausePressed = keysDown.Contains(configuration.InputPause);
+			var pauseButtonHeld = (pauseButtonToggle && pausePressed);
+			if (pausePressed)
+			{
+				if (!pauseButtonHeld) pauseButtonPressed = true;
+				pauseButtonToggle = true;
+			}
+			else if (pauseButtonToggle)
+				pauseButtonToggle = false;
+
+			/* Handle controllers */
+			portIoABPressed = 0;
+			portIoBMiscPressed = 0;
+
+			/* Keyboard */
+			if (keysDown.Contains(configuration.Joypad1Up)) portIoABPressed |= PortIoABValues.P1Up;
+			if (keysDown.Contains(configuration.Joypad1Down)) portIoABPressed |= PortIoABValues.P1Down;
+			if (keysDown.Contains(configuration.Joypad1Left)) portIoABPressed |= PortIoABValues.P1Left;
+			if (keysDown.Contains(configuration.Joypad1Right)) portIoABPressed |= PortIoABValues.P1Right;
+			if (keysDown.Contains(configuration.Joypad1Button1)) portIoABPressed |= PortIoABValues.P1Button1;
+			if (keysDown.Contains(configuration.Joypad1Button2)) portIoABPressed |= PortIoABValues.P1Button2;
+			if (keysDown.Contains(configuration.Joypad2Up)) portIoABPressed |= PortIoABValues.P2Up;
+			if (keysDown.Contains(configuration.Joypad2Down)) portIoABPressed |= PortIoABValues.P2Down;
+			if (keysDown.Contains(configuration.Joypad2Left)) portIoBMiscPressed |= PortIoBMiscValues.P2Left;
+			if (keysDown.Contains(configuration.Joypad2Right)) portIoBMiscPressed |= PortIoBMiscValues.P2Right;
+			if (keysDown.Contains(configuration.Joypad2Button1)) portIoBMiscPressed |= PortIoBMiscValues.P2Button1;
+			if (keysDown.Contains(configuration.Joypad2Button2)) portIoBMiscPressed |= PortIoBMiscValues.P2Button2;
+
+			/* XInput controller */
+			if (eventArgs.ControllerState.IsAnyUpDirectionPressed() && !eventArgs.ControllerState.IsAnyDownDirectionPressed()) portIoABPressed |= PortIoABValues.P1Up;
+			if (eventArgs.ControllerState.IsAnyDownDirectionPressed() && !eventArgs.ControllerState.IsAnyUpDirectionPressed()) portIoABPressed |= PortIoABValues.P1Down;
+			if (eventArgs.ControllerState.IsAnyLeftDirectionPressed() && !eventArgs.ControllerState.IsAnyRightDirectionPressed()) portIoABPressed |= PortIoABValues.P1Left;
+			if (eventArgs.ControllerState.IsAnyRightDirectionPressed() && !eventArgs.ControllerState.IsAnyLeftDirectionPressed()) portIoABPressed |= PortIoABValues.P1Right;
+			if (eventArgs.ControllerState.IsAPressed()) portIoABPressed |= PortIoABValues.P1Button1;
+			if (eventArgs.ControllerState.IsXPressed() || eventArgs.ControllerState.IsBPressed()) portIoABPressed |= PortIoABValues.P1Button2;
+
+			portIoBMiscPressed |= (PortIoBMiscValues.IC21Pin6 | PortIoBMiscValues.IC21Pin10 | PortIoBMiscValues.IC21Pin13);       /* Unused, always 1 */
+		}
+
+		private byte ReadMemory(ushort address)
+		{
+			if (address >= 0x0000 && address <= 0xBFFF)
+			{
+				return (cartridge != null ? cartridge.Read(address) : (byte)0x00);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				return wram[address & (ramSize - 1)];
+			}
+
+			/* Cannot read from address, return 0 */
+			return 0x00;
+		}
+
+		private void WriteMemory(ushort address, byte value)
+		{
+			if (address >= 0x0000 && address <= 0xBFFF)
+			{
+				cartridge?.Write(address, value);
+			}
+			else if (address >= 0xC000 && address <= 0xFFFF)
+			{
+				wram[address & (ramSize - 1)] = value;
+			}
+		}
+
+		private byte ReadPort(byte port)
+		{
+			switch (port & 0xC0)
+			{
+				case 0x80:
+					return vdp.ReadPort(port);
+
+				case 0xC0:
+					if ((port & 0x01) == 0)
+						return (byte)(PortIoABValues.Mask & ~portIoABPressed);
+					else
+						return (byte)(PortIoBMiscValues.Mask & ~portIoBMiscPressed);
+
+				default:
+					// TODO: handle properly
+					return 0x00;
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			switch (port & 0xC0)
+			{
+				case 0x40: psg.WritePort(port, value); break;
+				case 0x80: vdp.WritePort(port, value); break;
+				default: break; // TODO: handle properly
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs.meta b/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs.meta
new file mode 100644
index 0000000..2014a6e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Machines/SG1000.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cfe23eacb3173be4a94a9775b3289053
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Peripherals.meta b/Assets/Plugins/Essgee/Emulation/Peripherals.meta
new file mode 100644
index 0000000..5d5f4a8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Peripherals.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 53ebe1049b81852448c0a7671f0d499f
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs b/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs
new file mode 100644
index 0000000..238a5df
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation.Peripherals
+{
+	interface IPeripheral
+	{
+		void Startup();
+		void Shutdown();
+		void Reset();
+
+		byte ReadPort(byte port);
+		void WritePort(byte port, byte value);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs.meta b/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs.meta
new file mode 100644
index 0000000..064436b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Peripherals/IPeripheral.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 399a15f4c8bd5824fac256665037afad
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs b/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs
new file mode 100644
index 0000000..2ec7dd3
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.Exceptions;
+using Essgee.Utilities;
+
+namespace Essgee.Emulation.Peripherals
+{
+	public class Intel8255 : IPeripheral
+	{
+		[StateRequired]
+		public byte PortAInput { get; set; }
+		[StateRequired]
+		public byte PortBInput { get; set; }
+		[StateRequired]
+		public byte PortCInput { get; set; }
+		[StateRequired]
+		public byte PortAOutput { get; set; }
+		[StateRequired]
+		public byte PortBOutput { get; set; }
+		[StateRequired]
+		public byte PortCOutput { get; set; }
+
+		[StateRequired]
+		byte configByte, setResetControlByte;
+
+		int operatingModeGroupA => ((configByte >> 5) & 0x03);
+		bool isPortAInput => ((configByte & 0x10) == 0x10);
+		bool isPortCUInput => ((configByte & 0x08) == 0x08);
+		int operatingModeGroupB => ((configByte >> 2) & 0x01);
+		bool isPortBInput => ((configByte & 0x02) == 0x02);
+		bool isPortCLInput => ((configByte & 0x01) == 0x01);
+		int bitToChange => ((setResetControlByte >> 1) & 0x07);
+		bool isSetBitOperation => ((setResetControlByte & 0x01) == 0x01);
+
+		public Intel8255() { }
+
+		public void Startup()
+		{
+			//
+		}
+
+		public void Shutdown()
+		{
+			//
+		}
+
+		public void Reset()
+		{
+			PortAInput = PortAOutput = 0x00;
+			PortBInput = PortBOutput = 0x00;
+			PortCInput = PortCOutput = 0x00;
+
+			WritePort(0x03, 0x9B);
+		}
+
+		public byte ReadPort(byte port)
+		{
+			switch (port & 0x03)
+			{
+				case 0x00: return (isPortAInput ? PortAInput : PortAOutput);
+				case 0x01: return (isPortBInput ? PortBInput : PortBOutput);
+				case 0x02: return (byte)(((isPortCUInput ? PortCInput : PortCOutput) & 0xF0) | (isPortCLInput ? PortCInput : PortCOutput) & 0x0F);
+				case 0x03: return 0xFF; /* Cannot read control port */
+
+				default: throw new EmulationException(string.Format("i8255: Unsupported read from port 0x{0:X2}", port));
+			}
+		}
+
+		public void WritePort(byte port, byte value)
+		{
+			switch (port & 0x03)
+			{
+				case 0x00: PortAOutput = value; break;
+				case 0x01: PortBOutput = value; break;
+				case 0x02: PortCOutput = value; break;
+
+				case 0x03:
+					/* Control port */
+					if ((value & 0x80) == 0x80)
+					{
+						configByte = value;
+					}
+					else
+					{
+						setResetControlByte = value;
+
+						byte mask = (byte)(1 << bitToChange);
+						if (isSetBitOperation) PortCOutput |= mask;
+						else PortCOutput &= (byte)~mask;
+					}
+					break;
+
+				default: throw new EmulationException(string.Format("i8255: Unsupported write to port 0x{0:X2}, value 0x{1:X2}", port, value));
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs.meta b/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs.meta
new file mode 100644
index 0000000..a3b9b2d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Peripherals/Intel8255.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5d39dd8d77215c64e981c2bebaac1901
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs b/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs
new file mode 100644
index 0000000..88444be
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs
@@ -0,0 +1,145 @@
+using Essgee.Exceptions;
+using Essgee.Utilities;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Text;
+
+namespace Essgee.Emulation
+{
+    public static class SaveStateHandler
+	{
+		public static string ExpectedVersion = $"ESGST{new Version(StandInfo.ProductVersion).Major:D3}";
+
+		public static Dictionary<string, dynamic> Load(Stream stream, string machineName)
+		{
+			stream.Position = 0;
+
+			using (var reader = new BinaryReader(stream))
+			{
+				/* Read and check version string */
+				var version = Encoding.ASCII.GetString(reader.ReadBytes(ExpectedVersion.Length));
+				if (version != ExpectedVersion) throw new EmulationException("Unsupported savestate version");
+
+				/* Read and check filesize */
+				var filesize = reader.ReadUInt32();
+				if (filesize != reader.BaseStream.Length) throw new EmulationException("Savestate filesize mismatch");
+
+				/* Read CRC32 */
+				var crc32 = reader.ReadUInt32();
+
+				/* Read and check machine ID */
+				var machineId = Encoding.ASCII.GetString(reader.ReadBytes(16));
+				if (machineId != GenerateMachineIdString(machineName)) throw new EmulationException("Savestate machine mismatch");
+
+				/* Check CRC32 */
+				using (var stateStream = new MemoryStream())
+				{
+					reader.BaseStream.CopyTo(stateStream);
+					stateStream.Position = 0;
+					var expectedCrc32 = Crc32.Calculate(stateStream);
+					if (crc32 != expectedCrc32) throw new EmulationException("Savestate checksum error");
+
+					/* Read state data */
+					var binaryFormatter = new BinaryFormatter();
+					return (binaryFormatter.Deserialize(stateStream) as Dictionary<string, dynamic>);
+				}
+			}
+		}
+
+		public static void Save(Stream stream, string machineName, Dictionary<string, dynamic> state)
+		{
+			using (var writer = new BinaryWriter(new MemoryStream()))
+			{
+				/* Write version string */
+				writer.Write(Encoding.ASCII.GetBytes(ExpectedVersion));
+
+				/* Write filesize placeholder */
+				var filesizePosition = writer.BaseStream.Position;
+				writer.Write(uint.MaxValue);
+
+				/* Write CRC32 placeholder */
+				var crc32Position = writer.BaseStream.Position;
+				writer.Write(uint.MaxValue);
+
+				/* Write machine ID */
+				writer.Write(Encoding.ASCII.GetBytes(GenerateMachineIdString(machineName)));
+
+				/* Current position is end of header, store for later */
+				var headerSize = writer.BaseStream.Position;
+
+				/* Write state data */
+				var binaryFormatter = new BinaryFormatter();
+				binaryFormatter.Serialize(writer.BaseStream, state);
+
+				/* Write filesize */
+				var lastOffset = writer.BaseStream.Position;
+				writer.BaseStream.Position = filesizePosition;
+				writer.Write((uint)writer.BaseStream.Length);
+				writer.BaseStream.Position = lastOffset;
+
+				/* Calculate CRC32 for state data, then write CRC32 */
+				lastOffset = writer.BaseStream.Position;
+
+				writer.BaseStream.Position = 0;
+				var crc32 = Crc32.Calculate(writer.BaseStream, (int)headerSize, (int)(writer.BaseStream.Length - headerSize));
+
+				writer.BaseStream.Position = crc32Position;
+				writer.Write(crc32);
+				writer.BaseStream.Position = lastOffset;
+
+				/* Copy to file */
+				writer.BaseStream.Position = 0;
+				writer.BaseStream.CopyTo(stream);
+			}
+		}
+
+		private static string GenerateMachineIdString(string machineId)
+		{
+			return machineId.Substring(0, Math.Min(machineId.Length, 16)).PadRight(16);
+		}
+
+		public static void PerformSetState(object obj, Dictionary<string, dynamic> state)
+		{
+			if (obj != null)
+			{
+				/* Restore property values from state */
+				foreach (var prop in obj.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(x => x.GetCustomAttributes(typeof(StateRequiredAttribute), false).Length != 0))
+				{
+					prop.SetValue(obj, state[prop.Name]);
+				}
+
+				/* Restore field values from state */
+				foreach (var field in obj.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(x => x.GetCustomAttributes(typeof(StateRequiredAttribute), false).Length != 0))
+				{
+					field.SetValue(obj, state[field.Name]);
+				}
+			}
+		}
+
+		public static Dictionary<string, dynamic> PerformGetState(object obj)
+		{
+			var state = new Dictionary<string, dynamic>();
+
+			if (obj != null)
+			{
+				/* Copy property values to state */
+				foreach (var prop in obj.GetType().GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(x => x.GetCustomAttributes(typeof(StateRequiredAttribute), false).Length != 0))
+				{
+					state.Add(prop.Name, prop.GetValue(obj));
+				}
+
+				/* Copy field values to state */
+				foreach (var field in obj.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance).Where(x => x.GetCustomAttributes(typeof(StateRequiredAttribute), false).Length != 0))
+				{
+					state.Add(field.Name, field.GetValue(obj));
+				}
+			}
+
+			return state;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs.meta b/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs.meta
new file mode 100644
index 0000000..36a4413
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/SaveStateHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 174dc0116b075184494eb514aea3ad36
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Utilities.cs b/Assets/Plugins/Essgee/Emulation/Utilities.cs
new file mode 100644
index 0000000..80ee2e0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Utilities.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Emulation
+{
+	public static class Utilities
+	{
+		public static bool IsBitSet(byte value, int bit)
+		{
+			return ((value & (1 << bit)) != 0);
+		}
+
+		public static void RGB222toBGRA8888(int color, ref byte[] buffer, int address)
+		{
+			byte r = (byte)((color >> 0) & 0x3), g = (byte)((color >> 2) & 0x3), b = (byte)((color >> 4) & 0x3);
+			buffer[address + 0] = (byte)((b << 6) | (b << 4) | (b << 2) | b);
+			buffer[address + 1] = (byte)((g << 6) | (g << 4) | (g << 2) | g);
+			buffer[address + 2] = (byte)((r << 6) | (r << 4) | (r << 2) | r);
+			buffer[address + 3] = 0xFF;
+		}
+
+		public static void RGB444toBGRA8888(int color, ref byte[] buffer, int address)
+		{
+			byte r = (byte)((color >> 0) & 0xF), g = (byte)((color >> 4) & 0xF), b = (byte)((color >> 8) & 0xF);
+			buffer[address + 0] = (byte)((b << 4) | b);
+			buffer[address + 1] = (byte)((g << 4) | g);
+			buffer[address + 2] = (byte)((r << 4) | r);
+			buffer[address + 3] = 0xFF;
+		}
+
+		public static void RGBCGBtoBGRA8888(int color, ref byte[] buffer, int address)
+		{
+			/* https://byuu.net/video/color-emulation -- "LCD emulation: Game Boy Color" */
+			byte r = (byte)((color >> 0) & 0x1F), g = (byte)((color >> 5) & 0x1F), b = (byte)((color >> 10) & 0x1F);
+			buffer[address + 0] = (byte)(Math.Min(960, (r * 6) + (g * 4) + (b * 22)) >> 2);
+			buffer[address + 1] = (byte)(Math.Min(960, (g * 24) + (b * 8)) >> 2);
+			buffer[address + 2] = (byte)(Math.Min(960, (r * 26) + (g * 4) + (b * 2)) >> 2);
+			buffer[address + 3] = 0xFF;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Utilities.cs.meta b/Assets/Plugins/Essgee/Emulation/Utilities.cs.meta
new file mode 100644
index 0000000..96ee3cf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Utilities.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 09a14347df3fa794380f049632d22821
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video.meta b/Assets/Plugins/Essgee/Emulation/Video.meta
new file mode 100644
index 0000000..eed8189
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 38fa5b2e38bbb7b4695789da71a3506b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs b/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs
new file mode 100644
index 0000000..51e3c5c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs
@@ -0,0 +1,33 @@
+using System;
+
+using Essgee.EventArguments;
+
+namespace Essgee.Emulation.Video
+{
+	interface IVideo
+	{
+		(int X, int Y, int Width, int Height) Viewport { get; }
+
+		event EventHandler<RenderScreenEventArgs> RenderScreen;
+		void OnRenderScreen(RenderScreenEventArgs e);
+
+		event EventHandler<EventArgs> EndOfScanline;
+		void OnEndOfScanline(EventArgs e);
+
+		event EventHandler<SizeScreenEventArgs> SizeScreen;
+		void OnSizeScreen(SizeScreenEventArgs e);
+
+		(string Name, string Description)[] RuntimeOptions { get; }
+
+		object GetRuntimeOption(string name);
+		void SetRuntimeOption(string name, object value);
+
+		void Startup();
+		void Shutdown();
+		void Reset();
+		void Step(int clockCyclesInStep);
+
+		void SetClockRate(double clock);
+		void SetRefreshRate(double refresh);
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs.meta
new file mode 100644
index 0000000..a3fdf61
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/IVideo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4992318d978fee542b3b9cc4011f00d7
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video/Nintendo.meta b/Assets/Plugins/Essgee/Emulation/Video/Nintendo.meta
new file mode 100644
index 0000000..192c4e4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/Nintendo.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1ddba906f8855dd40bb9666a7106c3b9
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs
new file mode 100644
index 0000000..a2b25ca
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs
@@ -0,0 +1,539 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+using Essgee.Emulation.CPU;
+using Essgee.Exceptions;
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.CPU.SM83;
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Video.Nintendo
+{
+	public class CGBVideo : DMGVideo
+	{
+		protected override int numSkippedFramesLcdOn => 1;
+
+		// FF4F - VBK
+		byte vramBank;
+
+		// FF51 - HDMA1
+		byte dmaSourceHi;
+
+		// FF52 - HDMA2
+		byte dmaSourceLo;
+
+		// FF53 - HDMA3
+		byte dmaDestinationHi;
+
+		// FF54 - HDMA4
+		byte dmaDestinationLo;
+
+		// FF55 - HDMA5
+		byte dmaTransferBlockLength;
+		bool dmaTransferIsHDMA;
+
+		// FF68 - BCPS
+		byte bgPaletteIndex;
+		bool bgPaletteAutoIncrement;
+
+		// FF69 - BCPD
+
+		// FF6A - OCPS
+		byte objPaletteIndex;
+		bool objPaletteAutoIncrement;
+
+		// FF6B - OCPD
+
+		//
+
+		byte[] bgPaletteData, objPaletteData;
+
+		ushort dmaSourceAddress, dmaDestinationAddress;
+		int dmaTransferByteLength;
+
+		bool hdmaIsActive;
+		byte hdmaBytesLeft;
+
+		public int GDMAWaitCycles { get; set; }
+
+		protected const byte screenUsageBackgroundHighPriority = (1 << 3);
+
+		public CGBVideo(MemoryReadDelegate memoryRead, RequestInterruptDelegate requestInterrupt) : base(memoryRead, requestInterrupt)
+		{
+			vram = new byte[2, 0x2000];
+
+			//
+
+			bgPaletteData = new byte[64];
+			objPaletteData = new byte[64];
+		}
+
+		public override void Reset()
+		{
+			base.Reset();
+
+			vramBank = 0;
+
+			dmaTransferBlockLength = 0;
+			dmaTransferIsHDMA = false;
+
+			bgPaletteAutoIncrement = true;
+			objPaletteAutoIncrement = true;
+
+			for (var i = 0; i < bgPaletteData.Length; i += 2)
+			{
+				bgPaletteData[i + 1] = 0x7F;
+				bgPaletteData[i + 0] = 0xFF;
+			}
+			for (var i = 0; i < objPaletteData.Length; i += 2)
+			{
+				objPaletteData[i + 1] = 0x7F;
+				objPaletteData[i + 0] = 0xFF;
+			}
+
+			dmaSourceAddress = dmaDestinationAddress = 0;
+			dmaTransferByteLength = 0;
+
+			hdmaIsActive = false;
+			hdmaBytesLeft = 0;
+		}
+
+		//
+
+		protected override void StepHBlank()
+		{
+			/* Check and perform HDMA */
+			if (hdmaIsActive && dmaTransferIsHDMA)
+			{
+				if (hdmaBytesLeft > 0 && (cycleCount % 2) == 0)
+				{
+					WriteVram(dmaDestinationAddress, memoryReadDelegate(dmaSourceAddress));
+					dmaDestinationAddress++;
+					dmaSourceAddress++;
+
+					dmaDestinationAddress &= 0x1FFF;
+
+					dmaTransferByteLength--;
+					hdmaBytesLeft--;
+
+					if (dmaTransferByteLength == 0)
+						hdmaIsActive = false;
+
+					UpdateDMAStatus();
+				}
+			}
+
+			/* Increment cycle count & check for next LCD mode */
+			cycleCount++;
+			if (cycleCount == clockCyclesPerLine)
+			{
+				if (hdmaIsActive)
+					hdmaBytesLeft = (byte)Math.Min(0x10, 0x10 - (dmaTransferByteLength % 16));
+
+				EndHBlank();
+			}
+		}
+
+		//
+
+		protected override void RenderPixel(int y, int x)
+		{
+			if (x < 0 || x >= displayActiveWidth || y < 0 || y >= displayActiveHeight) return;
+
+			if (skipFrames > 0)
+			{
+				SetPixel(y, x, 0xFF, 0xFF, 0xFF);
+				return;
+			}
+
+			screenUsageFlags[x, y] = screenUsageEmpty;
+
+			RenderBackground(y, x);
+
+			if (wndEnable) RenderWindow(y, x);
+			if (objEnable) RenderSprites(y, x);
+		}
+
+		protected override void RenderBackground(int y, int x)
+		{
+			// Get base addresses
+			var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800);
+			var mapBase = (ushort)(bgMapSelect ? 0x1C00 : 0x1800);
+
+			// Calculate tilemap address & get tile
+			var yTransformed = (byte)(scrollY + y);
+			var xTransformed = (byte)(scrollX + x);
+			var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3);
+			var tileNumber = vram[0, mapAddress];
+			if (!bgWndTileSelect)
+				tileNumber = (byte)(tileNumber ^ 0x80);
+
+			// Get & extract tile attributes
+			var tileAttribs = vram[1, mapAddress];
+			var tileBgPalette = tileAttribs & 0b111;
+			var tileVramBank = (tileAttribs >> 3) & 0b1;
+			var tileHorizontalFlip = ((tileAttribs >> 5) & 0b1) == 0b1;
+			var tileVerticalFlip = ((tileAttribs >> 6) & 0b1) == 0b1;
+			var tileBgHasPriority = ((tileAttribs >> 7) & 0b1) == 0b1;
+
+			// Calculate tile address & get pixel color index
+			var xShift = tileHorizontalFlip ? (xTransformed % 8) : (7 - (xTransformed % 8));
+			var yShift = tileVerticalFlip ? (7 - (yTransformed & 7)) : (yTransformed & 7);
+			var tileAddress = tileBase + (tileNumber << 4) + (yShift << 1);
+			var ba = (vram[tileVramBank, tileAddress + 0] >> xShift) & 0b1;
+			var bb = (vram[tileVramBank, tileAddress + 1] >> xShift) & 0b1;
+			var c = (byte)((bb << 1) | ba);
+
+			// If color is not 0, note that a BG pixel (normal or high-priority) exists here
+			if (c != 0)
+				screenUsageFlags[x, y] |= tileBgHasPriority ? screenUsageBackgroundHighPriority : screenUsageBackground;
+
+			// Calculate color address in palette & draw pixel
+			if (layerBackgroundForceEnable)
+			{
+				var paletteAddress = (tileBgPalette << 3) + ((c & 0b11) << 1);
+				SetPixel(y, x, (ushort)((bgPaletteData[paletteAddress + 1] << 8) | bgPaletteData[paletteAddress + 0]));
+			}
+			else
+				SetPixel(y, x, (ushort)((bgPaletteData[1] << 8) | bgPaletteData[0]));
+		}
+
+		protected override void RenderWindow(int y, int x)
+		{
+			// Check if current coords are inside window
+			if (y < windowY) return;
+			if (x < (windowX - 7)) return;
+
+			// Get base addresses
+			var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800);
+			var mapBase = (ushort)(wndMapSelect ? 0x1C00 : 0x1800);
+
+			// Calculate tilemap address & get tile
+			var yTransformed = (byte)(y - windowY);
+			var xTransformed = (byte)((7 - windowX) + x);
+			var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3);
+			var tileNumber = vram[0, mapAddress];
+			if (!bgWndTileSelect)
+				tileNumber = (byte)(tileNumber ^ 0x80);
+
+			// Get & extract tile attributes
+			var tileAttribs = vram[1, mapAddress];
+			var tileBgPalette = tileAttribs & 0b111;
+			var tileVramBank = (tileAttribs >> 3) & 0b1;
+			var tileHorizontalFlip = ((tileAttribs >> 5) & 0b1) == 0b1;
+			var tileVerticalFlip = ((tileAttribs >> 6) & 0b1) == 0b1;
+			var tileBgHasPriority = ((tileAttribs >> 7) & 0b1) == 0b1;
+
+			// Calculate tile address & get pixel color index
+			var xShift = tileHorizontalFlip ? (xTransformed % 8) : (7 - (xTransformed % 8));
+			var yShift = tileVerticalFlip ? (7 - (yTransformed & 7)) : (yTransformed & 7);
+			var tileAddress = tileBase + (tileNumber << 4) + (yShift << 1);
+			var ba = (vram[tileVramBank, tileAddress + 0] >> xShift) & 0b1;
+			var bb = (vram[tileVramBank, tileAddress + 1] >> xShift) & 0b1;
+			var c = (byte)((bb << 1) | ba);
+
+			// If color is not 0, note that a Window pixel (normal or high-priority) exists here
+			if (c != 0)
+				screenUsageFlags[x, y] |= tileBgHasPriority ? screenUsageBackgroundHighPriority : screenUsageWindow;    // TODO correct?
+
+			// Calculate color address in palette & draw pixel
+			if (layerWindowForceEnable)
+			{
+				var paletteAddress = (tileBgPalette << 3) + ((c & 0b11) << 1);
+				SetPixel(y, x, (ushort)((bgPaletteData[paletteAddress + 1] << 8) | bgPaletteData[paletteAddress + 0]));
+			}
+			else
+				SetPixel(y, x, (ushort)((bgPaletteData[1] << 8) | bgPaletteData[0]));
+		}
+
+		protected override void RenderSprites(int y, int x)
+		{
+			var objHeight = objSize ? 16 : 8;
+
+			// Iterate over sprite on line backwards
+			for (var s = numSpritesOnLine - 1; s >= 0; s--)
+			{
+				var i = spritesOnLine[s];
+
+				// Get sprite Y coord & if sprite is not on current scanline, continue to next slot
+				var objY = (short)(oam[(i * 4) + 0] - 16);
+				if (y < objY || y >= (objY + objHeight)) continue;
+
+				// Get sprite X coord, tile number & attributes
+				var objX = (byte)(oam[(i * 4) + 1] - 8);
+				var objTileNumber = oam[(i * 4) + 2];
+				var objAttributes = oam[(i * 4) + 3];
+
+				// Extract attributes
+				var objFlipY = ((objAttributes >> 6) & 0b1) == 0b1;
+				var objFlipX = ((objAttributes >> 5) & 0b1) == 0b1;
+				var objVramBank = (objAttributes >> 3) & 0b1;
+				var objPalNumber = (objAttributes >> 0) & 0b111;
+
+				// Iterate over pixels
+				for (var px = 0; px < 8; px++)
+				{
+					// If sprite pixel X coord does not equal current rendering X coord, continue to next pixel
+					if (x != (byte)(objX + px)) continue;
+
+					// Calculate tile address
+					var xShift = objFlipX ? (px % 8) : (7 - (px % 8));
+					var yShift = objFlipY ? (7 - ((y - objY) % 8)) : ((y - objY) % 8);
+					if (objSize)
+					{
+						objTileNumber &= 0xFE;
+						if ((objFlipY && y < (objY + 8)) || (!objFlipY && y >= (objY + 8)))
+							objTileNumber |= 0x01;
+					}
+					var tileAddress = (objTileNumber << 4) + (yShift << 1);
+
+					// Get palette & bitplanes
+					var ba = (vram[objVramBank, tileAddress + 0] >> xShift) & 0b1;
+					var bb = (vram[objVramBank, tileAddress + 1] >> xShift) & 0b1;
+
+					// Combine to color index, continue drawing if color is not 0
+					var c = (byte)((bb << 1) | ba);
+					if (c != 0)
+					{
+						// If sprite does not have priority i.e. if sprite should not be drawn, continue to next pixel
+						if (!HasSpritePriority(y, x, i)) continue;
+
+						screenUsageFlags[x, y] |= screenUsageSprite;
+						screenUsageSpriteSlots[x, y] = (byte)i;
+						screenUsageSpriteXCoords[x, y] = objX;
+
+						// Calculate color address in palette & draw pixel
+						if (layerSpritesForceEnable)
+						{
+							var paletteAddress = (objPalNumber << 3) + ((c & 0b11) << 1);
+							SetPixel(y, x, (ushort)((objPaletteData[paletteAddress + 1] << 8) | objPaletteData[paletteAddress + 0]));
+						}
+					}
+				}
+			}
+		}
+
+		protected override bool HasSpritePriority(int y, int x, int objSlot)
+		{
+			// If BG and window have priority, check further conditions
+			if (bgEnable)
+			{
+				// Get new sprite OBJ-to-BG priority attribute
+				var objIsBehindBg = ((oam[(objSlot * 4) + 3] >> 7) & 0b1) == 0b1;
+
+				// If high-priority BG pixel has already been drawn, -or- new sprite is shown behind BG/Window -and- a BG/Window pixel has already been drawn, new sprite does not have priority
+				if (IsScreenUsageFlagSet(y, x, screenUsageBackgroundHighPriority) ||
+					(objIsBehindBg && (IsScreenUsageFlagSet(y, x, screenUsageBackground) || IsScreenUsageFlagSet(y, x, screenUsageWindow)))) return false;
+			}
+
+			// New sprite has priority
+			return true;
+		}
+
+		protected void SetPixel(int y, int x, ushort c)
+		{
+			WriteColorToFramebuffer(c, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4);
+		}
+
+		private void WriteColorToFramebuffer(ushort c, int address)
+		{
+			RGBCGBtoBGRA8888(c, ref outputFramebuffer, address);
+		}
+
+		//
+
+		private void RunGDMA()
+		{
+			while (--dmaTransferByteLength >= 0)
+			{
+				if ((dmaSourceAddress >= 0x0000 && dmaSourceAddress <= 0x7FFF) || (dmaSourceAddress >= 0xA000 && dmaSourceAddress <= 0xDFFF))
+					WriteVram(dmaDestinationAddress, memoryReadDelegate(dmaSourceAddress));
+
+				dmaDestinationAddress++;
+				dmaSourceAddress++;
+
+				dmaDestinationAddress &= 0x1FFF;
+			}
+
+			UpdateDMAStatus();
+		}
+
+		private void UpdateDMAStatus()
+		{
+			dmaTransferBlockLength = (byte)((dmaTransferByteLength >> 4) & 0xFF);
+
+			dmaSourceHi = (byte)((dmaSourceAddress >> 8) & 0x1F);
+			dmaSourceLo = (byte)((dmaSourceAddress >> 0) & 0xF0);
+			dmaDestinationHi = (byte)((dmaDestinationAddress >> 8) & 0xFF);
+			dmaDestinationLo = (byte)((dmaDestinationAddress >> 0) & 0xF0);
+		}
+
+		//
+
+		public override byte ReadVram(ushort address)
+		{
+			if (modeNumber != 3)
+				return vram[vramBank, address & 0x1FFF];
+			else
+				return 0xFF;
+		}
+
+		public override void WriteVram(ushort address, byte value)
+		{
+			if (modeNumber != 3)
+				vram[vramBank, address & 0x1FFF] = value;
+		}
+
+		public override byte ReadPort(byte port)
+		{
+			switch (port)
+			{
+				case 0x4F:
+					// VBK
+					return (byte)(
+						0xFE |
+						(vramBank & 0b1));
+
+				case 0x55:
+					// HDMA5
+					return dmaTransferBlockLength;
+
+				case 0x68:
+					// BCPS
+					return (byte)(
+						0x40 |
+						(bgPaletteIndex & 0x3F) |
+						(bgPaletteAutoIncrement ? (1 << 7) : 0));
+
+				case 0x69:
+					// BCPD
+					if (modeNumber != 3) return bgPaletteData[bgPaletteIndex];
+					else return 0xFF;
+
+				case 0x6A:
+					// OCPS
+					return (byte)(
+						0x40 |
+						(objPaletteIndex & 0x3F) |
+						(objPaletteAutoIncrement ? (1 << 7) : 0));
+
+				case 0x6B:
+					// OCPD
+					if (modeNumber != 3) return objPaletteData[objPaletteIndex];
+					else return 0xFF;
+
+				default:
+					return base.ReadPort(port);
+			}
+		}
+
+		public override void WritePort(byte port, byte value)
+		{
+			switch (port)
+			{
+				case 0x41:
+					// STAT
+					lycLyInterrupt = ((value >> 6) & 0b1) == 0b1;
+					m2OamInterrupt = ((value >> 5) & 0b1) == 0b1;
+					m1VBlankInterrupt = ((value >> 4) & 0b1) == 0b1;
+					m0HBlankInterrupt = ((value >> 3) & 0b1) == 0b1;
+
+					CheckAndRequestStatInterupt();
+					break;
+
+				case 0x4F:
+					// VBK
+					vramBank = (byte)(value & 0b1);
+					break;
+
+				case 0x51:
+					// HDMA1
+					dmaSourceHi = value;
+					break;
+
+				case 0x52:
+					// HDMA2
+					dmaSourceLo = (byte)(value & 0xF0);
+					break;
+
+				case 0x53:
+					// HDMA3
+					dmaDestinationHi = (byte)(value & 0x1F);
+					break;
+
+				case 0x54:
+					// HDMA4
+					dmaDestinationLo = (byte)(value & 0xF0);
+					break;
+
+				case 0x55:
+					// HDMA5
+					dmaTransferBlockLength = (byte)(value & 0x7F);
+					dmaTransferIsHDMA = ((value >> 7) & 0b1) == 0b1;
+
+					// Check for HDMA cancellation
+					if (!dmaTransferIsHDMA && hdmaIsActive)
+						hdmaIsActive = false;
+					else
+					{
+						// Calculate DMA addresses & length
+						dmaTransferByteLength = (dmaTransferBlockLength + 1) << 4;
+						dmaSourceAddress = (ushort)((dmaSourceHi << 8 | dmaSourceLo) & 0xFFF0);
+						dmaDestinationAddress = (ushort)((dmaDestinationHi << 8 | dmaDestinationLo) & 0x1FF0);
+
+						// Run General-Purpose DMA
+						if (!dmaTransferIsHDMA)
+						{
+							GDMAWaitCycles = 8 * (dmaTransferBlockLength + 1);
+							RunGDMA();
+						}
+						else
+							hdmaIsActive = true;
+					}
+					break;
+
+				case 0x68:
+					// BCPS
+					bgPaletteIndex = (byte)(value & 0x3F);
+					bgPaletteAutoIncrement = ((value >> 7) & 0b1) == 0b1;
+					break;
+
+				case 0x69:
+					// BCPD
+					if (modeNumber != 3) bgPaletteData[bgPaletteIndex] = value;
+					if (bgPaletteAutoIncrement)
+					{
+						bgPaletteIndex++;
+						bgPaletteIndex &= 0x3F;
+					}
+					break;
+
+				case 0x6A:
+					// OCPS
+					objPaletteIndex = (byte)(value & 0x3F);
+					objPaletteAutoIncrement = ((value >> 7) & 0b1) == 0b1;
+					break;
+
+				case 0x6B:
+					// OCPD
+					if (modeNumber != 3) objPaletteData[objPaletteIndex] = value;
+					if (objPaletteAutoIncrement)
+					{
+						objPaletteIndex++;
+						objPaletteIndex &= 0x3F;
+					}
+					break;
+
+				default:
+					base.WritePort(port, value);
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs.meta
new file mode 100644
index 0000000..874fe8d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/CGBVideo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4025ca74baf3418409354b5129430a4f
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs
new file mode 100644
index 0000000..c91b629
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs
@@ -0,0 +1,874 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+using Essgee.Exceptions;
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.CPU.SM83;
+
+namespace Essgee.Emulation.Video.Nintendo
+{
+	public class DMGVideo : IVideo
+	{
+		protected const int displayActiveWidth = 160;
+		protected const int displayActiveHeight = 144;
+		protected const int numDisplayPixels = displayActiveWidth * displayActiveHeight;
+
+		protected const int displayTotalHeight = 154;
+
+		protected const int numOamSlots = 40;
+		protected const int maxSpritesPerLine = 10;
+
+		protected const int mode2Boundary = 80;
+		protected const int mode3Boundary = mode2Boundary + 168;
+
+		protected const string layerBackgroundOptionName = "GraphicsLayersShowBackground";
+		protected const string layerWindowOptionName = "GraphicsLayersShowWindow";
+		protected const string layerSpritesOptionName = "GraphicsLayersShowSprites";
+
+		protected virtual int numSkippedFramesLcdOn => 4;
+
+		protected Action[] modeFunctions;
+
+		protected readonly MemoryReadDelegate memoryReadDelegate;
+		protected readonly RequestInterruptDelegate requestInterruptDelegate;
+
+		public virtual (int X, int Y, int Width, int Height) Viewport => (0, 0, displayActiveWidth, displayActiveHeight);
+
+		public virtual event EventHandler<SizeScreenEventArgs> SizeScreen;
+		public virtual void OnSizeScreen(SizeScreenEventArgs e) { SizeScreen?.Invoke(this, e); }
+
+		public virtual event EventHandler<RenderScreenEventArgs> RenderScreen;
+		public virtual void OnRenderScreen(RenderScreenEventArgs e) { RenderScreen?.Invoke(this, e); }
+
+		public virtual event EventHandler<EventArgs> EndOfScanline;
+		public virtual void OnEndOfScanline(EventArgs e) { EndOfScanline?.Invoke(this, e); }
+
+		//
+
+		protected double clockRate, refreshRate;
+
+		//
+
+		[StateRequired]
+		protected byte[,] vram;
+		[StateRequired]
+		protected byte[] oam;
+
+		// FF40 - LCDC
+		protected bool lcdEnable, wndMapSelect, wndEnable, bgWndTileSelect, bgMapSelect, objSize, objEnable, bgEnable;
+
+		// FF41 - STAT
+		protected bool lycLyInterrupt, m2OamInterrupt, m1VBlankInterrupt, m0HBlankInterrupt, coincidenceFlag;
+		protected byte modeNumber;
+
+		// FF42 - SCY
+		protected byte scrollY;
+		// FF43 - SCX
+		protected byte scrollX;
+
+		// FF44 - LY
+		protected byte ly;
+		// FF45 - LYC
+		protected byte lyCompare;
+
+		// FF46 - DMA
+		protected byte oamDmaStart;
+
+		// FF47 - BGP
+		protected byte bgPalette;
+		// FF48 - OBP0
+		protected byte obPalette0;
+		// FF49 - OBP1
+		protected byte obPalette1;
+
+		// FF4A - WY
+		protected byte windowY;
+		// FF4B - WX
+		protected byte windowX;
+
+		//
+
+		protected int numSpritesOnLine, skipFrames;
+		protected bool statIrqSignal, vBlankReady;
+		protected int[] spritesOnLine;
+
+		readonly byte[][] colorValuesBgr = new byte[][]
+		{
+			/*              B     G     R */
+			new byte[] { 0xF8, 0xF8, 0xF8 },	/* White */
+			new byte[] { 0x9B, 0x9B, 0x9B },	/* Light gray */
+			new byte[] { 0x3E, 0x3E, 0x3E },	/* Dark gray */
+			new byte[] { 0x1F, 0x1F, 0x1F },	/* Black */
+		};
+
+		protected const byte screenUsageEmpty = 0;
+		protected const byte screenUsageBackground = 1 << 0;
+		protected const byte screenUsageWindow = 1 << 1;
+		protected const byte screenUsageSprite = 1 << 2;
+		protected byte[,] screenUsageFlags, screenUsageSpriteXCoords, screenUsageSpriteSlots;
+
+		protected int cycleCount, cycleDotPause, currentScanline;
+		protected byte[] outputFramebuffer;
+
+		protected int clockCyclesPerLine;
+
+		public bool IsDoubleSpeed { get; set; }
+
+		//
+
+		public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[]
+		{
+			(layerBackgroundOptionName, "Background"),
+			(layerWindowOptionName, "Window"),
+			(layerSpritesOptionName, "Sprites"),
+		};
+
+		protected bool layerBackgroundForceEnable, layerWindowForceEnable, layerSpritesForceEnable;
+
+		public DMGVideo(MemoryReadDelegate memoryRead, RequestInterruptDelegate requestInterrupt)
+		{
+			vram = new byte[1, 0x2000];
+			oam = new byte[0xA0];
+
+			//
+
+			modeFunctions = new Action[] { StepHBlank, StepVBlank, StepOAMSearch, StepLCDTransfer };
+
+			spritesOnLine = new int[maxSpritesPerLine];
+
+			memoryReadDelegate = memoryRead;
+			requestInterruptDelegate = requestInterrupt;
+
+			layerBackgroundForceEnable = true;
+			layerWindowForceEnable = true;
+			layerSpritesForceEnable = true;
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			switch (name)
+			{
+				case layerBackgroundOptionName: return layerBackgroundForceEnable;
+				case layerWindowOptionName: return layerWindowForceEnable;
+				case layerSpritesOptionName: return layerSpritesForceEnable;
+				default: return null;
+			}
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			switch (name)
+			{
+				case layerBackgroundOptionName: layerBackgroundForceEnable = (bool)value; break;
+				case layerWindowOptionName: layerWindowForceEnable = (bool)value; break;
+				case layerSpritesOptionName: layerSpritesForceEnable = (bool)value; break;
+			}
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			if (memoryReadDelegate == null) throw new EmulationException("DMGVideo: Memory read delegate is null");
+			if (requestInterruptDelegate == null) throw new EmulationException("DMGVideo: Request interrupt delegate is null");
+
+			Debug.Assert(clockRate != 0.0, "Clock rate is zero", "{0} clock rate is not configured", GetType().FullName);
+			Debug.Assert(refreshRate != 0.0, "Refresh rate is zero", "{0} refresh rate is not configured", GetType().FullName);
+		}
+
+		public virtual void Shutdown()
+		{
+			//
+		}
+
+		public virtual void Reset()
+		{
+			for (var i = 0; i < vram.GetLength(0); i++) vram[0, i] = 0;
+			for (var i = 0; i < oam.Length; i++) oam[i] = 0;
+
+			for (var i = (byte)0x40; i < 0x4C; i++)
+			{
+				// skip OAM dma
+				if (i != 0x46) WritePort(i, 0x00);
+			}
+
+			numSpritesOnLine = skipFrames = 0;
+			statIrqSignal = vBlankReady = false;
+
+			for (var i = 0; i < spritesOnLine.Length; i++) spritesOnLine[i] = -1;
+
+			ClearScreenUsage();
+
+			cycleCount = cycleDotPause = currentScanline = 0;
+		}
+
+		public void SetClockRate(double clock)
+		{
+			clockRate = clock;
+
+			ReconfigureTimings();
+		}
+
+		public void SetRefreshRate(double refresh)
+		{
+			refreshRate = refresh;
+
+			ReconfigureTimings();
+		}
+
+		public virtual void SetRevision(int rev)
+		{
+			Debug.Assert(rev == 0, "Invalid revision", "{0} revision is invalid; only rev 0 is valid", GetType().FullName);
+		}
+
+		protected virtual void ReconfigureTimings()
+		{
+			/* Calculate cycles/line */
+			clockCyclesPerLine = (int)Math.Round((clockRate / refreshRate) / displayTotalHeight);
+
+			/* Create arrays */
+			screenUsageFlags = new byte[displayActiveWidth, displayActiveHeight];
+			screenUsageSpriteXCoords = new byte[displayActiveWidth, displayActiveHeight];
+			screenUsageSpriteSlots = new byte[displayActiveWidth, displayActiveHeight];
+			outputFramebuffer = new byte[numDisplayPixels * 4];
+
+			for (var y = 0; y < displayActiveHeight; y++)
+				SetLine(y, 0xFF, 0xFF, 0xFF);
+		}
+
+		public virtual void Step(int clockCyclesInStep)
+		{
+			for (var c = 0; c < clockCyclesInStep; c++)
+			{
+				if (lcdEnable)
+				{
+					/* LCD enabled, handle LCD modes */
+					modeFunctions[modeNumber]();
+				}
+				else
+				{
+					/* LCD disabled */
+					modeNumber = 0;
+					cycleCount = 0;
+					cycleDotPause = 0;
+
+					currentScanline = 0;
+					ly = 0;
+				}
+			}
+		}
+
+		/* TODO: dot clock pause! -- https://gbdev.io/pandocs/#properties-of-stat-modes
+		 * - non-zero (SCX % 8) causes pause of that many dots
+		 * - active window causes pause of *at least* 6 dots?
+		 * - sprite dot pauses??
+		 * - influence of CGB double-speed mode on this?
+		 * Required for: 
+		 * - GBVideoPlayer2, pixel column alignment & garbage on left screen edge
+		 * - snorpung/pocket demo, vertical scroller right edge of scroll area
+		 * - Prehistorik Man, scroller alignment (ex. level start "START" text should be centered on screen)
+		 * - ...probably more?
+		*/
+
+		protected virtual void StepVBlank()
+		{
+			// TODO: *should* be 4, but Altered Space hangs w/ any value lower than 8?
+			if (cycleCount == 8 && vBlankReady)
+			{
+				requestInterruptDelegate(InterruptSource.VBlank);
+				vBlankReady = false;
+			}
+
+			/* V-blank */
+			cycleCount++;
+			if (cycleCount == clockCyclesPerLine) EndVBlank();
+		}
+
+		protected virtual void EndVBlank()
+		{
+			/* End of scanline reached */
+			OnEndOfScanline(EventArgs.Empty);
+			currentScanline++;
+			ly = (byte)currentScanline;
+
+			/* Check for & request STAT interrupts */
+			CheckAndRequestStatInterupt();
+
+			if (currentScanline == displayTotalHeight - 1)
+			{
+				// TODO: specific cycle this happens?
+
+				/* LY reports as 0 on line 153 */
+				ly = 0;
+
+				// TODO: verify if STAT/LYC interrupt is supposed to happen here? currently breaks Shantae's sprites if done
+				//CheckAndRequestStatInterupt();
+			}
+			else if (currentScanline == displayTotalHeight)
+			{
+				/* End of V-blank reached */
+				modeNumber = 2;
+				currentScanline = 0;
+				ly = 0;
+
+				CheckAndRequestStatInterupt();
+
+				ClearScreenUsage();
+			}
+
+			cycleCount = 0;
+		}
+
+		protected virtual void StepOAMSearch()
+		{
+			/* OAM search */
+
+			if ((cycleCount % 2) == 0)
+			{
+				/* Get object Y coord */
+				var objIndex = cycleCount >> 1;
+				var objY = oam[(objIndex << 2) + 0] - 16;
+
+				/* Check if object is on current scanline & maximum number of objects was not exceeded, then increment counter */
+				if (currentScanline >= objY && currentScanline < (objY + (objSize ? 16 : 8)) && numSpritesOnLine < maxSpritesPerLine)
+				{
+					var objX = oam[(objIndex << 2) + 1] - 8;
+					cycleDotPause += 11 - Math.Min(5, (objX + (objX >= windowX ? (255 - windowX) : scrollX)) % 8);  // TODO: correct?
+					spritesOnLine[numSpritesOnLine++] = objIndex;
+				}
+			}
+
+			/* Increment cycle count & check for next LCD mode */
+			cycleCount++;
+			if (cycleCount == mode2Boundary) EndOAMSearch();
+		}
+
+		protected virtual void EndOAMSearch()
+		{
+			// 1) GBVideoPlayer2 & Prehistorik Man alignment, 2) GBVideoPlayer2 alignment
+			cycleDotPause += (6 << (IsDoubleSpeed ? 1 : 0)) + (scrollX % 8);
+
+			modeNumber = 3;
+			CheckAndRequestStatInterupt();
+		}
+
+		protected virtual void StepLCDTransfer()
+		{
+			/* Data transfer to LCD */
+
+			/* Render pixels */
+			RenderPixel(currentScanline, cycleCount - mode2Boundary - cycleDotPause);
+
+			/* Increment cycle count & check for next LCD mode */
+			cycleCount++;
+			// 3) snorpung/pocket scroller width
+			if (cycleCount == mode3Boundary + cycleDotPause + (wndEnable ? 12 : 0)) EndLCDTransfer();
+		}
+
+		protected virtual void EndLCDTransfer()
+		{
+			modeNumber = 0;
+			CheckAndRequestStatInterupt();
+		}
+
+		protected virtual void StepHBlank()
+		{
+			/* H-blank */
+
+			/* Increment cycle count & check for next LCD mode */
+			cycleCount++;
+			if (cycleCount == clockCyclesPerLine) EndHBlank();
+		}
+
+		protected virtual void EndHBlank()
+		{
+			/* End of scanline reached */
+			OnEndOfScanline(EventArgs.Empty);
+			currentScanline++;
+			ly = (byte)currentScanline;
+
+			CheckAndRequestStatInterupt();
+
+			for (var i = 0; i < spritesOnLine.Length; i++) spritesOnLine[i] = -1;
+			numSpritesOnLine = 0;
+
+			if (currentScanline == displayActiveHeight)
+			{
+				modeNumber = 1;
+				CheckAndRequestStatInterupt();
+
+				/* Reached V-blank, request V-blank interrupt */
+				vBlankReady = true;
+
+				if (skipFrames > 0) skipFrames--;
+
+				/* Submit screen for rendering */
+				OnRenderScreen(new RenderScreenEventArgs(displayActiveWidth, displayActiveHeight, outputFramebuffer.Clone() as byte[]));
+			}
+			else
+			{
+				modeNumber = 2;
+				CheckAndRequestStatInterupt();
+			}
+
+			cycleCount = 0;
+			cycleDotPause = 0;
+		}
+
+		protected void CheckAndRequestStatInterupt()
+		{
+			if (!lcdEnable) return;
+
+			var oldSignal = statIrqSignal;
+			statIrqSignal = false;
+
+			if (modeNumber == 0 && m0HBlankInterrupt) statIrqSignal = true;
+			if (modeNumber == 1 && m1VBlankInterrupt) statIrqSignal = true;
+			if (modeNumber == 2 && m2OamInterrupt) statIrqSignal = true;
+
+			coincidenceFlag = (ly == lyCompare);
+			if (coincidenceFlag && lycLyInterrupt) statIrqSignal = true;
+
+			if (!oldSignal && statIrqSignal)
+				requestInterruptDelegate(InterruptSource.LCDCStatus);
+		}
+
+		protected virtual void RenderPixel(int y, int x)
+		{
+			if (x < 0 || x >= displayActiveWidth || y < 0 || y >= displayActiveHeight) return;
+
+			if (skipFrames > 0)
+			{
+				SetPixel(y, x, 0xFF, 0xFF, 0xFF);
+				return;
+			}
+
+			screenUsageFlags[x, y] = screenUsageEmpty;
+
+			if (bgEnable)
+			{
+				RenderBackground(y, x);
+				if (wndEnable) RenderWindow(y, x);
+			}
+			else
+				SetPixel(y, x, 0xFF, 0xFF, 0xFF);
+
+			if (objEnable) RenderSprites(y, x);
+		}
+
+		protected virtual void RenderBackground(int y, int x)
+		{
+			// Get base addresses
+			var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800);
+			var mapBase = (ushort)(bgMapSelect ? 0x1C00 : 0x1800);
+
+			// Calculate tilemap address & get tile
+			var yTransformed = (byte)(scrollY + y);
+			var xTransformed = (byte)(scrollX + x);
+			var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3);
+			var tileNumber = vram[0, mapAddress];
+			if (!bgWndTileSelect)
+				tileNumber = (byte)(tileNumber ^ 0x80);
+
+			// Calculate tile address & get pixel color index
+			var tileAddress = tileBase + (tileNumber << 4) + ((yTransformed & 7) << 1);
+			var ba = (vram[0, tileAddress + 0] >> (7 - (xTransformed % 8))) & 0b1;
+			var bb = (vram[0, tileAddress + 1] >> (7 - (xTransformed % 8))) & 0b1;
+			var c = (byte)((bb << 1) | ba);
+
+			// If color is not 0, note that a BG pixel exists here
+			if (c != 0)
+				screenUsageFlags[x, y] |= screenUsageBackground;
+
+			// Draw pixel
+			if (layerBackgroundForceEnable)
+				SetPixel(y, x, (byte)((bgPalette >> (c << 1)) & 0x03));
+			else
+				SetPixel(y, x, (byte)(bgPalette & 0x03));
+		}
+
+		protected virtual void RenderWindow(int y, int x)
+		{
+			// Check if current coords are inside window
+			if (y < windowY) return;
+			if (x < (windowX - 7)) return;
+
+			// Get base addresses
+			var tileBase = (ushort)(bgWndTileSelect ? 0x0000 : 0x0800);
+			var mapBase = (ushort)(wndMapSelect ? 0x1C00 : 0x1800);
+
+			// Calculate tilemap address & get tile
+			var yTransformed = (byte)(y - windowY);
+			var xTransformed = (byte)((7 - windowX) + x);
+			var mapAddress = mapBase + ((yTransformed >> 3) << 5) + (xTransformed >> 3);
+			var tileNumber = vram[0, mapAddress];
+			if (!bgWndTileSelect)
+				tileNumber = (byte)(tileNumber ^ 0x80);
+
+			// Calculate tile address & get pixel color index
+			var tileAddress = tileBase + (tileNumber << 4) + ((yTransformed & 7) << 1);
+			var ba = (vram[0, tileAddress + 0] >> (7 - (xTransformed % 8))) & 0b1;
+			var bb = (vram[0, tileAddress + 1] >> (7 - (xTransformed % 8))) & 0b1;
+			var c = (byte)((bb << 1) | ba);
+
+			// If color is not 0, note that a Window pixel exists here
+			if (c != 0)
+				screenUsageFlags[x, y] |= screenUsageWindow;
+
+			// Draw pixel
+			if (layerWindowForceEnable)
+				SetPixel(y, x, (byte)((bgPalette >> (c << 1)) & 0x03));
+			else
+				SetPixel(y, x, (byte)(bgPalette & 0x03));
+		}
+
+		protected virtual void RenderSprites(int y, int x)
+		{
+			var objHeight = objSize ? 16 : 8;
+
+			// Iterate over sprite on line
+			for (var s = 0; s < numSpritesOnLine; s++)
+			{
+				var i = spritesOnLine[s];
+
+				// Get sprite Y coord & if sprite is not on current scanline, continue to next slot
+				var objY = (short)(oam[(i * 4) + 0] - 16);
+				if (y < objY || y >= (objY + objHeight)) continue;
+
+				// Get sprite X coord, tile number & attributes
+				var objX = (byte)(oam[(i * 4) + 1] - 8);
+				var objTileNumber = oam[(i * 4) + 2];
+				var objAttributes = oam[(i * 4) + 3];
+
+				// Extract attributes
+				var objFlipY = ((objAttributes >> 6) & 0b1) == 0b1;
+				var objFlipX = ((objAttributes >> 5) & 0b1) == 0b1;
+				var objPalNumber = (objAttributes >> 4) & 0b1;
+
+				// Iterate over pixels
+				for (var px = 0; px < 8; px++)
+				{
+					// If sprite pixel X coord does not equal current rendering X coord, continue to next pixel
+					if (x != (byte)(objX + px)) continue;
+
+					// Calculate tile address
+					var xShift = objFlipX ? (px % 8) : (7 - (px % 8));
+					var yShift = objFlipY ? (7 - ((y - objY) % 8)) : ((y - objY) % 8);
+					if (objSize)
+					{
+						objTileNumber &= 0xFE;
+						if ((objFlipY && y < (objY + 8)) || (!objFlipY && y >= (objY + 8)))
+							objTileNumber |= 0x01;
+					}
+					var tileAddress = (objTileNumber << 4) + (yShift << 1);
+
+					// Get palette & bitplanes
+					var pal = objPalNumber == 0 ? obPalette0 : obPalette1;
+					var ba = (vram[0, tileAddress + 0] >> xShift) & 0b1;
+					var bb = (vram[0, tileAddress + 1] >> xShift) & 0b1;
+
+					// Combine to color index, continue drawing if color is not 0
+					var c = (byte)((bb << 1) | ba);
+					if (c != 0)
+					{
+						// If sprite does not have priority i.e. if sprite should not be drawn, continue to next pixel
+						if (!HasSpritePriority(y, x, i)) continue;
+
+						screenUsageFlags[x, y] |= screenUsageSprite;
+						screenUsageSpriteSlots[x, y] = (byte)i;
+						screenUsageSpriteXCoords[x, y] = objX;
+
+						// Draw pixel
+						if (layerSpritesForceEnable)
+							SetPixel(y, x, (byte)((pal >> (c << 1)) & 0x03));
+					}
+				}
+			}
+		}
+
+		protected virtual bool HasSpritePriority(int y, int x, int objSlot)
+		{
+			// Get new sprite X coord
+			var objX = (byte)(oam[(objSlot * 4) + 1] - 8);
+
+			// Get potentially existing sprite X coord and slot
+			var prevX = screenUsageSpriteXCoords[x, y];
+			var prevSlot = screenUsageSpriteSlots[x, y];
+
+			// If existing sprite has lower X coord -or- both sprites have same X coord BUT existing sprite has lower slot, new sprite does not have priority
+			if (prevX < objX || (prevX == objX && prevSlot < objSlot))
+				return false;
+
+			// Get new sprite OBJ-to-BG priority attribute
+			var objIsBehindBg = ((oam[(objSlot * 4) + 3] >> 7) & 0b1) == 0b1;
+
+			// If new sprite is shown behind BG/Window -and- a BG/Window pixel has already been drawn, new sprite does not have priority
+			if (objIsBehindBg &&
+				(IsScreenUsageFlagSet(y, x, screenUsageBackground) || IsScreenUsageFlagSet(y, x, screenUsageWindow))) return false;
+
+			// New sprite has priority
+			return true;
+		}
+
+		protected void SetLine(int y, byte c)
+		{
+			for (int x = 0; x < displayActiveWidth; x++)
+				SetPixel(y, x, c);
+		}
+
+		protected void SetLine(int y, byte b, byte g, byte r)
+		{
+			for (int x = 0; x < displayActiveWidth; x++)
+				SetPixel(y, x, b, g, r);
+		}
+
+		protected void SetPixel(int y, int x, byte c)
+		{
+			WriteColorToFramebuffer(c, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4);
+		}
+
+		protected void SetPixel(int y, int x, byte b, byte g, byte r)
+		{
+			WriteColorToFramebuffer(b, g, r, ((y * displayActiveWidth) + (x % displayActiveWidth)) * 4);
+		}
+
+		protected virtual void WriteColorToFramebuffer(byte c, int address)
+		{
+			outputFramebuffer[address + 0] = colorValuesBgr[c & 0x03][0];
+			outputFramebuffer[address + 1] = colorValuesBgr[c & 0x03][1];
+			outputFramebuffer[address + 2] = colorValuesBgr[c & 0x03][2];
+			outputFramebuffer[address + 3] = 0xFF;
+		}
+
+		protected virtual void WriteColorToFramebuffer(byte b, byte g, byte r, int address)
+		{
+			outputFramebuffer[address + 0] = b;
+			outputFramebuffer[address + 1] = g;
+			outputFramebuffer[address + 2] = r;
+			outputFramebuffer[address + 3] = 0xFF;
+		}
+
+		protected virtual void ClearScreenUsage()
+		{
+			for (var y = 0; y < displayActiveHeight; y++)
+			{
+				for (var x = 0; x < displayActiveWidth; x++)
+				{
+					screenUsageFlags[x, y] = screenUsageEmpty;
+					screenUsageSpriteXCoords[x, y] = 255;
+					screenUsageSpriteSlots[x, y] = numOamSlots;
+				}
+			}
+		}
+
+		protected bool IsScreenUsageFlagSet(int y, int x, byte flag)
+		{
+			return (screenUsageFlags[x, y] & flag) == flag;
+		}
+
+		public virtual byte ReadVram(ushort address)
+		{
+			if (modeNumber != 3)
+				return vram[0, address & (vram.Length - 1)];
+			else
+				return 0xFF;
+		}
+
+		public virtual void WriteVram(ushort address, byte value)
+		{
+			if (modeNumber != 3)
+				vram[0, address & (vram.Length - 1)] = value;
+		}
+
+		public virtual byte ReadOam(ushort address)
+		{
+			if (modeNumber != 2 && modeNumber != 3)
+				return oam[address - 0xFE00];
+			else
+				return 0xFF;
+		}
+
+		public virtual void WriteOam(ushort address, byte value)
+		{
+			if (modeNumber != 2 && modeNumber != 3)
+				oam[address - 0xFE00] = value;
+		}
+
+		public virtual byte ReadPort(byte port)
+		{
+			switch (port)
+			{
+				case 0x40:
+					// LCDC
+					return (byte)(
+						(lcdEnable ? (1 << 7) : 0) |
+						(wndMapSelect ? (1 << 6) : 0) |
+						(wndEnable ? (1 << 5) : 0) |
+						(bgWndTileSelect ? (1 << 4) : 0) |
+						(bgMapSelect ? (1 << 3) : 0) |
+						(objSize ? (1 << 2) : 0) |
+						(objEnable ? (1 << 1) : 0) |
+						(bgEnable ? (1 << 0) : 0));
+
+				case 0x41:
+					// STAT
+					return (byte)(
+						0x80 |
+						(lycLyInterrupt ? (1 << 6) : 0) |
+						(m2OamInterrupt ? (1 << 5) : 0) |
+						(m1VBlankInterrupt ? (1 << 4) : 0) |
+						(m0HBlankInterrupt ? (1 << 3) : 0) |
+						(coincidenceFlag ? (1 << 2) : 0) |
+						((modeNumber & 0b11) << 0));
+
+				case 0x42:
+					// SCY
+					return scrollY;
+
+				case 0x43:
+					// SCX
+					return scrollX;
+
+				case 0x44:
+					// LY
+					return ly;
+
+				case 0x45:
+					// LYC
+					return lyCompare;
+
+				case 0x46:
+					// DMA
+					return oamDmaStart;
+
+				case 0x47:
+					// BGP
+					return bgPalette;
+
+				case 0x48:
+					// OBP0
+					return obPalette0;
+
+				case 0x49:
+					// OBP1
+					return obPalette1;
+
+				case 0x4A:
+					// WY
+					return windowY;
+
+				case 0x4B:
+					//WX
+					return windowX;
+
+				default:
+					return 0xFF;
+			}
+		}
+
+		public virtual void WritePort(byte port, byte value)
+		{
+			switch (port)
+			{
+				case 0x40:
+					// LCDC
+					{
+						var newLcdEnable = ((value >> 7) & 0b1) == 0b1;
+						if (lcdEnable != newLcdEnable)
+						{
+							modeNumber = 2;
+							currentScanline = 0;
+							ly = 0;
+
+							CheckAndRequestStatInterupt();
+
+							if (newLcdEnable)
+								skipFrames = numSkippedFramesLcdOn;
+						}
+
+						lcdEnable = newLcdEnable;
+						wndMapSelect = ((value >> 6) & 0b1) == 0b1;
+						wndEnable = ((value >> 5) & 0b1) == 0b1;
+						bgWndTileSelect = ((value >> 4) & 0b1) == 0b1;
+						bgMapSelect = ((value >> 3) & 0b1) == 0b1;
+						objSize = ((value >> 2) & 0b1) == 0b1;
+						objEnable = ((value >> 1) & 0b1) == 0b1;
+						bgEnable = ((value >> 0) & 0b1) == 0b1;
+					}
+					break;
+
+				case 0x41:
+					// STAT
+					lycLyInterrupt = ((value >> 6) & 0b1) == 0b1;
+					m2OamInterrupt = ((value >> 5) & 0b1) == 0b1;
+					m1VBlankInterrupt = ((value >> 4) & 0b1) == 0b1;
+					m0HBlankInterrupt = ((value >> 3) & 0b1) == 0b1;
+
+					CheckAndRequestStatInterupt();
+
+					// TODO: correct?
+					if (lcdEnable && modeNumber == 1 && currentScanline != 0)
+						requestInterruptDelegate(InterruptSource.LCDCStatus);
+					break;
+
+				case 0x42:
+					// SCY
+					scrollY = value;
+					break;
+
+				case 0x43:
+					// SCX
+					scrollX = value;
+					break;
+
+				case 0x44:
+					// LY
+					break;
+
+				case 0x45:
+					// LYC
+					lyCompare = value;
+					CheckAndRequestStatInterupt();
+					break;
+
+				case 0x46:
+					// DMA
+					oamDmaStart = value;
+					for (int src = 0, dst = oamDmaStart << 8; src < 0xA0; src++, dst++)
+						oam[src] = memoryReadDelegate((ushort)dst);
+					break;
+
+				case 0x47:
+					// BGP
+					bgPalette = value;
+					break;
+
+				case 0x48:
+					// OBP0
+					obPalette0 = value;
+					break;
+
+				case 0x49:
+					// OBP1
+					obPalette1 = value;
+					break;
+
+				case 0x4A:
+					// WY
+					windowY = value;
+					break;
+
+				case 0x4B:
+					// WX
+					windowX = value;
+					break;
+			}
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs.meta
new file mode 100644
index 0000000..93163d6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/Nintendo/DMGVideo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 42b7849ed9e0ba741a3d7cdfb174512d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs b/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs
new file mode 100644
index 0000000..892ec28
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Video
+{
+	/* Sega 315-5378, Game Gear */
+	public class SegaGGVDP : SegaSMSVDP
+	{
+		protected override int numTotalScanlines => NumTotalScanlinesNtsc;
+
+		public override (int X, int Y, int Width, int Height) Viewport => (0, 0, 160, 144);
+
+		[StateRequired]
+		ushort cramLatch;
+
+		public SegaGGVDP() : base()
+		{
+			cram = new byte[0x40];
+		}
+
+		public override void Reset()
+		{
+			base.Reset();
+
+			cramLatch = 0x0000;
+		}
+
+		public override void SetRevision(int rev)
+		{
+			// TODO: can GG VDP be detected by software? if so, implement diffs as revision
+			base.SetRevision(rev);
+		}
+
+		protected override void ReconfigureTimings()
+		{
+			/* Calculate cycles/line */
+			clockCyclesPerLine = (int)Math.Round((clockRate / refreshRate) / numTotalScanlines);
+
+			/* Create arrays */
+			screenUsage = new byte[numVisiblePixels * numVisibleScanlines];
+			outputFramebuffer = new byte[Viewport.Width * Viewport.Height * 4];
+
+			/* Update resolution/display timing */
+			UpdateResolution();
+		}
+
+		protected override void PrepareRenderScreen()
+		{
+			OnRenderScreen(new RenderScreenEventArgs(Viewport.Width, Viewport.Height, outputFramebuffer.Clone() as byte[]));
+		}
+
+		private bool ModifyAndVerifyCoordinates(ref int x, ref int y)
+		{
+			// TODO: correctly derive from timing/resolution values
+			x -= 61;
+			y -= 51;
+
+			return x >= 0 && x < Viewport.Width && y >= 0 && y < Viewport.Height;
+		}
+
+		protected override void SetPixel(int y, int x, int palette, int color)
+		{
+			if (!ModifyAndVerifyCoordinates(ref x, ref y)) return;
+			WriteColorToFramebuffer(palette, color, ((y * Viewport.Width) + (x % Viewport.Width)) * 4);
+		}
+
+		protected override void SetPixel(int y, int x, byte b, byte g, byte r)
+		{
+			if (!ModifyAndVerifyCoordinates(ref x, ref y)) return;
+			WriteColorToFramebuffer(b, g, r, ((y * Viewport.Width) + (x % Viewport.Width)) * 4);
+		}
+
+		protected override void WriteColorToFramebuffer(int palette, int color, int address)
+		{
+			int cramAddress = ((palette * 32) + (color * 2));
+			WriteColorToFramebuffer((ushort)(cram[cramAddress + 1] << 8 | cram[cramAddress]), address);
+		}
+
+		protected override void WriteColorToFramebuffer(ushort colorValue, int address)
+		{
+			RGB444toBGRA8888(colorValue, ref outputFramebuffer, address);
+		}
+
+		protected override void WriteDataPort(byte value)
+		{
+			isSecondControlWrite = false;
+
+			readBuffer = value;
+
+			switch (codeRegister)
+			{
+				case 0x00:
+				case 0x01:
+				case 0x02:
+					vram[addressRegister] = value;
+					break;
+				case 0x03:
+					if ((addressRegister & 0x0001) != 0)
+					{
+						cramLatch = (ushort)((cramLatch & 0x00FF) | (value << 8));
+						cram[(addressRegister & 0x003E) | 0x0000] = (byte)((cramLatch >> 0) & 0xFF);
+						cram[(addressRegister & 0x003E) | 0x0001] = (byte)((cramLatch >> 8) & 0xFF);
+					}
+					else
+						cramLatch = (ushort)((cramLatch & 0xFF00) | (value << 0));
+					break;
+			}
+
+			addressRegister++;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs.meta
new file mode 100644
index 0000000..0754e4f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/SegaGGVDP.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f75093c71287a3b45b7fe57be15d49b8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs b/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs
new file mode 100644
index 0000000..ef3c275
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs
@@ -0,0 +1,927 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Video
+{
+	/* Sega 315-5124 (Mark III, SMS) and 315-5246 (SMS 2); differences see 'VDPDIFF' comments */
+	public class SegaSMSVDP : TMS99xxA
+	{
+		/* VDPDIFF: switch for Mk3/SMS1 vs SMS2/GG behavior; configurable via SetRevision, maybe still split into separate classes instead? */
+		protected VDPTypes vdpType = VDPTypes.Mk3SMS1;
+
+		public const int NumActiveScanlinesLow = 192;
+		public const int NumActiveScanlinesMed = 224;
+		public const int NumActiveScanlinesHigh = 240;
+
+		protected const int NumSpritesMode4 = 64;
+		protected const int NumSpritesPerLineMode4 = 8;
+
+		public const int PortVCounter = 0x40;       // 0x7E canonically, but mirrored across bus
+		public const int PortHCounter = 0x41;       // 0x7F canonically, but mirrored across bus
+
+		[StateRequired]
+		protected byte[] cram;
+
+		[StateRequired]
+		protected int vCounter, hCounter;
+		protected int nametableHeight, vCounterTableIndex;
+		[StateRequired]
+		protected int lineInterruptCounter;
+		protected int screenHeight;
+
+		bool isLineInterruptEnabled => IsBitSet(registers[0x00], 4);
+		[StateRequired]
+		bool isLineInterruptPending;
+
+		bool isColumn0MaskEnabled => IsBitSet(registers[0x00], 5);
+		bool isVScrollPartiallyDisabled => IsBitSet(registers[0x00], 7);                /* Columns 24-31, i.e. pixels 192-255 */
+		bool isHScrollPartiallyDisabled => IsBitSet(registers[0x00], 6);                /* Rows 0-1, i.e. pixels 0-15 */
+
+		bool isBitM4Set => IsBitSet(registers[0x00], 2);
+
+		protected override bool isModeGraphics1 => !(isBitM1Set || isBitM2Set || isBitM3Set || isBitM4Set);
+		protected override bool isModeText => (isBitM1Set && !(isBitM2Set || isBitM3Set || isBitM4Set));
+		protected override bool isModeGraphics2 => (isBitM2Set && !(isBitM1Set || isBitM3Set || isBitM4Set));
+		protected override bool isModeMulticolor => (isBitM3Set && !(isBitM1Set || isBitM2Set || isBitM4Set));
+
+		protected bool isSMS240LineMode => (!isBitM1Set && isBitM2Set && isBitM3Set && isBitM4Set);
+		protected bool isSMS224LineMode => (isBitM1Set && isBitM2Set && !isBitM3Set && isBitM4Set);
+
+		bool isSpriteShiftLeft8 => IsBitSet(registers[0x00], 3);
+
+		protected override ushort nametableBaseAddress
+		{
+			get
+			{
+				if (isBitM4Set)
+				{
+					if (isSMS224LineMode || isSMS240LineMode)
+						return (ushort)(((registers[0x02] & 0x0C) << 10) | 0x700);
+					else
+						return (ushort)((registers[0x02] & 0x0E) << 10);
+				}
+				else
+					return (ushort)((registers[0x02] & 0x0F) << 10);
+			}
+		}
+		protected override ushort spriteAttribTableBaseAddress => (ushort)((registers[0x05] & (isBitM4Set ? 0x7E : 0x7F)) << 7);
+		protected override ushort spritePatternGenBaseAddress => (ushort)((registers[0x06] & (isBitM4Set ? 0x04 : 0x07)) << 11);
+
+		/* http://www.smspower.org/Development/Palette */
+		// TODO: verify these, SMSPower has some mistakes (RGB approx correct, palette value wrong)
+		// (not that we'll really use this, aside from for F-16 Fighting Falcon, as SG1000 games should always be loaded into the SG1000 core...)
+		readonly byte[] legacyColorMap = new byte[]
+		{
+			0x00,	/* Transparent */
+			0x00,	/* Black */
+			0x08,	/* Medium green */
+			0x0C,	/* Light green */
+			0x10,	/* Dark blue */
+			0x30,	/* Light blue */
+			0x01,	/* Dark red */
+			0x3C,	/* Cyan */
+			0x02,	/* Medium red */
+			0x03,	/* Light red */
+			0x05,	/* Dark yellow */
+			0x0F,	/* Light yellow */
+			0x04,	/* Dark green */
+			0x33,	/* Magenta */
+			0x15,	/* Gray */
+			0x3F	/* White */
+        };
+
+		readonly byte[][] vCounterTables = new byte[][]
+		{
+			/* NTSC, 192 lines */
+			new byte[]
+			{
+				/* Top blanking */
+				0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4,
+
+				/* Top border */
+				0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4,
+				0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Active display */
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+
+				/* Bottom border */
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7,
+
+				/* Bottom blanking */
+				0xD8, 0xD9, 0xDA,
+
+				/* Vertical blanking */
+				0xD5, 0xD6, 0xD7
+			},
+			/* NTSC, 224 lines */
+			new byte[]
+			{
+				/* Top blanking */
+				0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4,
+
+				/* Top border */
+				0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Active display */
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
+
+				/* Bottom border */
+				0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7,
+
+				/* Bottom blanking */
+				0xE8, 0xE9, 0xEA,
+
+				/* Vertical blanking */
+				0xE5, 0xE6, 0xE7,
+			},
+			/* NTSC, 240 lines (invalid) */
+			new byte[]
+			{
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
+				0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
+				0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06
+			},
+			/* PAL, 192 lines */
+			new byte[]
+			{
+				/* Top blanking */
+				0xBD, 0xBE, 0xBF, 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9,
+
+				/* Top border */
+				0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9,
+				0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9,
+				0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
+				0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Active display */
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+
+				/* Bottom border */
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
+				0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
+
+				/* Bottom blanking */
+				0xF0, 0xF1, 0xF2,
+
+				/* Vertical blanking */
+				0xBA, 0xBB, 0xBC
+			},
+			/* PAL, 224 lines */
+			new byte[]
+			{
+				/* Top blanking */
+				0xCD, 0xCE, 0xCF, 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9,
+
+				/* Top border */
+				0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9,
+				0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9,
+				0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Active display */
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
+
+				/* Bottom border */
+				0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
+				0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Bottom blanking */
+				0x00, 0x01, 0x02,
+
+				/* Vertical blanking */
+				0xCA, 0xCB, 0xCC,
+			},
+			/* PAL, 240 lines */
+			new byte[]
+			{
+				/* Top blanking */
+				0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF, 0xE0, 0xE1,
+
+				/* Top border */
+				0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF, 0xF0, 0xF1,
+				0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+
+				/* Active display */
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
+				0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
+				0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F,
+				0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
+				0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
+				0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F,
+				0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
+				0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F,
+				0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F,
+				0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F,
+				0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+				0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF,
+				0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF,
+				0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, 0xDF,
+				0xE0, 0xE1, 0xE2, 0xE3, 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
+
+				/* Bottom border */
+				0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+				0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+
+				/* Bottom blanking */
+				0x08, 0x09, 0x0A,
+
+				/* Vertical blanking */
+				0xD2, 0xD3, 0xD4,
+			}
+		};
+
+		/* For H-counter emulation */
+		readonly byte[] hCounterTable = new byte[]
+		{
+			0x00, 0x01, 0x02, 0x02, 0x03, 0x04, 0x05, 0x05, 0x06, 0x07, 0x08, 0x08, 0x09, 0x0A, 0x0B, 0x0B,
+			0x0C, 0x0D, 0x0E, 0x0E, 0x0F, 0x10, 0x11, 0x11, 0x12, 0x13, 0x14, 0x14, 0x15, 0x16, 0x17, 0x17,
+			0x18, 0x19, 0x1A, 0x1A, 0x1B, 0x1C, 0x1D, 0x1D, 0x1E, 0x1F, 0x20, 0x20, 0x21, 0x22, 0x23, 0x23,
+			0x24, 0x25, 0x26, 0x26, 0x27, 0x28, 0x29, 0x29, 0x2A, 0x2B, 0x2C, 0x2C, 0x2D, 0x2E, 0x2F, 0x2F,
+			0x30, 0x31, 0x32, 0x32, 0x33, 0x34, 0x35, 0x35, 0x36, 0x37, 0x38, 0x38, 0x39, 0x3A, 0x3B, 0x3B,
+			0x3C, 0x3D, 0x3E, 0x3E, 0x3F, 0x40, 0x41, 0x41, 0x42, 0x43, 0x44, 0x44, 0x45, 0x46, 0x47, 0x47,
+			0x48, 0x49, 0x4A, 0x4A, 0x4B, 0x4C, 0x4D, 0x4D, 0x4E, 0x4F, 0x50, 0x50, 0x51, 0x52, 0x53, 0x53,
+			0x54, 0x55, 0x56, 0x56, 0x57, 0x58, 0x59, 0x59, 0x5A, 0x5B, 0x5C, 0x5C, 0x5D, 0x5E, 0x5F, 0x5F,
+			0x60, 0x61, 0x62, 0x62, 0x63, 0x64, 0x65, 0x65, 0x66, 0x67, 0x68, 0x68, 0x69, 0x6A, 0x6B, 0x6B,
+			0x6C, 0x6D, 0x6E, 0x6E, 0x6F, 0x70, 0x71, 0x71, 0x72, 0x73, 0x74, 0x74, 0x75, 0x76, 0x77, 0x77,
+			0x78, 0x79, 0x7A, 0x7A, 0x7B, 0x7C, 0x7D, 0x7D, 0x7E, 0x7F, 0x80, 0x80, 0x81, 0x82, 0x83, 0x83,
+			0x84, 0x85, 0x86, 0x86, 0x87, 0x88, 0x89, 0x89, 0x8A, 0x8B, 0x8C, 0x8C, 0x8D, 0x8E, 0x8F, 0x8F,
+			0x90, 0x91, 0x92, 0x92, 0x93,
+
+			0xE9, 0xEA, 0xEA, 0xEB, 0xEC, 0xED, 0xED, 0xEE, 0xEF, 0xF0, 0xF0, 0xF1, 0xF2, 0xF3, 0xF3, 0xF4,
+			0xF5, 0xF6, 0xF6, 0xF7, 0xF8, 0xF9, 0xF9, 0xFA, 0xFB, 0xFC, 0xFC, 0xFD, 0xFE, 0xFF, 0xFF
+		};
+
+		[StateRequired]
+		byte horizontalScrollLatched, verticalScrollLatched;
+
+		const byte screenUsageBgLowPriority = screenUsageBackground;
+		const byte screenUsageBgHighPriority = (1 << 2);
+
+		public int ScreenHeight => screenHeight;
+		public int CurrentScanline => currentScanline;
+
+		public SegaSMSVDP() : base()
+		{
+			registers = new byte[0x0B];
+			cram = new byte[0x20];
+
+			spriteBuffer = new (int Number, int Y, int X, int Pattern, int Attribute)[NumActiveScanlinesHigh][];
+			for (int i = 0; i < spriteBuffer.Length; i++) spriteBuffer[i] = new (int Number, int Y, int X, int Pattern, int Attribute)[NumSpritesPerLineMode4];
+		}
+
+		public override void Reset()
+		{
+			base.Reset();
+
+			WriteRegister(0x00, 0x36);
+			WriteRegister(0x01, 0x80);
+			WriteRegister(0x02, 0xFF);
+			WriteRegister(0x03, 0xFF);
+			WriteRegister(0x04, 0xFF);
+			WriteRegister(0x05, 0xFF);
+			WriteRegister(0x06, 0xFB);
+			WriteRegister(0x07, 0x00);
+			WriteRegister(0x08, 0x00);
+			WriteRegister(0x09, 0x00);
+			WriteRegister(0x0A, 0xFF);
+
+			for (int i = 0; i < cram.Length; i++) cram[i] = 0;
+
+			vCounter = hCounter = 0;
+			lineInterruptCounter = registers[0x0A];
+
+			isLineInterruptPending = false;
+
+			horizontalScrollLatched = verticalScrollLatched = 0;
+
+			UpdateResolution();
+		}
+
+		public override void SetRevision(int rev)
+		{
+			VDPTypes type = (VDPTypes)rev;
+			Debug.Assert(Enum.IsDefined(typeof(VDPTypes), type), "Invalid revision", "{0} revision is invalid; only rev 0 (MK3/SMS1) or 1 (SMS2/GG) is valid", GetType().FullName);
+			vdpType = type;
+		}
+
+		protected override void ReconfigureTimings()
+		{
+			/* Calculate cycles/line */
+			clockCyclesPerLine = (int)Math.Round((clockRate / refreshRate) / numTotalScanlines);
+
+			/* Create arrays */
+			screenUsage = new byte[numVisiblePixels * numVisibleScanlines];
+			outputFramebuffer = new byte[(numVisiblePixels * numVisibleScanlines) * 4];
+
+			/* Update resolution/display timing */
+			UpdateResolution();
+		}
+
+		public override void Step(int clockCyclesInStep)
+		{
+			InterruptLine = (((isFrameInterruptEnabled && isFrameInterruptPending) || (isLineInterruptEnabled && isLineInterruptPending)) ? InterruptState.Assert : InterruptState.Clear);
+
+			cycleCount += clockCyclesInStep;
+
+			hCounter = hCounterTable[(int)Math.Round((cycleCount + 578) / 3.0) % hCounterTable.Length];
+
+			if (cycleCount >= clockCyclesPerLine)
+			{
+				OnEndOfScanline(EventArgs.Empty);
+
+				horizontalScrollLatched = registers[0x08];
+
+				if (currentScanline == scanlineActiveDisplay)
+					verticalScrollLatched = registers[0x09];
+
+				CheckSpriteOverflow(currentScanline);
+
+				RenderLine(currentScanline);
+
+				if (currentScanline >= scanlineActiveDisplay && currentScanline <= scanlineBottomBorder)
+				{
+					lineInterruptCounter--;
+					if (lineInterruptCounter < 0)
+					{
+						lineInterruptCounter = registers[0x0A];
+						isLineInterruptPending = true;
+					}
+				}
+				else
+					lineInterruptCounter = registers[0x0A];
+
+				if (currentScanline == (scanlineBottomBorder + 1))
+					isFrameInterruptPending = true;
+
+				vCounter = vCounterTables[vCounterTableIndex][currentScanline];
+
+				currentScanline++;
+				if (currentScanline == numTotalScanlines)
+				{
+					currentScanline = 0;
+					ClearScreenUsage();
+
+					PrepareRenderScreen();
+				}
+
+				ParseSpriteTable(currentScanline);
+
+				cycleCount -= clockCyclesPerLine;
+				if (cycleCount <= -clockCyclesPerLine) cycleCount = 0;
+			}
+		}
+
+		protected override void PrepareRenderScreen()
+		{
+			OnRenderScreen(new RenderScreenEventArgs(numVisiblePixels, numVisibleScanlines, outputFramebuffer.Clone() as byte[]));
+		}
+
+		protected override byte ReadVram(ushort address)
+		{
+			return vram[address & vramMask16k];
+		}
+
+		protected override void WriteVram(ushort address, byte value)
+		{
+			vram[address & vramMask16k] = value;
+		}
+
+		protected override void RenderLine(int y)
+		{
+			if (y >= scanlineTopBorder && y < scanlineActiveDisplay)
+			{
+				if (layerBordersForceEnable) SetLine(y, 1, backgroundColor);
+				else SetLine(y, 0x00, 0x00, 0x00);
+			}
+			else if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
+			{
+				if (layerBackgroundForceEnable)
+				{
+					if (isBitM4Set)
+						RenderLineMode4Background(y);
+					else if (isModeGraphics1)
+						RenderLineGraphics1Background(y);
+					else if (isModeGraphics2)
+						RenderLineGraphics2Background(y);
+					else if (isModeMulticolor)
+						RenderLineMulticolorBackground(y);
+					else if (isModeText)
+						RenderLineTextBackground(y);
+				}
+				else
+					SetLine(y, 0x00, 0x00, 0x00);
+
+				if (layerSpritesForceEnable)
+				{
+					if (isBitM4Set)
+						RenderLineMode4Sprites(y);
+					else if (!isModeText && !isBitM4Set)
+						RenderLineSprites(y);
+				}
+
+				RenderBorders(y);
+			}
+			else if (y >= scanlineBottomBorder && y < numVisibleScanlines)
+			{
+				if (layerBordersForceEnable) SetLine(y, 1, backgroundColor);
+				else SetLine(y, 0x00, 0x00, 0x00);
+			}
+		}
+
+		protected override void RenderBorders(int y)
+		{
+			for (int x = pixelLeftBorder; x < pixelActiveDisplay; x++)
+			{
+				if (layerBordersForceEnable) SetPixel(y, x, 1, backgroundColor);
+				else SetPixel(y, x, 0x00, 0x00, 0x00);
+			}
+			for (int x = pixelRightBorder; x < numVisiblePixels; x++)
+			{
+				if (layerBordersForceEnable) SetPixel(y, x, 1, backgroundColor);
+				else SetPixel(y, x, 0x00, 0x00, 0x00);
+			}
+		}
+
+		protected void SetLine(int y, int palette, int color)
+		{
+			for (int x = 0; x < numVisiblePixels; x++)
+				SetPixel(y, x, palette, color);
+		}
+
+		protected virtual void SetPixel(int y, int x, int palette, int color)
+		{
+			WriteColorToFramebuffer(palette, color, ((y * numVisiblePixels) + (x % numVisiblePixels)) * 4);
+		}
+
+		private void RenderLineMode4Background(int y)
+		{
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			/* Determine H scrolling parameters */
+			int currentHorizontalScroll = ((isHScrollPartiallyDisabled && activeDisplayY < 16) ? 0 : horizontalScrollLatched);
+			int horizontalScrollCoarse = (currentHorizontalScroll >> 3);
+			int horizontalScrollFine = (currentHorizontalScroll & 0x07);
+
+			ushort currentNametableBaseAddress = nametableBaseAddress;
+			bool currentIsVScrollPartiallyDisabled = isVScrollPartiallyDisabled;
+			bool currentIsColumn0MaskEnabled = isColumn0MaskEnabled;
+
+			for (int x = 0; x < numTotalPixelsPerScanline; x++)
+			{
+				int activeDisplayX = (x - pixelActiveDisplay);
+				if (activeDisplayX < 0 || activeDisplayX >= NumActivePixelsPerScanline) continue;
+
+				/* Determine V scrolling parameters */
+				int currentVerticalScroll = ((currentIsVScrollPartiallyDisabled && activeDisplayX >= 192) ? 0 : verticalScrollLatched);
+				int verticalScrollCoarse = (currentVerticalScroll >> 3);
+				int verticalScrollFine = (currentVerticalScroll & 0x07);
+
+				/* Calculate current scrolled column and row */
+				int numColumns = 32;
+				int currentColumn = (((activeDisplayX - horizontalScrollFine) / 8) - horizontalScrollCoarse) & (numColumns - 1);
+				int currentRow = (((activeDisplayY + verticalScrollFine) / 8) + verticalScrollCoarse) % (nametableHeight / 8);
+
+				/* VDPDIFF: Mk3/SMS1 VDP only, adjust current row according to mask bit; http://www.smspower.org/Development/TilemapMirroring
+				 * NOTE: Emulating this breaks 224/240-line mode games (ex. Fantastic Dizzy, Cosmic Spacehead, Micro Machines)?
+				 */
+				if (vdpType == VDPTypes.Mk3SMS1)
+					currentRow &= (((registers[0x02] & 0x01) << 4) | 0x0F);
+
+				/* Fetch data from nametable & extract properties */
+				ushort nametableAddress = (ushort)(currentNametableBaseAddress + (currentRow * (numColumns * 2)) + (currentColumn * 2));
+				ushort nametableData = (ushort)(ReadVram((ushort)(nametableAddress + 1)) << 8 | ReadVram(nametableAddress));
+
+				int tileIndex = (nametableData & 0x01FF);
+				bool hFlip = ((nametableData & 0x0200) == 0x0200);
+				bool vFlip = ((nametableData & 0x0400) == 0x0400);
+				int palette = (((nametableData & 0x0800) >> 11) & 0x0001);
+				bool priority = ((nametableData & 0x1000) == 0x1000);
+
+				/* Fetch pixel data for current pixel line */
+				int hPixel = ((activeDisplayX - horizontalScrollFine) % 8);
+				hPixel = (hFlip ? hPixel : (7 - hPixel));
+				int vPixel = ((activeDisplayY + verticalScrollFine) % 8);
+				vPixel = (!vFlip ? vPixel : (7 - vPixel));
+
+				ushort tileAddress = (ushort)((tileIndex << 5) + (vPixel << 2));
+				int c = (((ReadVram((ushort)(tileAddress + 0)) >> hPixel) & 0x01) << 0);
+				c |= (((ReadVram((ushort)(tileAddress + 1)) >> hPixel) & 0x01) << 1);
+				c |= (((ReadVram((ushort)(tileAddress + 2)) >> hPixel) & 0x01) << 2);
+				c |= (((ReadVram((ushort)(tileAddress + 3)) >> hPixel) & 0x01) << 3);
+
+				/* Record screen usage, write to framebuffer */
+				if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
+				{
+					if ((currentIsColumn0MaskEnabled && (activeDisplayX / 8) == 0) || isDisplayBlanked)
+						SetPixel(y, x, 1, backgroundColor);
+					else
+						SetPixel(y, x, palette, c);
+
+					SetScreenUsageFlag(y, x, (c != 0 && priority) ? screenUsageBgHighPriority : screenUsageBgLowPriority);
+				}
+			}
+		}
+
+		protected override void CheckSpriteOverflow(int y)
+		{
+			if (!isBitM4Set)
+			{
+				/* Not in Master System video mode */
+				base.CheckSpriteOverflow(y);
+			}
+			else
+			{
+				/* Ensure current scanline is within active display */
+				if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
+				{
+					int activeDisplayY = (y - scanlineActiveDisplay);
+
+					/* If last sprite in buffer is valid, sprite overflow occured */
+					int lastSpriteInBuffer = spriteBuffer[activeDisplayY][NumSpritesPerLineMode4 - 1].Number;
+					if (lastSpriteInBuffer != -1)
+					{
+						isSpriteOverflow = true;
+
+						/* Store sprite number in status register */
+						/* NOTE: the last illegal sprite is *technically* only stored here in TMS99xxA modes, but still emulating it in Mode 4 should be fine */
+						WriteSpriteNumberToStatus(lastSpriteInBuffer);
+					}
+				}
+			}
+		}
+
+		protected override void ParseSpriteTable(int y)
+		{
+			if (!isBitM4Set)
+			{
+				/* Not in Master System video mode */
+				base.ParseSpriteTable(y);
+			}
+			else
+			{
+				if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
+
+				/* Determine coordinates in active display */
+				int activeDisplayY = (y - scanlineActiveDisplay);
+
+				/* Clear sprite list for current line */
+				for (int i = 0; i < spriteBuffer[activeDisplayY].Length; i++) spriteBuffer[activeDisplayY][i] = (-1, 0, 0, 0, 0);
+
+				/* Determine sprite size & get zoomed sprites adjustment */
+				int zoomShift = (isZoomedSprites ? 1 : 0);
+				int spriteHeight = ((isLargeSprites ? 16 : 8) << zoomShift);
+
+				int numValidSprites = 0;
+				for (int sprite = 0; sprite < NumSpritesMode4; sprite++)
+				{
+					int yCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + sprite));
+
+					/* Ignore following if Y coord is 208 in 192-line mode */
+					if (yCoordinate == 208 && screenHeight == NumActiveScanlinesLow)
+					{
+						/* Store first "illegal sprite" number in status register */
+						WriteSpriteNumberToStatus(sprite);
+						return;
+					}
+
+					/* Modify Y coord as needed */
+					yCoordinate++;
+					if (yCoordinate > screenHeight + 32) yCoordinate -= 256;
+
+					/* Ignore this sprite if on incorrect lines */
+					if (activeDisplayY < yCoordinate || activeDisplayY >= (yCoordinate + spriteHeight)) continue;
+
+					/* Check if maximum number of sprites per line is reached */
+					numValidSprites++;
+					if (numValidSprites > NumSpritesPerLineMode4) return;
+
+					/* Mark sprite for rendering */
+					int xCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + 0x80 + (sprite * 2)));
+					int patternNumber = ReadVram((ushort)(spriteAttribTableBaseAddress + 0x80 + (sprite * 2) + 1));
+					int unusedData = ReadVram((ushort)(spriteAttribTableBaseAddress + 0x40 + (sprite * 2)));
+
+					spriteBuffer[activeDisplayY][numValidSprites - 1] = (sprite, yCoordinate, xCoordinate, patternNumber, unusedData);
+				}
+
+				/* Because we didn't bow out before already, store total number of sprites in status register */
+				WriteSpriteNumberToStatus(NumSprites - 1);
+			}
+		}
+
+		private void RenderLineMode4Sprites(int y)
+		{
+			if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
+
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			/* Determine sprite size & get zoomed sprites adjustment */
+			int zoomShift = (isZoomedSprites ? 1 : 0);
+			int spriteHeight = ((isLargeSprites ? 16 : 8) << zoomShift);
+
+			for (int s = 0; s < spriteBuffer[activeDisplayY].Length; s++)
+			{
+				var sprite = spriteBuffer[activeDisplayY][s];
+
+				if (sprite.Number == -1) continue;
+
+				/* VDPDIFF: Mk3/SMS1 VDP zoomed sprites bug, only first four sprites (same as max sprites/line on TMS99xxA) can be zoomed horizontally
+				 * Zoom works normally on SMS2/GG */
+				int spriteWidth = (8 << zoomShift);
+				if (vdpType == VDPTypes.Mk3SMS1 && s >= NumSpritesPerLine) spriteWidth = 8;
+
+				if (!isDisplayBlanked)
+				{
+					int yCoordinate = sprite.Y;
+					int xCoordinate = sprite.X;
+					int patternNumber = sprite.Pattern;
+					int unusedData = sprite.Attribute;
+
+					if (isSpriteShiftLeft8) xCoordinate -= 8;
+
+					for (int pixel = 0; pixel < spriteWidth; pixel++)
+					{
+						/* Ignore pixel if column 0 masking is enabled and sprite pixel is in column 0 */
+						if (isColumn0MaskEnabled && (xCoordinate + pixel) < 8) continue;
+
+						/* Check if sprite is outside active display, else continue to next sprite */
+						if ((xCoordinate + pixel) < 0 || (xCoordinate + pixel) >= NumActivePixelsPerScanline) continue;
+
+						/* Determine coordinate inside sprite */
+						int inSpriteXCoord = (pixel >> zoomShift) % spriteWidth;
+						int inSpriteYCoord = ((activeDisplayY - yCoordinate) >> zoomShift) % spriteHeight;
+
+						/* Calculate address and fetch pixel data */
+						int tileIndex = patternNumber;
+						if (isLargeSprites) tileIndex &= ~0x01;
+						ushort tileAddress = (ushort)(spritePatternGenBaseAddress + (tileIndex << 5) + (inSpriteYCoord << 2));
+
+						/* Get color & check transparency and position */
+						int c = (((ReadVram((ushort)(tileAddress + 0)) >> (7 - inSpriteXCoord)) & 0x1) << 0);
+						c |= (((ReadVram((ushort)(tileAddress + 1)) >> (7 - inSpriteXCoord)) & 0x1) << 1);
+						c |= (((ReadVram((ushort)(tileAddress + 2)) >> (7 - inSpriteXCoord)) & 0x1) << 2);
+						c |= (((ReadVram((ushort)(tileAddress + 3)) >> (7 - inSpriteXCoord)) & 0x1) << 3);
+
+						if (c == 0) continue;
+
+						int x = pixelActiveDisplay + (xCoordinate + pixel);
+						if (IsScreenUsageFlagSet(y, x, screenUsageSprite))
+						{
+							/* If sprite was already at this location, set sprite collision flag */
+							isSpriteCollision = true;
+						}
+						else if (!IsScreenUsageFlagSet(y, x, screenUsageBgHighPriority))
+						{
+							/* Draw if pixel isn't occupied by high-priority BG */
+							SetPixel(y, x, 1, c);
+						}
+
+						/* Note that there is a sprite here regardless */
+
+						/* VDPDIFF: Mk3 / SMS1 VDP zoomed sprites bug, horizontally zoomed area of sprite (i.e. pixels 9-16) is ignored by collision; do not mark location as containing a sprite
+						 * https://www.smspower.org/forums/post109677#109677 
+						 * TODO: verify behavior somehow?
+						 */
+						if ((vdpType == VDPTypes.Mk3SMS1 && isZoomedSprites && pixel < 8) || (vdpType == VDPTypes.Mk3SMS1 && !isZoomedSprites) || vdpType == VDPTypes.SMS2GG)
+							SetScreenUsageFlag(y, x, screenUsageSprite);
+					}
+				}
+			}
+		}
+
+		protected void UpdateResolution()
+		{
+			/* Check screenmode */
+			if (isSMS240LineMode)
+			{
+				screenHeight = NumActiveScanlinesHigh;
+				nametableHeight = 256;
+				vCounterTableIndex = (isPalChip ? 5 : 2);
+			}
+			else if (isSMS224LineMode)
+			{
+				screenHeight = NumActiveScanlinesMed;
+				nametableHeight = 256;
+				vCounterTableIndex = (isPalChip ? 4 : 1);
+			}
+			else
+			{
+				screenHeight = NumActiveScanlinesLow;
+				nametableHeight = 224;
+				vCounterTableIndex = (isPalChip ? 3 : 0);
+			}
+
+			/* Scanline parameters */
+			if (!isPalChip)
+			{
+				/* NTSC */
+				if (screenHeight == NumActiveScanlinesHigh)
+				{
+					/* 240 active lines, invalid on NTSC (dummy values); "Line Interrupt Test #1" mode 1 will show blue screen */
+					topBorderSize = 0;
+					verticalActiveDisplaySize = 0;
+					bottomBorderSize = 0;
+				}
+				else if (screenHeight == NumActiveScanlinesMed)
+				{
+					/* 224 active lines */
+					topBorderSize = 11;
+					verticalActiveDisplaySize = 224;
+					bottomBorderSize = 8;
+				}
+				else
+				{
+					/* 192 active lines */
+					topBorderSize = 27;
+					verticalActiveDisplaySize = 192;
+					bottomBorderSize = 24;
+				}
+			}
+			else
+			{
+				/* PAL */
+				if (screenHeight == NumActiveScanlinesHigh)
+				{
+					/* 240 active lines */
+					topBorderSize = 30;
+					verticalActiveDisplaySize = 240;
+					bottomBorderSize = 24;
+				}
+				else if (screenHeight == NumActiveScanlinesMed)
+				{
+					/* 224 active lines */
+					topBorderSize = 38;
+					verticalActiveDisplaySize = 224;
+					bottomBorderSize = 32;
+				}
+				else
+				{
+					/* 192 active lines */
+					topBorderSize = 54;
+					verticalActiveDisplaySize = 192;
+					bottomBorderSize = 48;
+				}
+			}
+
+			scanlineTopBorder = 0;
+			scanlineActiveDisplay = (scanlineTopBorder + topBorderSize);
+			scanlineBottomBorder = (scanlineActiveDisplay + verticalActiveDisplaySize);
+
+			numVisibleScanlines = (topBorderSize + verticalActiveDisplaySize + bottomBorderSize);
+
+			/* Pixel parameters */
+			leftBorderSize = 13;
+			horizontalActiveDisplaySize = 256;
+			rightBorderSize = 15;
+
+			pixelLeftBorder = 0;
+			pixelActiveDisplay = (pixelLeftBorder + leftBorderSize);
+			pixelRightBorder = (pixelActiveDisplay + horizontalActiveDisplaySize);
+
+			numVisiblePixels = (leftBorderSize + horizontalActiveDisplaySize + rightBorderSize);
+		}
+
+		protected virtual void WriteColorToFramebuffer(int palette, int color, int address)
+		{
+			WriteColorToFramebuffer(cram[((palette * 16) + color)], address);
+		}
+
+		protected override void WriteColorToFramebuffer(ushort colorValue, int address)
+		{
+			/* If not in Master System video mode, color value is index into legacy colormap */
+			if (!isBitM4Set)
+				colorValue = (legacyColorMap[colorValue & 0x000F]);
+
+			RGB222toBGRA8888(colorValue, ref outputFramebuffer, address);
+		}
+
+		protected override void WriteDataPort(byte value)
+		{
+			isSecondControlWrite = false;
+
+			readBuffer = value;
+
+			switch (codeRegister)
+			{
+				case 0x00:
+				case 0x01:
+				case 0x02:
+					WriteVram(addressRegister, value);
+					break;
+				case 0x03:
+					cram[(addressRegister & 0x001F)] = value;
+					break;
+			}
+
+			addressRegister++;
+		}
+
+		protected override byte ReadControlPort()
+		{
+			byte statusCurrent = (byte)statusFlags;
+
+			statusFlags = StatusFlags.None;
+			isSecondControlWrite = false;
+
+			isLineInterruptPending = false;
+
+			InterruptLine = InterruptState.Clear;
+
+			return statusCurrent;
+		}
+
+		public override byte ReadPort(byte port)
+		{
+			if ((port & 0x40) == 0x40)
+				if ((port & 0x01) == 0)
+					return (byte)vCounter;      /* V counter */
+				else
+					return (byte)hCounter;      /* H counter */
+			else
+				return base.ReadPort(port);
+		}
+
+		protected override void WriteRegister(byte register, byte value)
+		{
+			if (register < registers.Length)
+				registers[register] = value;
+
+			if (register == 0x00 || register == 0x01)
+				UpdateResolution();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs.meta
new file mode 100644
index 0000000..0d3a6b4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/SegaSMSVDP.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b2208cc9d471f2340aa4019b18607dda
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs b/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs
new file mode 100644
index 0000000..f356b8d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs
@@ -0,0 +1,881 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+using Essgee.EventArguments;
+using Essgee.Utilities;
+
+using static Essgee.Emulation.Utilities;
+
+namespace Essgee.Emulation.Video
+{
+	/* Texas Instruments TMS99xxA family */
+	public class TMS99xxA : IVideo
+	{
+		public const int NumTotalScanlinesPal = 313;
+		public const int NumTotalScanlinesNtsc = 262;
+
+		public const int NumActiveScanlines = 192;
+		public const int NumActivePixelsPerScanline = 256;
+
+		protected const string layerBackgroundOptionName = "GraphicsLayersShowBackground";
+		protected const string layerSpritesOptionName = "GraphicsLayersShowSprites";
+		protected const string layerBordersOptionName = "GraphicsLayersShowBorders";
+
+		protected readonly int numTotalPixelsPerScanline = 342;
+		protected virtual int numTotalScanlines => (isPalChip ? NumTotalScanlinesPal : NumTotalScanlinesNtsc);
+
+		protected int topBorderSize, verticalActiveDisplaySize, bottomBorderSize;
+		protected int scanlineTopBorder, scanlineActiveDisplay, scanlineBottomBorder;
+		protected int numVisibleScanlines;
+
+		protected int leftBorderSize, horizontalActiveDisplaySize, rightBorderSize;
+		protected int pixelLeftBorder, pixelActiveDisplay, pixelRightBorder;
+		protected int numVisiblePixels;
+
+		public virtual (int X, int Y, int Width, int Height) Viewport => (0, 0, numVisiblePixels, numVisibleScanlines);
+
+		public virtual event EventHandler<SizeScreenEventArgs> SizeScreen;
+		public virtual void OnSizeScreen(SizeScreenEventArgs e) { SizeScreen?.Invoke(this, e); }
+
+		public virtual event EventHandler<RenderScreenEventArgs> RenderScreen;
+		public virtual void OnRenderScreen(RenderScreenEventArgs e) { RenderScreen?.Invoke(this, e); }
+
+		public virtual event EventHandler<EventArgs> EndOfScanline;
+		public virtual void OnEndOfScanline(EventArgs e) { EndOfScanline?.Invoke(this, e); }
+
+		protected const int NumSprites = 32;
+		protected const int NumSpritesPerLine = 4;
+
+		protected double clockRate, refreshRate;
+		protected bool isPalChip;
+
+		[StateRequired]
+		protected byte[] registers, vram;
+		[StateRequired]
+		protected (int Number, int Y, int X, int Pattern, int Attribute)[][] spriteBuffer;
+
+		protected ushort vramMask16k => 0x3FFF;
+		protected ushort vramMask4k => 0x0FFF;
+
+		[StateRequired]
+		protected bool isSecondControlWrite;
+		[StateRequired]
+		protected ushort controlWord;
+		[StateRequired]
+		protected byte readBuffer;
+
+		protected byte codeRegister => (byte)((controlWord >> 14) & 0x03);
+		protected ushort addressRegister
+		{
+			get { return (ushort)(controlWord & 0x3FFF); }
+			set { controlWord = (ushort)((controlWord & 0xC000) | (value & 0x3FFF)); }
+		}
+
+		[Flags]
+		protected enum StatusFlags : byte
+		{
+			None = 0,
+			SpriteCollision = (1 << 5),
+			SpriteOverflow = (1 << 6),
+			FrameInterruptPending = (1 << 7)
+		}
+		[StateRequired]
+		protected StatusFlags statusFlags;
+		protected bool isSpriteCollision
+		{
+			get { return ((statusFlags & StatusFlags.SpriteCollision) == StatusFlags.SpriteCollision); }
+			set { statusFlags = ((statusFlags & ~StatusFlags.SpriteCollision) | (value ? StatusFlags.SpriteCollision : StatusFlags.None)); }
+		}
+		protected bool isSpriteOverflow
+		{
+			get { return ((statusFlags & StatusFlags.SpriteOverflow) == StatusFlags.SpriteOverflow); }
+			set { statusFlags = ((statusFlags & ~StatusFlags.SpriteOverflow) | (value ? StatusFlags.SpriteOverflow : StatusFlags.None)); }
+		}
+		protected bool isFrameInterruptPending
+		{
+			get { return ((statusFlags & StatusFlags.FrameInterruptPending) == StatusFlags.FrameInterruptPending); }
+			set { statusFlags = ((statusFlags & ~StatusFlags.FrameInterruptPending) | (value ? StatusFlags.FrameInterruptPending : StatusFlags.None)); }
+		}
+		protected bool isFrameInterruptEnabled => IsBitSet(registers[0x01], 5);
+
+		[StateRequired]
+		public InterruptState InterruptLine { get; set; }
+
+		[StateRequired]
+		protected int currentScanline;
+
+		protected bool isDisplayBlanked => !IsBitSet(registers[0x01], 6);
+
+		protected bool is16kVRAMEnabled => IsBitSet(registers[0x01], 7);
+
+		protected bool isBitM1Set => IsBitSet(registers[0x01], 4);
+		protected bool isBitM2Set => IsBitSet(registers[0x00], 1);
+		protected bool isBitM3Set => IsBitSet(registers[0x01], 3);
+
+		protected virtual bool isModeGraphics1 => !(isBitM1Set || isBitM2Set || isBitM3Set);
+		protected virtual bool isModeText => (isBitM1Set && !(isBitM2Set || isBitM3Set));
+		protected virtual bool isModeGraphics2 => (isBitM2Set && !(isBitM1Set || isBitM3Set));
+		protected virtual bool isModeMulticolor => (isBitM3Set && !(isBitM1Set || isBitM2Set));
+
+		protected bool isLargeSprites => IsBitSet(registers[0x01], 1);
+		protected bool isZoomedSprites => IsBitSet(registers[0x01], 0);
+
+		protected virtual ushort nametableBaseAddress => (ushort)((registers[0x02] & 0x0F) << 10);
+		protected virtual ushort spriteAttribTableBaseAddress => (ushort)((registers[0x05] & 0x7F) << 7);
+		protected virtual ushort spritePatternGenBaseAddress => (ushort)((registers[0x06] & 0x07) << 11);
+
+		protected byte backgroundColor => (byte)(registers[0x07] & 0x0F);
+		protected byte textColor => (byte)((registers[0x07] >> 4) & 0x0F);
+
+		/* http://www.smspower.org/Development/Palette */
+		readonly byte[][] colorValuesBgr = new byte[][]
+		{
+			/*              B     G     R */
+			new byte[] { 0x00, 0x00, 0x00 },	/* Transparent */
+			new byte[] { 0x00, 0x00, 0x00 },	/* Black */
+			new byte[] { 0x3B, 0xB7, 0x47 },	/* Medium green */
+			new byte[] { 0x6F, 0xCF, 0x7C },	/* Light green */
+			new byte[] { 0xFF, 0x4E, 0x5D },	/* Dark blue */
+			new byte[] { 0xFF, 0x72, 0x80 },	/* Light blue */
+			new byte[] { 0x47, 0x62, 0xB6 },	/* Dark red */
+			new byte[] { 0xED, 0xC8, 0x5D },	/* Cyan */
+			new byte[] { 0x48, 0x6B, 0xD7 },	/* Medium red */
+			new byte[] { 0x6C, 0x8F, 0xFB },	/* Light red */
+			new byte[] { 0x41, 0xCD, 0xC3 },	/* Dark yellow */
+			new byte[] { 0x76, 0xDA, 0xD3 },	/* Light yellow */
+			new byte[] { 0x2F, 0x9F, 0x3E },	/* Dark green */
+			new byte[] { 0xC7, 0x64, 0xB6 },	/* Magenta */
+			new byte[] { 0xCC, 0xCC, 0xCC },	/* Gray */
+			new byte[] { 0xFF, 0xFF, 0xFF }		/* White */
+		};
+
+		protected const byte screenUsageEmpty = 0;
+		protected const byte screenUsageSprite = (1 << 0);
+		protected const byte screenUsageBackground = (1 << 1);
+		[StateRequired]
+		protected byte[] screenUsage;
+
+		[StateRequired]
+		protected int cycleCount;
+		protected byte[] outputFramebuffer;
+
+		protected int clockCyclesPerLine;
+
+		public (string Name, string Description)[] RuntimeOptions => new (string name, string description)[]
+		{
+			(layerBackgroundOptionName, "Background"),
+			(layerSpritesOptionName, "Sprites"),
+			(layerBordersOptionName, "Borders"),
+		};
+
+		protected bool layerBackgroundForceEnable, layerSpritesForceEnable, layerBordersForceEnable;
+
+		public TMS99xxA()
+		{
+			registers = new byte[0x08];
+			vram = new byte[0x4000];
+
+			spriteBuffer = new (int Number, int Y, int X, int Pattern, int Attribute)[NumActiveScanlines][];
+			for (int i = 0; i < spriteBuffer.Length; i++) spriteBuffer[i] = new (int Number, int Y, int X, int Pattern, int Attribute)[NumSpritesPerLine];
+
+			layerBackgroundForceEnable = true;
+			layerSpritesForceEnable = true;
+			layerBordersForceEnable = true;
+		}
+
+		public object GetRuntimeOption(string name)
+		{
+			switch (name)
+			{
+				case layerBackgroundOptionName: return layerBackgroundForceEnable;
+				case layerSpritesOptionName: return layerSpritesForceEnable;
+				case layerBordersOptionName: return layerBordersForceEnable;
+				default: return null;
+			}
+		}
+
+		public void SetRuntimeOption(string name, object value)
+		{
+			switch (name)
+			{
+				case layerBackgroundOptionName: layerBackgroundForceEnable = (bool)value; break;
+				case layerSpritesOptionName: layerSpritesForceEnable = (bool)value; break;
+				case layerBordersOptionName: layerBordersForceEnable = (bool)value; break;
+			}
+		}
+
+		public virtual void Startup()
+		{
+			Reset();
+
+			Debug.Assert(clockRate != 0.0, "Clock rate is zero", "{0} clock rate is not configured", GetType().FullName);
+			Debug.Assert(refreshRate != 0.0, "Refresh rate is zero", "{0} refresh rate is not configured", GetType().FullName);
+		}
+
+		public virtual void Shutdown()
+		{
+			//
+		}
+
+		public virtual void Reset()
+		{
+			for (int i = 0; i < registers.Length; i++) registers[i] = 0;
+			for (int i = 0; i < vram.Length; i++) vram[i] = 0;
+
+			for (int i = 0; i < spriteBuffer.Length; i++)
+				for (int j = 0; j < spriteBuffer[i].Length; j++)
+					spriteBuffer[i][j] = (-1, 0, 0, 0, 0);
+
+			isSecondControlWrite = false;
+			controlWord = 0x0000;
+			readBuffer = 0;
+
+			statusFlags = StatusFlags.None;
+
+			// TODO/FIXME: begin on random scanline (i.e. Vcounter for SMS/GG) on reset, http://www.smspower.org/forums/post62735#62735
+			currentScanline = new Random().Next(scanlineTopBorder, numVisibleScanlines);
+
+			ClearScreenUsage();
+
+			cycleCount = 0;
+		}
+
+		public void SetClockRate(double clock)
+		{
+			clockRate = clock;
+
+			ReconfigureTimings();
+		}
+
+		public void SetRefreshRate(double refresh)
+		{
+			refreshRate = refresh;
+			isPalChip = (refreshRate <= 50.0);
+
+			ReconfigureTimings();
+		}
+
+		public virtual void SetRevision(int rev)
+		{
+			Debug.Assert(rev == 0, "Invalid revision", "{0} revision is invalid; only rev 0 is valid", GetType().FullName);
+		}
+
+		protected virtual void ReconfigureTimings()
+		{
+			/* Calculate cycles/line */
+			clockCyclesPerLine = (int)Math.Round((clockRate / refreshRate) / numTotalScanlines);
+
+			/* Create arrays */
+			screenUsage = new byte[numVisiblePixels * numVisibleScanlines];
+			outputFramebuffer = new byte[(numVisiblePixels * numVisibleScanlines) * 4];
+
+			/* Scanline parameters */
+			if (!isPalChip)
+			{
+				topBorderSize = 27;
+				verticalActiveDisplaySize = 192;
+				bottomBorderSize = 24;
+			}
+			else
+			{
+				topBorderSize = 54;
+				verticalActiveDisplaySize = 192;
+				bottomBorderSize = 48;
+			}
+
+			scanlineTopBorder = 0;
+			scanlineActiveDisplay = (scanlineTopBorder + topBorderSize);
+			scanlineBottomBorder = (scanlineActiveDisplay + verticalActiveDisplaySize);
+
+			numVisibleScanlines = (topBorderSize + verticalActiveDisplaySize + bottomBorderSize);
+
+			/* Pixel parameters */
+			leftBorderSize = 13;
+			horizontalActiveDisplaySize = 256;
+			rightBorderSize = 15;
+
+			pixelLeftBorder = 0;
+			pixelActiveDisplay = (pixelLeftBorder + leftBorderSize);
+			pixelRightBorder = (pixelActiveDisplay + horizontalActiveDisplaySize);
+
+			numVisiblePixels = (leftBorderSize + horizontalActiveDisplaySize + rightBorderSize);
+
+			OnSizeScreen(new SizeScreenEventArgs(numVisiblePixels, numVisibleScanlines));
+		}
+
+		public virtual void Step(int clockCyclesInStep)
+		{
+			InterruptLine = ((isFrameInterruptEnabled && isFrameInterruptPending) ? InterruptState.Assert : InterruptState.Clear);
+
+			cycleCount += clockCyclesInStep;
+
+			if (cycleCount >= clockCyclesPerLine)
+			{
+				OnEndOfScanline(EventArgs.Empty);
+
+				CheckSpriteOverflow(currentScanline);
+
+				RenderLine(currentScanline);
+
+				if (currentScanline == (scanlineBottomBorder + 1))
+					isFrameInterruptPending = true;
+
+				currentScanline++;
+				if (currentScanline == numTotalScanlines)
+				{
+					currentScanline = 0;
+					ClearScreenUsage();
+
+					PrepareRenderScreen();
+				}
+
+				ParseSpriteTable(currentScanline);
+
+				cycleCount -= clockCyclesPerLine;
+				if (cycleCount <= -clockCyclesPerLine) cycleCount = 0;
+			}
+		}
+
+		protected virtual void PrepareRenderScreen()
+		{
+			OnRenderScreen(new RenderScreenEventArgs(numVisiblePixels, numVisibleScanlines, outputFramebuffer.Clone() as byte[]));
+		}
+
+		protected virtual void ClearScreenUsage()
+		{
+			for (int i = 0; i < screenUsage.Length; i++)
+				screenUsage[i] = screenUsageEmpty;
+		}
+
+		protected virtual void RenderLine(int y)
+		{
+			if (y >= scanlineTopBorder && y < scanlineActiveDisplay)
+			{
+				if (layerBordersForceEnable) SetLine(y, backgroundColor);
+				else SetLine(y, 0x00, 0x00, 0x00);
+			}
+			else if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
+			{
+				if (layerBackgroundForceEnable)
+				{
+					if (isModeGraphics1)
+						RenderLineGraphics1Background(y);
+					else if (isModeGraphics2)
+						RenderLineGraphics2Background(y);
+					else if (isModeMulticolor)
+						RenderLineMulticolorBackground(y);
+					else if (isModeText)
+						RenderLineTextBackground(y);
+				}
+				else
+					SetLine(y, 0x00, 0x00, 0x00);
+
+				if (layerSpritesForceEnable)
+				{
+					if (!isModeText)
+						RenderLineSprites(y);
+				}
+
+				RenderBorders(y);
+			}
+			else if (y >= scanlineBottomBorder && y < numVisibleScanlines)
+			{
+				if (layerBordersForceEnable) SetLine(y, backgroundColor);
+				else SetLine(y, 0x00, 0x00, 0x00);
+			}
+		}
+
+		protected virtual void RenderBorders(int y)
+		{
+			for (int x = pixelLeftBorder; x < pixelActiveDisplay; x++)
+			{
+				if (layerBordersForceEnable) SetPixel(y, x, backgroundColor);
+				else SetPixel(y, x, 0x00, 0x00, 0x00);
+			}
+			for (int x = pixelRightBorder; x < numVisiblePixels; x++)
+			{
+				if (layerBordersForceEnable) SetPixel(y, x, backgroundColor);
+				else SetPixel(y, x, 0x00, 0x00, 0x00);
+			}
+		}
+
+		protected void SetLine(int y, ushort colorValue)
+		{
+			for (int x = 0; x < numVisiblePixels; x++)
+				SetPixel(y, x, colorValue);
+		}
+
+		protected virtual void SetLine(int y, byte b, byte g, byte r)
+		{
+			for (int x = 0; x < numVisiblePixels; x++)
+				SetPixel(y, x, b, g, r);
+		}
+
+		protected void SetPixel(int y, int x, ushort colorValue)
+		{
+			WriteColorToFramebuffer(colorValue, ((y * numVisiblePixels) + (x % numVisiblePixels)) * 4);
+		}
+
+		protected virtual void SetPixel(int y, int x, byte b, byte g, byte r)
+		{
+			WriteColorToFramebuffer(b, g, r, ((y * numVisiblePixels) + (x % numVisiblePixels)) * 4);
+		}
+
+		protected byte GetScreenUsageFlag(int y, int x)
+		{
+			return screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)];
+		}
+
+		protected bool IsScreenUsageFlagSet(int y, int x, byte flag)
+		{
+			return ((GetScreenUsageFlag(y, x) & flag) == flag);
+		}
+
+		protected void SetScreenUsageFlag(int y, int x, byte flag)
+		{
+			screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)] |= flag;
+		}
+
+		protected void ClearScreenUsageFlag(int y, int x, byte flag)
+		{
+			screenUsage[(y * numVisiblePixels) + (x % numVisiblePixels)] &= (byte)~flag;
+		}
+
+		protected void RenderLineGraphics1Background(int y)
+		{
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			int numColumns = 32;
+			ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
+			ushort colorTableBaseAddress = (ushort)(registers[0x03] << 6);
+
+			for (int column = 0; column < numColumns; column++)
+			{
+				/* Calculate nametable address, fetch character number */
+				ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
+				byte characterNumber = ReadVram(nametableAddress);
+
+				/* Fetch pixel and color data for current pixel line (1 byte, 8 pixels) */
+				byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + (activeDisplayY % 8)));
+				byte pixelLineColor = ReadVram((ushort)(colorTableBaseAddress + (characterNumber / 8)));
+
+				/* Extract background and foreground color indices */
+				byte[] colorIndicesBackgroundForeground = new byte[2];
+				colorIndicesBackgroundForeground[0] = (byte)(pixelLineColor & 0x0F);
+				colorIndicesBackgroundForeground[1] = (byte)(pixelLineColor >> 4);
+
+				for (int pixel = 0; pixel < 8; pixel++)
+				{
+					/* Fetch color index for current pixel (bit clear means background, bit set means foreground color) */
+					byte c = colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)];
+					/* Color index 0 is transparent, use background color */
+					if (c == 0 || isDisplayBlanked) c = backgroundColor;
+
+					/* Record screen usage, write to framebuffer */
+					int x = pixelActiveDisplay + (column * 8) + pixel;
+					if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
+					{
+						SetPixel(y, x, c);
+						SetScreenUsageFlag(y, x, screenUsageBackground);
+					}
+				}
+			}
+		}
+
+		protected void RenderLineGraphics2Background(int y)
+		{
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			int numColumns = (NumActivePixelsPerScanline / 8);
+
+			/* Calculate some base addresses */
+			ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x04) << 11);
+			ushort colorTableBaseAddress = (ushort)((registers[0x03] & 0x80) << 6);
+
+			for (int column = 0; column < numColumns; column++)
+			{
+				/* Calculate nametable address */
+				ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
+
+				/* Calculate character number and masks */
+				ushort characterNumber = (ushort)(((activeDisplayY / 64) << 8) | ReadVram(nametableAddress));
+				ushort characterNumberDataMask = (ushort)(((registers[0x04] & 0x03) << 8) | 0xFF);
+				ushort characterNumberColorMask = (ushort)(((registers[0x03] & 0x7F) << 3) | 0x07);
+
+				/* Fetch pixel and color data for current pixel line (1 byte, 8 pixels) */
+				byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + ((characterNumber & characterNumberDataMask) * 8) + (activeDisplayY % 8)));
+				byte pixelLineColor = ReadVram((ushort)(colorTableBaseAddress + ((characterNumber & characterNumberColorMask) * 8) + (activeDisplayY % 8)));
+
+				/* Extract background and foreground color indices */
+				byte[] colorIndicesBackgroundForeground = new byte[2];
+				colorIndicesBackgroundForeground[0] = (byte)(pixelLineColor & 0x0F);
+				colorIndicesBackgroundForeground[1] = (byte)(pixelLineColor >> 4);
+
+				for (int pixel = 0; pixel < 8; pixel++)
+				{
+					/* Fetch color index for current pixel (bit clear means background, bit set means foreground color) */
+					byte c = colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)];
+					/* Color index 0 is transparent, use background color */
+					if (c == 0 || isDisplayBlanked) c = backgroundColor;
+
+					/* Record screen usage, write to framebuffer */
+					int x = pixelActiveDisplay + (column * 8) + pixel;
+					if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
+					{
+						SetPixel(y, x, c);
+						SetScreenUsageFlag(y, x, screenUsageBackground);
+					}
+				}
+			}
+		}
+
+		protected void RenderLineMulticolorBackground(int y)
+		{
+			// TODO: check accuracy w/ games besides Smurfs Paint & Play Workshop (if there are any)
+
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			int numColumns = 32;
+			int row = (activeDisplayY / 8);
+			ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
+
+			for (int column = 0; column < numColumns; column++)
+			{
+				/* Calculate nametable address, fetch character number */
+				ushort nametableAddress = (ushort)(nametableBaseAddress + (row * numColumns) + column);
+				byte characterNumber = ReadVram(nametableAddress);
+
+				/* Calculate pattern generator address, fetch pattern data */
+				ushort patternGeneratorAddress = (ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + ((row & 0x03) * 2) + ((activeDisplayY / 4) % 2));
+				byte patternData = ReadVram(patternGeneratorAddress);
+
+				for (int block = 0; block < 8; block += 4)
+				{
+					/* Fetch color index for current 4x4 block */
+					byte c = (byte)((patternData >> (4 - block)) & 0x0F);
+
+					/* Color index 0 is transparent, use background color */
+					if (c == 0 || isDisplayBlanked) c = backgroundColor;
+
+					for (int pixel = 0; pixel < 4; pixel++)
+					{
+						/* Record screen usage, write to framebuffer */
+						int x = pixelActiveDisplay + (column * 8) + (block + pixel);
+						if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
+						{
+							SetPixel(y, x, c);
+							SetScreenUsageFlag(y, x, screenUsageBackground);
+						}
+					}
+				}
+			}
+		}
+
+		protected void RenderLineTextBackground(int y)
+		{
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			int numColumns = 40;
+			ushort patternGeneratorBaseAddress = (ushort)((registers[0x04] & 0x07) << 11);
+
+			/* Get background and text color indices */
+			byte[] colorIndicesBackgroundForeground = new byte[2];
+			colorIndicesBackgroundForeground[0] = backgroundColor;
+			colorIndicesBackgroundForeground[1] = textColor;
+
+			/* Draw left and right 8px borders */
+			for (int pixel = 0; pixel < 8; pixel++)
+			{
+				int x = pixelActiveDisplay + pixel;
+				SetPixel(y, x + 0, backgroundColor);
+				SetPixel(y, x + 8 + (numColumns * 6), backgroundColor);
+			}
+
+			/* Draw text columns */
+			for (int column = 0; column < numColumns; column++)
+			{
+				/* Calculate nametable address, fetch character number */
+				ushort nametableAddress = (ushort)(nametableBaseAddress + ((activeDisplayY / 8) * numColumns) + column);
+				byte characterNumber = ReadVram(nametableAddress);
+
+				/* Fetch pixel data for current pixel line (1 byte, 8 pixels) */
+				byte pixelLineData = ReadVram((ushort)(patternGeneratorBaseAddress + (characterNumber * 8) + (activeDisplayY % 8)));
+
+				for (int pixel = 0; pixel < 6; pixel++)
+				{
+					/* Fetch color index for current pixel (bit clear means background, bit set means text color) */
+					byte c = (isDisplayBlanked ? backgroundColor : colorIndicesBackgroundForeground[((pixelLineData >> (7 - pixel)) & 0x01)]);
+
+					/* Record screen usage, write to framebuffer */
+					int x = pixelActiveDisplay + 8 + (column * 6) + pixel;
+					if (GetScreenUsageFlag(y, x) == screenUsageEmpty)
+					{
+						SetPixel(y, x, c);
+						SetScreenUsageFlag(y, x, screenUsageBackground);
+					}
+				}
+			}
+		}
+
+		protected virtual void WriteSpriteNumberToStatus(int spriteNumber)
+		{
+			statusFlags &= (StatusFlags.FrameInterruptPending | StatusFlags.SpriteOverflow | StatusFlags.SpriteCollision);
+			statusFlags |= (StatusFlags)spriteNumber;
+		}
+
+		protected virtual void CheckSpriteOverflow(int y)
+		{
+			/* Ensure current scanline is within active display */
+			if (y >= scanlineActiveDisplay && y < scanlineBottomBorder)
+			{
+				int activeDisplayY = (y - scanlineActiveDisplay);
+
+				/* If last sprite in buffer is valid, sprite overflow occured */
+				int lastSpriteInBuffer = spriteBuffer[activeDisplayY][NumSpritesPerLine - 1].Number;
+				if (lastSpriteInBuffer != -1)
+				{
+					isSpriteOverflow = true;
+
+					/* Store sprite number in status register */
+					WriteSpriteNumberToStatus(lastSpriteInBuffer);
+				}
+			}
+		}
+
+		protected virtual void ParseSpriteTable(int y)
+		{
+			if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
+
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			/* Clear sprite list for current line */
+			for (int i = 0; i < spriteBuffer[activeDisplayY].Length; i++) spriteBuffer[activeDisplayY][i] = (-1, 0, 0, 0, 0);
+
+			/* Determine sprite size & get zoomed sprites adjustment */
+			int zoomShift = (isZoomedSprites ? 1 : 0);
+			int spriteHeight = ((isLargeSprites ? 16 : 8) << zoomShift);
+
+			int numValidSprites = 0;
+			for (int sprite = 0; sprite < NumSprites; sprite++)
+			{
+				int yCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4)));
+
+				/* Ignore following if Y coord is 208 */
+				if (yCoordinate == 208)
+				{
+					/* Store first "illegal sprite" number in status register */
+					WriteSpriteNumberToStatus(sprite);
+					return;
+				}
+
+				/* Modify Y coord as needed */
+				yCoordinate++;
+				if (yCoordinate > NumActiveScanlines + 32) yCoordinate -= 256;
+
+				/* Ignore this sprite if on incorrect lines */
+				if (activeDisplayY < yCoordinate || activeDisplayY >= (yCoordinate + spriteHeight)) continue;
+
+				/* Check if maximum number of sprites per line is reached */
+				numValidSprites++;
+				if (numValidSprites > NumSpritesPerLine) return;
+
+				/* Mark sprite for rendering */
+				int xCoordinate = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 1));
+				int patternNumber = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 2));
+				int attributes = ReadVram((ushort)(spriteAttribTableBaseAddress + (sprite * 4) + 3));
+
+				spriteBuffer[activeDisplayY][numValidSprites - 1] = (sprite, yCoordinate, xCoordinate, patternNumber, attributes);
+			}
+
+			/* Because we didn't bow out before already, store total number of sprites in status register */
+			WriteSpriteNumberToStatus(NumSprites - 1);
+		}
+
+		protected void RenderLineSprites(int y)
+		{
+			if (y < scanlineActiveDisplay || y >= scanlineBottomBorder) return;
+
+			/* Determine coordinates in active display */
+			int activeDisplayY = (y - scanlineActiveDisplay);
+
+			/* Determine sprite size & get zoomed sprites adjustment */
+			int spriteSize = (isLargeSprites ? 16 : 8);
+			int zoomShift = (isZoomedSprites ? 1 : 0);
+			int numSpritePixels = (spriteSize << zoomShift);
+
+			foreach (var sprite in spriteBuffer[activeDisplayY])
+			{
+				if (sprite.Number == -1) continue;
+
+				if (!isDisplayBlanked)
+				{
+					int yCoordinate = sprite.Y;
+					int xCoordinate = sprite.X;
+					int patternNumber = sprite.Pattern;
+					int attributes = sprite.Attribute;
+
+					/* Fetch sprite information, extract attributes */
+					bool earlyClock = ((attributes & 0x80) == 0x80);
+					int spriteColor = (attributes & 0x0F);
+
+					/* Adjust according to registers/attributes */
+					if (earlyClock) xCoordinate -= 32;
+					if (isLargeSprites) patternNumber &= 0xFC;
+
+					for (int pixel = 0; pixel < numSpritePixels; pixel++)
+					{
+						/* Check if sprite is outside active display, else continue to next sprite */
+						if ((xCoordinate + pixel) < 0 || (xCoordinate + pixel) >= NumActivePixelsPerScanline) continue;
+
+						/* Determine coordinate inside sprite */
+						int inSpriteXCoord = (pixel >> zoomShift) % spriteSize;
+						int inSpriteYCoord = ((activeDisplayY - yCoordinate) >> zoomShift) % spriteSize;
+
+						/* Calculate address and fetch pixel data */
+						ushort spritePatternAddress = spritePatternGenBaseAddress;
+						spritePatternAddress += (ushort)(patternNumber << 3);
+						spritePatternAddress += (ushort)inSpriteYCoord;
+						if (inSpriteXCoord >= 8) spritePatternAddress += 16;
+
+						byte pixelLineData = ReadVram(spritePatternAddress);
+
+						/* Check if pixel from pattern needs to be drawn, else continue to next sprite */
+						if (((pixelLineData >> (7 - (inSpriteXCoord % 8))) & 0x01) == 0x00) continue;
+
+						int x = pixelActiveDisplay + (xCoordinate + pixel);
+						if (IsScreenUsageFlagSet(y, x, screenUsageSprite))
+						{
+							/* If sprite was already at this location, set sprite collision flag */
+							isSpriteCollision = true;
+						}
+						else
+						{
+							/* If color isn't transparent, draw pixel to framebuffer */
+							if (spriteColor != 0)
+								SetPixel(y, x, (ushort)spriteColor);
+						}
+
+						/* Note that there is a sprite here regardless */
+						SetScreenUsageFlag(y, x, screenUsageSprite);
+					}
+				}
+			}
+		}
+
+		protected virtual void WriteColorToFramebuffer(byte b, byte g, byte r, int address)
+		{
+			outputFramebuffer[address + 0] = b;
+			outputFramebuffer[address + 1] = g;
+			outputFramebuffer[address + 2] = r;
+			outputFramebuffer[address + 3] = 0xFF;
+		}
+
+		protected virtual void WriteColorToFramebuffer(ushort colorValue, int address)
+		{
+			outputFramebuffer[address + 0] = colorValuesBgr[colorValue & 0x0F][0];
+			outputFramebuffer[address + 1] = colorValuesBgr[colorValue & 0x0F][1];
+			outputFramebuffer[address + 2] = colorValuesBgr[colorValue & 0x0F][2];
+			outputFramebuffer[address + 3] = 0xFF;
+		}
+
+		protected virtual byte ReadVram(ushort address)
+		{
+			if (is16kVRAMEnabled)
+				return vram[address & vramMask16k];
+			else
+				return vram[address & vramMask4k];
+		}
+
+		protected virtual void WriteVram(ushort address, byte value)
+		{
+			if (is16kVRAMEnabled)
+				vram[address & vramMask16k] = value;
+			else
+				vram[address & vramMask4k] = value;
+		}
+
+		protected virtual byte ReadDataPort()
+		{
+			isSecondControlWrite = false;
+			statusFlags = StatusFlags.None;
+
+			byte data = readBuffer;
+			readBuffer = ReadVram(addressRegister);
+			addressRegister++;
+
+			return data;
+		}
+
+		protected virtual byte ReadControlPort()
+		{
+			byte statusCurrent = (byte)statusFlags;
+
+			statusFlags = StatusFlags.None;
+			isSecondControlWrite = false;
+
+			InterruptLine = InterruptState.Clear;
+
+			return statusCurrent;
+		}
+
+		public virtual byte ReadPort(byte port)
+		{
+			if ((port & 0x01) == 0x00)
+				return ReadDataPort();
+			else
+				return ReadControlPort();
+		}
+
+		protected virtual void WriteDataPort(byte value)
+		{
+			isSecondControlWrite = false;
+
+			readBuffer = value;
+			WriteVram(addressRegister, value);
+			addressRegister++;
+		}
+
+		protected virtual void WriteControlPort(byte value)
+		{
+			if (!isSecondControlWrite)
+				controlWord = (ushort)((controlWord & 0xFF00) | (value << 0));
+			else
+			{
+				controlWord = (ushort)((controlWord & 0x00FF) | (value << 8));
+
+				switch (codeRegister)
+				{
+					case 0x00: readBuffer = ReadVram(addressRegister); addressRegister++; break;
+					case 0x01: break;
+					case 0x02: WriteRegister((byte)((controlWord >> 8) & 0x0F), (byte)(controlWord & 0x00FF)); break;
+					case 0x03: break;
+				}
+			}
+
+			isSecondControlWrite = !isSecondControlWrite;
+		}
+
+		public virtual void WritePort(byte port, byte value)
+		{
+			if ((port & 0x01) == 0x00)
+				WriteDataPort(value);
+			else
+				WriteControlPort(value);
+		}
+
+		protected virtual void WriteRegister(byte register, byte value)
+		{
+			// TODO: confirm register mirroring
+			registers[register & 0x07] = value;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs.meta b/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs.meta
new file mode 100644
index 0000000..7baf48f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Emulation/Video/TMS99xxA.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a235296639e8898418d408c99ebe70fc
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Enumerations.cs b/Assets/Plugins/Essgee/Enumerations.cs
new file mode 100644
index 0000000..3f4b685
--- /dev/null
+++ b/Assets/Plugins/Essgee/Enumerations.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee
+{
+	public enum ScreenSizeMode { Stretch, Scale, Integer }
+
+	public enum ExceptionResult { Continue, StopEmulation, ExitApplication }
+
+	public enum ExtraDataTypes { Raw, Image }
+
+	[Flags]
+	public enum ExtraDataOptions
+	{
+		IncludeDateTime = (1 << 0),
+		AllowOverwrite = (1 << 1)
+	}
+}
diff --git a/Assets/Plugins/Essgee/Enumerations.cs.meta b/Assets/Plugins/Essgee/Enumerations.cs.meta
new file mode 100644
index 0000000..cfecbcd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Enumerations.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2f9f52ebdd4aee949bf1ee9b137c9ec7
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EssgeeLogger.cs b/Assets/Plugins/Essgee/EssgeeLogger.cs
new file mode 100644
index 0000000..d4bd5cd
--- /dev/null
+++ b/Assets/Plugins/Essgee/EssgeeLogger.cs
@@ -0,0 +1,14 @@
+using UnityEngine;
+
+public class EssgeeLogger
+{
+    public static void WriteLine(string message = null)
+    {
+        
+    }
+
+    public static void EnqueueMessageSuccess(string message = null)
+    {
+
+    }
+}
diff --git a/Assets/Plugins/Essgee/EssgeeLogger.cs.meta b/Assets/Plugins/Essgee/EssgeeLogger.cs.meta
new file mode 100644
index 0000000..35abea2
--- /dev/null
+++ b/Assets/Plugins/Essgee/EssgeeLogger.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cdcc61f43394b604d9ff021fce4eeace
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments.meta b/Assets/Plugins/Essgee/EventArguments.meta
new file mode 100644
index 0000000..b85ebf7
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 281db18b9e0f1054e8b4abae8427ba41
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs
new file mode 100644
index 0000000..4428cdf
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class ChangeViewportEventArgs : EventArgs
+	{
+		public (int X, int Y, int Width, int Height) Viewport { get; private set; }
+
+		public ChangeViewportEventArgs((int, int, int, int) viewport)
+		{
+			Viewport = viewport;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs.meta
new file mode 100644
index 0000000..7094092
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/ChangeViewportEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a5fdb88b89c3ee84b861d3156f877eec
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs
new file mode 100644
index 0000000..e485702
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class EnqueueSamplesEventArgs : EventArgs
+	{
+		public int NumChannels { get; set; }
+		public short[][] ChannelSamples { get; set; }
+		public bool[] IsChannelMuted { get; set; }
+		public short[] MixedSamples { get; set; }
+
+		public EnqueueSamplesEventArgs(int numChannels, short[][] channelSamples, bool[] isMuted, short[] mixedSamples)
+		{
+			NumChannels = numChannels;
+			ChannelSamples = channelSamples;
+			IsChannelMuted = isMuted;
+			MixedSamples = mixedSamples;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs.meta
new file mode 100644
index 0000000..83facde
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/EnqueueSamplesEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 43ad2fa85569796468e3bd2923aaf5db
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs
new file mode 100644
index 0000000..e8072dc
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs
@@ -0,0 +1,26 @@
+using Essgee.Utilities.XInput;
+using System;
+using System.Collections.Generic;
+
+namespace Essgee.EventArguments
+{
+    public class PollInputEventArgs : EventArgs
+	{
+		public IEnumerable<Keys> Keyboard { get; set; }
+
+		public MouseButtons MouseButtons { get; set; }
+		public (int X, int Y) MousePosition { get; set; }
+
+		public ControllerState ControllerState { get; set; }
+
+		public PollInputEventArgs()
+		{
+			Keyboard = new List<Keys>();
+
+			MouseButtons = MouseButtons.None;
+			MousePosition = (0, 0);
+
+			ControllerState = new ControllerState();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs.meta
new file mode 100644
index 0000000..62f987e
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/PollInputEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ada5c874b28f2c24f8622fd7dce81592
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs
new file mode 100644
index 0000000..49835fc
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class RenderScreenEventArgs : EventArgs
+	{
+		public int Width { get; private set; }
+		public int Height { get; private set; }
+		public byte[] FrameData { get; private set; }
+
+		public RenderScreenEventArgs(int width, int height, byte[] data)
+		{
+			Width = width;
+			Height = height;
+			FrameData = data;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs.meta
new file mode 100644
index 0000000..bbe7faf
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/RenderScreenEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: effb4e50cecefc94194ff7943b6d9bf9
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs
new file mode 100644
index 0000000..50f3fa0
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class SaveExtraDataEventArgs : EventArgs
+	{
+		public ExtraDataTypes DataType { get; private set; }
+		public ExtraDataOptions Options { get; private set; }
+
+		public string Description { get; private set; }
+		public object Data { get; private set; }
+
+		public SaveExtraDataEventArgs(ExtraDataTypes type, ExtraDataOptions option, string desc, object data)
+		{
+			DataType = type;
+			Options = option;
+			Description = desc;
+			Data = data;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs.meta
new file mode 100644
index 0000000..66b734e
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SaveExtraDataEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 239da568cc1e77a4c8cf9692726fbd37
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs
new file mode 100644
index 0000000..00c664c
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class SendLogMessageEventArgs : EventArgs
+	{
+		public string Message { get; private set; }
+
+		public SendLogMessageEventArgs(string message)
+		{
+			Message = message;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs.meta
new file mode 100644
index 0000000..50355e8
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SendLogMessageEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 73ff88c017bde9147946020156991f15
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs b/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs
new file mode 100644
index 0000000..25f36a3
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.EventArguments
+{
+	public class SizeScreenEventArgs : EventArgs
+	{
+		public int Width { get; private set; }
+		public int Height { get; private set; }
+
+		public SizeScreenEventArgs(int width, int height)
+		{
+			Width = width;
+			Height = height;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs.meta b/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs.meta
new file mode 100644
index 0000000..3a61f86
--- /dev/null
+++ b/Assets/Plugins/Essgee/EventArguments/SizeScreenEventArgs.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9ab5875b41ee4fa4790e97a80748fcbd
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Exceptions.meta b/Assets/Plugins/Essgee/Exceptions.meta
new file mode 100644
index 0000000..afa813c
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f1e895bd77f41874fad666b2f480f482
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs b/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs
new file mode 100644
index 0000000..ecd42b9
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Essgee.Exceptions
+{
+	[Serializable]
+	public class CartridgeLoaderException : Exception
+	{
+		public CartridgeLoaderException() : base() { }
+		public CartridgeLoaderException(string message) : base(message) { }
+		public CartridgeLoaderException(string message, Exception innerException) : base(message, innerException) { }
+		public CartridgeLoaderException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs.meta b/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs.meta
new file mode 100644
index 0000000..2907438
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/CartridgeLoaderException.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d5e871a4c155d6343910ef62bb192f5a
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Exceptions/EmulationException.cs b/Assets/Plugins/Essgee/Exceptions/EmulationException.cs
new file mode 100644
index 0000000..263de32
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/EmulationException.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Essgee.Exceptions
+{
+	[Serializable]
+	public class EmulationException : Exception
+	{
+		public EmulationException() : base() { }
+		public EmulationException(string message) : base(message) { }
+		public EmulationException(string message, Exception innerException) : base(message, innerException) { }
+		public EmulationException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Exceptions/EmulationException.cs.meta b/Assets/Plugins/Essgee/Exceptions/EmulationException.cs.meta
new file mode 100644
index 0000000..3292cff
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/EmulationException.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d29afc05220518e41b9c5ebb04cec0a6
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs b/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs
new file mode 100644
index 0000000..9f3a8f4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Essgee.Exceptions
+{
+	[Serializable]
+	public class GraphicsException : Exception
+	{
+		public GraphicsException() : base() { }
+		public GraphicsException(string message) : base(message) { }
+		public GraphicsException(string message, Exception innerException) : base(message, innerException) { }
+		public GraphicsException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs.meta b/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs.meta
new file mode 100644
index 0000000..2d7a3d1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/GraphicsException.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 292f826ddc00e6f41b4f4ba46a9a6ece
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Exceptions/HandlerException.cs b/Assets/Plugins/Essgee/Exceptions/HandlerException.cs
new file mode 100644
index 0000000..0114f29
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/HandlerException.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Essgee.Exceptions
+{
+	[Serializable]
+	public class HandlerException : Exception
+	{
+		public HandlerException() : base() { }
+		public HandlerException(string message) : base(message) { }
+		public HandlerException(string message, Exception innerException) : base(message, innerException) { }
+		public HandlerException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Exceptions/HandlerException.cs.meta b/Assets/Plugins/Essgee/Exceptions/HandlerException.cs.meta
new file mode 100644
index 0000000..4b03e09
--- /dev/null
+++ b/Assets/Plugins/Essgee/Exceptions/HandlerException.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6c6b7a048bc880a449b2b81ae303f70d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Extensions.meta b/Assets/Plugins/Essgee/Extensions.meta
new file mode 100644
index 0000000..01847d1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 1be95ab09a576dd4db60392be7d5f9f0
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs b/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs
new file mode 100644
index 0000000..3f38165
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.IO;
+using System.Drawing;
+
+namespace Essgee.Extensions
+{
+	public static class AssemblyExtensionMethods
+	{
+		public static T GetAttribute<T>(this ICustomAttributeProvider assembly, bool inherit = false) where T : Attribute
+		{
+			return assembly.GetCustomAttributes(typeof(T), inherit).OfType<T>().FirstOrDefault();
+		}
+
+		public static bool IsEmbeddedResourceAvailable(this Assembly assembly, string resourceName)
+		{
+			using (var stream = assembly.GetManifestResourceStream(resourceName))
+				return (stream != null);
+		}
+
+		public static string ReadEmbeddedTextFile(this Assembly assembly, string resourceName)
+		{
+			using (var reader = new StreamReader(assembly.GetManifestResourceStream(resourceName)))
+				return reader.ReadToEnd();
+		}
+
+		//public static Bitmap ReadEmbeddedImageFile(this Assembly assembly, string resourceName)
+		//{
+		//	using (var stream = assembly.GetManifestResourceStream(resourceName))
+		//		return new Bitmap(stream);
+		//}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs.meta b/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs.meta
new file mode 100644
index 0000000..d971fb3
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/AssemblyExtensionMethods.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2a919f96fbccdab4f98631971a5ad641
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs b/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs
new file mode 100644
index 0000000..423ffb4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs
@@ -0,0 +1,11 @@
+//namespace Essgee.Extensions
+//{
+//    public static class FormExtensionMethods
+//	{
+//		public static void CheckInvokeMethod(this Form form, MethodInvoker methodInvoker)
+//		{
+//			if (form.InvokeRequired) form.BeginInvoke(methodInvoker);
+//			else methodInvoker();
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs.meta b/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs.meta
new file mode 100644
index 0000000..29c7235
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/FormExtensionMethods.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6f4a1f0be012aa141a62c9f7259b614c
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs b/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs
new file mode 100644
index 0000000..a2b5e62
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+
+namespace Essgee.Extensions
+{
+	public static class GeneralExtensionMethods
+	{
+		// https://www.c-sharpcorner.com/UploadFile/ff2f08/deep-copy-of-object-in-C-Sharp/
+		public static T CloneObject<T>(this T source)
+		{
+			var type = source.GetType();
+			var target = (T)Activator.CreateInstance(type);
+
+			foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
+			{
+				if (property.CanWrite)
+				{
+					if (property.PropertyType.IsValueType || property.PropertyType.IsEnum || property.PropertyType.Equals(typeof(string)) || property.PropertyType.Equals(typeof(Type)))
+						property.SetValue(target, property.GetValue(source, null), null);
+					else
+					{
+						object objPropertyValue = property.GetValue(source, null);
+						if (objPropertyValue == null)
+							property.SetValue(target, null, null);
+						else
+							property.SetValue(target, objPropertyValue.CloneObject(), null);
+					}
+				}
+			}
+			return target;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs.meta b/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs.meta
new file mode 100644
index 0000000..29f74bd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/GeneralExtensionMethods.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b82a62a18635b33479a83fdcf68428e5
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs b/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs
new file mode 100644
index 0000000..cc0f697
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+
+using Newtonsoft.Json;
+
+namespace Essgee.Extensions
+{
+	public static class SerializationExtensionMethods
+	{
+		public static void SerializeToFile(this object obj, string jsonFileName)
+		{
+			SerializeToFile(obj, jsonFileName, new JsonSerializerSettings());
+		}
+
+		public static void SerializeToFile(this object obj, string jsonFileName, JsonSerializerSettings serializerSettings)
+		{
+			using (var writer = new StreamWriter(jsonFileName))
+			{
+				writer.Write(JsonConvert.SerializeObject(obj, Formatting.Indented, serializerSettings));
+			}
+		}
+
+		public static T DeserializeFromFile<T>(this string jsonFileName)
+		{
+			using (var reader = new StreamReader(jsonFileName))
+			{
+				return (T)JsonConvert.DeserializeObject(reader.ReadToEnd(), typeof(T), new JsonSerializerSettings() { Formatting = Formatting.Indented });
+			}
+		}
+
+		public static T DeserializeObject<T>(this string jsonString)
+		{
+			return (T)JsonConvert.DeserializeObject(jsonString, typeof(T), new JsonSerializerSettings() { Formatting = Formatting.Indented });
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs.meta b/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs.meta
new file mode 100644
index 0000000..f489bc1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Extensions/SerializationExtensionMethods.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 016c6214586d0ed44a4181b198587385
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics.meta b/Assets/Plugins/Essgee/Graphics.meta
new file mode 100644
index 0000000..e68fab0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 414f2e9f45929a04a9c56ae02bd0ff9e
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Graphics/Enumerations.cs b/Assets/Plugins/Essgee/Graphics/Enumerations.cs
new file mode 100644
index 0000000..b8e3fab
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Enumerations.cs
@@ -0,0 +1,28 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+
+//namespace Essgee.Graphics
+//{
+//	public enum FilterMode
+//	{
+//		Linear,
+//		Nearest
+//	}
+
+//	public enum WrapMode
+//	{
+//		Repeat,
+//		Edge,
+//		Border,
+//		Mirror
+//	}
+
+//	public enum PixelFormat
+//	{
+//		Rgba8888,
+//		Rgb888
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/Enumerations.cs.meta b/Assets/Plugins/Essgee/Graphics/Enumerations.cs.meta
new file mode 100644
index 0000000..7fbe9a2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Enumerations.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: eedf5bf3a0ae6eb41acf730b466dabc9
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs b/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs
new file mode 100644
index 0000000..29cc4cb
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Reflection;
+using System.Reflection.Emit;
+
+namespace Essgee.Graphics
+{
+	/* http://www.codeproject.com/Articles/14593/A-General-Fast-Method-Invoker */
+
+	internal delegate object FastInvokeHandler(object target, object[] paramters);
+
+	internal class FastMethodInvoker
+	{
+		public static FastInvokeHandler GetMethodInvoker(MethodInfo methodInfo)
+		{
+			DynamicMethod dynamicMethod = new DynamicMethod(string.Empty, typeof(object), new Type[] { typeof(object), typeof(object[]) }, methodInfo.DeclaringType.Module);
+
+			ILGenerator il = dynamicMethod.GetILGenerator();
+			ParameterInfo[] ps = methodInfo.GetParameters();
+
+			Type[] paramTypes = new Type[ps.Length];
+			for (int i = 0; i < paramTypes.Length; i++)
+			{
+				if (ps[i].ParameterType.IsByRef)
+					paramTypes[i] = ps[i].ParameterType.GetElementType();
+				else
+					paramTypes[i] = ps[i].ParameterType;
+			}
+
+			LocalBuilder[] locals = new LocalBuilder[paramTypes.Length];
+			for (int i = 0; i < paramTypes.Length; i++)
+				locals[i] = il.DeclareLocal(paramTypes[i], true);
+
+			for (int i = 0; i < paramTypes.Length; i++)
+			{
+				il.Emit(OpCodes.Ldarg_1);
+				EmitFastInt(il, i);
+				il.Emit(OpCodes.Ldelem_Ref);
+				EmitCastToReference(il, paramTypes[i]);
+				il.Emit(OpCodes.Stloc, locals[i]);
+			}
+
+			if (!methodInfo.IsStatic)
+				il.Emit(OpCodes.Ldarg_0);
+
+			for (int i = 0; i < paramTypes.Length; i++)
+			{
+				if (ps[i].ParameterType.IsByRef)
+					il.Emit(OpCodes.Ldloca_S, locals[i]);
+				else
+					il.Emit(OpCodes.Ldloc, locals[i]);
+			}
+
+			if (methodInfo.IsStatic)
+				il.EmitCall(OpCodes.Call, methodInfo, null);
+			else
+				il.EmitCall(OpCodes.Callvirt, methodInfo, null);
+
+			if (methodInfo.ReturnType == typeof(void))
+				il.Emit(OpCodes.Ldnull);
+			else
+				EmitBoxIfNeeded(il, methodInfo.ReturnType);
+
+			for (int i = 0; i < paramTypes.Length; i++)
+			{
+				if (ps[i].ParameterType.IsByRef)
+				{
+					il.Emit(OpCodes.Ldarg_1);
+					EmitFastInt(il, i);
+					il.Emit(OpCodes.Ldloc, locals[i]);
+
+					if (locals[i].LocalType.IsValueType)
+						il.Emit(OpCodes.Box, locals[i].LocalType);
+
+					il.Emit(OpCodes.Stelem_Ref);
+				}
+			}
+
+			il.Emit(OpCodes.Ret);
+
+			FastInvokeHandler invoder = (FastInvokeHandler)dynamicMethod.CreateDelegate(typeof(FastInvokeHandler));
+			return invoder;
+		}
+
+		private static void EmitCastToReference(ILGenerator il, Type type)
+		{
+			if (type.IsValueType)
+				il.Emit(OpCodes.Unbox_Any, type);
+			else
+				il.Emit(OpCodes.Castclass, type);
+		}
+
+		private static void EmitBoxIfNeeded(ILGenerator il, Type type)
+		{
+			if (type.IsValueType)
+				il.Emit(OpCodes.Box, type);
+		}
+
+		private static void EmitFastInt(ILGenerator il, int value)
+		{
+			switch (value)
+			{
+				case -1:
+					il.Emit(OpCodes.Ldc_I4_M1);
+					return;
+				case 0:
+					il.Emit(OpCodes.Ldc_I4_0);
+					return;
+				case 1:
+					il.Emit(OpCodes.Ldc_I4_1);
+					return;
+				case 2:
+					il.Emit(OpCodes.Ldc_I4_2);
+					return;
+				case 3:
+					il.Emit(OpCodes.Ldc_I4_3);
+					return;
+				case 4:
+					il.Emit(OpCodes.Ldc_I4_4);
+					return;
+				case 5:
+					il.Emit(OpCodes.Ldc_I4_5);
+					return;
+				case 6:
+					il.Emit(OpCodes.Ldc_I4_6);
+					return;
+				case 7:
+					il.Emit(OpCodes.Ldc_I4_7);
+					return;
+				case 8:
+					il.Emit(OpCodes.Ldc_I4_8);
+					return;
+			}
+
+			if (value > -129 && value < 128)
+				il.Emit(OpCodes.Ldc_I4_S, (sbyte)value);
+			else
+				il.Emit(OpCodes.Ldc_I4, value);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs.meta b/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs.meta
new file mode 100644
index 0000000..d297deb
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/FastMethodInvoker.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d46aa7b243f39584b93c79dc84079999
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs b/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs
new file mode 100644
index 0000000..df5b3e2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs
@@ -0,0 +1,247 @@
+//using Essgee.Graphics.Shaders;
+//using System;
+//using System.ComponentModel;
+//using System.Diagnostics;
+//using System.Drawing;
+
+//namespace Essgee.Graphics
+//{
+//    public class GraphicsHandler : IDisposable
+//	{
+//		static readonly CommonVertex[] vertices = new CommonVertex[]
+//		{
+//			new CommonVertex() { Position = new Vector3(0.0f, 1.0f, 0.0f), TexCoord = new Vector2(0.0f, 1.0f), Color = Color4.White },
+//			new CommonVertex() { Position = new Vector3(0.0f, 0.0f, 0.0f), TexCoord = new Vector2(0.0f, 0.0f), Color = Color4.White },
+//			new CommonVertex() { Position = new Vector3(1.0f, 0.0f, 0.0f), TexCoord = new Vector2(1.0f, 0.0f), Color = Color4.White },
+//			new CommonVertex() { Position = new Vector3(1.0f, 1.0f, 0.0f), TexCoord = new Vector2(1.0f, 1.0f), Color = Color4.White }
+//		};
+
+//		static readonly byte[] indices = new byte[] { 0, 1, 2, 2, 3, 0 };
+
+//		public string GLRenderer { get; private set; }
+//		public string GLVersion { get; private set; }
+
+//		VertexBuffer vertexBuffer;
+//		ShaderBundle shaderBundle;
+
+//		Texture[] textures;
+//		int lastTextureUpdate;
+
+//		OnScreenDisplayHandler onScreenDisplayHandler;
+
+//		(int Width, int Height) textureSize;
+//		(int X, int Y, int Width, int Height) inputViewport, outputViewport;
+//		Matrix4 projectionMatrix, modelviewMatrix;
+
+//		bool refreshRendererAndShader;
+
+//		Stopwatch stopwatch;
+//		float deltaTime;
+
+//		bool disposed = false;
+
+//		public GraphicsHandler(OnScreenDisplayHandler osdHandler)
+//		{
+//			onScreenDisplayHandler = osdHandler;
+
+//			if (AppEnvironment.EnableOpenGLDebug)
+//				GL.Enable(EnableCap.DebugOutput);
+
+//			GLRenderer = GL.GetString(StringName.Renderer);
+//			GLVersion = GL.GetString(StringName.Version);
+
+//			GL.Enable(EnableCap.Blend);
+//			GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+
+//			vertexBuffer = new VertexBuffer();
+//			vertexBuffer.SetVertexData(vertices);
+//			vertexBuffer.SetIndices(indices);
+
+//			textures = new Texture[ShaderBundle.MaxNumSourceSamplers];
+
+//			lastTextureUpdate = 0;
+
+//			projectionMatrix = Matrix4.Identity;
+//			modelviewMatrix = Matrix4.Identity;
+
+//			stopwatch = Stopwatch.StartNew();
+
+//			Application.Idle += (s, e) =>
+//			{
+//				if (LicenseManager.UsageMode == LicenseUsageMode.Designtime || GraphicsContext.CurrentContext == null) return;
+
+//				stopwatch.Stop();
+//				deltaTime = (float)stopwatch.Elapsed.TotalMilliseconds / 10.0f;
+//				stopwatch.Reset();
+//				stopwatch.Start();
+//			};
+
+//			onScreenDisplayHandler.EnqueueMessageSuccess($"Graphics initialized; {GLRenderer}, {GLVersion}.");
+//		}
+
+//		~GraphicsHandler()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed) return;
+
+//			if (disposing)
+//			{
+//				if (vertexBuffer != null) vertexBuffer.Dispose();
+//				if (shaderBundle != null) shaderBundle.Dispose();
+//			}
+
+//			disposed = true;
+//		}
+
+//		public void LoadShaderBundle(string shaderName)
+//		{
+//			var lastShaderFilter = shaderBundle?.Manifest.Filter;
+//			var lastShaderWrap = shaderBundle?.Manifest.Wrap;
+
+//			shaderBundle = new ShaderBundle(shaderName, typeof(CommonVertex));
+
+//			if ((lastShaderFilter != null && lastShaderFilter != shaderBundle.Manifest.Filter) || (lastShaderWrap != null && lastShaderWrap != shaderBundle.Manifest.Wrap))
+//				CreateTextures();
+
+//			FlushTextures();
+
+//			refreshRendererAndShader = true;
+
+//			onScreenDisplayHandler.EnqueueMessage($"Loaded shader '{shaderName}'.");
+//		}
+
+//		public void SetTextureSize(int width, int height)
+//		{
+//			textureSize = (width, height);
+
+//			CreateTextures();
+
+//			refreshRendererAndShader = true;
+//		}
+
+//		private void CreateTextures()
+//		{
+//			if (textureSize.Width == 0 || textureSize.Height == 0)
+//				return;
+
+//			for (int i = 0; i < textures.Length; i++)
+//				textures[i] = new Texture(textureSize.Width, textureSize.Height, PixelFormat.Rgba8888, shaderBundle.Manifest.Filter, shaderBundle.Manifest.Wrap);
+
+//			lastTextureUpdate = 0;
+//		}
+
+//		public void SetTextureData(byte[] data)
+//		{
+//			textures[lastTextureUpdate].SetData(data);
+//			lastTextureUpdate++;
+//			if (lastTextureUpdate >= shaderBundle.Manifest.Samplers) lastTextureUpdate = 0;
+//		}
+
+//		public void SetScreenViewport((int, int, int, int) viewport)
+//		{
+//			inputViewport = viewport;
+
+//			lastTextureUpdate = 0;
+
+//			refreshRendererAndShader = true;
+//		}
+
+//		public void FlushTextures()
+//		{
+//			for (int i = 0; i < shaderBundle.Manifest.Samplers; i++)
+//				textures[i]?.ClearData();
+
+//			lastTextureUpdate = 0;
+
+//			refreshRendererAndShader = true;
+//		}
+
+//		public void Resize(Rectangle clientRectangle, Size screenSize)
+//		{
+//			GL.Viewport(0, 0, clientRectangle.Width, clientRectangle.Height);
+
+//			projectionMatrix = Matrix4.CreateOrthographicOffCenter(0.0f, clientRectangle.Width, clientRectangle.Height, 0.0f, -10.0f, 10.0f);
+
+//			switch (Program.Configuration.ScreenSizeMode)
+//			{
+//				case ScreenSizeMode.Stretch:
+//					{
+//						modelviewMatrix = Matrix4.CreateScale(clientRectangle.Width, clientRectangle.Height, 1.0f);
+
+//						outputViewport = (clientRectangle.X, clientRectangle.Y, clientRectangle.Width, clientRectangle.Height);
+//					}
+//					break;
+
+//				case ScreenSizeMode.Scale:
+//					{
+//						var multiplier = (float)Math.Min(clientRectangle.Width / (double)screenSize.Width, clientRectangle.Height / (double)screenSize.Height);
+
+//						var adjustedWidth = screenSize.Width * multiplier;
+//						var adjustedHeight = screenSize.Height * multiplier;
+//						var adjustedX = (float)Math.Floor((clientRectangle.Width - adjustedWidth) / 2.0f);
+//						var adjustedY = (float)Math.Floor((clientRectangle.Height - adjustedHeight) / 2.0f);
+
+//						modelviewMatrix = Matrix4.CreateScale(adjustedWidth, adjustedHeight, 1.0f) * Matrix4.CreateTranslation(adjustedX, adjustedY, 1.0f);
+
+//						outputViewport = (clientRectangle.X, clientRectangle.Y, clientRectangle.Width, clientRectangle.Height);
+//					}
+//					break;
+
+//				case ScreenSizeMode.Integer:
+//					{
+//						var multiplier = (float)Math.Min(Math.Floor(clientRectangle.Width / (double)inputViewport.Width), Math.Floor(clientRectangle.Height / (double)inputViewport.Height));
+
+//						var adjustedWidth = inputViewport.Width * multiplier;
+//						var adjustedHeight = inputViewport.Height * multiplier;
+//						var adjustedX = (float)Math.Floor((clientRectangle.Width - adjustedWidth) / 2.0f);
+//						var adjustedY = (float)Math.Floor((clientRectangle.Height - adjustedHeight) / 2.0f);
+
+//						modelviewMatrix = Matrix4.CreateScale(adjustedWidth, adjustedHeight, 1.0f) * Matrix4.CreateTranslation(adjustedX, adjustedY, 1.0f);
+
+//						outputViewport = ((int)adjustedX, (int)adjustedY, (int)adjustedWidth, (int)adjustedHeight);
+//					}
+//					break;
+//			}
+
+//			onScreenDisplayHandler.SetViewport((clientRectangle.X, clientRectangle.Y, clientRectangle.Width, clientRectangle.Height));
+//			onScreenDisplayHandler.SetProjectionMatrix(projectionMatrix);
+//			onScreenDisplayHandler.SetModelviewMatrix(modelviewMatrix);
+
+//			refreshRendererAndShader = true;
+//		}
+
+//		public void Render()
+//		{
+//			if (refreshRendererAndShader)
+//			{
+//				shaderBundle.SetTextureSize(new Vector2(textureSize.Width, textureSize.Height));
+//				shaderBundle.SetInputViewport(new Vector4(inputViewport.X, inputViewport.Y, inputViewport.Width, inputViewport.Height));
+//				shaderBundle.SetOutputViewport(new Vector4(outputViewport.X, outputViewport.Y, outputViewport.Width, outputViewport.Height));
+//				shaderBundle.SetProjectionMatrix(projectionMatrix);
+//				shaderBundle.SetModelviewMatrix(modelviewMatrix);
+
+//				GC.Collect();
+
+//				refreshRendererAndShader = false;
+//			}
+
+//			for (int i = 0; i < shaderBundle.Manifest.Samplers; i++)
+//				textures[i]?.Activate(TextureUnit.Texture0 + ((lastTextureUpdate + i) % shaderBundle.Manifest.Samplers));
+
+//			shaderBundle.Activate();
+//			vertexBuffer.Render();
+
+//			onScreenDisplayHandler.Render(deltaTime);
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs.meta b/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs.meta
new file mode 100644
index 0000000..aee3b89
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/GraphicsHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 20fd3917138ed8943be76a9d49cd4327
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs b/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs
new file mode 100644
index 0000000..120b79f
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs
@@ -0,0 +1,374 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+//using System.Drawing;
+//using System.Runtime.InteropServices;
+//using System.Reflection;
+
+//using OpenTK;
+//using OpenTK.Graphics;
+
+//using Essgee.Graphics.Shaders;
+
+//namespace Essgee.Graphics
+//{
+//	public class OnScreenDisplayHandler : IDisposable
+//	{
+//		readonly static int messageDefaultSeconds = 5;
+//		readonly static int maxStringListLength = 128;
+//		readonly static int stringListPurgeSize = 16;
+
+//		readonly static string glslUniformProjection = "projection";
+//		readonly static string glslUniformModelview = "modelview";
+//		readonly static string glslUniformSourceSampler = "source";
+//		readonly static string glslUniformTextColor = "textColor";
+//		readonly static string glslUniformFontSize = "fontSize";
+//		readonly static string glslUniformCharacterOffset = "characterOffset";
+
+//		readonly static string glslVersion = "#version 300 es\n";
+//		readonly static string glslESPrecision = "precision mediump float; precision mediump int;\n";
+//		readonly static string glslMainStart = "void main(void){";
+//		readonly static string glslMainEnd = "}\n";
+
+//		readonly static string vertexUniforms = $"uniform mat4 {glslUniformProjection}; uniform mat4 {glslUniformModelview};\n";
+//		readonly static string vertexOuts = "out vec2 vertTexCoord;\n";
+//		readonly static string vertexMain = $"vertTexCoord = inTextureCoords; gl_Position = {glslUniformProjection} * {glslUniformModelview} * vec4(inPosition.x, inPosition.y, 0.0, 1.0);\n";
+
+//		readonly static string fragmentUniforms = $"uniform sampler2D {glslUniformSourceSampler}; uniform vec4 {glslUniformTextColor}; uniform vec2 {glslUniformFontSize}; uniform vec2 {glslUniformCharacterOffset};\n";
+//		readonly static string fragmentIns = "in vec2 vertTexCoord;\n";
+//		readonly static string fragmentOuts = "out vec4 fragColor;\n";
+
+//		readonly static string fragmentMain =
+//			$"vec2 localTexCoord = vec2(({glslUniformCharacterOffset}.x + vertTexCoord.x) / {glslUniformFontSize}.x, ({glslUniformCharacterOffset}.y + vertTexCoord.y) / {glslUniformFontSize}.y);\n" +
+//			$"vec4 outColor = textColor * texture(source, localTexCoord);\n" +
+//			"if(outColor.a == 0.0) discard;\n" +
+//			"fragColor = outColor;\n";
+
+//		readonly static ushort[] characterIndices = new ushort[] { 0, 1, 2, 2, 3, 0 };
+
+//		readonly static Color4 colorSuccess = new Color4(192, 255, 192, 255);
+//		readonly static Color4 colorWarning = new Color4(255, 192, 160, 255);
+//		readonly static Color4 colorError = new Color4(255, 128, 128, 255);
+//		readonly static Color4 colorCore = new Color4(192, 192, 255, 255);
+//		readonly static Color4 colorDebug = new Color4(192, 128, 255, 255);
+
+//		readonly OnScreenDisplayVertex[] characterVertices;
+
+//		readonly List<VertexElement> vertexElements;
+//		readonly int vertexStructSize;
+//		readonly VertexBuffer characterVertexBuffer;
+
+//		readonly Texture fontTexture;
+//		readonly float characterSourceSize;
+//		readonly float characterDefaultWidth;
+//		readonly GLSLShader shader;
+
+//		readonly Dictionary<char, Vector2> characterOffsetDict;
+//		readonly Dictionary<char, (float start, float width)> characterWidthDict;
+
+//		readonly List<OnScreenDisplayMessage> stringList;
+
+//		(int X, int Y, int Width, int Height) viewport;
+
+//		bool disposed = false;
+
+//		public OnScreenDisplayHandler(Bitmap osdFontBitmap)
+//		{
+//			characterSourceSize = (osdFontBitmap.Width / 16.0f);
+//			characterDefaultWidth = characterSourceSize - (2.0f * (characterSourceSize / 8));
+
+//			characterOffsetDict = new Dictionary<char, Vector2>();
+//			characterWidthDict = new Dictionary<char, (float, float)>();
+//			for (var ch = '\0'; ch < (char)((osdFontBitmap.Width / characterSourceSize) * (osdFontBitmap.Height / characterSourceSize)); ch++)
+//			{
+//				float x = (ch % (osdFontBitmap.Width / (int)characterSourceSize)) * characterSourceSize;
+//				float y = (ch / (osdFontBitmap.Width / (int)characterSourceSize)) * characterSourceSize;
+
+//				float width = characterSourceSize;
+//				float start = 0.0f;
+//				for (float xc = x + (characterSourceSize - 1); xc >= x; xc--)
+//				{
+//					var pixel = osdFontBitmap.GetPixel((int)xc, (int)y);
+//					if (pixel.R == 0xFF && pixel.G == 0x00 && pixel.B == 0x00 && pixel.A == 0xFF)
+//					{
+//						width = (xc - x);
+//						osdFontBitmap.SetPixel((int)xc, (int)y, Color.Transparent);
+//					}
+
+//					if (pixel.R == 0xFF && pixel.G == 0xFF && pixel.B == 0x00 && pixel.A == 0xFF)
+//					{
+//						start = (xc - x);
+//						osdFontBitmap.SetPixel((int)xc, (int)y, Color.Transparent);
+//					}
+//				}
+
+//				characterOffsetDict.Add(ch, new Vector2(x, y));
+//				characterWidthDict.Add(ch, (start, width));
+//			}
+
+//			fontTexture = new Texture(osdFontBitmap, filter: FilterMode.Nearest);
+
+//			(vertexElements, vertexStructSize) = VertexBuffer.DeconstructVertexLayout<OnScreenDisplayVertex>();
+
+//			characterVertices = new OnScreenDisplayVertex[]
+//			{
+//				new OnScreenDisplayVertex() { Position = new Vector2(0.0f,                  characterSourceSize),   TextureCoords = new Vector2(0.0f,                   characterSourceSize) },
+//				new OnScreenDisplayVertex() { Position = new Vector2(0.0f,                  0.0f),                  TextureCoords = new Vector2(0.0f,                   0.0f) },
+//				new OnScreenDisplayVertex() { Position = new Vector2(characterSourceSize,   0.0f),                  TextureCoords = new Vector2(characterSourceSize,    0.0f) },
+//				new OnScreenDisplayVertex() { Position = new Vector2(characterSourceSize,   characterSourceSize),   TextureCoords = new Vector2(characterSourceSize,    characterSourceSize) },
+//			};
+
+//			characterVertexBuffer = new VertexBuffer();
+//			characterVertexBuffer.SetVertexData(characterVertices);
+//			characterVertexBuffer.SetIndices(characterIndices);
+
+//			shader = new GLSLShader();
+//			shader.SetVertexShaderCode(glslVersion, glslESPrecision, vertexUniforms, VertexBuffer.GetShaderPreamble(vertexElements), vertexOuts, glslMainStart, vertexMain, glslMainEnd);
+//			shader.SetFragmentShaderCode(glslVersion, glslESPrecision, fragmentUniforms, fragmentIns, fragmentOuts, glslMainStart, fragmentMain, glslMainEnd);
+//			shader.LinkProgram();
+
+//			shader.SetUniformMatrix(glslUniformProjection, false, Matrix4.Identity);
+//			shader.SetUniformMatrix(glslUniformModelview, false, Matrix4.Identity);
+//			shader.SetUniform(glslUniformSourceSampler, 0);
+//			shader.SetUniform(glslUniformTextColor, Color4.White);
+//			shader.SetUniform(glslUniformFontSize, new Vector2(fontTexture.Width, fontTexture.Height));
+//			shader.SetUniform(glslUniformCharacterOffset, Vector2.Zero);
+
+//			stringList = new List<OnScreenDisplayMessage>();
+//		}
+
+//		~OnScreenDisplayHandler()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed) return;
+
+//			if (disposing)
+//			{
+//				if (fontTexture != null) fontTexture.Dispose();
+//				if (characterVertexBuffer != null) characterVertexBuffer.Dispose();
+//				if (shader != null) shader.Dispose();
+//			}
+
+//			disposed = true;
+//		}
+
+//		public void SetViewport(ValueTuple<int, int, int, int> view)
+//		{
+//			viewport = view;
+//		}
+
+//		public void SetProjectionMatrix(Matrix4 mat4)
+//		{
+//			shader.SetUniformMatrix(glslUniformProjection, false, mat4);
+//		}
+
+//		public void SetModelviewMatrix(Matrix4 mat4)
+//		{
+//			shader.SetUniformMatrix(glslUniformModelview, false, mat4);
+//		}
+
+//		public void SendString(string str, int x, int y)
+//		{
+//			SendString(str, x, y, Color4.White);
+//		}
+
+//		public void SendString(string str, int x, int y, Color4 color)
+//		{
+//			stringList.Add(new OnScreenDisplayMessage()
+//			{
+//				X = x,
+//				Y = y,
+//				Color = color,
+//				Text = str,
+//				ShowUntil = DateTime.Now,
+//				IsLogEntry = false
+//			});
+//		}
+
+//		public void EnqueueMessage(string str)
+//		{
+//			EnqueueMessage(str, Color4.White);
+//		}
+
+//		public void EnqueueMessage(string str, Color4 color)
+//		{
+//			var split = str.Split(new string[] { Environment.NewLine, "\n" }, StringSplitOptions.RemoveEmptyEntries);
+//			for (var i = 0; i < split.Length; i++)
+//			{
+//				stringList.Add(new OnScreenDisplayMessage()
+//				{
+//					X = 0,
+//					Y = 0,
+//					Color = color,
+//					Text = split[i],
+//					ShowUntil = DateTime.Now + TimeSpan.FromTicks(i) + TimeSpan.FromSeconds(messageDefaultSeconds),
+//					IsLogEntry = true
+//				});
+//			}
+//		}
+
+//		public void EnqueueMessageSuccess(string str)
+//		{
+//			EnqueueMessage(str, colorSuccess);
+//		}
+
+//		public void EnqueueMessageWarning(string str)
+//		{
+//			EnqueueMessage(str, colorWarning);
+//		}
+
+//		public void EnqueueMessageError(string str)
+//		{
+//			EnqueueMessage(str, colorError);
+//		}
+
+//		public void EnqueueMessageCore(string str)
+//		{
+//			EnqueueMessage(str, colorCore);
+//		}
+
+//		public void EnqueueMessageDebug(string str)
+//		{
+//			if (Program.AppEnvironment.DebugMode)
+//				EnqueueMessage(str, colorDebug);
+//		}
+
+//		public void Render(float deltaTime)
+//		{
+//			shader.Activate();
+//			fontTexture.Activate();
+
+//			RenderStrings(deltaTime);
+//			RenderLogMessages(deltaTime);
+
+//			stringList.RemoveAll(x => !x.IsLogEntry && x.ShowUntil < DateTime.Now);
+//			stringList.RemoveAll(x => x.IsLogEntry && x.Color.A <= 0.0f);
+
+//			if (stringList.Count > maxStringListLength)
+//				stringList.RemoveRange(0, stringListPurgeSize);
+
+//			fontTexture.Deactivate();
+//		}
+
+//		private int MeasureString(string @string)
+//		{
+//			float width = 0.0f;
+//			foreach (var ch in @string)
+//			{
+//				if (characterWidthDict.ContainsKey(ch))
+//				{
+//					width -= characterWidthDict[ch].start;
+//					width += characterWidthDict[ch].width;
+//				}
+//				else
+//					width += characterDefaultWidth;
+//			}
+//			return (int)width;
+//		}
+
+//		private void RenderStrings(float deltaTime)
+//		{
+//			foreach (var @string in stringList.ToList().Where(x => !x.IsLogEntry))
+//			{
+//				float x = @string.X, y = @string.Y;
+//				if (x < 0.0f) x = (viewport.Width + x) - MeasureString(@string.Text);
+//				if (y < 0.0f) y = (viewport.Height + y) - characterSourceSize;
+//				shader.SetUniform(glslUniformTextColor, @string.Color);
+//				ParseAndRenderString(@string, ref x, ref y);
+//			}
+//		}
+
+//		private void RenderLogMessages(float deltaTime)
+//		{
+//			var logY = (viewport.Height - (characterSourceSize + (characterSourceSize / 2)));
+//			foreach (var @string in stringList.ToList().Where(x => x.IsLogEntry).OrderByDescending(x => x.ShowUntil))
+//			{
+//				float x = characterSourceSize / 2.0f, y = logY;
+//				logY -= characterSourceSize;
+//				shader.SetUniform(glslUniformTextColor, @string.Color);
+//				ParseAndRenderString(@string, ref x, ref y);
+
+//				if ((logY + characterSourceSize) < 0) break;
+//			}
+
+//			var timeNow = DateTime.Now;
+//			foreach (var @string in stringList.ToList().Where(x => x.IsLogEntry))
+//			{
+//				if ((@string.ShowUntil.Ticks - timeNow.Ticks) < TimeSpan.TicksPerSecond)
+//					@string.Color = new Color4(@string.Color.R, @string.Color.G, @string.Color.B, Math.Max(0.0f, @string.Color.A - (deltaTime / 25.0f)));
+//			}
+//		}
+
+//		private void ParseAndRenderString(OnScreenDisplayMessage @string, ref float x, ref float y)
+//		{
+//			foreach (var c in @string.Text)
+//			{
+//				var ch = c;
+
+//				if (!characterWidthDict.ContainsKey(ch))
+//					ch = '\0';
+
+//				x -= characterWidthDict[ch].start;
+
+//				if (ch == '\n' || ch == '\r')
+//				{
+//					x = @string.X;
+//					y += characterSourceSize;
+//					continue;
+//				}
+//				else if (ch != ' ')
+//				{
+//					var osdModelview = Matrix4.Identity;
+//					osdModelview *= Matrix4.CreateTranslation(x, y, 0.0f);
+
+//					shader.SetUniformMatrix(glslUniformModelview, false, osdModelview);
+//					shader.SetUniform(glslUniformCharacterOffset, characterOffsetDict[ch]);
+
+//					characterVertexBuffer.Render();
+//				}
+
+//				x += characterWidthDict[ch].width;
+//			}
+//		}
+
+//		[StructLayout(LayoutKind.Sequential, Pack = 1)]
+//		public struct OnScreenDisplayVertex : IVertexStruct
+//		{
+//			[VertexElement(AttributeIndex = 0)]
+//			public Vector2 Position;
+//			[VertexElement(AttributeIndex = 1)]
+//			public Vector2 TextureCoords;
+//		}
+
+//		public class OnScreenDisplayMessage
+//		{
+//			public int X { get; set; }
+//			public int Y { get; set; }
+//			public Color4 Color { get; set; }
+//			public string Text { get; set; }
+//			public DateTime ShowUntil { get; set; }
+//			public bool IsLogEntry { get; set; }
+
+//			public OnScreenDisplayMessage()
+//			{
+//				X = Y = 0;
+//				Color = Color4.White;
+//				Text = string.Empty;
+//				ShowUntil = DateTime.Now;
+//				IsLogEntry = false;
+//			}
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs.meta b/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs.meta
new file mode 100644
index 0000000..5b4e979
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/OnScreenDisplayHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d339431b097f84640a5625605af96f59
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/RenderControl.cs b/Assets/Plugins/Essgee/Graphics/RenderControl.cs
new file mode 100644
index 0000000..289e66e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/RenderControl.cs
@@ -0,0 +1,108 @@
+//using System;
+//using System.ComponentModel;
+//using System.Diagnostics;
+//using System.Diagnostics.CodeAnalysis;
+//using System.Drawing;
+//using System.Runtime.InteropServices;
+
+//namespace Essgee.Graphics
+//{
+//    [DesignTimeVisible(true), ToolboxItem(true)]
+//	[SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly",
+//		Justification = "False positive.  IDisposable is inherited via IFunctionality.  See http://stackoverflow.com/questions/8925925/code-analysis-ca1063-fires-when-deriving-from-idisposable-and-providing-implemen for details.")]
+//	public class RenderControl : GLControl, IComponent
+//	{
+//		public event EventHandler<EventArgs> Render;
+
+//		bool isRuntime => (LicenseManager.UsageMode != LicenseUsageMode.Designtime);
+//		bool isReady => (isRuntime && GraphicsContext.CurrentContext != null);
+
+//		DebugProc debugCallback;
+
+//		bool wasShown = false;
+
+//		public RenderControl() : base(GraphicsMode.Default, 3, 0, GraphicsContextFlags.Default)
+//		{
+//			if (!isRuntime) return;
+
+//			Application.Idle += ((s, e) => { if (isReady) Invalidate(); });
+//		}
+
+//		protected override bool IsInputKey(Keys keyData)
+//		{
+//			switch (keyData)
+//			{
+//				case Keys.Right:
+//				case Keys.Left:
+//				case Keys.Up:
+//				case Keys.Down:
+//				case (Keys.Shift | Keys.Right):
+//				case (Keys.Shift | Keys.Left):
+//				case (Keys.Shift | Keys.Up):
+//				case (Keys.Shift | Keys.Down):
+//					return true;
+
+//				default:
+//					return base.IsInputKey(keyData);
+//			}
+//		}
+
+//		protected override void OnPaint(PaintEventArgs e)
+//		{
+//			if (!isReady)
+//			{
+//				e.Graphics.Clear(BackColor);
+//				using (Pen pen = new Pen(Color.Red, 3.0f))
+//				{
+//					e.Graphics.InterpolationMode = InterpolationMode.HighQualityBilinear;
+//					e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
+//					e.Graphics.SmoothingMode = SmoothingMode.HighQuality;
+//					e.Graphics.DrawLine(pen, Point.Empty, new Point(ClientRectangle.Right, ClientRectangle.Bottom));
+//					e.Graphics.DrawLine(pen, new Point(0, ClientRectangle.Bottom), new Point(ClientRectangle.Right, 0));
+//				}
+//				return;
+//			}
+
+//			if (!wasShown)
+//			{
+//				OnResize(EventArgs.Empty);
+//				wasShown = true;
+//			}
+
+//			OnRender(EventArgs.Empty);
+
+//			SwapBuffers();
+//		}
+
+//		protected virtual void OnRender(EventArgs e)
+//		{
+//			if (!isReady) return;
+//			Render?.Invoke(this, e);
+//		}
+
+//		protected override void OnLoad(EventArgs e)
+//		{
+//			if (!isReady) return;
+
+//			if (Program.AppEnvironment.EnableOpenGLDebug)
+//			{
+//				debugCallback = new DebugProc(GLDebugCallback);
+//				GL.DebugMessageCallback(debugCallback, IntPtr.Zero);
+//			}
+
+//			GL.ClearColor(BackColor);
+//			base.OnLoad(e);
+//		}
+
+//		protected override void OnResize(EventArgs e)
+//		{
+//			if (!isReady) return;
+//			base.OnResize(e);
+//		}
+
+//		private void GLDebugCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length, IntPtr message, IntPtr userParam)
+//		{
+//			Debug.Print($"{(type == DebugType.DebugTypeError ? "GL ERROR!" : "GL callback")} - source={source}, type={type}, severity={severity}, message={Marshal.PtrToStringAnsi(message)}");
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/RenderControl.cs.meta b/Assets/Plugins/Essgee/Graphics/RenderControl.cs.meta
new file mode 100644
index 0000000..4192dcc
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/RenderControl.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bd60d4cba4b596b4caca2011e09f52f2
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders.meta b/Assets/Plugins/Essgee/Graphics/Shaders.meta
new file mode 100644
index 0000000..b653ad8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9883a8b3cddfccc44b67d176b7f1ac47
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs b/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs
new file mode 100644
index 0000000..c73bbe6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs
@@ -0,0 +1,27 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+
+//using Newtonsoft.Json;
+//using Newtonsoft.Json.Converters;
+
+//namespace Essgee.Graphics.Shaders
+//{
+//	public class BundleManifest
+//	{
+//		[JsonConverter(typeof(StringEnumConverter))]
+//		public FilterMode Filter { get; set; }
+//		[JsonConverter(typeof(StringEnumConverter))]
+//		public WrapMode Wrap { get; set; }
+//		public int Samplers { get; set; }
+
+//		public BundleManifest()
+//		{
+//			Filter = FilterMode.Linear;
+//			Wrap = WrapMode.Repeat;
+//			Samplers = 3;
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs.meta b/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs.meta
new file mode 100644
index 0000000..dd9f7f6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/BundleManifest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: eb0a360511a90b64291ef7d4b8bf80c0
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs b/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs
new file mode 100644
index 0000000..6483991
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs
@@ -0,0 +1,214 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+//using System.Reflection;
+
+//using OpenTK.Graphics.OpenGL;
+
+//using Essgee.Exceptions;
+
+//namespace Essgee.Graphics.Shaders
+//{
+//	public class GLSLShader : IDisposable
+//	{
+//		int vertexShaderObject, fragmentShaderObject, geometryShaderObject;
+//		List<string> vertexShaderCode, fragmentShaderCode, geometryShaderCode;
+
+//		readonly int programObject;
+
+//		static readonly string[] uniformSetMethods =
+//		{
+//			"Uniform1", "Uniform2", "Uniform3", "Uniform4"
+//		};
+
+//		static readonly string[] uniformSetMethodsMatrix =
+//		{
+//			"UniformMatrix2", "UniformMatrix2x3", "UniformMatrix2x4",
+//			"UniformMatrix3", "UniformMatrix3x2", "UniformMatrix3x4",
+//			"UniformMatrix4", "UniformMatrix4x2", "UniformMatrix4x3"
+//		};
+
+//		Dictionary<string, int> uniformLocations;
+//		Dictionary<string, dynamic> uniformData;
+//		Dictionary<Type, FastInvokeHandler> uniformMethods;
+
+//		bool disposed = false;
+
+//		public GLSLShader()
+//		{
+//			vertexShaderObject = fragmentShaderObject = geometryShaderObject = -1;
+//			programObject = GL.CreateProgram();
+
+//			uniformLocations = new Dictionary<string, int>();
+//			uniformData = new Dictionary<string, dynamic>();
+//			uniformMethods = new Dictionary<Type, FastInvokeHandler>();
+//		}
+
+//		~GLSLShader()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed)
+//				return;
+
+//			if (disposing)
+//			{
+//				DetachDeleteShader(programObject, vertexShaderObject);
+//				DetachDeleteShader(programObject, fragmentShaderObject);
+//				DetachDeleteShader(programObject, geometryShaderObject);
+
+//				GL.DeleteProgram(programObject);
+//			}
+
+//			disposed = true;
+//		}
+
+//		public void SetVertexShaderCode(params string[] shaderCode)
+//		{
+//			DetachDeleteShader(programObject, vertexShaderObject);
+//			vertexShaderObject = GenerateShader(ShaderType.VertexShader, vertexShaderCode = shaderCode.ToList());
+//		}
+
+//		public void SetFragmentShaderCode(params string[] shaderCode)
+//		{
+//			DetachDeleteShader(programObject, fragmentShaderObject);
+//			fragmentShaderObject = GenerateShader(ShaderType.FragmentShader, fragmentShaderCode = shaderCode.ToList());
+//		}
+
+//		public void SetGeometryShaderCode(params string[] shaderCode)
+//		{
+//			DetachDeleteShader(programObject, geometryShaderObject);
+//			geometryShaderObject = GenerateShader(ShaderType.GeometryShader, geometryShaderCode = shaderCode.ToList());
+//		}
+
+//		private int GenerateShader(ShaderType shaderType, List<string> shaderCode)
+//		{
+//			var handle = GL.CreateShader(shaderType);
+//			GL.ShaderSource(handle, shaderCode.Count, shaderCode.ToArray(), shaderCode.Select(x => x.Length).ToArray());
+//			GL.CompileShader(handle);
+
+//			GL.GetShaderInfoLog(handle, out string infoLog);
+//			GL.GetShader(handle, ShaderParameter.CompileStatus, out int statusCode);
+//			if (statusCode != 1)
+//				throw new GraphicsException($"Shader compile for {shaderType} failed: {infoLog}");
+
+//			return handle;
+//		}
+
+//		private void DetachDeleteShader(int programObject, int shaderObject)
+//		{
+//			if (shaderObject != -1 && GL.IsShader(shaderObject))
+//			{
+//				GL.DetachShader(programObject, shaderObject);
+//				GL.DeleteShader(shaderObject);
+//			}
+//		}
+
+//		public void LinkProgram()
+//		{
+//			LinkProgram(programObject, new int[] { vertexShaderObject, fragmentShaderObject, geometryShaderObject });
+//		}
+
+//		private void LinkProgram(int programObject, int[] shaderObjects)
+//		{
+//			foreach (var shaderObject in shaderObjects.Where(x => x != -1))
+//				GL.AttachShader(programObject, shaderObject);
+
+//			GL.LinkProgram(programObject);
+//			GL.GetProgramInfoLog(programObject, out string infoLog);
+//			GL.GetProgram(programObject, GetProgramParameterName.LinkStatus, out int statusCode);
+//			if (statusCode != 1)
+//				throw new GraphicsException($"Shader program link failed: {infoLog}");
+//		}
+
+//		public void Activate()
+//		{
+//			if (programObject == -1) throw new GraphicsException("Invalid shader program handle");
+//			GL.UseProgram(programObject);
+//		}
+
+//		public void SetUniform(string name, dynamic data)
+//		{
+//			Activate();
+
+//			Type type = data.GetType();
+
+//			if (!uniformLocations.ContainsKey(name))
+//				uniformLocations.Add(name, GL.GetUniformLocation(programObject, name));
+
+//			uniformData[name] = data;
+
+//			if (uniformMethods.ContainsKey(type))
+//			{
+//				uniformMethods[type](null, new object[] { uniformLocations[name], data });
+//			}
+//			else
+//			{
+//				foreach (string methodName in uniformSetMethods)
+//				{
+//					Type[] argTypes = new Type[] { typeof(int), type };
+//					MethodInfo methodInfo = typeof(GL).GetMethod(methodName, argTypes);
+
+//					if (methodInfo != null)
+//					{
+//						uniformMethods[type] = FastMethodInvoker.GetMethodInvoker(methodInfo);
+//						uniformMethods[type](null, new object[] { uniformLocations[name], data });
+//						return;
+//					}
+//				}
+
+//				throw new GraphicsException("No Uniform method found");
+//			}
+//		}
+
+//		public void SetUniformMatrix(string name, bool transpose, dynamic data)
+//		{
+//			Activate();
+
+//			Type type = data.GetType();
+//			if (!uniformLocations.ContainsKey(name))
+//				uniformLocations.Add(name, GL.GetUniformLocation(programObject, name));
+
+//			uniformData[name] = data;
+
+//			if (uniformMethods.ContainsKey(type))
+//			{
+//				uniformMethods[type](null, new object[] { uniformLocations[name], transpose, data });
+//			}
+//			else
+//			{
+//				foreach (string methodName in uniformSetMethodsMatrix)
+//				{
+//					Type[] argTypes = new Type[] { typeof(int), typeof(bool), data.GetType().MakeByRefType() };
+//					MethodInfo methodInfo = typeof(GL).GetMethod(methodName, argTypes);
+
+//					if (methodInfo != null)
+//					{
+//						uniformMethods[type] = FastMethodInvoker.GetMethodInvoker(methodInfo);
+//						uniformMethods[type](null, new object[] { uniformLocations[name], transpose, data });
+//						return;
+//					}
+//				}
+
+//				throw new GraphicsException("No UniformMatrix method found");
+//			}
+//		}
+
+//		public dynamic GetUniform(string name)
+//		{
+//			if (!uniformData.ContainsKey(name)) throw new ArgumentException();
+//			return uniformData[name];
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs.meta b/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs.meta
new file mode 100644
index 0000000..732b934
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/GLSLShader.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a68ffee335bae1a4a916390d6cf1c0aa
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs b/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs
new file mode 100644
index 0000000..818e1a0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs
@@ -0,0 +1,201 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+//using System.IO;
+//using System.Reflection;
+//using System.Windows.Forms;
+
+//using OpenTK;
+
+//using Essgee.Extensions;
+
+//namespace Essgee.Graphics.Shaders
+//{
+//	public class ShaderBundle : IDisposable
+//	{
+//		public const int MaxNumSourceSamplers = 8;
+
+//		readonly static string manifestFilename = "Manifest.json";
+//		readonly static string fragmentFilename = "Fragment.glsl";
+
+//		readonly static string glslUniformSourceSamplers = "source";
+
+//		readonly static string glslUniformProjection = "projection";
+//		readonly static string glslUniformModelview = "modelview";
+//		readonly static string glslUniformTextureSize = "textureSize";
+//		readonly static string glslUniformInputViewport = "inputViewport";
+//		readonly static string glslUniformOutputViewport = "outputViewport";
+
+//		readonly static string glslVersion = "#version 300 es\n";
+//		readonly static string glslESPrecision = "precision mediump float; precision mediump int;\n";
+//		readonly static string glslMainStart = "void main(void){";
+//		readonly static string glslMainEnd = "}\n";
+
+//		readonly static string defaultVertexUniforms = $"uniform mat4 {glslUniformProjection}; uniform mat4 {glslUniformModelview}; uniform vec2 {glslUniformTextureSize}; uniform vec4 {glslUniformInputViewport};\n";
+//		readonly static string defaultVertexOuts = "out vec4 vertColor; out vec2 vertTexCoord;\n";
+//		readonly static string defaultVertexMain = $"vertColor = inColor; gl_Position = {glslUniformProjection} * {glslUniformModelview} * vec4(inPosition.x, inPosition.y, inPosition.z, 1.0);\n";
+
+//		readonly static string defaultVertexTexCoord = "vertTexCoord = inTexCoord;";
+
+//		readonly static string defaultFragmentUniforms = $"uniform sampler2D {glslUniformSourceSamplers}[{MaxNumSourceSamplers}]; uniform vec2 {glslUniformTextureSize}; uniform vec4 {glslUniformInputViewport}; uniform vec4 {glslUniformOutputViewport};";
+//		readonly static string defaultFragmentIns = "in vec4 vertColor; in vec2 vertTexCoord; layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord;\n";
+//		readonly static string defaultFragmentOuts = "out vec4 fragColor;\n";
+
+//		readonly List<VertexElement> vertexElements;
+//		readonly int vertexStructSize;
+
+//		readonly string vertexPreamble, vertexMain, fragmentPreamble;
+//		string manifestJson, fragmentMain;
+
+//		public BundleManifest Manifest { get; private set; }
+
+//		GLSLShader internalShader;
+
+//		bool disposed = false;
+
+//		public ShaderBundle(Type vertexType)
+//		{
+//			(vertexElements, vertexStructSize) = VertexBuffer.DeconstructVertexLayout(vertexType);
+
+//			var vertexPreambleBuilder = new StringBuilder();
+//			vertexPreambleBuilder.Append(glslVersion);
+//			vertexPreambleBuilder.Append(glslESPrecision);
+//			vertexPreambleBuilder.Append(defaultVertexUniforms);
+//			vertexPreambleBuilder.Append(VertexBuffer.GetShaderPreamble(vertexElements));
+//			vertexPreambleBuilder.Append(defaultVertexOuts);
+//			vertexPreamble = vertexPreambleBuilder.ToString();
+
+//			var vertexMainBuilder = new StringBuilder();
+//			vertexMainBuilder.Append(glslMainStart);
+//			vertexMainBuilder.Append(defaultVertexMain);
+//			vertexMainBuilder.Append(defaultVertexTexCoord);
+//			vertexMainBuilder.Append(glslMainEnd);
+//			vertexMain = vertexMainBuilder.ToString();
+
+//			var fragmentPreambleBuilder = new StringBuilder();
+//			fragmentPreambleBuilder.Append(glslVersion);
+//			fragmentPreambleBuilder.Append(glslESPrecision);
+//			fragmentPreambleBuilder.Append(defaultFragmentUniforms);
+//			fragmentPreambleBuilder.Append(defaultFragmentIns);
+//			fragmentPreambleBuilder.Append(defaultFragmentOuts);
+//			fragmentPreamble = fragmentPreambleBuilder.ToString();
+//		}
+
+//		public ShaderBundle(string shaderName, Type vertexType) : this(vertexType)
+//		{
+//			LoadBundle(shaderName);
+//		}
+
+//		~ShaderBundle()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed)
+//				return;
+
+//			if (disposing)
+//				internalShader.Dispose();
+
+//			disposed = true;
+//		}
+
+//		public void LoadBundle(string shaderName)
+//		{
+//			/* Try loading embedded shader first... */
+//			var shaderEmbeddedPath = $"{Application.ProductName}.Assets.Shaders.{shaderName}";
+//			var shaderEmbeddedManifestFile = $"{shaderEmbeddedPath}.{manifestFilename}";
+//			if (Assembly.GetExecutingAssembly().IsEmbeddedResourceAvailable(shaderEmbeddedManifestFile))
+//			{
+//				manifestJson = Assembly.GetExecutingAssembly().ReadEmbeddedTextFile(shaderEmbeddedManifestFile);
+//				fragmentMain = Assembly.GetExecutingAssembly().ReadEmbeddedTextFile($"{shaderEmbeddedPath}.{fragmentFilename}");
+//			}
+//			/* If embedded shader wasn't found, try loading from assets directory... */
+//			else
+//			{
+//				var bundlePath = Path.Combine(Program.ShaderPath, shaderName);
+
+//				if (!Directory.Exists(bundlePath)) throw new DirectoryNotFoundException($"Shader {shaderName} not found");
+
+//				var manifestPath = Path.Combine(bundlePath, manifestFilename);
+//				if (!File.Exists(manifestPath)) throw new FileNotFoundException($"Manifest {manifestFilename} not found in {bundlePath}");
+
+//				manifestJson = File.ReadAllText(manifestPath);
+
+//				var fragmentPath = Path.Combine(bundlePath, fragmentFilename);
+//				if (!File.Exists(fragmentPath)) throw new FileNotFoundException($"Fragment shader {fragmentFilename} not found in {bundlePath}");
+
+//				using (var reader = new StreamReader(new FileStream(fragmentPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
+//				{
+//					fragmentMain = reader.ReadToEnd();
+//				}
+//			}
+
+//			/* Now initialize GLSL shader using manifest and fragment code */
+//			InitializeBundle(manifestJson, fragmentMain);
+//		}
+
+//		private void InitializeBundle(string manifestJson, string fragmentMain)
+//		{
+//			Manifest = manifestJson.DeserializeObject<BundleManifest>();
+//			if (Manifest.Samplers > MaxNumSourceSamplers)
+//			{
+//				// TODO: give user a warning or something?
+//				Manifest.Samplers = MaxNumSourceSamplers;
+//			}
+
+//			internalShader = new GLSLShader();
+//			internalShader.SetVertexShaderCode(vertexPreamble, vertexMain);
+//			internalShader.SetFragmentShaderCode(fragmentPreamble, fragmentMain);
+//			internalShader.LinkProgram();
+
+//			internalShader.SetUniformMatrix(glslUniformModelview, false, Matrix4.Identity);
+//			internalShader.SetUniform(glslUniformTextureSize, Vector2.One);
+//			internalShader.SetUniform(glslUniformInputViewport, Vector4.One);
+//			internalShader.SetUniform(glslUniformOutputViewport, Vector4.One);
+
+//			for (int i = 0; i < MaxNumSourceSamplers; i++)
+//				internalShader.SetUniform($"{glslUniformSourceSamplers}[{i}]", i);
+//		}
+
+//		public void SetProjectionMatrix(Matrix4 mat4)
+//		{
+//			internalShader.SetUniformMatrix(glslUniformProjection, false, mat4);
+//		}
+
+//		public void SetModelviewMatrix(Matrix4 mat4)
+//		{
+//			internalShader.SetUniformMatrix(glslUniformModelview, false, mat4);
+//		}
+
+//		public void SetTextureSize(Vector2 vec2)
+//		{
+//			internalShader.SetUniform(glslUniformTextureSize, vec2);
+//		}
+
+//		public void SetInputViewport(Vector4 vec4)
+//		{
+//			internalShader.SetUniform(glslUniformInputViewport, vec4);
+//		}
+
+//		public void SetOutputViewport(Vector4 vec4)
+//		{
+//			internalShader.SetUniform(glslUniformOutputViewport, vec4);
+//		}
+
+//		public void Activate()
+//		{
+//			internalShader.Activate();
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs.meta b/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs.meta
new file mode 100644
index 0000000..8a71e13
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Shaders/ShaderBundle.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9505180589ab61c4488cd36d3aed6245
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/Texture.cs b/Assets/Plugins/Essgee/Graphics/Texture.cs
new file mode 100644
index 0000000..7072a21
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Texture.cs
@@ -0,0 +1,200 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+//using System.Drawing;
+//using System.Drawing.Imaging;
+//using System.Runtime.InteropServices;
+
+//using OpenTK.Graphics.OpenGL;
+
+//using GlPixelFormat = OpenTK.Graphics.OpenGL.PixelFormat;
+//using GdiPixelFormat = System.Drawing.Imaging.PixelFormat;
+
+//namespace Essgee.Graphics
+//{
+//	public class Texture : IDisposable
+//	{
+//		static readonly Dictionary<GdiPixelFormat, PixelFormat> gdiPixelFormatMap = new Dictionary<GdiPixelFormat, PixelFormat>()
+//		{
+//			{ GdiPixelFormat.Format32bppArgb, PixelFormat.Rgba8888 },
+//			{ GdiPixelFormat.Format24bppRgb, PixelFormat.Rgb888 }
+//		};
+
+//		static readonly Dictionary<PixelFormat, (PixelInternalFormat, GlPixelFormat, int)> glPixelFormatMap = new Dictionary<PixelFormat, (PixelInternalFormat, GlPixelFormat, int)>()
+//		{
+//			{ PixelFormat.Rgba8888, (PixelInternalFormat.Rgba8, GlPixelFormat.Bgra, 4) },
+//			{ PixelFormat.Rgb888, (PixelInternalFormat.Rgb8, GlPixelFormat.Bgr, 3) }
+//		};
+
+//		readonly static int maxTextureSize;
+
+//		int textureHandle;
+
+//		public int Width { get; private set; }
+//		public int Height { get; private set; }
+
+//		PixelInternalFormat pixelInternalFormat;
+//		GlPixelFormat glPixelFormat;
+//		int bytesPerPixel, dataSize;
+//		byte[] currentData;
+
+//		TextureMinFilter minFilter;
+//		TextureMagFilter magFilter;
+//		TextureWrapMode wrapMode;
+
+//		bool disposed = false;
+
+//		static Texture()
+//		{
+//			maxTextureSize = GL.GetInteger(GetPName.MaxTextureSize);
+//		}
+
+//		public Texture(int width, int height, PixelFormat pixelFormat, FilterMode filter = FilterMode.Linear, WrapMode wrap = WrapMode.Repeat)
+//		{
+//			InitializeRaw(width, height, pixelFormat, filter, wrap);
+//		}
+
+//		public Texture(Bitmap image, FilterMode filter = FilterMode.Linear, WrapMode wrap = WrapMode.Repeat)
+//		{
+//			if (!gdiPixelFormatMap.ContainsKey(image.PixelFormat))
+//				throw new ArgumentException($"Unsupported pixel format {image.PixelFormat}", nameof(image));
+
+//			InitializeRaw(image.Width, image.Height, gdiPixelFormatMap[image.PixelFormat], filter, wrap);
+
+//			var bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadOnly, image.PixelFormat);
+//			var imageData = new byte[bitmapData.Height * bitmapData.Stride];
+//			Marshal.Copy(bitmapData.Scan0, imageData, 0, imageData.Length);
+//			SetData(imageData);
+//			image.UnlockBits(bitmapData);
+//		}
+
+//		~Texture()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed)
+//				return;
+
+//			if (disposing)
+//			{
+//				if (GL.IsTexture(textureHandle))
+//					GL.DeleteTexture(textureHandle);
+//			}
+
+//			disposed = true;
+//		}
+
+//		private void InitializeRaw(int width, int height, PixelFormat pixelFormat, FilterMode filter, WrapMode wrap)
+//		{
+//			if (width <= 0 || width > maxTextureSize) throw new ArgumentOutOfRangeException(nameof(width), $"Invalid width {width}");
+//			Width = width;
+
+//			if (height <= 0 || height > maxTextureSize) throw new ArgumentOutOfRangeException(nameof(height), $"Invalid height {height}");
+//			Height = height;
+
+//			if (!glPixelFormatMap.ContainsKey(pixelFormat)) throw new ArgumentException($"Unsupported pixel format {pixelFormat}", nameof(pixelFormat));
+//			(pixelInternalFormat, glPixelFormat, bytesPerPixel) = glPixelFormatMap[pixelFormat];
+
+//			dataSize = (width * height * bytesPerPixel);
+
+//			switch (filter)
+//			{
+//				case FilterMode.Linear:
+//					minFilter = TextureMinFilter.Linear;
+//					magFilter = TextureMagFilter.Linear;
+//					break;
+
+//				case FilterMode.Nearest:
+//					minFilter = TextureMinFilter.Nearest;
+//					magFilter = TextureMagFilter.Nearest;
+//					break;
+
+//				default:
+//					throw new ArgumentException("Invalid filter mode", nameof(filter));
+//			}
+
+//			switch (wrap)
+//			{
+//				case WrapMode.Repeat: wrapMode = TextureWrapMode.Repeat; break;
+//				case WrapMode.Border: wrapMode = TextureWrapMode.ClampToBorder; break;
+//				case WrapMode.Edge: wrapMode = TextureWrapMode.ClampToEdge; break;
+//				case WrapMode.Mirror: wrapMode = TextureWrapMode.MirroredRepeat; break;
+//				default: throw new ArgumentException("Invalid wrap mode", nameof(wrap));
+//			}
+
+//			GenerateHandles();
+//			InitializeTexture();
+//		}
+
+//		private void GenerateHandles()
+//		{
+//			textureHandle = GL.GenTexture();
+//		}
+
+//		private void InitializeTexture()
+//		{
+//			GL.BindTexture(TextureTarget.Texture2D, textureHandle);
+//			GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)minFilter);
+//			GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)magFilter);
+//			GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)wrapMode);
+//			GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)wrapMode);
+//			if (bytesPerPixel != 4) GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
+//			GL.TexImage2D(TextureTarget.Texture2D, 0, pixelInternalFormat, Width, Height, 0, glPixelFormat, PixelType.UnsignedByte, IntPtr.Zero);
+//			GL.BindTexture(TextureTarget.Texture2D, 0);
+//		}
+
+//		public void SetData(byte[] data)
+//		{
+//			if (data == null) throw new ArgumentNullException(nameof(data), "Image data is null");
+//			if (data.Length != dataSize) throw new ArgumentException($"Image data size mismatch; excepted {dataSize} bytes, got {data.Length} bytes", nameof(data));
+
+//			GL.BindTexture(TextureTarget.Texture2D, textureHandle);
+//			GL.TexSubImage2D(TextureTarget.Texture2D, 0, 0, 0, Width, Height, glPixelFormat, PixelType.UnsignedByte, (currentData = data));
+//		}
+
+//		public byte[] GetData()
+//		{
+//			return currentData;
+//		}
+
+//		public void ClearData()
+//		{
+//			var emptyData = new byte[dataSize];
+//			SetData(emptyData);
+//		}
+
+//		public void Activate()
+//		{
+//			Activate(TextureUnit.Texture0);
+//		}
+
+//		public void Activate(TextureUnit textureUnit)
+//		{
+//			if (textureHandle == -1) throw new InvalidOperationException("Invalid texture handle");
+//			GL.ActiveTexture(textureUnit);
+//			GL.BindTexture(TextureTarget.Texture2D, textureHandle);
+//		}
+
+//		public void Deactivate()
+//		{
+//			Deactivate(TextureUnit.Texture0);
+//		}
+
+//		public void Deactivate(TextureUnit textureUnit)
+//		{
+//			GL.ActiveTexture(textureUnit);
+//			GL.BindTexture(TextureTarget.Texture2D, 0);
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/Texture.cs.meta b/Assets/Plugins/Essgee/Graphics/Texture.cs.meta
new file mode 100644
index 0000000..c5778e8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/Texture.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 27df8906b58443d4ab2ca4181c41616e
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs b/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs
new file mode 100644
index 0000000..b6c4f6e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs
@@ -0,0 +1,276 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using System.Text;
+//using System.Threading.Tasks;
+//using System.Runtime.InteropServices;
+//using System.Reflection;
+
+//using OpenTK;
+//using OpenTK.Graphics;
+//using OpenTK.Graphics.OpenGL;
+
+//using Essgee.Exceptions;
+
+//namespace Essgee.Graphics
+//{
+//	public class VertexBuffer : IDisposable
+//	{
+//		static readonly Dictionary<Type, VertexAttribPointerType> pointerTypeTranslator = new Dictionary<Type, VertexAttribPointerType>()
+//		{
+//			{ typeof(byte), VertexAttribPointerType.UnsignedByte },
+//			{ typeof(sbyte), VertexAttribPointerType.Byte },
+//			{ typeof(ushort), VertexAttribPointerType.UnsignedShort },
+//			{ typeof(short), VertexAttribPointerType.Short },
+//			{ typeof(uint), VertexAttribPointerType.UnsignedInt },
+//			{ typeof(int), VertexAttribPointerType.Int },
+//			{ typeof(float), VertexAttribPointerType.Float },
+//			{ typeof(double), VertexAttribPointerType.Double },
+//			{ typeof(Vector2), VertexAttribPointerType.Float },
+//			{ typeof(Vector3), VertexAttribPointerType.Float },
+//			{ typeof(Vector4), VertexAttribPointerType.Float },
+//			{ typeof(Color4), VertexAttribPointerType.Float }
+//		};
+
+//		static readonly Dictionary<Type, string> glslTypeTranslator = new Dictionary<Type, string>()
+//		{
+//			{ typeof(uint), "uint" },
+//			{ typeof(int), "int" },
+//			{ typeof(float), "float" },
+//			{ typeof(double), "double" },
+//			{ typeof(Vector2), "vec2" },
+//			{ typeof(Vector3), "vec3" },
+//			{ typeof(Vector4), "vec4" },
+//			{ typeof(Color4), "vec4" }
+//		};
+
+//		static readonly Dictionary<Type, DrawElementsType> drawElementsTypeTranslator = new Dictionary<Type, DrawElementsType>()
+//		{
+//			{ typeof(byte), DrawElementsType.UnsignedByte },
+//			{ typeof(ushort), DrawElementsType.UnsignedShort },
+//			{ typeof(uint), DrawElementsType.UnsignedInt }
+//		};
+
+//		readonly int vaoHandle, vboHandle;
+//		int numElementsToDraw;
+
+//		PrimitiveType primitiveType;
+
+//		int elementBufferHandle;
+//		DrawElementsType drawElementsType;
+
+//		List<VertexElement> vertexElements;
+//		int vertexStructSize;
+
+//		bool disposed = false;
+
+//		public VertexBuffer()
+//		{
+//			vaoHandle = GL.GenVertexArray();
+//			vboHandle = GL.GenBuffer();
+//			numElementsToDraw = -1;
+
+//			primitiveType = PrimitiveType.Triangles;
+
+//			elementBufferHandle = -1;
+//			drawElementsType = DrawElementsType.UnsignedByte;
+
+//			vertexElements = null;
+//			vertexStructSize = -1;
+//		}
+
+//		~VertexBuffer()
+//		{
+//			Dispose(false);
+//		}
+
+//		public void Dispose()
+//		{
+//			Dispose(true);
+//			GC.SuppressFinalize(this);
+//		}
+
+//		protected virtual void Dispose(bool disposing)
+//		{
+//			if (disposed)
+//				return;
+
+//			if (disposing)
+//			{
+//				if (GL.IsVertexArray(vaoHandle))
+//					GL.DeleteVertexArray(vaoHandle);
+
+//				if (GL.IsBuffer(vboHandle))
+//					GL.DeleteBuffer(vboHandle);
+
+//				if (GL.IsBuffer(elementBufferHandle))
+//					GL.DeleteBuffer(elementBufferHandle);
+//			}
+
+//			disposed = true;
+//		}
+
+//		public static (List<VertexElement>, int) DeconstructVertexLayout<T>() where T : struct, IVertexStruct
+//		{
+//			return DeconstructVertexLayout(typeof(T));
+//		}
+
+//		public static (List<VertexElement>, int) DeconstructVertexLayout(Type vertexType)
+//		{
+//			if (!typeof(IVertexStruct).IsAssignableFrom(vertexType)) throw new Exceptions.GraphicsException("Cannot deconstruct layout of non-vertex type");
+
+//			var elements = new List<VertexElement>();
+//			var structSize = Marshal.SizeOf(vertexType);
+
+//			foreach (var field in vertexType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+//			{
+//				var attribs = field.GetCustomAttributes(typeof(VertexElementAttribute), false);
+//				if (attribs == null || attribs.Length != 1) continue;
+
+//				var elementAttribute = (attribs[0] as VertexElementAttribute);
+
+//				var numComponents = Marshal.SizeOf(field.FieldType);
+
+//				if (field.FieldType.IsValueType && !field.FieldType.IsEnum)
+//				{
+//					var structFields = field.FieldType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+//					if (structFields == null || structFields.Length < 1 || structFields.Length > 4) throw new Exceptions.GraphicsException("Invalid number of fields in struct");
+//					numComponents = structFields.Length;
+//				}
+
+//				elements.Add(new VertexElement()
+//				{
+//					AttributeIndex = elementAttribute.AttributeIndex,
+//					DataType = field.FieldType,
+//					NumComponents = numComponents,
+//					OffsetInVertex = Marshal.OffsetOf(vertexType, field.Name).ToInt32(),
+//					Name = field.Name
+//				});
+//			}
+
+//			return (elements, structSize);
+//		}
+
+//		public void SetPrimitiveType(PrimitiveType primType)
+//		{
+//			primitiveType = primType;
+//		}
+
+//		public PrimitiveType GetPrimitiveType()
+//		{
+//			return primitiveType;
+//		}
+
+//		public void SetVertexData<TVertex>(TVertex[] vertices) where TVertex : struct, IVertexStruct
+//		{
+//			(vertexElements, vertexStructSize) = DeconstructVertexLayout<TVertex>();
+
+//			GL.BindVertexArray(vaoHandle);
+
+//			GL.BindBuffer(BufferTarget.ArrayBuffer, vboHandle);
+//			GL.BufferData(BufferTarget.ArrayBuffer, new IntPtr(vertexStructSize * vertices.Length), vertices, BufferUsageHint.StaticDraw);
+
+//			foreach (var element in vertexElements)
+//			{
+//				GL.EnableVertexAttribArray(element.AttributeIndex);
+//				GL.VertexAttribPointer(element.AttributeIndex, element.NumComponents, GetVertexAttribPointerType(element.DataType), false, vertexStructSize, element.OffsetInVertex);
+//			}
+
+//			numElementsToDraw = vertices.Length;
+
+//			GL.BindVertexArray(0);
+//		}
+
+//		public void SetIndices<TIndex>(TIndex[] indices) where TIndex : struct, IConvertible
+//		{
+//			drawElementsType = GetDrawElementsType(typeof(TIndex));
+
+//			if (elementBufferHandle == -1)
+//				elementBufferHandle = GL.GenBuffer();
+
+//			GL.BindBuffer(BufferTarget.ElementArrayBuffer, elementBufferHandle);
+//			GL.BufferData(BufferTarget.ElementArrayBuffer, new IntPtr(Marshal.SizeOf(typeof(TIndex)) * indices.Length), indices, BufferUsageHint.StaticDraw);
+
+//			numElementsToDraw = indices.Length;
+//		}
+
+//		private VertexAttribPointerType GetVertexAttribPointerType(Type type)
+//		{
+//			if (pointerTypeTranslator.ContainsKey(type))
+//				return pointerTypeTranslator[type];
+//			else
+//				throw new ArgumentException("Unimplemented or unsupported vertex attribute pointer type");
+//		}
+
+//		private DrawElementsType GetDrawElementsType(Type type)
+//		{
+//			if (drawElementsTypeTranslator.ContainsKey(type))
+//				return drawElementsTypeTranslator[type];
+//			else
+//				throw new ArgumentException("Unsupported draw elements type");
+//		}
+
+//		private static string GetGlslDataType(Type type)
+//		{
+//			if (glslTypeTranslator.ContainsKey(type))
+//				return glslTypeTranslator[type];
+//			else
+//				throw new ArgumentException("Unimplemented or unsupported GLSL data type");
+//		}
+
+//		public string GetShaderPreamble(string prefix = "in")
+//		{
+//			return GetShaderPreamble(vertexElements, prefix);
+//		}
+
+//		public static string GetShaderPreamble(List<VertexElement> vertexElements, string prefix = "in")
+//		{
+//			var stringBuilder = new StringBuilder();
+//			for (int i = 0; i < vertexElements.Count; i++)
+//			{
+//				var element = vertexElements[i];
+//				stringBuilder.AppendLine($"layout(location = {element.AttributeIndex}) in {GetGlslDataType(element.DataType)} {prefix}{element.Name};");
+//			}
+//			return stringBuilder.ToString();
+//		}
+
+//		public void Render()
+//		{
+//			GL.BindVertexArray(vaoHandle);
+
+//			if (elementBufferHandle != -1)
+//			{
+//				GL.BindBuffer(BufferTarget.ElementArrayBuffer, elementBufferHandle);
+//				GL.DrawElements(primitiveType, numElementsToDraw, drawElementsType, 0);
+//			}
+//			else
+//				GL.DrawArrays(primitiveType, 0, numElementsToDraw);
+//		}
+//	}
+
+//	public interface IVertexStruct { }
+
+//	[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+//	public class VertexElementAttribute : Attribute
+//	{
+//		public int AttributeIndex { get; set; }
+
+//		public VertexElementAttribute()
+//		{
+//			AttributeIndex = -1;
+//		}
+//	}
+
+//	[StructLayout(LayoutKind.Sequential, Pack = 1)]
+//	public struct CommonVertex : IVertexStruct
+//	{
+//		[VertexElement(AttributeIndex = 0)]
+//		public Vector3 Position;
+//		[VertexElement(AttributeIndex = 1)]
+//		public Vector3 Normal;
+//		[VertexElement(AttributeIndex = 2)]
+//		public Color4 Color;
+//		[VertexElement(AttributeIndex = 3)]
+//		public Vector2 TexCoord;
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs.meta b/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs.meta
new file mode 100644
index 0000000..bffd985
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/VertexBuffer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f6abd7f4ed8a2dc4a8950cb920e39bda
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Graphics/VertexElement.cs b/Assets/Plugins/Essgee/Graphics/VertexElement.cs
new file mode 100644
index 0000000..3d7a1d6
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/VertexElement.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Graphics
+{
+	public sealed class VertexElement
+	{
+		public int AttributeIndex { get; internal set; }
+		public Type DataType { get; internal set; }
+		public int NumComponents { get; internal set; }
+		public int OffsetInVertex { get; internal set; }
+		public string Name { get; internal set; }
+
+		public VertexElement()
+		{
+			AttributeIndex = -1;
+			DataType = null;
+			NumComponents = -1;
+			OffsetInVertex = -1;
+			Name = string.Empty;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Graphics/VertexElement.cs.meta b/Assets/Plugins/Essgee/Graphics/VertexElement.cs.meta
new file mode 100644
index 0000000..04469e5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Graphics/VertexElement.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2e3f41f8d2cd2534a8906786dbe34d25
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Keys.cs b/Assets/Plugins/Essgee/Keys.cs
new file mode 100644
index 0000000..8e10ef0
--- /dev/null
+++ b/Assets/Plugins/Essgee/Keys.cs
@@ -0,0 +1,216 @@
+using System.Runtime.InteropServices;
+using System;
+using UnityEngine;
+using System.ComponentModel;
+
+[Flags]
+[ComVisible(true)]
+public enum Keys
+{
+    KeyCode = 0xFFFF,
+    Modifiers = -65536,
+    None = 0,
+    LButton = 1,
+    RButton = 2,
+    Cancel = 3,
+    MButton = 4,
+    XButton1 = 5,
+    XButton2 = 6,
+    Back = 8,
+    Tab = 9,
+    LineFeed = 0xA,
+    Clear = 0xC,
+    Return = 0xD,
+    Enter = 0xD,
+    ShiftKey = 0x10,
+    ControlKey = 0x11,
+    Menu = 0x12,
+    Pause = 0x13,
+    Capital = 0x14,
+    CapsLock = 0x14,
+    KanaMode = 0x15,
+    HanguelMode = 0x15,
+    HangulMode = 0x15,
+    JunjaMode = 0x17,
+    FinalMode = 0x18,
+    HanjaMode = 0x19,
+    KanjiMode = 0x19,
+    Escape = 0x1B,
+    IMEConvert = 0x1C,
+    IMENonconvert = 0x1D,
+    IMEAccept = 0x1E,
+    IMEAceept = 0x1E,
+    IMEModeChange = 0x1F,
+    Space = 0x20,
+    Prior = 0x21,
+    PageUp = 0x21,
+    Next = 0x22,
+    PageDown = 0x22,
+    End = 0x23,
+    Home = 0x24,
+    Left = 0x25,
+    Up = 0x26,
+    Right = 0x27,
+    Down = 0x28,
+    Select = 0x29,
+    Print = 0x2A,
+    Execute = 0x2B,
+    Snapshot = 0x2C,
+    PrintScreen = 0x2C,
+    Insert = 0x2D,
+    Delete = 0x2E,
+    Help = 0x2F,
+    D0 = 0x30,
+    D1 = 0x31,
+    D2 = 0x32,
+    D3 = 0x33,
+    D4 = 0x34,
+    D5 = 0x35,
+    D6 = 0x36,
+    D7 = 0x37,
+    D8 = 0x38,
+    D9 = 0x39,
+    A = 0x41,
+    B = 0x42,
+    C = 0x43,
+    D = 0x44,
+    E = 0x45,
+    F = 0x46,
+    G = 0x47,
+    H = 0x48,
+    I = 0x49,
+    J = 0x4A,
+    K = 0x4B,
+    L = 0x4C,
+    M = 0x4D,
+    N = 0x4E,
+    O = 0x4F,
+    P = 0x50,
+    Q = 0x51,
+    R = 0x52,
+    S = 0x53,
+    T = 0x54,
+    U = 0x55,
+    V = 0x56,
+    W = 0x57,
+    X = 0x58,
+    Y = 0x59,
+    Z = 0x5A,
+    LWin = 0x5B,
+    RWin = 0x5C,
+    Apps = 0x5D,
+    Sleep = 0x5F,
+    NumPad0 = 0x60,
+    NumPad1 = 0x61,
+    NumPad2 = 0x62,
+    NumPad3 = 0x63,
+    NumPad4 = 0x64,
+    NumPad5 = 0x65,
+    NumPad6 = 0x66,
+    NumPad7 = 0x67,
+    NumPad8 = 0x68,
+    NumPad9 = 0x69,
+    Multiply = 0x6A,
+    Add = 0x6B,
+    Separator = 0x6C,
+    Subtract = 0x6D,
+    Decimal = 0x6E,
+    Divide = 0x6F,
+    F1 = 0x70,
+    F2 = 0x71,
+    F3 = 0x72,
+    F4 = 0x73,
+    F5 = 0x74,
+    F6 = 0x75,
+    F7 = 0x76,
+    F8 = 0x77,
+    F9 = 0x78,
+    F10 = 0x79,
+    F11 = 0x7A,
+    F12 = 0x7B,
+    F13 = 0x7C,
+    F14 = 0x7D,
+    F15 = 0x7E,
+    F16 = 0x7F,
+    F17 = 0x80,
+    F18 = 0x81,
+    F19 = 0x82,
+    F20 = 0x83,
+    F21 = 0x84,
+    F22 = 0x85,
+    F23 = 0x86,
+    F24 = 0x87,
+    NumLock = 0x90,
+    Scroll = 0x91,
+    LShiftKey = 0xA0,
+    RShiftKey = 0xA1,
+    LControlKey = 0xA2,
+    RControlKey = 0xA3,
+    LMenu = 0xA4,
+    RMenu = 0xA5,
+    BrowserBack = 0xA6,
+    BrowserForward = 0xA7,
+    BrowserRefresh = 0xA8,
+    BrowserStop = 0xA9,
+    BrowserSearch = 0xAA,
+    BrowserFavorites = 0xAB,
+    BrowserHome = 0xAC,
+    VolumeMute = 0xAD,
+    VolumeDown = 0xAE,
+    VolumeUp = 0xAF,
+    MediaNextTrack = 0xB0,
+    MediaPreviousTrack = 0xB1,
+    MediaStop = 0xB2,
+    MediaPlayPause = 0xB3,
+    LaunchMail = 0xB4,
+    SelectMedia = 0xB5,
+    LaunchApplication1 = 0xB6,
+    LaunchApplication2 = 0xB7,
+    OemSemicolon = 0xBA,
+    Oem1 = 0xBA,
+    Oemplus = 0xBB,
+    Oemcomma = 0xBC,
+    OemMinus = 0xBD,
+    OemPeriod = 0xBE,
+    OemQuestion = 0xBF,
+    Oem2 = 0xBF,
+    Oemtilde = 0xC0,
+    Oem3 = 0xC0,
+    OemOpenBrackets = 0xDB,
+    Oem4 = 0xDB,
+    OemPipe = 0xDC,
+    Oem5 = 0xDC,
+    OemCloseBrackets = 0xDD,
+    Oem6 = 0xDD,
+    OemQuotes = 0xDE,
+    Oem7 = 0xDE,
+    Oem8 = 0xDF,
+    OemBackslash = 0xE2,
+    Oem102 = 0xE2,
+    ProcessKey = 0xE5,
+    Packet = 0xE7,
+    Attn = 0xF6,
+    Crsel = 0xF7,
+    Exsel = 0xF8,
+    EraseEof = 0xF9,
+    Play = 0xFA,
+    Zoom = 0xFB,
+    NoName = 0xFC,
+    Pa1 = 0xFD,
+    OemClear = 0xFE,
+    Shift = 0x10000,
+    Control = 0x20000,
+    Alt = 0x40000
+}
+
+[Flags]
+[ComVisible(true)]
+public enum MouseButtons
+{
+    Left = 0x100000,
+    None = 0,
+    Right = 0x200000,
+    Middle = 0x400000,
+    XButton1 = 0x800000,
+    XButton2 = 0x1000000
+}
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Keys.cs.meta b/Assets/Plugins/Essgee/Keys.cs.meta
new file mode 100644
index 0000000..e6bc2dc
--- /dev/null
+++ b/Assets/Plugins/Essgee/Keys.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c8194d726152edf4aa339a393f801f58
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Metadata.meta b/Assets/Plugins/Essgee/Metadata.meta
new file mode 100644
index 0000000..3dedfe3
--- /dev/null
+++ b/Assets/Plugins/Essgee/Metadata.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5b91d039db189d445a206d1852ae838a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Metadata/GameMetadata.cs b/Assets/Plugins/Essgee/Metadata/GameMetadata.cs
new file mode 100644
index 0000000..e44a316
--- /dev/null
+++ b/Assets/Plugins/Essgee/Metadata/GameMetadata.cs
@@ -0,0 +1,21 @@
+using System;
+
+using Essgee.Emulation;
+
+namespace Essgee.Metadata
+{
+	public class GameMetadata
+	{
+		public string FileName { get; set; } = string.Empty;
+		public string KnownName { get; set; } = string.Empty;
+		public string Notes { get; set; } = string.Empty;
+		public uint RomCrc32 { get; set; } = 0xFFFFFFFF;
+		public int RomSize { get; set; } = 0;
+		public int RamSize { get; set; } = 0;
+		public Type MapperType { get; set; } = null;
+		public bool HasNonVolatileRam { get; set; } = false;
+		public TVStandard PreferredTVStandard { get; set; } = TVStandard.Auto;
+		public Region PreferredRegion { get; set; } = Region.Auto;
+		public bool AllowMemoryControl { get; set; } = true;
+	}
+}
diff --git a/Assets/Plugins/Essgee/Metadata/GameMetadata.cs.meta b/Assets/Plugins/Essgee/Metadata/GameMetadata.cs.meta
new file mode 100644
index 0000000..2a11bd7
--- /dev/null
+++ b/Assets/Plugins/Essgee/Metadata/GameMetadata.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b2847d634a727684b8739d5ea9e6ee73
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs b/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs
new file mode 100644
index 0000000..ce16cb2
--- /dev/null
+++ b/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs
@@ -0,0 +1,280 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.IO;
+using System.Xml.Serialization;
+using System.ComponentModel;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+using Essgee.Emulation;
+using Essgee.Exceptions;
+using Essgee.Extensions;
+using Essgee.Graphics;
+using Essgee.Utilities;
+
+namespace Essgee.Metadata
+{
+	public class GameMetadataHandler
+	{
+		readonly static string datDirectoryPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "No-Intro");
+		readonly static string metadataDatabaseFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "MetadataDatabase.json");
+
+		readonly Dictionary<string, DatFile> datFiles;
+		readonly List<CartridgeJSON> cartMetadataDatabase;
+
+
+		public int NumKnownSystems { get { return datFiles.Count; } }
+		public int NumKnownGames { get { return datFiles.Sum(x => x.Value.Game.Count()); } }
+
+		public GameMetadataHandler()
+		{
+
+			XmlRootAttribute root;
+			XmlSerializer serializer;
+
+			/* Read No-Intro .dat files */
+			datFiles = new Dictionary<string, DatFile>();
+			foreach (var file in Directory.EnumerateFiles(datDirectoryPath, "*.dat"))
+			{
+				root = new XmlRootAttribute("datafile") { IsNullable = true };
+				serializer = new XmlSerializer(typeof(DatFile), root);
+				using (FileStream stream = new FileStream(Path.Combine(datDirectoryPath, file), FileMode.Open))
+				{
+					datFiles.Add(Path.GetFileName(file), (DatFile)serializer.Deserialize(stream));
+				}
+			}
+
+			/* Read cartridge metadata database */
+			cartMetadataDatabase = metadataDatabaseFilePath.DeserializeFromFile<List<CartridgeJSON>>();
+
+            EssgeeLogger.EnqueueMessageSuccess($"Metadata initialized; {NumKnownGames} game(s) known across {NumKnownSystems} system(s).");
+		}
+
+		public GameMetadata GetGameMetadata(string datFilename, string romFilename, uint romCrc32, int romSize)
+		{
+			/* Sanity checks */
+			if (!datFiles.ContainsKey(datFilename)) throw new HandlerException("Requested .dat file not found");
+
+			/* Get information from No-Intro .dat */
+			var datFile = datFiles[datFilename];
+			var crcString = string.Format("{0:X8}", romCrc32);
+			var sizeString = string.Format("{0:D}", romSize);
+			var gameInfo = datFile.Game.FirstOrDefault(x => x.Rom.Any(y => y.Crc == crcString && y.Size == sizeString));
+
+			/* Get information from cartridge metadata database */
+			var cartridgeInfo = cartMetadataDatabase.FirstOrDefault(x => x.Crc32 == romCrc32 && x.RomSize == romSize);
+
+			/* Create game metadata */
+			var gameMetadata = new GameMetadata()
+			{
+				FileName = Path.GetFileName(romFilename),
+				KnownName = gameInfo?.Name,
+				RomCrc32 = romCrc32,
+				RomSize = romSize
+			};
+
+			if (cartridgeInfo != null)
+			{
+				if (gameMetadata.KnownName == null)
+					gameMetadata.KnownName = cartridgeInfo.Name;
+
+				gameMetadata.Notes = cartridgeInfo.Notes;
+				gameMetadata.RamSize = cartridgeInfo.RamSize;
+				gameMetadata.MapperType = cartridgeInfo.Mapper;
+				gameMetadata.HasNonVolatileRam = cartridgeInfo.HasNonVolatileRam;
+				gameMetadata.PreferredTVStandard = cartridgeInfo.PreferredTVStandard;
+				gameMetadata.PreferredRegion = cartridgeInfo.PreferredRegion;
+				gameMetadata.AllowMemoryControl = cartridgeInfo.AllowMemoryControl;
+			}
+
+			if (gameMetadata.KnownName == null)
+				gameMetadata.KnownName = "unrecognized game";
+
+			return gameMetadata;
+		}
+
+		public class CartridgeJSON
+		{
+			[JsonProperty(Required = Required.Always)]
+			public string Name { get; set; } = string.Empty;
+
+			[JsonProperty(Required = Required.Always)]
+			public string Notes { get; set; } = string.Empty;
+
+			[JsonProperty(Required = Required.Always), JsonConverter(typeof(HexadecimalJsonConverter))]
+			public uint Crc32 { get; set; } = 0xFFFFFFFF;
+
+			[JsonProperty(Required = Required.Always)]
+			public int RomSize { get; set; } = 0;
+
+			[JsonProperty(Required = Required.Default), DefaultValue(0)]
+			public int RamSize { get; set; } = 0;
+
+			[JsonProperty(Required = Required.Default), JsonConverter(typeof(TypeNameJsonConverter), "Essgee.Emulation.Cartridges"), DefaultValue(null)]
+			public Type Mapper { get; set; } = null;
+
+			[JsonProperty(Required = Required.Default), DefaultValue(false)]
+			public bool HasNonVolatileRam { get; set; } = false;
+
+			[JsonProperty(Required = Required.Default), JsonConverter(typeof(StringEnumConverter)), DefaultValue(TVStandard.Auto)]
+			public TVStandard PreferredTVStandard { get; set; } = TVStandard.Auto;
+
+			[JsonProperty(Required = Required.Default), JsonConverter(typeof(StringEnumConverter)), DefaultValue(Region.Auto)]
+			public Region PreferredRegion { get; set; } = Region.Auto;
+
+			[JsonProperty(Required = Required.Default), DefaultValue(true)]
+			public bool AllowMemoryControl { get; set; } = true;
+		}
+
+		public class DatHeader
+		{
+			[XmlElement("name")]
+			public string Name { get; set; }
+			[XmlElement("description")]
+			public string Description { get; set; }
+			[XmlElement("category")]
+			public string Category { get; set; }
+			[XmlElement("version")]
+			public string Version { get; set; }
+			[XmlElement("date")]
+			public string Date { get; set; }
+			[XmlElement("author")]
+			public string Author { get; set; }
+			[XmlElement("email")]
+			public string Email { get; set; }
+			[XmlElement("homepage")]
+			public string Homepage { get; set; }
+			[XmlElement("url")]
+			public string Url { get; set; }
+			[XmlElement("comment")]
+			public string Comment { get; set; }
+		}
+
+		public class DatRelease
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+			[XmlAttribute("region")]
+			public string Region { get; set; }
+			[XmlAttribute("language")]
+			public string Language { get; set; }
+			[XmlAttribute("date")]
+			public string Date { get; set; }
+			[XmlAttribute("default")]
+			public string Default { get; set; }
+		}
+
+		public class DatBiosSet
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+			[XmlAttribute("description")]
+			public string Description { get; set; }
+			[XmlAttribute("default")]
+			public string Default { get; set; }
+		}
+
+		public class DatRom
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+			[XmlAttribute("size")]
+			public string Size { get; set; }
+			[XmlAttribute("crc")]
+			public string Crc { get; set; }
+			[XmlAttribute("sha1")]
+			public string Sha1 { get; set; }
+			[XmlAttribute("md5")]
+			public string Md5 { get; set; }
+			[XmlAttribute("merge")]
+			public string Merge { get; set; }
+			[XmlAttribute("status")]
+			public string Status { get; set; }
+			[XmlAttribute("date")]
+			public string Date { get; set; }
+		}
+
+		public class DatDisk
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+			[XmlAttribute("sha1")]
+			public string Sha1 { get; set; }
+			[XmlAttribute("md5")]
+			public string Md5 { get; set; }
+			[XmlAttribute("merge")]
+			public string Merge { get; set; }
+			[XmlAttribute("status")]
+			public string Status { get; set; }
+		}
+
+		public class DatSample
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+		}
+
+		public class DatArchive
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+		}
+
+		public class DatGame
+		{
+			[XmlAttribute("name")]
+			public string Name { get; set; }
+			[XmlAttribute("sourcefile")]
+			public string SourceFile { get; set; }
+			[XmlAttribute("isbios")]
+			public string IsBios { get; set; }
+			[XmlAttribute("cloneof")]
+			public string CloneOf { get; set; }
+			[XmlAttribute("romof")]
+			public string RomOf { get; set; }
+			[XmlAttribute("sampleof")]
+			public string SampleOf { get; set; }
+			[XmlAttribute("board")]
+			public string Board { get; set; }
+			[XmlAttribute("rebuildto")]
+			public string RebuildTo { get; set; }
+
+			[XmlElement("year")]
+			public string Year { get; set; }
+			[XmlElement("manufacturer")]
+			public string Manufacturer { get; set; }
+
+			[XmlElement("release")]
+			public DatRelease[] Release { get; set; }
+
+			[XmlElement("biosset")]
+			public DatBiosSet[] BiosSet { get; set; }
+
+			[XmlElement("rom")]
+			public DatRom[] Rom { get; set; }
+
+			[XmlElement("disk")]
+			public DatDisk[] Disk { get; set; }
+
+			[XmlElement("sample")]
+			public DatSample[] Sample { get; set; }
+
+			[XmlElement("archive")]
+			public DatArchive[] Archive { get; set; }
+		}
+
+		[Serializable()]
+		public class DatFile
+		{
+			[XmlElement("header")]
+			public DatHeader Header { get; set; }
+
+			[XmlElement("game")]
+			public DatGame[] Game { get; set; }
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs.meta b/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs.meta
new file mode 100644
index 0000000..ae67271
--- /dev/null
+++ b/Assets/Plugins/Essgee/Metadata/GameMetadataHandler.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5a21d7f6de7eac540bfe506b411d4ef8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/StandInfo.cs b/Assets/Plugins/Essgee/StandInfo.cs
new file mode 100644
index 0000000..767b9ac
--- /dev/null
+++ b/Assets/Plugins/Essgee/StandInfo.cs
@@ -0,0 +1,40 @@
+
+using Essgee;
+using System;
+using System.IO;
+
+public static class StandInfo
+{
+    const string jsonConfigFileName = "Config.json";
+    const string saveDataDirectoryName = "Saves";
+    const string screenshotDirectoryName = "Screenshots";
+    const string saveStateDirectoryName = "Savestates";
+    const string extraDataDirectoryName = "Extras";
+    static string ProductName = "";
+
+    readonly static string programDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), ProductName);
+    readonly static string programConfigPath = Path.Combine(programDataDirectory, jsonConfigFileName);
+
+    public static Configuration Configuration { get; set; }
+
+    public static string ShaderPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Assets", "Shaders");
+    public static string SaveDataPath = Path.Combine(programDataDirectory, saveDataDirectoryName);
+    public static string ScreenshotPath = Path.Combine(programDataDirectory, screenshotDirectoryName);
+    public static string SaveStatePath = Path.Combine(programDataDirectory, saveStateDirectoryName);
+    public static string ExtraDataPath = Path.Combine(programDataDirectory, extraDataDirectoryName);
+
+    static Random mRandom;
+    public static Random Random
+    {
+        get
+        {
+            if (mRandom == null)
+            {
+                mRandom = new Random();
+            }
+            return mRandom;
+        }
+    }
+
+    public static string ProductVersion { get; internal set; }
+}
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/StandInfo.cs.meta b/Assets/Plugins/Essgee/StandInfo.cs.meta
new file mode 100644
index 0000000..c4e673b
--- /dev/null
+++ b/Assets/Plugins/Essgee/StandInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 96771057db04a714b94d5bc96a516946
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities.meta b/Assets/Plugins/Essgee/Utilities.meta
new file mode 100644
index 0000000..5748152
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: efbb7663b0390d544a2f62d060d8441d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs b/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs
new file mode 100644
index 0000000..2429c1b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs
@@ -0,0 +1,10 @@
+//namespace Essgee.Utilities
+//{
+//    class AltKeyFilter : IMessageFilter
+//	{
+//		public bool PreFilterMessage(ref Message m)
+//		{
+//			return (m.Msg == 0x0104 && ((int)m.LParam & 0x20000000) != 0);
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs.meta b/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs.meta
new file mode 100644
index 0000000..4f2957a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/AltKeyFilter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0857ccfa13cf054469f9f76a26a93de9
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs b/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs
new file mode 100644
index 0000000..cdac512
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs
@@ -0,0 +1,48 @@
+//using System;
+//using System.Windows.Forms;
+//using System.Drawing;
+//using System.Diagnostics.CodeAnalysis;
+
+//namespace Essgee.Utilities
+//{
+//	[SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly",
+//		Justification = "False positive.  IDisposable is inherited via IFunctionality.  See http://stackoverflow.com/questions/8925925/code-analysis-ca1063-fires-when-deriving-from-idisposable-and-providing-implemen for details.")]
+//	public class BindableToolStripMenuItem : ToolStripMenuItem, IBindableComponent
+//	{
+//		public BindableToolStripMenuItem() : base() { }
+//		public BindableToolStripMenuItem(string text) : base(text) { }
+//		public BindableToolStripMenuItem(Image image) : base(image) { }
+//		public BindableToolStripMenuItem(string text, Image image) : base(text, image) { }
+//		public BindableToolStripMenuItem(string text, Image image, EventHandler onClick) : base(text, image, onClick) { }
+//		public BindableToolStripMenuItem(string text, Image image, params ToolStripMenuItem[] dropDownItems) : base(text, image, dropDownItems) { }
+//		public BindableToolStripMenuItem(string text, Image image, EventHandler onClick, Keys shortcutKeys) : base(text, image, onClick, shortcutKeys) { }
+//		public BindableToolStripMenuItem(string text, Image image, EventHandler onClick, string name) : base(text, image, onClick, name) { }
+
+//		BindingContext bindingContext;
+//		ControlBindingsCollection dataBindings;
+
+//		public BindingContext BindingContext
+//		{
+//			get
+//			{
+//				if (bindingContext == null)
+//					bindingContext = new BindingContext();
+//				return bindingContext;
+//			}
+//			set
+//			{
+//				bindingContext = value;
+//			}
+//		}
+
+//		public ControlBindingsCollection DataBindings
+//		{
+//			get
+//			{
+//				if (dataBindings == null)
+//					dataBindings = new ControlBindingsCollection(this);
+//				return dataBindings;
+//			}
+//		}
+//	}
+//}
diff --git a/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs.meta b/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs.meta
new file mode 100644
index 0000000..ce5bf9a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/BindableToolStripMenuItem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bd5e5aa99d4a1b642b4f6c18a4a29988
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/Crc32.cs b/Assets/Plugins/Essgee/Utilities/Crc32.cs
new file mode 100644
index 0000000..ae1110a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/Crc32.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+
+namespace Essgee.Utilities
+{
+	public static class Crc32
+	{
+		static readonly uint[] crcTable;
+		static readonly uint crcPolynomial = 0xEDB88320;
+		static readonly uint crcSeed = 0xFFFFFFFF;
+
+		static Crc32()
+		{
+			crcTable = new uint[256];
+
+			for (int i = 0; i < 256; i++)
+			{
+				uint entry = (uint)i;
+				for (int j = 0; j < 8; j++)
+				{
+					if ((entry & 0x00000001) == 0x00000001)
+						entry = (entry >> 1) ^ crcPolynomial;
+					else
+						entry = (entry >> 1);
+				}
+				crcTable[i] = entry;
+			}
+		}
+
+		private static void VerifyStartAndLength(int dataLength, int segmentStart, int segmentLength)
+		{
+			if (segmentStart >= dataLength) throw new Crc32Exception("Segment start offset is greater than total length");
+			if (segmentLength > dataLength) throw new Crc32Exception("Segment length is greater than total length");
+			if ((segmentStart + segmentLength) > dataLength) throw new Crc32Exception("Segment end offset is greater than total length");
+		}
+
+		public static uint Calculate(FileInfo fileInfo)
+		{
+			return Calculate(fileInfo, 0, (int)fileInfo.Length);
+		}
+
+		public static uint Calculate(FileInfo fileInfo, int start, int length)
+		{
+			VerifyStartAndLength((int)fileInfo.Length, start, length);
+
+			using (FileStream file = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+			{
+				return Calculate(file, start, length);
+			}
+		}
+
+		public static uint Calculate(Stream stream)
+		{
+			return Calculate(stream, 0, (int)stream.Length);
+		}
+
+		public static uint Calculate(Stream stream, int start, int length)
+		{
+			VerifyStartAndLength((int)stream.Length, start, length);
+
+			uint crc = 0;
+
+			var lastStreamPosition = stream.Position;
+
+			byte[] data = new byte[length];
+			stream.Position = start;
+			stream.Read(data, 0, length);
+			crc = Calculate(data, 0, data.Length);
+			stream.Position = lastStreamPosition;
+
+			return crc;
+		}
+
+		public static uint Calculate(byte[] data)
+		{
+			return Calculate(data, 0, data.Length);
+		}
+
+		public static uint Calculate(byte[] data, int start, int length)
+		{
+			VerifyStartAndLength(data.Length, start, length);
+
+			uint crc = crcSeed;
+			for (int i = start; i < (start + length); i++)
+				crc = ((crc >> 8) ^ crcTable[data[i] ^ (crc & 0x000000FF)]);
+			return ~crc;
+		}
+	}
+
+	[Serializable]
+	public class Crc32Exception : Exception
+	{
+		public Crc32Exception() : base() { }
+		public Crc32Exception(string message) : base(message) { }
+		public Crc32Exception(string message, Exception innerException) : base(message, innerException) { }
+		public Crc32Exception(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/Crc32.cs.meta b/Assets/Plugins/Essgee/Utilities/Crc32.cs.meta
new file mode 100644
index 0000000..2c4e673
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/Crc32.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 21bde0d19c7628445ac123f8bb7c8f75
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs b/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs
new file mode 100644
index 0000000..dcb7624
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Newtonsoft.Json;
+
+namespace Essgee.Utilities
+{
+	public class HexadecimalJsonConverter : JsonConverter
+	{
+		public override bool CanConvert(Type objectType)
+		{
+			// TODO: maybe actually check things?
+			return true;
+		}
+
+		public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+		{
+			switch (Type.GetTypeCode(value.GetType()))
+			{
+				case TypeCode.Byte:
+				case TypeCode.SByte:
+					writer.WriteValue($"0x{value:X2}");
+					break;
+				case TypeCode.UInt16:
+				case TypeCode.Int16:
+					writer.WriteValue($"0x{value:X4}");
+					break;
+				case TypeCode.UInt32:
+				case TypeCode.Int32:
+					writer.WriteValue($"0x{value:X8}");
+					break;
+				case TypeCode.UInt64:
+				case TypeCode.Int64:
+					writer.WriteValue($"0x{value:X16}");
+					break;
+				default:
+					throw new JsonSerializationException();
+			}
+		}
+
+		public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+		{
+			if ((reader.Value is string value) && value.StartsWith("0x"))
+				return Convert.ChangeType(Convert.ToUInt64(value, 16), objectType);
+			else
+				throw new JsonSerializationException();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs.meta b/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs.meta
new file mode 100644
index 0000000..f4e9327
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/HexadecimalJsonConverter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 33727d827e6ef604baf39a57c3b9211d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs b/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs
new file mode 100644
index 0000000..d6dd61b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Essgee.Utilities
+{
+	public class InterfaceDictionaryConverter<TInterface> : JsonConverter
+	{
+		public override bool CanConvert(Type objectType)
+		{
+			return (objectType == typeof(TInterface));
+		}
+
+		public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+		{
+			if (!objectType.IsGenericType || objectType.GetGenericTypeDefinition() != typeof(Dictionary<,>)) throw new InvalidOperationException("Can only deserialize dictionaries");
+
+			var dictionary = (System.Collections.IDictionary)Activator.CreateInstance(objectType);
+
+			var jObject = JObject.Load(reader);
+			foreach (var child in jObject.Children())
+			{
+				Type type = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(y => typeof(TInterface).IsAssignableFrom(y) && !y.IsInterface && !y.IsAbstract && y.Name == child.Path);
+				if (type != null)
+					dictionary.Add(child.Path, JsonConvert.DeserializeObject(child.First.ToString(), type));
+			}
+
+			return dictionary;
+		}
+
+		public override bool CanWrite
+		{
+			get { return false; }
+		}
+
+		public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+		{
+			throw new NotImplementedException();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs.meta b/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs.meta
new file mode 100644
index 0000000..c094ee4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/InterfaceDictionaryConverter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 38387730a3743044698acbb9afe6aa67
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs b/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs
new file mode 100644
index 0000000..0ea7cb5
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities
+{
+	[AttributeUsage(AttributeTargets.Property)]
+	public class IsBootstrapRomPathAttribute : Attribute { }
+}
diff --git a/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs.meta b/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs.meta
new file mode 100644
index 0000000..705a2f4
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/IsBootstrapRomPathAttribute.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 12ab3971c05a8034ea04df685e2c6f2d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs b/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs
new file mode 100644
index 0000000..c950e67
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities
+{
+	[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+	public class MachineIndexAttribute : Attribute
+	{
+		public int Index { get; private set; }
+
+		public MachineIndexAttribute(int index)
+		{
+			Index = index;
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs.meta b/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs.meta
new file mode 100644
index 0000000..9b7b2a8
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/MachineIndexAttribute.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 63aa5f9e1fb94184c96245b9b13e4325
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs b/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs
new file mode 100644
index 0000000..9153286
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities
+{
+	[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+	public class StateRequiredAttribute : Attribute { }
+}
diff --git a/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs.meta b/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs.meta
new file mode 100644
index 0000000..6b79703
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/StateRequiredAttribute.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7cc6e75e78730c445a27a5f59f93c4e5
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs b/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs
new file mode 100644
index 0000000..dd77492
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using Newtonsoft.Json;
+
+namespace Essgee.Utilities
+{
+	public class TypeNameJsonConverter : JsonConverter
+	{
+		readonly string searchNamespace;
+
+		public TypeNameJsonConverter(string searchNamespace)
+		{
+			this.searchNamespace = searchNamespace;
+		}
+
+		public override bool CanConvert(Type objectType)
+		{
+			// TODO: maybe actually check things?
+			return true;
+		}
+
+		public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+		{
+			if (value is Type)
+			{
+				var type = (value as Type);
+				if (type.Namespace != searchNamespace) throw new JsonSerializationException();
+				writer.WriteValue(type.Name);
+			}
+			else
+				throw new JsonSerializationException();
+		}
+
+		public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+		{
+			var type = Type.GetType($"{searchNamespace}.{reader.Value}");
+			if (type != null) return type;
+			else throw new JsonSerializationException();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs.meta b/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs.meta
new file mode 100644
index 0000000..6614f15
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/TypeNameJsonConverter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7a7c859a28b7ac348b1fc4904077955d
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput.meta b/Assets/Plugins/Essgee/Utilities/XInput.meta
new file mode 100644
index 0000000..0e74294
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 37b3e1d7093e0f545807ab54c1209c8b
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs b/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs
new file mode 100644
index 0000000..f959b27
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities.XInput
+{
+	public class Controller
+	{
+		XInputState inputStatesCurrent, inputStatesPrev;
+		bool timedVibrationEnabled;
+		DateTime vibrationStopTime;
+
+		public bool IsConnected { get; private set; }
+		public int UserIndex { get; private set; }
+
+		public Controller(int index)
+		{
+			inputStatesCurrent = inputStatesPrev = new XInputState();
+			timedVibrationEnabled = false;
+			vibrationStopTime = DateTime.Now;
+
+			IsConnected = false;
+			UserIndex = index;
+		}
+
+		public void Update()
+		{
+			XInputState newInputState = new XInputState();
+			Errors result = (Errors)NativeMethods.GetState(UserIndex, ref newInputState);
+			if (result == Errors.Success)
+			{
+				IsConnected = true;
+				inputStatesPrev = inputStatesCurrent;
+				inputStatesCurrent = newInputState;
+
+				if ((inputStatesCurrent.Gamepad.sThumbLX < XInputGamepad.LeftThumbDeadzone && inputStatesCurrent.Gamepad.sThumbLX > -XInputGamepad.LeftThumbDeadzone) &&
+					(inputStatesCurrent.Gamepad.sThumbLY < XInputGamepad.LeftThumbDeadzone && inputStatesCurrent.Gamepad.sThumbLY > -XInputGamepad.LeftThumbDeadzone))
+				{
+					inputStatesCurrent.Gamepad.sThumbLX = inputStatesCurrent.Gamepad.sThumbLY = 0;
+				}
+
+				if ((inputStatesCurrent.Gamepad.sThumbRX < XInputGamepad.RightThumbDeadzone && inputStatesCurrent.Gamepad.sThumbRX > -XInputGamepad.RightThumbDeadzone) &&
+					(inputStatesCurrent.Gamepad.sThumbRY < XInputGamepad.RightThumbDeadzone && inputStatesCurrent.Gamepad.sThumbRY > -XInputGamepad.RightThumbDeadzone))
+				{
+					inputStatesCurrent.Gamepad.sThumbRX = inputStatesCurrent.Gamepad.sThumbRY = 0;
+				}
+
+				if (inputStatesCurrent.Gamepad.bLeftTrigger < XInputGamepad.TriggerThreshold) inputStatesCurrent.Gamepad.bLeftTrigger = 0;
+				if (inputStatesCurrent.Gamepad.bRightTrigger < XInputGamepad.TriggerThreshold) inputStatesCurrent.Gamepad.bRightTrigger = 0;
+
+				if (timedVibrationEnabled && DateTime.Now >= vibrationStopTime)
+				{
+					timedVibrationEnabled = false;
+					Vibrate(0.0f, 0.0f);
+				}
+			}
+			else if (result == Errors.DeviceNotConnected)
+			{
+				IsConnected = false;
+			}
+			else
+				throw new Exception(string.Format("Error code {0}", (int)result));
+		}
+
+		public ControllerState GetControllerState()
+		{
+			return new ControllerState
+			{
+				Buttons = inputStatesCurrent.Gamepad.Buttons,
+				LeftThumbstick = new ThumbstickPosition(inputStatesCurrent.Gamepad.sThumbLX / 32768.0f, inputStatesCurrent.Gamepad.sThumbLY / 32768.0f),
+				RightThumbstick = new ThumbstickPosition(inputStatesCurrent.Gamepad.sThumbRX / 32768.0f, inputStatesCurrent.Gamepad.sThumbRY / 32768.0f),
+				LeftTrigger = (inputStatesCurrent.Gamepad.bLeftTrigger / 255.0f),
+				RightTrigger = (inputStatesCurrent.Gamepad.bRightTrigger / 255.0f)
+			};
+		}
+
+		public bool IsDPadUpPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.DPadUp);
+		}
+
+		public bool IsDPadDownPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.DPadDown);
+		}
+
+		public bool IsDPadLeftPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.DPadLeft);
+		}
+
+		public bool IsDPadRightPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.DPadRight);
+		}
+
+		public bool IsStartPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.Start);
+		}
+
+		public bool IsBackPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.Back);
+		}
+
+		public bool IsLeftThumbPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.LeftThumb);
+		}
+
+		public bool IsRightThumbPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.RightThumb);
+		}
+
+		public bool IsLeftShoulderPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.LeftShoulder);
+		}
+
+		public bool IsRightShoulderPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.RightShoulder);
+		}
+
+		public bool IsAPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.A);
+		}
+
+		public bool IsBPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.B);
+		}
+
+		public bool IsXPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.X);
+		}
+
+		public bool IsYPressed()
+		{
+			return inputStatesCurrent.Gamepad.Buttons.HasFlag(Buttons.Y);
+		}
+
+		public void Vibrate(float leftMotor, float rightMotor)
+		{
+			XInputVibration vibrationState = new XInputVibration();
+			vibrationState.wLeftMotorSpeed = (ushort)(leftMotor * 65535.0f);
+			vibrationState.wRightMotorSpeed = (ushort)(rightMotor * 65535.0f);
+			NativeMethods.SetState(UserIndex, ref vibrationState);
+		}
+
+		public void Vibrate(float leftMotor, float rightMotor, TimeSpan duration)
+		{
+			Vibrate(leftMotor, rightMotor);
+
+			vibrationStopTime = DateTime.Now.Add(duration);
+			timedVibrationEnabled = true;
+		}
+	}
+
+	public class ThumbstickPosition
+	{
+		public float X { get; private set; }
+		public float Y { get; private set; }
+
+		public ThumbstickPosition(float x, float y)
+		{
+			X = x;
+			Y = y;
+		}
+
+		public override string ToString()
+		{
+			return string.Format(System.Globalization.CultureInfo.InvariantCulture, "({0}, {1})", X, Y);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs.meta
new file mode 100644
index 0000000..146f2f1
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/Controller.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 208c78465e149914e92390762f768a04
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs b/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs
new file mode 100644
index 0000000..71d3333
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities.XInput
+{
+	public static class ControllerManager
+	{
+		const int maxControllers = 4;
+
+		static Controller[] controllers;
+
+		static ControllerManager()
+		{
+			controllers = new Controller[maxControllers];
+			for (int i = 0; i < controllers.Length; i++)
+				controllers[i] = new Controller(i);
+		}
+
+		public static Controller GetController(int index)
+		{
+			if (index < 0 || index >= maxControllers) throw new Exception("Controller index out of range");
+			return controllers[index];
+		}
+
+		public static void Update()
+		{
+			for (int i = 0; i < controllers.Length; i++)
+				controllers[i].Update();
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs.meta
new file mode 100644
index 0000000..e0cc01e
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/ControllerManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d3b109e035af2d0488fee85624c9a90b
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs b/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs
new file mode 100644
index 0000000..0883f6d
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Essgee.Utilities.XInput
+{
+	public class ControllerState
+	{
+		public Buttons Buttons { get; set; }
+		public ThumbstickPosition LeftThumbstick { get; set; }
+		public ThumbstickPosition RightThumbstick { get; set; }
+		public float LeftTrigger { get; set; }
+		public float RightTrigger { get; set; }
+
+		public bool IsConnected { get; set; }
+		public int UserIndex { get; set; }
+
+		public ControllerState()
+		{
+			Buttons = Buttons.None;
+			LeftThumbstick = new ThumbstickPosition(0.0f, 0.0f);
+			RightThumbstick = new ThumbstickPosition(0.0f, 0.0f);
+			LeftTrigger = 0.0f;
+			RightTrigger = 0.0f;
+
+			IsConnected = false;
+			UserIndex = -1;
+		}
+
+		public bool IsAnyUpDirectionPressed()
+		{
+			return IsDPadUpPressed() || LeftThumbstick.Y > 0.5f;
+		}
+
+		public bool IsAnyDownDirectionPressed()
+		{
+			return IsDPadDownPressed() || LeftThumbstick.Y < -0.5f;
+		}
+
+		public bool IsAnyLeftDirectionPressed()
+		{
+			return IsDPadLeftPressed() || LeftThumbstick.X < -0.5f;
+		}
+
+		public bool IsAnyRightDirectionPressed()
+		{
+			return IsDPadRightPressed() || LeftThumbstick.X > 0.5f;
+		}
+
+		public bool IsDPadUpPressed()
+		{
+			return Buttons.HasFlag(Buttons.DPadUp);
+		}
+
+		public bool IsDPadDownPressed()
+		{
+			return Buttons.HasFlag(Buttons.DPadDown);
+		}
+
+		public bool IsDPadLeftPressed()
+		{
+			return Buttons.HasFlag(Buttons.DPadLeft);
+		}
+
+		public bool IsDPadRightPressed()
+		{
+			return Buttons.HasFlag(Buttons.DPadRight);
+		}
+
+		public bool IsStartPressed()
+		{
+			return Buttons.HasFlag(Buttons.Start);
+		}
+
+		public bool IsBackPressed()
+		{
+			return Buttons.HasFlag(Buttons.Back);
+		}
+
+		public bool IsLeftThumbPressed()
+		{
+			return Buttons.HasFlag(Buttons.LeftThumb);
+		}
+
+		public bool IsRightThumbPressed()
+		{
+			return Buttons.HasFlag(Buttons.RightThumb);
+		}
+
+		public bool IsLeftShoulderPressed()
+		{
+			return Buttons.HasFlag(Buttons.LeftShoulder);
+		}
+
+		public bool IsRightShoulderPressed()
+		{
+			return Buttons.HasFlag(Buttons.RightShoulder);
+		}
+
+		public bool IsAPressed()
+		{
+			return Buttons.HasFlag(Buttons.A);
+		}
+
+		public bool IsBPressed()
+		{
+			return Buttons.HasFlag(Buttons.B);
+		}
+
+		public bool IsXPressed()
+		{
+			return Buttons.HasFlag(Buttons.X);
+		}
+
+		public bool IsYPressed()
+		{
+			return Buttons.HasFlag(Buttons.Y);
+		}
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs.meta
new file mode 100644
index 0000000..da9a3ae
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/ControllerState.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 62cf2ece5ee5c44458ac5646bb2f8af8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs b/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs
new file mode 100644
index 0000000..fe2aae7
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+
+namespace Essgee.Utilities.XInput
+{
+	static class NativeMethods
+	{
+		const string dllName = "xinput9_1_0.dll";
+
+		public const int FlagGamepad = 0x00000001;
+
+		[DllImport(dllName, EntryPoint = "XInputGetState")]
+		public static extern int GetState(int dwUserIndex, ref XInputState pState);
+		[DllImport(dllName, EntryPoint = "XInputSetState")]
+		public static extern int SetState(int dwUserIndex, ref XInputVibration pVibration);
+		[DllImport(dllName, EntryPoint = "XInputGetCapabilities")]
+		public static extern int GetCapabilities(int dwUserIndex, int dwFlags, ref XInputCapabilities pCapabilities);
+	}
+
+	public enum Errors
+	{
+		Success = 0x00000000,
+		BadArguments = 0x000000A0,
+		DeviceNotConnected = 0x0000048F
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs.meta
new file mode 100644
index 0000000..3f0a5cf
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/NativeMethods.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2f4639905082c50448ca8a1477b55057
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs b/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs
new file mode 100644
index 0000000..2c6ffd3
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Essgee.Utilities.XInput
+{
+	/* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_capabilities%28v=vs.85%29.aspx */
+	[StructLayout(LayoutKind.Explicit)]
+	public struct XInputCapabilities
+	{
+		[FieldOffset(0)]
+		byte type;
+		[FieldOffset(1)]
+		byte subType;
+		[FieldOffset(2)]
+		ushort flags;
+		[FieldOffset(4)]
+		public XInputGamepad Gamepad;
+		[FieldOffset(16)]
+		public XInputVibration Vibration;
+
+		public DeviceType Type { get { return (DeviceType)type; } }
+		public DeviceSubType SubType { get { return (DeviceSubType)subType; } }
+		public DeviceFlags Flags { get { return (DeviceFlags)flags; } }
+	}
+
+	public enum DeviceType
+	{
+		Gamepad = 0x01
+	}
+
+	public enum DeviceSubType
+	{
+		Gamepad = 0x01,
+		Wheel = 0x02,
+		ArcadeStick = 0x03,
+		FlightStick = 0x04,
+		DancePad = 0x05,
+		Guitar = 0x06,
+		DrumKit = 0x08
+	}
+
+	[Flags]
+	public enum DeviceFlags
+	{
+		VoiceSupported = 0x0004
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs.meta
new file mode 100644
index 0000000..c916d61
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputCapabilities.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 400677b412126194caf14f5d627b34a2
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs b/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs
new file mode 100644
index 0000000..6793fdd
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Essgee.Utilities.XInput
+{
+	/* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_gamepad%28v=vs.85%29.aspx */
+	[StructLayout(LayoutKind.Explicit)]
+	public struct XInputGamepad
+	{
+		[FieldOffset(0)]
+		ushort wButtons;
+		[FieldOffset(2)]
+		public byte bLeftTrigger;
+		[FieldOffset(3)]
+		public byte bRightTrigger;
+		[FieldOffset(4)]
+		public short sThumbLX;
+		[FieldOffset(6)]
+		public short sThumbLY;
+		[FieldOffset(8)]
+		public short sThumbRX;
+		[FieldOffset(10)]
+		public short sThumbRY;
+
+		public const int LeftThumbDeadzone = 7849;
+		public const int RightThumbDeadzone = 8689;
+		public const int TriggerThreshold = 30;
+
+		public Buttons Buttons { get { return (Buttons)wButtons; } }
+	}
+
+	[Flags]
+	public enum Buttons
+	{
+		None = 0x0000,
+		DPadUp = 0x0001,
+		DPadDown = 0x0002,
+		DPadLeft = 0x0004,
+		DPadRight = 0x0008,
+		Start = 0x0010,
+		Back = 0x0020,
+		LeftThumb = 0x0040,
+		RightThumb = 0x0080,
+		LeftShoulder = 0x0100,
+		RightShoulder = 0x0200,
+		A = 0x1000,
+		B = 0x2000,
+		X = 0x4000,
+		Y = 0x8000
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs.meta
new file mode 100644
index 0000000..86f6d2b
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputGamepad.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: dee5c4630dd189049abf4d41bd4348d8
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs b/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs
new file mode 100644
index 0000000..8ecf014
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace Essgee.Utilities.XInput
+{
+	/* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_state%28v=vs.85%29.aspx */
+	[StructLayout(LayoutKind.Explicit)]
+	public struct XInputState
+	{
+		[FieldOffset(0)]
+		public uint dwPacketNumber;
+		[FieldOffset(4)]
+		public XInputGamepad Gamepad;
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs.meta
new file mode 100644
index 0000000..64bb742
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputState.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b18170cd37fc33d4ba897ffbe7198deb
\ No newline at end of file
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs b/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs
new file mode 100644
index 0000000..dc3730a
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace Essgee.Utilities.XInput
+{
+	/* https://msdn.microsoft.com/en-us/library/windows/desktop/microsoft.directx_sdk.reference.xinput_vibration%28v=vs.85%29.aspx */
+	[StructLayout(LayoutKind.Explicit)]
+	public struct XInputVibration
+	{
+		[FieldOffset(0)]
+		public ushort wLeftMotorSpeed;
+		[FieldOffset(2)]
+		public ushort wRightMotorSpeed;
+	}
+}
diff --git a/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs.meta b/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs.meta
new file mode 100644
index 0000000..c25fa98
--- /dev/null
+++ b/Assets/Plugins/Essgee/Utilities/XInput/XInputVibration.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 66e6171736477a944b9299a66469f407
\ No newline at end of file
diff --git a/Assets/Plugins/Newtonsoft.Json.dll b/Assets/Plugins/Newtonsoft.Json.dll
new file mode 100644
index 0000000..341d08f
Binary files /dev/null and b/Assets/Plugins/Newtonsoft.Json.dll differ
diff --git a/Assets/Plugins/Newtonsoft.Json.dll.meta b/Assets/Plugins/Newtonsoft.Json.dll.meta
new file mode 100644
index 0000000..7124f76
--- /dev/null
+++ b/Assets/Plugins/Newtonsoft.Json.dll.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1a58e3ab1bbec0a4998d14ae26c5651d
\ No newline at end of file