Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("VisualPinball.Engine.DMD.Unity.Test")]
2 changes: 2 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/AssemblyInfo.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 41 additions & 10 deletions VisualPinball.Engine.DMD.Unity/Runtime/DmdBridgePlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -196,8 +197,9 @@ private void HandleDisplaysRequested(object sender, RequestedDisplays requestedD
continue;
}

EnsurePipeline(display);
_pipeline?.Clear();
if (EnsurePipeline(display)) {
_pipeline?.Clear();
}
return;
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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)
Expand Down
88 changes: 88 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System;
using VisualPinball.Unity;

namespace VisualPinball.Engine.DMD.Unity
{
internal enum DmdColorizationPipelineAction
{
None,
Bypass,
Restore,
}

/// <summary>
/// Tracks whether the selected display must bypass a colorization source that cannot consume
/// the format emitted by its gamelogic engine.
/// </summary>
internal sealed class DmdColorizationPolicy
{
private DisplayConfig _display;
private bool _warningIssued;

public bool BypassColorization { get; private set; }
public bool AwaitingSupportedColorizedFrame { get; private set; }

/// <summary>
/// Selects a display topology. An identical re-announcement deliberately preserves the
/// bypass latch; selecting a different id, geometry, or horizontal orientation resets it.
/// </summary>
public void SelectDisplay(DisplayConfig display)
{
if (SameTopology(_display, display)) {
return;
}

_display = display;
BypassColorization = false;
AwaitingSupportedColorizedFrame = false;
_warningIssued = false;
}

/// <summary>
/// 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.
/// </summary>
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;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

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;
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/DmdPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IDestination> 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})",
Expand Down
10 changes: 10 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/VpeGleSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -17,6 +19,7 @@ internal abstract class VpeGleSource : AbstractSource, ISource

private readonly Subject<Unit> _onResume = new Subject<Unit>();
private readonly Subject<Unit> _onPause = new Subject<Unit>();
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

protected VpeGleSource(string displayId, int width, int height)
{
Expand Down Expand Up @@ -104,6 +107,7 @@ private sealed class ColorizableVpeGleSource : VpeGleSource, IGray2Source, IGray
{
private readonly Subject<DmdFrame> _gray2Frames = new Subject<DmdFrame>();
private readonly Subject<DmdFrame> _gray4Frames = new Subject<DmdFrame>();
private bool _unsupportedFormatWarningLogged;

public ColorizableVpeGleSource(string displayId, int width, int height) : base(displayId, width, height)
{
Expand All @@ -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;
}
}

Expand Down
8 changes: 8 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Tests.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Tests/Runtime.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading