dmd: support DMD Studio colorization bypass#1
Conversation
Greptile SummaryThis PR introduces
Confidence Score: 5/5Safe to merge. The bypass state machine is well-scoped, all transitions are covered by tests, and the pipeline rebuild is correctly guarded on the main thread. The new DmdColorizationPolicy is a small, self-contained state machine with 17 tests that exercise every documented transition. The integration into DmdBridgePlayer is straightforward: policy observation and pipeline rebuilds happen on the same main thread that Unity's frame handler runs on, avoiding any new threading concerns. _warningIssued is correctly reset in the Restore branch, and the triggering frame is forwarded to the newly built pipeline without a gap. No correctness issues were found. No files require special attention. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant GLE as GamelogicEngine
participant DBP as DmdBridgePlayer (main thread)
participant DCP as DmdColorizationPolicy
participant DP as DmdPipeline
GLE->>DBP: OnDisplaysRequested(display)
DBP->>DCP: SelectDisplay(display)
DBP->>DP: new DmdPipeline(colorized)
GLE->>DBP: OnDisplayUpdateFrame(Dmd24 frame)
DBP->>DCP: "ObserveFrame(Dmd24, usesColorization=true)"
DCP-->>DBP: "Bypass (sticky), shouldWarn=true"
DBP->>DBP: Log warning
DBP->>DP: Dispose old pipeline
DBP->>DCP: SelectDisplay(same display) no-op
DBP->>DP: new DmdPipeline(passthrough)
DBP->>GLE: RequestSourceFrameFormat(Dmd8)
DBP->>DP: Push(Dmd24 frame) passthrough
GLE->>DBP: OnDisplayUpdateFrame(Dmd8 frame)
DBP->>DCP: "ObserveFrame(Dmd8, usesColorization=false)"
DCP-->>DBP: None (sticky bypass, no recovery)
DBP->>DP: Push(Dmd8 frame) passthrough
Note over DBP,DP: Transient Dmd8 recovery path
GLE->>DBP: OnDisplayUpdateFrame(Dmd8 frame)
DBP->>DCP: "ObserveFrame(Dmd8, usesColorization=true)"
DCP-->>DBP: "Bypass (transient), AwaitingSupportedColorizedFrame=true"
DBP->>DP: Dispose then new passthrough pipeline
DBP->>GLE: RequestSourceFrameFormat(Dmd4)
DBP->>DP: Push(Dmd8 frame)
GLE->>DBP: OnDisplayUpdateFrame(Dmd4 frame)
DBP->>DCP: "ObserveFrame(Dmd4, usesColorization=false)"
DCP-->>DBP: Restore
DBP->>DP: Dispose then new colorized pipeline
DBP->>DP: Push(Dmd4 frame) colorized
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant GLE as GamelogicEngine
participant DBP as DmdBridgePlayer (main thread)
participant DCP as DmdColorizationPolicy
participant DP as DmdPipeline
GLE->>DBP: OnDisplaysRequested(display)
DBP->>DCP: SelectDisplay(display)
DBP->>DP: new DmdPipeline(colorized)
GLE->>DBP: OnDisplayUpdateFrame(Dmd24 frame)
DBP->>DCP: "ObserveFrame(Dmd24, usesColorization=true)"
DCP-->>DBP: "Bypass (sticky), shouldWarn=true"
DBP->>DBP: Log warning
DBP->>DP: Dispose old pipeline
DBP->>DCP: SelectDisplay(same display) no-op
DBP->>DP: new DmdPipeline(passthrough)
DBP->>GLE: RequestSourceFrameFormat(Dmd8)
DBP->>DP: Push(Dmd24 frame) passthrough
GLE->>DBP: OnDisplayUpdateFrame(Dmd8 frame)
DBP->>DCP: "ObserveFrame(Dmd8, usesColorization=false)"
DCP-->>DBP: None (sticky bypass, no recovery)
DBP->>DP: Push(Dmd8 frame) passthrough
Note over DBP,DP: Transient Dmd8 recovery path
GLE->>DBP: OnDisplayUpdateFrame(Dmd8 frame)
DBP->>DCP: "ObserveFrame(Dmd8, usesColorization=true)"
DCP-->>DBP: "Bypass (transient), AwaitingSupportedColorizedFrame=true"
DBP->>DP: Dispose then new passthrough pipeline
DBP->>GLE: RequestSourceFrameFormat(Dmd4)
DBP->>DP: Push(Dmd8 frame)
GLE->>DBP: OnDisplayUpdateFrame(Dmd4 frame)
DBP->>DCP: "ObserveFrame(Dmd4, usesColorization=false)"
DCP-->>DBP: Restore
DBP->>DP: Dispose then new colorized pipeline
DBP->>DP: Push(Dmd4 frame) colorized
Reviews (2): Last reviewed commit: "dmd: rearm transient bypass diagnostics" | Re-trigger Greptile |
Overview
This is the display-pipeline companion to VisualPinball.Engine#570. It ensures DMD Studio output remains visible when a display has colorization configured but the incoming frame format cannot be consumed by that colorizer.
Without this behavior, native RGB content and certain startup races can leave the display blank because frames continue to enter a pipeline that only accepts indexed monochrome input.
Functional behavior
User-visible result
Tables can combine configured DMD colorization with DMD Studio content without producing a black display when RGB content is played. Existing monochrome colorization remains active for compatible frames, and a temporary negotiation mismatch can recover without requiring a scene reload or display reconfiguration.
Verification