Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2165429
plugins: add player lifecycle, configuration and resource contracts
freezy Oct 13, 2026
6dfc664
unity: include insert shaders required by standalone player
freezy Oct 13, 2026
73121aa
packaging: use runtime ZIP streams for standalone table reads
freezy Oct 13, 2026
8f0b3f9
plugins: add VPE copyright and MIT source headers
freezy Oct 13, 2026
2d6e486
plugins: clarify MIT source license references
freezy Oct 13, 2026
11058ff
plugins: define Player-owned configuration and media contracts
freezy Oct 13, 2026
3215077
plugins: expose compact configuration layout hints
freezy Oct 13, 2026
658694a
plugins: expose shared global settings and mechanical sound gain
freezy Oct 14, 2026
a37c566
plugins: publish native controller outputs and segment displays
freezy Oct 14, 2026
1cc1a80
native windows: shared borderless GPU windows for plugins
freezy Oct 14, 2026
0e6507d
native windows: move sources to VisualPinball.Native~
freezy Oct 14, 2026
635b5a2
plugins: address review findings on cleanup, formats, drafts and segm…
freezy Oct 14, 2026
63e443d
plugins: document the plugin contract APIs
freezy Oct 14, 2026
e7d91f3
plugins: document resource contracts, the host and settings sessions
freezy Oct 14, 2026
413ca0a
plugins: let refused display bindings retry when a format lease ends
freezy Oct 14, 2026
ff122a6
plugins: document the registry, format negotiation and the gamelogic …
freezy Oct 14, 2026
283de89
plugins: keep resource notices that arrive while a binding releases i…
freezy Oct 14, 2026
4946002
plugins: describe mapped output values and present chosen displays
freezy Oct 14, 2026
889edb8
camera: bound orbit zoom radius and drop drag state on disable
freezy Oct 14, 2026
1d49856
display: add cabinet screens that show plugin surfaces in the scene
freezy Oct 14, 2026
7b0109a
plugins: multiple choice settings
freezy Oct 14, 2026
90671f0
packaging: render screenshots on every export with a fixed HDRI
freezy Oct 14, 2026
a20c268
packaging: light only the inserts in the lights-on screenshot
freezy Oct 14, 2026
a69cebf
packaging: keep flasher-driven insert bulbs off in screenshots
freezy Oct 14, 2026
8f19b60
packaging: leave inserts under light groups to the insert pass
freezy Oct 14, 2026
def5ab7
packaging: resolve flasher insert bulbs like the game does
freezy Oct 14, 2026
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
20 changes: 20 additions & 0 deletions .github/workflows/plugins.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Plugin contracts
on:
push:
paths: ['VisualPinball.Plugins/**', 'VisualPinball.Plugins.Test/**', '.github/workflows/plugins.yml']
pull_request:
paths: ['VisualPinball.Plugins/**', 'VisualPinball.Plugins.Test/**', '.github/workflows/plugins.yml']
permissions:
contents: read
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- run: dotnet test VisualPinball.Plugins.Test/VisualPinball.Plugins.Test.csproj -c Release
87 changes: 59 additions & 28 deletions VisualPinball.Engine.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VisualPinball.Engine.Test", "VisualPinball.Engine.Test\VisualPinball.Engine.Test.csproj", "{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{E74E1502-B527-475D-BCEC-B8F60699BA5F}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
.editorconfig = .editorconfig
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
CONTRIBUTING.md = CONTRIBUTING.md
package.json = package.json
EndProjectSection
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
.editorconfig = .editorconfig
.gitignore = .gitignore
CHANGELOG.md = CHANGELOG.md
CONTRIBUTING.md = CONTRIBUTING.md
package.json = package.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{E6A28E8F-1DE7-4CF4-ADB7-A55A6E6467AF}"
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\dependents.yml = .github\workflows\dependents.yml
.github\workflows\documentation.yml = .github\workflows\documentation.yml
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
ProjectSection(SolutionItems) = preProject
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\dependents.yml = .github\workflows\dependents.yml
.github\workflows\documentation.yml = .github\workflows\documentation.yml
.github\workflows\publish.yml = .github\workflows\publish.yml
.github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualPinball.Plugins", "VisualPinball.Plugins\VisualPinball.Plugins.csproj", "{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisualPinball.Plugins.Test", "VisualPinball.Plugins.Test\VisualPinball.Plugins.Test.csproj", "{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -36,42 +40,69 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x64.ActiveCfg = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x64.Build.0 = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x86.ActiveCfg = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|x86.Build.0 = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.Build.0 = Release|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x64.ActiveCfg = Release|x64
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x64.Build.0 = Release|x64
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x86.ActiveCfg = Release|x86
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|x86.Build.0 = Release|x86
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EFE73640-F311-4109-9F3E-1573F86990BB}.Release|Any CPU.Build.0 = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|x64.Build.0 = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|x86.Build.0 = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.Build.0 = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|x64.ActiveCfg = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|x64.Build.0 = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|x86.ActiveCfg = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|x86.Build.0 = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A259010-3234-4402-BE50-980B4887902C}.Release|Any CPU.Build.0 = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x64.Build.0 = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x86.ActiveCfg = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|x86.Build.0 = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.Build.0 = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x64.ActiveCfg = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x64.Build.0 = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x86.ActiveCfg = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|x86.Build.0 = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D642DA1-C750-4EB0-AE33-E2A2CDA3DDEB}.Release|Any CPU.Build.0 = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x64.ActiveCfg = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x64.Build.0 = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x86.ActiveCfg = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Debug|x86.Build.0 = Debug|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|Any CPU.Build.0 = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x64.ActiveCfg = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x64.Build.0 = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x86.ActiveCfg = Release|Any CPU
{7FFF4D1E-9DD7-4E69-B312-2E12929ABAE9}.Release|x86.Build.0 = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x64.ActiveCfg = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x64.Build.0 = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x86.ActiveCfg = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Debug|x86.Build.0 = Debug|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|Any CPU.Build.0 = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x64.ActiveCfg = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x64.Build.0 = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x86.ActiveCfg = Release|Any CPU
{CABEBB0E-FB15-49E5-A85E-78EB7FAC2822}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{E6A28E8F-1DE7-4CF4-ADB7-A55A6E6467AF} = {E74E1502-B527-475D-BCEC-B8F60699BA5F}
Expand Down
64 changes: 64 additions & 0 deletions VisualPinball.Native~/NativeWindows/Desktop/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
cmake_minimum_required(VERSION 3.22)
if(APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "14.0" CACHE STRING "Minimum macOS version")
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "macOS architectures (universal binary by default)")
endif()
project(VisualPinballNativeWindows LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
set(UNITY_PLUGIN_API "" CACHE PATH "Installed Unity Editor Data/PluginAPI directory")
option(VPNW_BUILD_TESTS "Build native window fixtures" ON)
option(VPNW_DEPLOY "Copy the built library into VisualPinball.Unity/Plugins" ON)
if(NOT EXISTS "${UNITY_PLUGIN_API}/IUnityInterface.h")
message(FATAL_ERROR "Set UNITY_PLUGIN_API to the installed Unity PluginAPI directory.")
endif()
get_filename_component(VPNW_API_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
get_filename_component(VPNW_PACKAGE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE)
set(SOURCES WindowRegistry.cpp)
if(APPLE)
enable_language(OBJCXX)
list(APPEND SOURCES Mac/MetalWindows.mm)
set(PLATFORM_LIBRARIES "-framework Cocoa" "-framework QuartzCore" "-framework Metal" "-framework ApplicationServices")
set(VPNW_PLUGIN_DIRECTORY "${VPNW_PACKAGE_ROOT}/VisualPinball.Unity/Plugins/osx" CACHE PATH "Deployment directory")
elseif(UNIX)
find_package(Vulkan REQUIRED)
find_package(X11 REQUIRED)
if(NOT X11_Xrender_FOUND)
message(FATAL_ERROR "The X Rendering Extension development files (libxrender-dev) are required.")
endif()
list(APPEND SOURCES Linux/X11Windows.cpp Linux/VulkanWindows.cpp)
set(PLATFORM_LIBRARIES Vulkan::Vulkan X11::X11 X11::Xrender dl pthread m)
set(VPNW_PLUGIN_DIRECTORY "${VPNW_PACKAGE_ROOT}/VisualPinball.Unity/Plugins/linux-x64" CACHE PATH "Deployment directory")
else()
message(FATAL_ERROR "This CMake project builds the macOS and Linux library; Windows uses VisualPinball.Native~/NativeWindows/Windows/Build.ps1.")
endif()
add_library(VisualPinballNativeWindows SHARED ${SOURCES})
target_include_directories(VisualPinballNativeWindows PRIVATE "${UNITY_PLUGIN_API}" "${VPNW_API_DIRECTORY}" "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(VisualPinballNativeWindows PRIVATE ${PLATFORM_LIBRARIES})
target_compile_options(VisualPinballNativeWindows PRIVATE -Wall -Wextra -Werror -Wno-missing-field-initializers)
if(APPLE)
target_compile_options(VisualPinballNativeWindows PRIVATE -fobjc-arc)
set_target_properties(VisualPinballNativeWindows PROPERTIES PREFIX "lib" SUFFIX ".dylib")
endif()
if(VPNW_DEPLOY)
add_custom_command(TARGET VisualPinballNativeWindows POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E make_directory "${VPNW_PLUGIN_DIRECTORY}"
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "$<TARGET_FILE:VisualPinballNativeWindows>" "${VPNW_PLUGIN_DIRECTORY}/"
COMMENT "Deploying VisualPinballNativeWindows to ${VPNW_PLUGIN_DIRECTORY}")
endif()
if(VPNW_BUILD_TESTS)
enable_testing()
add_executable(NativeWindowTests DesktopTests.cpp)
if(APPLE)
set_source_files_properties(DesktopTests.cpp PROPERTIES LANGUAGE OBJCXX)
target_compile_options(NativeWindowTests PRIVATE -fobjc-arc)
endif()
target_include_directories(NativeWindowTests PRIVATE "${UNITY_PLUGIN_API}" "${VPNW_API_DIRECTORY}" "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(NativeWindowTests PRIVATE VisualPinballNativeWindows ${PLATFORM_LIBRARIES})
add_test(NAME NativeWindows COMMAND NativeWindowTests)
endif()
49 changes: 49 additions & 0 deletions VisualPinball.Native~/NativeWindows/Desktop/DesktopLayout.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Visual Pinball Engine
// Copyright (C) 2026 freezy and VPE Team
//
// SPDX-License-Identifier: MIT
// Licensed under MIT. See https://opensource.org/license/mit.

#pragma once
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>

namespace vpnw
{
struct DesktopRect { double x, y, width, height; };
struct Monitor { DesktopRect logical; double pixelWidth, pixelHeight; };
// Anchor the primary display at (0,0), then preserve monitor adjacency in physical pixels.
// Scaling every global origin by the primary DPI creates gaps/overlaps on mixed-DPI desktops.
inline std::vector<DesktopRect> PhysicalLayout(const std::vector<Monitor>& monitors)
{
std::vector<DesktopRect> result(monitors.size()); if (monitors.empty()) return result;
std::vector<bool> placed(monitors.size(), false); placed[0] = true;
result[0] = { 0, 0, monitors[0].pixelWidth, monitors[0].pixelHeight };
for (std::size_t count = 1; count < monitors.size(); count++)
{
std::size_t target = 0, anchor = 0; double nearest = std::numeric_limits<double>::infinity(), shared = -1;
for (std::size_t i = 0; i < monitors.size(); i++) if (!placed[i])
for (std::size_t j = 0; j < monitors.size(); j++) if (placed[j])
{
auto a = monitors[j].logical, b = monitors[i].logical;
double dx = std::max({ a.x - b.x - b.width, b.x - a.x - a.width, 0.0 });
double dy = std::max({ a.y - b.y - b.height, b.y - a.y - a.height, 0.0 });
double distance = dx * dx + dy * dy;
double overlap = std::max(0.0, std::min(a.x + a.width, b.x + b.width) - std::max(a.x, b.x)) +
std::max(0.0, std::min(a.y + a.height, b.y + b.height) - std::max(a.y, b.y));
if (distance < nearest || (distance == nearest && overlap > shared)) { nearest = distance; shared = overlap; target = i; anchor = j; }
}
auto parent = monitors[anchor].logical, child = monitors[target].logical, pixels = result[anchor];
double sx = monitors[target].pixelWidth / child.width, sy = monitors[target].pixelHeight / child.height;
DesktopRect rectangle{ pixels.x + (child.x - parent.x) * sx, pixels.y + (child.y - parent.y) * sy, monitors[target].pixelWidth, monitors[target].pixelHeight };
if (child.x >= parent.x + parent.width) rectangle.x = pixels.x + pixels.width + (child.x - parent.x - parent.width) * sx;
else if (child.x + child.width <= parent.x) rectangle.x = pixels.x - (parent.x - child.x - child.width) * sx - rectangle.width;
else if (child.y >= parent.y + parent.height) rectangle.y = pixels.y + pixels.height + (child.y - parent.y - parent.height) * sy;
else if (child.y + child.height <= parent.y) rectangle.y = pixels.y - (parent.y - child.y - child.height) * sy - rectangle.height;
result[target] = rectangle; placed[target] = true;
}
return result;
}
}
Loading
Loading