Skip to content

Add cross-platform LibDmd.Core library and NuGet packaging.#563

Open
freezy wants to merge 24 commits into
masterfrom
core
Open

Add cross-platform LibDmd.Core library and NuGet packaging.#563
freezy wants to merge 24 commits into
masterfrom
core

Conversation

@freezy

@freezy freezy commented Jun 26, 2026

Copy link
Copy Markdown
Owner

This PP adds a cross-platform LibDmd.Core extraction and NuGet packaging path for use by VPE and other non-WPF consumers.

  • Added LibDmd.Core targeting netstandard2.1 and net8.0
  • Added WPF-free shims for color, analytics, and native library loading
  • Guarded or excluded WPF/bitmap/registry/Windows-only paths from the core build
  • Added LibDmd.Core.Harness to verify native interop and RenderGraph flow
  • Added NuGet packaging for managed and per-RID native packages
  • Added CI workflow to build packages and publish on core-v* tags

Extract a WPF-free, cross-platform core from LibDmd so it can be
consumed on non-Windows targets and published as a NuGet package.

- Split ImageUtil into a bitmap half (ImageUtil.cs, excluded from
  the core) and portable array-only methods (ImageUtil.Portable.cs)
  via a partial class.
- Add LibDmd.Core project that compiles the portable sources with
  shims (DmdColor, DmdColors, Analytics, WpfNamespaceMarker) and a
  NativeLibraryLoader for resolving native dependencies.
- Move shared types (ScalerMode, WrongFormatException, PluginConfig)
  into standalone files and guard WPF-only paths so they compile in
  both flavors.
- Add LibDmd.Core.Harness console app for exercising the core.
- Add native NuGet packaging (nuspecs and targets for win-x64,
  linux-x64, osx-x64, osx-arm64), build-nuget scripts, and a
  build-core-nuget GitHub Actions workflow.
@freezy freezy self-assigned this Jun 26, 2026
@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a cross-platform LibDmd.Core package path. The main changes are:

  • New LibDmd.Core projects and harnesses for non-WPF consumers.
  • Platform shims for native loading, color types, analytics, and window backends.
  • NuGet packaging for managed and per-runtime native packages.
  • CI workflow support for building and publishing Core packages.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
native/nuget/LibDmd.Core.Native.nuspec The native meta-package now includes the build assets needed for consumer copy targets.
native/nuget/targets/LibDmd.Core.Native.osx-arm64.targets The arm64 macOS target now selects by requested runtime architecture before host fallback.
native/nuget/targets/LibDmd.Core.Native.osx-x64.targets The x64 macOS target now selects by requested runtime architecture before host fallback.

Reviews (6): Last reviewed commit: "core: Vendor macOS libSDL2 dylibs for th..." | Re-trigger Greptile

Comment thread native/nuget/LibDmd.Core.Native.nuspec Outdated
Comment thread native/nuget/targets/LibDmd.Core.Native.osx-x64.targets Outdated
Comment thread LibDmd/Frame/DmdFrame.cs
Comment thread LibDmd/Frame/DmdFrame.cs
Comment thread LibDmd/Frame/ColoredFrame.cs
freezy added 16 commits June 27, 2026 10:31
Introduce a Windows-specific project that renders DMD frames to a
native window without WPF. NativeWindowDestination hosts a Win32
window on a dedicated STA thread and implements the Gray2/Gray4/
Gray8/Rgb24/Rgb565 destination interfaces, while NativeOpenGlRenderer
uploads frames to an OpenGL texture with an optional dot-matrix
shader.

Wire the project into DmdExtensions.sln and LibDmd.Core.slnx, and
align NLog to 4.7.15 across LibDmd.Core and the new project.
Replace the single-pass DMD shader with the full dmdext render
pipeline using offscreen framebuffers. Two separable blur passes
generate dot-glow and back-glow textures from the source, which the
DMD fragment shader then composites with per-dot rounded boxes,
brightness and gamma to match dmdext's virtual DMD appearance.

- Add framebuffer/render-texture management and the GL entry points
  required for it (glGenFramebuffers, glActiveTexture, etc.).
- Upload the source frame flipped into a reusable buffer so the
  offscreen passes and final quad share a consistent orientation.
- Rename shader/texture members from the old single-program model to
  the multi-program pipeline and update related log messages.
Move the DMD glow rendering (shaders, framebuffers, blur passes
and GL function bindings) out of NativeOpenGlRenderer into a new
VirtualDmdOpenGlPipeline class. NativeOpenGlRenderer now owns only
the window context and delegates frame rendering to the pipeline,
making the glow pipeline reusable across renderers.
Move VirtualDmdOpenGlPipeline into LibDmd (linked back into
LibDmd.Core.Windows) and make it public so the WPF VirtualDmdControl
can reuse it. When a frame is available, the control now converts it
to RGBA and draws via the shared pipeline, falling back to the
legacy SharpGL path otherwise.

Add RGBA conversion helpers for Gray2/4/8, ColoredGray6 and Rgb24
frames, and dispose the shared pipeline on teardown.
Map the WPF DmdStyle (dot size, rounding, sharpness, glow,
brightness, gamma, glass) into a new VirtualDmdRenderStyle and
feed it to the shared OpenGL pipeline.

Build the DMD fragment shader dynamically from the active style,
gating dot overlap, glow, brightness, unlit dots, glass and gamma
behind #define blocks so each effect is only compiled when used.
Recompile the program when the style changes and add glass texture
and color uniforms (glUniform4f) to support glass lighting.
Upload the glass overlay as a dedicated RGBA texture bound to its
own texture unit instead of reusing the source texture, and gate
the GLASS shader define on the glass data actually being present.
The WPF control converts the loaded bitmap to RGBA and forwards it
to the pipeline, reapplying it whenever the pipeline is recreated.
The LogFrame diagnostics that logged the first non-empty and first
empty frame per format were temporary debugging aids. Remove them
along with the now-unused tracking fields.
Extend NativeWindowDestination with configurable window position,
size, stay-on-top behavior, and render style. New constructor
overloads and ConfigureWindow/ConfigureRenderStyle let callers
position the window and update its style at runtime.

Track window layout by handling WM_MOVE, WM_SIZE, and the
enter/exit size-move messages, reading back the actual client rect
so the reported placement stays in sync after user drag/resize.

Thread the render style through NativeOpenGlRenderer so the shared
OpenGL pipeline can be restyled without recreating the renderer.
Add render/configure pending flags so repeated paint and restyle
requests collapse into a single posted window message instead of
queuing duplicates. Move window configuration onto the UI thread via
a WM_CONFIGURE_WINDOW message and skip rendering while the window is
being moved or resized.

Replace the per-message SyncRoot locks with a shared TryGetDestination
helper.
Switch the native DMD window to a WS_POPUP style with a thick frame
so it has no title bar or border chrome. Drag the window by its body
via WM_NCHITTEST returning HTCAPTION, and resize from a bottom-right
grip returning HTBOTTOMRIGHT. Strip the non-client area with
WM_NCCALCSIZE and drop the AdjustWindowRect calls since client and
window rects now coincide.
Refactor NativeWindowDestination from a single Win32-bound
implementation into a thin facade over an INativeWindowBackend
abstraction, selecting a Win32, SDL, or null backend at runtime.
This lifts the native DMD window off Windows-only Win32/STA-thread
plumbing so it can render on macOS/Linux through SDL.

- Add INativeWindowBackend with Win32, SDL, and null backends
- Move the Win32 message loop and the SDL window/event loop into
  their own backend classes
- Make VirtualDmdOpenGlPipeline backend-agnostic by injecting a GL
  function loader, so SDL can supply its own GL entry points
- Wire a --native-window mode into the harness and reference
  LibDmd.Core.Windows; copy per-RID native libs and allow AnyCPU
  for osx-arm64
- Fix Serum_Load null check to compare against IntPtr.Zero
Move ConfigureRenderStyle (and SDL window configuration) off the
caller thread and onto the message/render loop. The SDL backend now
sets pending flags applied in the loop, and the Win32 backend posts a
WM_CONFIGURE_RENDER_STYLE message handled in the window procedure.
This keeps all window mutations on the UI thread.

In AbstractConverter, guard the last-frame fields with a lock so the
clock Tick and Convert() callers no longer race. Also return the
previously stored DMD frame to the pool on replace and on Dispose,
fixing a frame pool leak.
Introduce a WPF/GL-free interface that the VPE bridge can talk to,
along with neutral DmdWindowLayout/DmdWindowStyle types and a
reflection-based NativeDmdWindow.TryCreate factory that resolves the
platform implementation at runtime (returning null where no backend
ships).

Implement INativeDmdWindow on NativeWindowDestination and add a
strongly-typed constructor so consumers never touch reflection or the
GL-coupled style type. Self-driven backends report RequiresHostPump
false and no-op Pump(); the upcoming host-pumped (macOS) backend will
override both.
Introduce LibDmd.Core.Sdl, a host-pumped native DMD window for
macOS/Linux (and mobile-capable) built on SDL2 + an OpenGL ES 2.0
port of the dmdext virtual-DMD renderer (dot SDF shader plus the
two-stage gaussian glow) running through ANGLE.

The backend constructs on the host main thread (SDL/window/GL
context init), buffers frames on the worker thread, and renders
on the host pump. Glass overlay is not yet ported.

Wire NativeDmdWindow to select a backend per OS: prefer the Win32
window on Windows and fall back to SDL, use SDL elsewhere. Wrap
construction in a try/catch so a present-but-unusable backend
(missing SDL/ANGLE runtime) returns null instead of throwing.
…back.

Make cross-thread state safe and fix GL resource leaks in the native
DMD window backends, plus let the factory fall through to the next
backend when one fails to initialize.

- Mark _disposed (and Win32 _isMovingOrSizing) volatile, since render
  methods run on the worker/rotation thread while Dispose runs on the
  main/window thread.
- Snapshot _color once per frame; SetColor may run on another thread
  during color rotation.
- GlesDmdPipeline: delete the old shader program before rebuilding on
  style change, and free all GL programs/textures/buffers on dispose;
  reuse a letterbox rect array to avoid per-frame allocation.
- Filter SDL mouse events by window id so a shared global event queue
  (Unity or another plugin) can't drive our borderless-drag.
- TryCreate now attempts every candidate backend in preference order,
  logging and skipping any that throws during construction.
- Remove the obsolete self-driven SdlNativeWindowDestination; macOS and
  Linux use the host-pumped GL-ES backend in LibDmd.Core.Sdl, so this
  assembly's backend is Windows-only.
- Document the threading contract on INativeDmdWindow.
Add the build asset group to the native RID dependencies so the
.targets files actually flow to consumers, and restrict the osx-x64
and osx-arm64 copy steps to their matching process architecture to
avoid copying the wrong binaries on Apple Silicon vs Intel.
Comment thread native/nuget/targets/LibDmd.Core.Native.osx-arm64.targets Outdated
freezy added 3 commits June 27, 2026 23:37
Split the PIN2DMD USB transport per build so LibDmd.Core can drive
the device without the WPF-bound LibUsbDotNet 2.x dependency.

- Make Pin2DmdBase partial and guard the LibUsbDotNet path with
  #if !LIBDMD_CORE (Init/RenderRaw/ReadConfig/Dispose and the
  _pin2DmdDevice field).
- Add LibDmd.Core's libusb-1.0 counterpart in Pin2DmdBase.Usb3.cs:
  enumerate by VID/PID, match the product string, claim the
  interface, bulk-transfer frames and read config.
- Add a thin libusb-1.0 P/Invoke binding (LibUsb.cs), netstandard2.1
  and IL2CPP-safe, consistent with the libserum/libzedmd bindings.
- Resolve libusb-1.0 per-OS in NativeLibraryLoader (it doesn't follow
  the "<name>64" naming convention).
- Link the shared Pin2Dmd sources into LibDmd.Core.
Split the FTDI and USB transports per build so LibDmd.Core gets
WPF-free, cross-platform paths while the legacy code stays intact:

- PinDmd1: new ftd2xx (D2XX) P/Invoke binding and a partial-class
  Ftdi transport; the FTD2XX_NET path in PinDmd1.cs is now
  #if !LIBDMD_CORE.
- PinDmd2: new libusb-1.0 USB transport (shares the PIN2DMD vendor
  id, told apart by the "pinDMD V2" product string); the
  LibUsbDotNet 2.x path in PinDmd2.cs is now #if !LIBDMD_CORE.
- Link the shared PinDmd1/PinDmd2 sources into LibDmd.Core and
  register ftd2xx with NativeLibraryLoader.

Vendor libusb-1.0 (all four RIDs) and win-x64 ftd2xx.dll under
native/vendor/, staged by fetch-natives.sh with a warning when
libusb-1.0 is missing. macOS/Linux libftd2xx must be dropped in
manually (FTDI gates downloads); see native/vendor/README.md.
Unity's Mono runtime cannot load the real net6.0+ System.IO.Ports
("TypeLoadException: Invalid type System.IO.Ports.SerialPort"), which
breaks LibDmd's Pixelcade and PinDMD3 serial drivers.

Add a drop-in replacement assembly named System.IO.Ports (netstandard2.0,
AssemblyVersion 8.0.0.0) that exposes the minimal SerialPort surface those
drivers use, backed by the native libserialport already shipped with
dmd-extensions. LibDmd.Core compiles against the real reference assembly;
at runtime Unity loads only this shim from Plugins, so the drivers bind to
it transparently.
Comment thread native/nuget/targets/LibDmd.Core.Native.osx-arm64.targets Outdated
Comment thread native/nuget/targets/LibDmd.Core.Native.osx-x64.targets Outdated
freezy added 4 commits June 28, 2026 11:48
The osx-arm64/osx-x64 targets gated the native dylib copy solely on
the build host's ProcessArchitecture. That pulled the wrong-arch
dylibs during cross-RID publishes (e.g. `publish -r osx-x64` on an
Apple Silicon host, or `-r osx-arm64` on Intel).

Prefer the requested RuntimeIdentifier's architecture when a RID is
set, and fall back to the host ProcessArchitecture only for non-RID
builds.
Extract the dot/glow/glass fragment shaders and blur kernels into a
shared DmdShaderSource so the desktop OpenGL pipeline (GLSL 1.20) and
the SDL GL ES pipeline (GLSL ES 1.00) render identically and can't
drift. Each pipeline now prepends only its own profile header and keeps
its own vertex shader; bodies and the #define/const config block are
shared verbatim.

Link DmdShaderSource into the Core.Sdl, Core.Windows, and LibDmd
projects, and rename the GL ES blur uniforms (uTex/uDir -> texture/
direction) to match the shared source.

Add glass-overlay parity to the GL ES pipeline: SetGlassTexture/
ClearGlassTexture, the GLASS-path uniforms, and RGBA upload, mirroring
the desktop renderer.
Switch the harness from the Win32-only NativeWindowDestination to the
NativeDmdWindow factory, which resolves the Win32 window on Windows and
the host-pumped SDL/GL-ES (ANGLE) window on macOS/Linux. Reference
LibDmd.Core.Sdl so the backend ships next to the harness, drive Pump()
once per frame when RequiresHostPump is set, and configure a realistic
DMD layout/style so the dots-plus-glow shader is actually exercised.

Add publish-macos.sh to produce a self-contained osx-arm64/osx-x64 app
(bundling SDL2 + ANGLE dylibs from native/vendor when present) and
README-macos-test.md documenting how to validate the GL-ES window on a
Mac without the .NET SDK or Unity.
Stage the universal libSDL2 dylib (osx-arm64 + osx-x64, from the
pysdl2-dll 2.30.1 wheel) needed by the macOS virtual-DMD window.

Update native/vendor/README.md to document the new libSDL2 entry,
the build-handled libserialport, and the outstanding ANGLE blocker
for the macOS virtual window, and clarify the libftd2xx notes.
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