Skip to content

Player plugin system, native outputs and shared native windows - #588

Merged
freezy merged 26 commits into
masterfrom
feat/player-plugins
Oct 14, 2026
Merged

freezy merged 26 commits into
masterfrom
feat/player-plugins

Conversation

@freezy

@freezy freezy commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Adds the plugin system the Player uses to load optional integrations (PinUP, DMD, B2S backglasses, …), plus the shared services those plugins need.

Plugin system (VisualPinball.Plugins, MIT)

  • Player lifecycle hooks, plugin descriptors, ordering and capability resolution.
  • A declarative configuration schema with global and per-game scopes, discovery catalogs, validation, actions, previews and live reconfiguration. The Player renders the settings UI.
  • A resource registry through which gamelogic engines publish controllers, displays, audio and surfaces. Plugins bind to these resources instead of to engine types.
  • Global settings that plugins can expose on shared Player pages, such as the volume mixer, and mechanical sound gain.

Native outputs and segment displays

  • A new ResourceKind.Outputs carries every lamp, solenoid, GI string and mech by the controller's own number, with its value semantics (binary, levels, physical, position) and a poll-end marker. It is separate from the mapped Controller resource, so role-less controller bindings stay unambiguous.
  • Engines opt in through IGamelogicNativeOutputs.
  • Height-one displays are published as Segments resources. Each announces its layout, order and element count before its first frame.
  • Choice, action and validation requests carry the selected table package (PluginPackageReference).

Shared native windows

  • One native library, VisualPinballNativeWindows (VPNW_ API), presents a Unity render texture in a borderless desktop window:
    • Windows: Direct3D 11/12 with DirectComposition.
    • macOS: Metal.
    • Linux: Vulkan/X11.
  • It was extracted from PinUP so every plugin can use it. PinUP and the B2S backglass plugin both do.
  • The sources and test fixtures are in VisualPinball.Native~/NativeWindows. Binaries are built locally and gitignored, like NativeInput's; only their .meta files are committed.
  • On the managed side, VisualPinball.Unity gets NativeWindowSurface and a shared NativeWindowHost presenter.
  • The macOS library has not been built yet.

Other

  • Insert shaders required by the standalone player are included in builds.
  • Standalone table reads use runtime ZIP streams.

Related PRs

All of these are on the feat/player-plugins branch:

  • VisualPinball.Engine.PinMAME
  • VisualPinball.Engine.PinUp
  • VisualPinball.Engine.Player
  • VisualPinball.Engine.Player.Hdrp

freezy added 11 commits October 13, 2026 18:04
- New ResourceKind.Outputs carries every lamp, solenoid, GI string and mech by the controller's own number, with the value's semantics (binary, levels, physical, position) and maximum, plus a poll-end marker. It is separate from the mapped Controller resource, so role-less controller bindings stay unambiguous.
- Gamelogic engines opt in through IGamelogicNativeOutputs; GamelogicPluginResources publishes the outputs resource.
- Height-one displays are published as Segments resources with layout and hardware hints. IPluginSegmentSource announces the display's order and element count before the first frame.
- Choice, action and validation requests carry the table package (PluginPackageReference), for catalogs that look next to the table.
One native library, VisualPinballNativeWindows (VPNW_ API), extracted from PinUp's desktop output so every plugin can present a Unity render texture in a borderless desktop window: Direct3D11/12 with DirectComposition on Windows, Metal on macOS, Vulkan/X11 on Linux. Sources and fixtures live in Native~/NativeWindows. Binaries are built locally and ignored, like NativeInput.

Managed side in VisualPinball.Unity: NativeWindowSurface (placement, z-order, placement mode, diagnostics; reports unsupported instead of throwing when the library is missing) and NativeWindowHost, one shared presenter issuing a draw/present event pair per frame and draining deferred GPU releases.
Groups native sources under a VisualPinball-prefixed folder that Unity still ignores, since the package root is the repository root.
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High impact] The latest changes appear safe to merge; no new actionable issue was found.

Summary

Adds the Player plugin contracts, settings store, resource streams and shared native windows.

  • The Player can load plugins through shared lifecycle and settings contracts.
  • Plugins can bind game displays, controllers, audio, and native outputs through shared resources.
  • Plugins can show Unity textures in shared borderless desktop windows.
  • Table scenes can show plugin output on named cabinet screens.
  • Standalone table reads use runtime ZIP streams, and Player builds include insert shaders.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Engine[Gamelogic engine] --> Resources[Shared resource registry]
  Settings[Saved plugin settings] --> Host[Plugin host]
  Host --> Plugins[Optional integrations]
  Resources --> Plugins
  Plugins --> Surfaces[Unity textures]
  Surfaces --> Cabinet[Cabinet screens]
  Surfaces --> Windows[Shared native windows]
Loading

Reviews (13) · Last reviewed commit: "packaging: resolve flasher insert bulbs ..." · Reviewed by Greptile

Comment thread VisualPinball.Plugins/Runtime/PluginHost.cs Outdated
Comment thread VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs Outdated
Comment thread VisualPinball.Plugins/Configuration/PluginSettingsSession.cs Outdated
Comment thread VisualPinball.Plugins/Resources/ResourceContracts.cs
Comment thread VisualPinball.Unity/VisualPinball.Unity/Plugins/PackagedGameInformation.cs Outdated
Comment thread VisualPinball.Native~/NativeWindows/Desktop/Mac/MetalWindows.mm
…ents

- An unresponsive or emergency-stopped plugin keeps its scopes (native handles, device leases) until its running hooks and Quiescence have finished, instead of losing them while its worker still runs. Such instances are quarantined and block a new instance of the plugin until they have drained. The host tracks every running lifecycle hook per instance for this.
- The identification display encoding is chosen from what the consumer accepts. A later consumer must accept the encoding already delivered.
- A successful settings apply keeps edits made while it was running, instead of reverting them to the saved profile.
- Segment sources announce their layout and hardware with their order and size.
- The packaged game identity cache compares paths ordinally, so case-distinct files on Linux no longer share an entry.
- The macOS native window keeps its drawable at the window's pixel size after a resize or display change.
Comment thread VisualPinball.Plugins/Resources/DisplayFormatNegotiator.cs
Every public type and member of the plugin API now has XML documentation saying what it is for, with a use case from the Player, PinUP, B2S, DMD or PinMAME integrations, examples for what plugins implement or call, and the threading, lifetime and ownership rules. This covers the descriptors and runtime hooks, the configuration schema, store and global settings, the runtime helpers, the resource registry and display sources, the Unity plugin contracts and the native window service.

One-line enums and interfaces are split over several lines so that each member can carry its own comment; no code changed.
XML documentation for the resource contracts (kinds, descriptors, selections, controller, display, segment, audio and surface payloads), the ready-made signal sources, the plugin host and its quarantine rules, settings sessions and packaged game identification. One-line enums and interfaces are split so each member can carry its own comment; no code changed.
A binding refused because another consumer holds a raw identification encoding it cannot read now retries as soon as that lease ends, instead of staying blocked until the game restarts.

- DisplayFormatNegotiator signals when its last identification lease is released.
- The gamelogic bridge forwards that through the new PluginResourceRegistry.NotifyChanged, which raises ResourceChangeKind.Changed.
- Bindings guard against re-entrant refreshes. A notice caused by releasing a binding's own view does not retry the same attempt, and a binding disposed from a notification stays disposed.
…bridge

XML documentation for the resource registry and bindings, display format negotiation and the gamelogic resources bridge, including the table of resources a gamelogic engine publishes. No code changed.
Comment thread VisualPinball.Plugins/Resources/PluginResourceRegistry.cs Outdated
…ts view

A binding ignored every notice while releasing its view, so a display published or withdrawn during that release left it with stale candidates. Now it only ignores change notices that cannot help it: the one for the resource it is releasing, and those for resources outside its selection, as an engine-wide format lease raises for every display. Publications and withdrawals always trigger another pass.
- Mapped lamp events on the controller resource now carry what their raw values mean. The rules come from the table's lamp mapping, the same ones LampPlayer applies: on/off lamps are binary, fading lamps run to their fading steps, and RGB channels run to 255. Consumers without native outputs, such as a backglass, no longer light faded lamps or GI levels fully. Numeric IDs are registered with and without leading zeros, as LampPlayer does.
- A display selection with the presentation intent and a chosen resource ID also matches the presentation views derived from that display, and prefers them. A display picked by hand keeps its colorization.
Wheel zoom-out multiplied the radius without an upper bound, sending the camera to float-precision distances where SendMouseEvents fails with "Screen position out of view frustum".
CabinetScreenComponent marks a renderer as the backglass, topper, DMD, full DMD or a custom screen of the cabinet, with an optional root to hide when no plugin provides it. CabinetScreenBinder shows the surface a plugin publishes for the role in the screen's material slot, picking among providers by a preference order. A DMD screen only shows a plugin's DMD when the gamelogic engine announces none. SurfaceRoles lists the well-known role names.
PluginField.Multiple lets a choice field pick any number of catalog items, stored as a list of IDs, validated and checked against the catalog like single choices. PluginSettingsSnapshot.Strings reads such lists.
Exports only packed whatever an earlier "Generate Screenshot" had left behind, so a removed screenshot folder silently produced packages without table images, and stale ones shipped otherwise. The export runner now renders them first, dropping old and partial ones, and asks before exporting without them.

Screenshots are always lit with the bundled studio HDRI at exposure 4.5; the HDRI the default pointed to had never been committed. The camera's aspect is set to the portrait render target before framing, since its gate-fitted field of view otherwise followed the Game view and shrank the table.
Hybrid inserts show their authored intensity outside play mode and were never switched, so the lights-on and lights-off shots came out the same. The generator now drives every insert, including its extra bulbs, on for the lights-on shot and off otherwise, restoring them afterwards. GI, bulbs and flashers stay off in all shots.
Coil mappings are resolved to the insert bulbs they drive, directly by bulb, through a lamp above the insert, or per group member, and those bulbs stay off in the lights-on shot.
The orphan group pass picked up the spill lights and meshes of inserts parented under a light group and switched them off in the lights-on shot, overriding the insert pass.
A lamp above an insert drives only its main bulb, and only while no mapping takes that bulb over; extra bulbs follow their own mappings. The screenshot generator now records the bulbs mappings drive directly and applies a flasher lamp above an insert to the main bulb only, so normal bulbs no longer stay dark.
@freezy
freezy merged commit dc6dfc9 into master Oct 14, 2026
18 checks passed
@freezy
freezy deleted the feat/player-plugins branch October 14, 2026 16:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant