diff --git a/src/BizHawk.Tests/BizHawk.Tests.csproj b/src/BizHawk.Tests/BizHawk.Tests.csproj
index aea54efd1d..2904403492 100644
--- a/src/BizHawk.Tests/BizHawk.Tests.csproj
+++ b/src/BizHawk.Tests/BizHawk.Tests.csproj
@@ -16,6 +16,7 @@
+
@@ -25,4 +26,7 @@
+
+
+
diff --git a/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest1.lua b/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest1.lua
new file mode 100644
index 0000000000..b88fb6a12e
--- /dev/null
+++ b/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest1.lua
@@ -0,0 +1,4 @@
+while true do
+ gui.drawRectangle(2, 2, 0, 0, 0xffff0000, 0, "client")
+ emu.frameadvance()
+end
diff --git a/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest2.lua b/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest2.lua
new file mode 100644
index 0000000000..91d9ccac07
--- /dev/null
+++ b/src/BizHawk.Tests/Client.Common/lua/LuaScripts/DrawTest2.lua
@@ -0,0 +1,4 @@
+while true do
+ gui.drawRectangle(2, 4, 0, 0, 0xff00ff00, 0, "client")
+ emu.frameadvance()
+end
diff --git a/src/BizHawk.Tests/Client.Common/lua/TestLuaDrawing.cs b/src/BizHawk.Tests/Client.Common/lua/TestLuaDrawing.cs
new file mode 100644
index 0000000000..5e7f1bf18a
--- /dev/null
+++ b/src/BizHawk.Tests/Client.Common/lua/TestLuaDrawing.cs
@@ -0,0 +1,78 @@
+using System.Drawing;
+using System.IO;
+
+using BizHawk.Bizware.BizwareGL;
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+using BizHawk.Tests.Mocks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace BizHawk.Tests.Client.Common.Lua
+{
+ [TestClass]
+ public class TestLuaDrawing
+ {
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+ // null values are initialized in the setup method
+ private ILuaLibraries luaLibraries = null;
+ private DisplayManagerBase displayManager = null;
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+
+ private const string pathToTestLuaScripts = "Client.Common/lua/LuaScripts";
+
+ [TestInitialize]
+ public void TestSetup()
+ {
+ Config config = new Config();
+ IGameInfo gameInfo = new GameInfo();
+
+ IMainFormForApi mainForm = new MockMainFormForApi(new NullEmulator());
+ displayManager = new TestDisplayManager(mainForm.Emulator);
+
+ luaLibraries = new TestLuaLibraries(
+ mainForm,
+ displayManager,
+ config,
+ gameInfo
+ );
+ luaLibraries.Restart(config, gameInfo);
+ }
+
+ private LuaFile AddScript(string path, bool autoStart = true)
+ {
+ LuaFile luaFile = new LuaFile("", path);
+ luaLibraries.ScriptList.Add(luaFile);
+ luaLibraries.EnableLuaFile(luaFile);
+
+ if (autoStart)
+ luaLibraries.ResumeScript(luaFile);
+
+ return luaFile;
+ }
+
+ [TestMethod]
+ public void TestDrawingWithOneScript()
+ {
+ AddScript(Path.Combine(pathToTestLuaScripts, "DrawTest1.lua"));
+
+ BitmapBufferVideoProvider vp = new BitmapBufferVideoProvider(new BitmapBuffer(8, 8));
+ var buffer = displayManager.RenderOffscreenLua(vp);
+
+ Assert.AreEqual(buffer.GetPixel(2, 2), Color.Red.ToArgb());
+ }
+
+ [TestMethod]
+ public void TestDrawingWithTwoScripts()
+ {
+ AddScript(Path.Combine(pathToTestLuaScripts, "DrawTest1.lua"));
+ AddScript(Path.Combine(pathToTestLuaScripts, "DrawTest2.lua"));
+
+ BitmapBufferVideoProvider vp = new BitmapBufferVideoProvider(new BitmapBuffer(8, 8));
+ var buffer = displayManager.RenderOffscreenLua(vp);
+
+ Assert.AreEqual(buffer.GetPixel(2, 2), Color.Red.ToArgb());
+ Assert.AreEqual(buffer.GetPixel(2, 4), 0xff00ff00);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/BizHawk.Tests/Implementations/TestDisplayManager.cs b/src/BizHawk.Tests/Implementations/TestDisplayManager.cs
new file mode 100644
index 0000000000..2fe3653378
--- /dev/null
+++ b/src/BizHawk.Tests/Implementations/TestDisplayManager.cs
@@ -0,0 +1,31 @@
+using System.Drawing;
+
+using BizHawk.Bizware.Graphics;
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests
+{
+ internal class TestDisplayManager : DisplayManagerBase
+ {
+ private Size _screenSize;
+
+ private TestDisplayManager(Config config, IEmulator emulator, InputManager inputManager, IGL_GDIPlus gl)
+ : base(config, emulator, inputManager, null, gl.DispMethodEnum, gl, new GDIPlusGuiRenderer(gl))
+ {
+ var vp = emulator.AsVideoProviderOrDefault();
+ _screenSize = new Size(vp.BufferWidth, vp.BufferHeight);
+
+ }
+ public TestDisplayManager(IEmulator emulator)
+ : this(new Config(), emulator, new InputManager(), new IGL_GDIPlus())
+ { }
+
+ public override void ActivateOpenGLContext() { } // Nothing. We only use GDIPlus here.
+ public override Size GetPanelNativeSize() => _screenSize;
+ protected override void ActivateGraphicsControlContext() { }
+ protected override Size GetGraphicsControlSize() => _screenSize;
+ protected override Point GraphicsControlPointToClient(Point p) => p;
+ protected override void SwapBuffersOfGraphicsControl() { }
+ }
+}
diff --git a/src/BizHawk.Tests/Implementations/TestLuaLibraries.cs b/src/BizHawk.Tests/Implementations/TestLuaLibraries.cs
new file mode 100644
index 0000000000..7d3a74d70e
--- /dev/null
+++ b/src/BizHawk.Tests/Implementations/TestLuaLibraries.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests
+{
+ internal class TestLuaLibraries : LuaLibrariesBase
+ {
+ public TestLuaLibraries(IMainFormForApi mainForm, DisplayManagerBase displayManager, Config config, IGameInfo game)
+ : base(new LuaFileList(
+ new List(), () => { }),
+ new LuaFunctionList(() => { }),
+ mainForm,
+ displayManager,
+ new InputManager(),
+ config,
+ game
+ )
+ { }
+ }
+}
diff --git a/src/BizHawk.Tests/Mocks/MockMainFormForApi.cs b/src/BizHawk.Tests/Mocks/MockMainFormForApi.cs
new file mode 100644
index 0000000000..e6c40f37c4
--- /dev/null
+++ b/src/BizHawk.Tests/Mocks/MockMainFormForApi.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Drawing;
+
+using BizHawk.Client.Common;
+using BizHawk.Emulation.Common;
+
+namespace BizHawk.Tests.Mocks
+{
+ internal class MockMainFormForApi : IMainFormForApi
+ {
+ public (HttpCommunication HTTP, MemoryMappedFiles MMF, SocketServer Sockets) NetworkingHelpers => (null!, null!, null!);
+
+ public IEmulator Emulator { get; }
+
+ public IMovieSession MovieSession => null!;
+
+ public IToolManager Tools => null!;
+
+ public MockMainFormForApi(IEmulator emulator)
+ {
+ Emulator = emulator;
+ }
+
+
+ // Everything below here is not implemented.
+ public CheatCollection CheatList => throw new NotImplementedException();
+
+ public Point DesktopLocation => throw new NotImplementedException();
+
+ public bool EmulatorPaused => throw new NotImplementedException();
+
+ public bool InvisibleEmulation { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public bool IsSeeking => throw new NotImplementedException();
+
+ public bool IsTurboing => throw new NotImplementedException();
+
+ public bool PauseAvi { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+
+ public void ClearHolds() => throw new NotImplementedException();
+ public void ClickSpeedItem(int num) => throw new NotImplementedException();
+ public void CloseEmulator(int? exitCode = null) => throw new NotImplementedException();
+ public void CloseRom(bool clearSram = false) => throw new NotImplementedException();
+ public void EnableRewind(bool enabled) => throw new NotImplementedException();
+ public bool FlushSaveRAM(bool autosave = false) => throw new NotImplementedException();
+ public void FrameAdvance() => throw new NotImplementedException();
+ public void FrameBufferResized() => throw new NotImplementedException();
+ public void FrameSkipMessage() => throw new NotImplementedException();
+ public int GetApproxFramerate() => throw new NotImplementedException();
+ public bool LoadMovie(string filename, string? archive = null) => throw new NotImplementedException();
+ public bool LoadQuickSave(int slot, bool suppressOSD = false) => throw new NotImplementedException();
+ public bool LoadRom(string path, LoadRomArgs args) => throw new NotImplementedException();
+ public bool LoadState(string path, string userFriendlyStateName, bool suppressOSD = false) => throw new NotImplementedException();
+ public void PauseEmulator() => throw new NotImplementedException();
+ public bool RebootCore() => throw new NotImplementedException();
+ public void Render() => throw new NotImplementedException();
+ public bool RestartMovie() => throw new NotImplementedException();
+ public void SaveQuickSave(int slot, bool suppressOSD = false, bool fromLua = false) => throw new NotImplementedException();
+ public void SaveState(string path, string userFriendlyStateName, bool fromLua = false, bool suppressOSD = false) => throw new NotImplementedException();
+ public void SeekFrameAdvance() => throw new NotImplementedException();
+ public void StepRunLoop_Throttle() => throw new NotImplementedException();
+ public void StopMovie(bool saveChanges = true) => throw new NotImplementedException();
+ public void TakeScreenshot() => throw new NotImplementedException();
+ public void TakeScreenshot(string path) => throw new NotImplementedException();
+ public void TakeScreenshotToClipboard() => throw new NotImplementedException();
+ public void TogglePause() => throw new NotImplementedException();
+ public void ToggleSound() => throw new NotImplementedException();
+ public void UnpauseEmulator() => throw new NotImplementedException();
+ }
+}
diff --git a/src/BizHawk.Tests/PreTests.cs b/src/BizHawk.Tests/PreTests.cs
new file mode 100644
index 0000000000..cf76408cf7
--- /dev/null
+++ b/src/BizHawk.Tests/PreTests.cs
@@ -0,0 +1,17 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace BizHawk.Tests
+{
+ [TestClass]
+ public class PreTests
+ {
+ [AssemblyInitialize]
+ public static void PreTestsMethod(TestContext context)
+ {
+ // This method will run only once, before all tests.
+ // So this seems a good place to initialize static classes.
+ BizHawk.Client.Common.ApiManager.FindApis(BizHawk.Client.Common.ReflectionCache.Types);
+ }
+ }
+
+}