diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs b/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs
new file mode 100644
index 0000000..9d1ff13
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("VisualPinball.Engine.DMD.Unity.Test")]
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs.meta b/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs.meta
new file mode 100644
index 0000000..96cdb92
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a5f12fcd519342c0b5e97983ccb41169
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/DmdBridgePlayer.cs b/VisualPinball.Engine.DMD.Unity/Runtime/DmdBridgePlayer.cs
index 5e5a2b4..f6967b9 100644
--- a/VisualPinball.Engine.DMD.Unity/Runtime/DmdBridgePlayer.cs
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/DmdBridgePlayer.cs
@@ -97,6 +97,7 @@ public class DmdBridgePlayer : MonoBehaviour
private DmdBridgeSettings _settings;
private DmdBridgeSettings _appliedSettings;
private DmdBridgeSettings _lastFallbackSettings;
+ private readonly DmdColorizationPolicy _colorizationPolicy = new DmdColorizationPolicy();
private DateTime _appliedConfigWriteTimeUtc;
private string _appliedConfigPath;
private bool _missingDisplayWarningLogged;
@@ -196,8 +197,9 @@ private void HandleDisplaysRequested(object sender, RequestedDisplays requestedD
continue;
}
- EnsurePipeline(display);
- _pipeline?.Clear();
+ if (EnsurePipeline(display)) {
+ _pipeline?.Clear();
+ }
return;
}
}
@@ -231,7 +233,24 @@ private void HandleDisplayUpdateFrame(object sender, DisplayFrameData frame)
}
}
- _pipeline.Push(frame);
+ // This handler runs on Unity's main thread. Rebuilding here is intentional: the pipeline
+ // worker cannot dispose itself because disposal waits for that worker to finish.
+ var colorizationAction = _colorizationPolicy.ObserveFrame(frame.Format,
+ _pipeline != null && _pipeline.UsesColorization, out var shouldWarn);
+ if (colorizationAction == DmdColorizationPipelineAction.Bypass) {
+ if (shouldWarn) {
+ var recovery = _colorizationPolicy.AwaitingSupportedColorizedFrame
+ ? " temporarily; colorization will resume when Dmd2/Dmd4 arrives"
+ : string.Empty;
+ Logger.Warn($"[DMD] Unsupported colorization source format {frame.Format} — bypassing colorization{recovery} for display \"{frame.Id}\".");
+ }
+ EnsurePipeline(_currentDisplay, force: true);
+ } else if (colorizationAction == DmdColorizationPipelineAction.Restore) {
+ Logger.Info($"[DMD] Supported colorization format {frame.Format} arrived — restoring colorization for display \"{frame.Id}\".");
+ EnsurePipeline(_currentDisplay, force: true);
+ }
+
+ _pipeline?.Push(frame);
}
private bool IsTargetDisplay(string id)
@@ -242,7 +261,7 @@ private bool IsTargetDisplay(string id)
: string.Equals(id, targetDisplayId, StringComparison.OrdinalIgnoreCase);
}
- private static bool TryInferDisplayConfig(DisplayFrameData frame, out DisplayConfig display)
+ internal static bool TryInferDisplayConfig(DisplayFrameData frame, out DisplayConfig display)
{
display = null;
if (frame?.Data == null) {
@@ -290,11 +309,12 @@ private static bool TryInferDimensions(int pixelCount, out int width, out int he
}
}
- private void EnsurePipeline(DisplayConfig display, bool force = false)
+ private bool EnsurePipeline(DisplayConfig display, bool force = false)
{
+ _colorizationPolicy.SelectDisplay(display);
_currentDisplay = display;
if (!force && _pipeline != null && _pipeline.Matches(display)) {
- return;
+ return false;
}
_pipeline?.Dispose();
@@ -303,14 +323,25 @@ private void EnsurePipeline(DisplayConfig display, bool force = false)
if (destinations.Count == 0) {
Logger.Warn("[DMD] No DMD destinations are available; bridge will ignore frames.");
_pipeline = null;
- return;
+ return true;
}
- var converter = CreateConverter();
- _resolvedConverter = converter?.Name ?? "none";
- RequestSourceFrameFormat(converter != null ? DisplayFrameFormat.Dmd4 : DisplayFrameFormat.Dmd8);
+ var converter = _colorizationPolicy.BypassColorization ? null : CreateConverter();
+ _resolvedConverter = _colorizationPolicy.BypassColorization
+ ? "bypassed (unsupported source format)"
+ : converter?.Name ?? "none";
+ RequestSourceFrameFormat(PreferredSourceFormat(converter != null, _colorizationPolicy));
_pipeline = new DmdPipeline(display, destinations, converter, _settings.FlipHorizontally);
Logger.Info($"[DMD] Pipeline for \"{display.Id}\" created with {destinations.Count} destination(s).");
+ return true;
+ }
+
+ internal static DisplayFrameFormat PreferredSourceFormat(bool converterAvailable,
+ DmdColorizationPolicy policy)
+ {
+ return converterAvailable || policy != null && policy.AwaitingSupportedColorizedFrame
+ ? DisplayFrameFormat.Dmd4
+ : DisplayFrameFormat.Dmd8;
}
private void RequestSourceFrameFormat(DisplayFrameFormat format)
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs b/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs
new file mode 100644
index 0000000..0edf30b
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs
@@ -0,0 +1,88 @@
+using System;
+using VisualPinball.Unity;
+
+namespace VisualPinball.Engine.DMD.Unity
+{
+ internal enum DmdColorizationPipelineAction
+ {
+ None,
+ Bypass,
+ Restore,
+ }
+
+ ///
+ /// Tracks whether the selected display must bypass a colorization source that cannot consume
+ /// the format emitted by its gamelogic engine.
+ ///
+ internal sealed class DmdColorizationPolicy
+ {
+ private DisplayConfig _display;
+ private bool _warningIssued;
+
+ public bool BypassColorization { get; private set; }
+ public bool AwaitingSupportedColorizedFrame { get; private set; }
+
+ ///
+ /// Selects a display topology. An identical re-announcement deliberately preserves the
+ /// bypass latch; selecting a different id, geometry, or horizontal orientation resets it.
+ ///
+ public void SelectDisplay(DisplayConfig display)
+ {
+ if (SameTopology(_display, display)) {
+ return;
+ }
+
+ _display = display;
+ BypassColorization = false;
+ AwaitingSupportedColorizedFrame = false;
+ _warningIssued = false;
+ }
+
+ ///
+ /// Decides whether the bridge must rebuild its pipeline. A Dmd8 frame may be the transient
+ /// pre-preference frame from a missed display announcement, so its bypass recovers when the
+ /// requested Dmd2/Dmd4 stream arrives. Dmd24 is RGB-authored and remains a sticky bypass.
+ ///
+ public DmdColorizationPipelineAction ObserveFrame(DisplayFrameFormat format,
+ bool pipelineUsesColorization, out bool shouldWarn)
+ {
+ if (!pipelineUsesColorization) {
+ if (BypassColorization && AwaitingSupportedColorizedFrame && SupportsColorization(format)) {
+ BypassColorization = false;
+ AwaitingSupportedColorizedFrame = false;
+ _warningIssued = false;
+ shouldWarn = false;
+ return DmdColorizationPipelineAction.Restore;
+ }
+ shouldWarn = false;
+ return DmdColorizationPipelineAction.None;
+ }
+
+ if (SupportsColorization(format)) {
+ shouldWarn = false;
+ return DmdColorizationPipelineAction.None;
+ }
+
+ BypassColorization = true;
+ AwaitingSupportedColorizedFrame = format == DisplayFrameFormat.Dmd8;
+ shouldWarn = !_warningIssued;
+ _warningIssued = true;
+ return DmdColorizationPipelineAction.Bypass;
+ }
+
+ public static bool SupportsColorization(DisplayFrameFormat format)
+ {
+ return format == DisplayFrameFormat.Dmd2 || format == DisplayFrameFormat.Dmd4;
+ }
+
+ private static bool SameTopology(DisplayConfig first, DisplayConfig second)
+ {
+ if (ReferenceEquals(first, second)) {
+ return true;
+ }
+ return first != null && second != null &&
+ string.Equals(first.Id, second.Id, StringComparison.OrdinalIgnoreCase) &&
+ first.Width == second.Width && first.Height == second.Height && first.FlipX == second.FlipX;
+ }
+ }
+}
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs.meta b/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs.meta
new file mode 100644
index 0000000..fc88f5e
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c86cffc0c9004283899b93931ae35da3
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/DmdPipeline.cs b/VisualPinball.Engine.DMD.Unity/Runtime/DmdPipeline.cs
index bb20b51..897e9a6 100644
--- a/VisualPinball.Engine.DMD.Unity/Runtime/DmdPipeline.cs
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/DmdPipeline.cs
@@ -49,11 +49,13 @@ internal sealed class DmdPipeline : IDisposable
private volatile bool _running = true;
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
+ public bool UsesColorization { get; }
public DmdPipeline(DisplayConfig display, List destinations, AbstractConverter converter, bool flipHorizontally)
{
_display = display;
_destinations = destinations;
+ UsesColorization = converter != null;
_source = VpeGleSource.Create(display, converter != null);
_renderGraph = new RenderGraph(new UndisposedReferences(), runOnMainThread: true) {
Name = $"VPE DMD ({display.Id})",
diff --git a/VisualPinball.Engine.DMD.Unity/Runtime/VpeGleSource.cs b/VisualPinball.Engine.DMD.Unity/Runtime/VpeGleSource.cs
index df4dd46..f17cf15 100644
--- a/VisualPinball.Engine.DMD.Unity/Runtime/VpeGleSource.cs
+++ b/VisualPinball.Engine.DMD.Unity/Runtime/VpeGleSource.cs
@@ -3,7 +3,9 @@
using System.Reactive.Subjects;
using LibDmd.Frame;
using LibDmd.Input;
+using NLog;
using VisualPinball.Unity;
+using Logger = NLog.Logger;
namespace VisualPinball.Engine.DMD.Unity
{
@@ -17,6 +19,7 @@ internal abstract class VpeGleSource : AbstractSource, ISource
private readonly Subject _onResume = new Subject();
private readonly Subject _onPause = new Subject();
+ private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
protected VpeGleSource(string displayId, int width, int height)
{
@@ -104,6 +107,7 @@ private sealed class ColorizableVpeGleSource : VpeGleSource, IGray2Source, IGray
{
private readonly Subject _gray2Frames = new Subject();
private readonly Subject _gray4Frames = new Subject();
+ private bool _unsupportedFormatWarningLogged;
public ColorizableVpeGleSource(string displayId, int width, int height) : base(displayId, width, height)
{
@@ -118,6 +122,12 @@ public override void Push(DisplayFrameFormat format, byte[] data, int length)
case DisplayFrameFormat.Dmd4:
_gray4Frames.OnNext(CreateFrame(data, length, 4));
break;
+ default:
+ if (!_unsupportedFormatWarningLogged) {
+ Logger.Warn($"[DMD] Colorization source cannot consume {format}; frame ignored while the main-thread bridge switches to passthrough.");
+ _unsupportedFormatWarningLogged = true;
+ }
+ break;
}
}
diff --git a/VisualPinball.Engine.DMD.Unity/Tests.meta b/VisualPinball.Engine.DMD.Unity/Tests.meta
new file mode 100644
index 0000000..6791542
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 18d02616676d4a629a14db0997a07cd3
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine.DMD.Unity/Tests/Runtime.meta b/VisualPinball.Engine.DMD.Unity/Tests/Runtime.meta
new file mode 100644
index 0000000..9b16b4a
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 008aec85a48c44f0ad1953f715bdfc54
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs
new file mode 100644
index 0000000..b84c962
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs
@@ -0,0 +1,179 @@
+using System.Linq;
+using System.Reflection;
+using NUnit.Framework;
+using UnityEngine;
+using VisualPinball.Unity;
+
+namespace VisualPinball.Engine.DMD.Unity.Test
+{
+ public class DmdColorizationPolicyTests
+ {
+ [Test]
+ public void ColorizingAndPassthroughSourcesAdvertiseMatchingCapabilities()
+ {
+ var display = new DisplayConfig("dmd0", 128, 32);
+
+ var colorizing = CreateSource(display, true);
+ Assert.That(Advertises(colorizing, "IGray2Source"), Is.True);
+ Assert.That(Advertises(colorizing, "IGray4Source"), Is.True);
+ Assert.That(Advertises(colorizing, "IGray8Source"), Is.False);
+ Assert.That(Advertises(colorizing, "IRgb24Source"), Is.False);
+
+ var passthrough = CreateSource(display, false);
+ Assert.That(Advertises(passthrough, "IGray2Source"), Is.True);
+ Assert.That(Advertises(passthrough, "IGray4Source"), Is.True);
+ Assert.That(Advertises(passthrough, "IGray8Source"), Is.True);
+ Assert.That(Advertises(passthrough, "IRgb24Source"), Is.True);
+ }
+
+ [TestCase(DisplayFrameFormat.Dmd8, 128 * 16, 128, 16)]
+ [TestCase(DisplayFrameFormat.Dmd8, 128 * 32, 128, 32)]
+ [TestCase(DisplayFrameFormat.Dmd8, 192 * 64, 192, 64)]
+ [TestCase(DisplayFrameFormat.Dmd8, 256 * 64, 256, 64)]
+ [TestCase(DisplayFrameFormat.Dmd24, 128 * 32 * 3, 128, 32)]
+ public void BridgeInfersSupportedStandardFrameSizes(DisplayFrameFormat format, int length,
+ int width, int height)
+ {
+ var frame = new DisplayFrameData("dmd0", format, new byte[length]);
+
+ Assert.That(DmdBridgePlayer.TryInferDisplayConfig(frame, out var display), Is.True);
+ Assert.That(display.Id, Is.EqualTo(frame.Id));
+ Assert.That(display.Width, Is.EqualTo(width));
+ Assert.That(display.Height, Is.EqualTo(height));
+ }
+
+ [Test]
+ public void BridgeNeedsAnnouncementForNonStandardOrMalformedFrames()
+ {
+ Assert.That(DmdBridgePlayer.TryInferDisplayConfig(
+ new DisplayFrameData("dmd0", DisplayFrameFormat.Dmd8, new byte[140 * 36]), out _), Is.False);
+ Assert.That(DmdBridgePlayer.TryInferDisplayConfig(
+ new DisplayFrameData("dmd0", DisplayFrameFormat.Dmd24, new byte[128 * 32 * 3 + 1]), out _),
+ Is.False);
+ }
+
+ [TestCase(DisplayFrameFormat.Dmd2, true)]
+ [TestCase(DisplayFrameFormat.Dmd4, true)]
+ [TestCase(DisplayFrameFormat.Dmd8, false)]
+ [TestCase(DisplayFrameFormat.Dmd24, false)]
+ [TestCase(DisplayFrameFormat.Segment, false)]
+ public void ReportsFormatsSupportedByTheColorizationSource(DisplayFrameFormat format, bool supported)
+ {
+ Assert.That(DmdColorizationPolicy.SupportsColorization(format), Is.EqualTo(supported));
+ }
+
+ [Test]
+ public void Rgb24LatchesStickyBypassAndWarnsOnce()
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("dmd0", 128, 32));
+
+ Assert.That(policy.ObserveFrame(DisplayFrameFormat.Dmd24, true, out var firstWarning),
+ Is.EqualTo(DmdColorizationPipelineAction.Bypass));
+ Assert.That(policy.BypassColorization, Is.True);
+ Assert.That(policy.AwaitingSupportedColorizedFrame, Is.False);
+ Assert.That(firstWarning, Is.True);
+ Assert.That(policy.ObserveFrame(DisplayFrameFormat.Dmd4, false, out var secondWarning),
+ Is.EqualTo(DmdColorizationPipelineAction.None));
+ Assert.That(policy.BypassColorization, Is.True);
+ Assert.That(secondWarning, Is.False);
+ }
+
+ [Test]
+ public void TransientDmd8BypassKeepsPreferenceAndRestoresOnSupportedFrame()
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("dmd0", 128, 32));
+
+ Assert.That(policy.ObserveFrame(DisplayFrameFormat.Dmd8, true, out var warning),
+ Is.EqualTo(DmdColorizationPipelineAction.Bypass));
+ Assert.That(warning, Is.True);
+ Assert.That(policy.BypassColorization, Is.True);
+ Assert.That(policy.AwaitingSupportedColorizedFrame, Is.True);
+
+ Assert.That(policy.ObserveFrame(DisplayFrameFormat.Dmd4, false, out warning),
+ Is.EqualTo(DmdColorizationPipelineAction.Restore));
+ Assert.That(warning, Is.False);
+ Assert.That(policy.BypassColorization, Is.False);
+ Assert.That(policy.AwaitingSupportedColorizedFrame, Is.False);
+ }
+
+ [Test]
+ public void TransientDmd8BypassWarnsAgainAfterRestoringColorization()
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("dmd0", 128, 32));
+
+ policy.ObserveFrame(DisplayFrameFormat.Dmd8, true, out var firstWarning);
+ policy.ObserveFrame(DisplayFrameFormat.Dmd4, false, out _);
+ policy.ObserveFrame(DisplayFrameFormat.Dmd8, true, out var secondWarning);
+
+ Assert.That(firstWarning, Is.True);
+ Assert.That(secondWarning, Is.True);
+ }
+
+ [Test]
+ public void BridgeKeepsRequestingColorizableFramesDuringTransientBypass()
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("dmd0", 128, 32));
+ policy.ObserveFrame(DisplayFrameFormat.Dmd8, true, out _);
+
+ Assert.That(DmdBridgePlayer.PreferredSourceFormat(false, policy),
+ Is.EqualTo(DisplayFrameFormat.Dmd4));
+
+ policy.SelectDisplay(new DisplayConfig("dmd1", 128, 32));
+ policy.ObserveFrame(DisplayFrameFormat.Dmd24, true, out _);
+ Assert.That(DmdBridgePlayer.PreferredSourceFormat(false, policy),
+ Is.EqualTo(DisplayFrameFormat.Dmd8));
+ }
+
+ [Test]
+ public void IdenticalTopologyReannouncementPreservesBypass()
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("DMD0", 140, 36, true, Color.red, Color.black));
+ policy.ObserveFrame(DisplayFrameFormat.Dmd24, true, out _);
+
+ policy.SelectDisplay(new DisplayConfig("dmd0", 140, 36, true, Color.green, Color.white));
+
+ Assert.That(policy.BypassColorization, Is.True);
+ policy.ObserveFrame(DisplayFrameFormat.Dmd8, false, out var shouldWarn);
+ Assert.That(shouldWarn, Is.False);
+ }
+
+ [Test]
+ public void ChangedDisplayTopologyResetsBypassAndWarning()
+ {
+ AssertTopologyChangeResets(new DisplayConfig("dmd1", 140, 36, true));
+ AssertTopologyChangeResets(new DisplayConfig("dmd0", 141, 36, true));
+ AssertTopologyChangeResets(new DisplayConfig("dmd0", 140, 37, true));
+ AssertTopologyChangeResets(new DisplayConfig("dmd0", 140, 36, false));
+ }
+
+ private static void AssertTopologyChangeResets(DisplayConfig changed)
+ {
+ var policy = new DmdColorizationPolicy();
+ policy.SelectDisplay(new DisplayConfig("dmd0", 140, 36, true));
+ policy.ObserveFrame(DisplayFrameFormat.Dmd24, true, out _);
+
+ policy.SelectDisplay(changed);
+
+ Assert.That(policy.BypassColorization, Is.False);
+ policy.ObserveFrame(DisplayFrameFormat.Dmd24, true, out var shouldWarn);
+ Assert.That(shouldWarn, Is.True);
+ }
+
+ private static bool Advertises(object source, string interfaceName)
+ {
+ return source.GetType().GetInterfaces().Any(type => type.Name == interfaceName);
+ }
+
+ private static object CreateSource(DisplayConfig display, bool colorize)
+ {
+ var create = typeof(VpeGleSource).GetMethod("Create", BindingFlags.Static | BindingFlags.Public);
+ Assert.That(create, Is.Not.Null);
+ return create.Invoke(null, new object[] { display, colorize });
+ }
+ }
+}
diff --git a/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs.meta b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs.meta
new file mode 100644
index 0000000..fbf146f
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/DmdColorizationPolicyTests.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4723db222d7846b895226f764b9c0a9f
diff --git a/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef
new file mode 100644
index 0000000..9d0f07f
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef
@@ -0,0 +1,24 @@
+{
+ "name": "VisualPinball.Engine.DMD.Unity.Test",
+ "rootNamespace": "VisualPinball.Engine.DMD.Unity.Test",
+ "references": [
+ "VisualPinball.Engine.DMD.Unity",
+ "VisualPinball.Unity"
+ ],
+ "includePlatforms": [
+ "Editor"
+ ],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false,
+ "optionalUnityReferences": [
+ "TestAssemblies"
+ ]
+}
diff --git a/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef.meta b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef.meta
new file mode 100644
index 0000000..2c98301
--- /dev/null
+++ b/VisualPinball.Engine.DMD.Unity/Tests/Runtime/VisualPinball.Engine.DMD.Unity.Test.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 05fcbbf3b96142d4bf24404b82df32b5
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant: