Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
87 changes: 87 additions & 0 deletions VisualPinball.Engine.DMD.Unity/Runtime/DmdColorizationPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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;
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