diff --git a/MUNGA_L4/L4KEYLIGHT.cpp b/MUNGA_L4/L4KEYLIGHT.cpp new file mode 100644 index 0000000..197d6cc --- /dev/null +++ b/MUNGA_L4/L4KEYLIGHT.cpp @@ -0,0 +1,397 @@ +//===========================================================================// +// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror. +// +// COMPILED APART FROM THE ENGINE: /std:c++17, DEFAULT struct packing, +// conformance mode (see the per-file settings in Munga_L4.vcxproj). +// No engine headers may be included here - the engine compiles /Zp1 +// and its types would take a different layout in this translation +// unit. The l4keylight.h interface is scalars only for that reason. +// +// Everything WinRT runs on a private worker thread: device watcher, +// keyboard claiming, and a 100 ms paint loop that mirrors the lamp +// bytes (same flash formula as the on-screen cockpit buttons in +// L4MFDVIEW, so the board and the screen blink in step). +//===========================================================================// + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "windowsapp.lib") + +#include "l4keylight.h" + +namespace +{ + using namespace winrt; + using namespace winrt::Windows::Devices::Enumeration; + using namespace winrt::Windows::Devices::Lights; + using winrt::Windows::UI::Color; + using winrt::Windows::System::VirtualKey; + + struct KeyEntry + { + int virtualKey; + int address; + bool yellow; + }; + + struct ClaimedArray + { + hstring id; + LampArray array{ nullptr }; + bool perKey = false; + bool baseCoated = false; + }; + + std::mutex gLock; + std::vector gMap; + unsigned char gLamps[64] = {}; + void (*gLogger)(const char *) = nullptr; + + std::atomic gRunning{ false }; + std::thread gWorker; + + void Log(const char *line) + { + void (*logger)(const char *); + { + std::lock_guard hold(gLock); + logger = gLogger; + } + if (logger != nullptr) + { + logger(line); + } + } + + void Logf(const char *format, ...) + { + char line[256]; + va_list args; + va_start(args, format); + _vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args); + va_end(args); + Log(line); + } + + //--------------------------------------------------------------- + // Lamp byte -> brightness level 0-3, animating the flash modes. + // Identical to LampLevel in L4MFDVIEW.cpp so the keyboard and the + // on-screen buttons blink together. + //--------------------------------------------------------------- + int LampLevel(int lamp_state) + { + int mode = lamp_state & 0x03; + int level1 = (lamp_state >> 2) & 0x03; + int level2 = (lamp_state >> 4) & 0x03; + + if (mode == 0) + { + return level1; + } + + static const int half_period[4] = { 0, 500, 250, 125 }; + return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1; + } + + //--------------------------------------------------------------- + // The panel palette (vRIO's KeyboardLampMirror shades): red for + // the banks, yellow for Secondary/Screen; the off shade keeps the + // bound keys faintly visible so the board reads as a button field. + //--------------------------------------------------------------- + Color Shade(int level, bool yellow) + { + Color color; + color.A = 255; + if (yellow) + { + if (level >= 3) { color.R = 245; color.G = 210; color.B = 60; } + else if (level >= 1) { color.R = 140; color.G = 118; color.B = 38; } + else { color.R = 70; color.G = 60; color.B = 24; } + } + else + { + if (level >= 3) { color.R = 230; color.G = 70; color.B = 70; } + else if (level >= 1) { color.R = 120; color.G = 50; color.B = 50; } + else { color.R = 64; color.G = 40; color.B = 40; } + } + return color; + } + + bool SameColor(const Color &a, const Color &b) + { + return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B; + } + + //--------------------------------------------------------------- + // The worker: watcher + claim + paint loop + //--------------------------------------------------------------- + void Worker() + { + try + { + init_apartment(); + } + catch (...) + { + // apartment already set on this thread; carry on + } + + std::mutex claimedLock; + std::vector claimed; + bool anySeen = false; + + DeviceWatcher watcher{ nullptr }; + try + { + watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector()); + + watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info) + { + try + { + LampArray array = LampArray::FromIdAsync(info.Id()).get(); + if (array == nullptr || + array.LampArrayKind() != LampArrayKind::Keyboard) + { + return; // mice / strips / cases stay untouched + } + anySeen = true; + ClaimedArray entry; + entry.id = info.Id(); + entry.array = array; + entry.perKey = array.SupportsVirtualKeys(); + { + std::lock_guard hold(claimedLock); + claimed.push_back(entry); + } + Logf(entry.perKey + ? "KeyLight: + %ls (%d LEDs, per-key)" + : "KeyLight: + %ls (%d zones - board-wide mirror)", + info.Name().c_str(), (int) array.LampCount()); + } + catch (...) + { + Log("KeyLight: could not open a lamp array"); + } + }); + watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update) + { + std::lock_guard hold(claimedLock); + for (size_t i = 0; i < claimed.size(); ++i) + { + if (claimed[i].id == update.Id()) + { + claimed.erase(claimed.begin() + i); + Log("KeyLight: keyboard disconnected"); + break; + } + } + }); + watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &) + { + // required for the watcher to progress + }); + watcher.Start(); + } + catch (...) + { + Log("KeyLight: Dynamic Lighting unavailable on this system"); + return; + } + + // + // Paint loop: 100 ms cadence, repaint only on change + // + std::vector lastColors; + int waited = 0; + while (gRunning.load()) + { + Sleep(50); + waited += 50; + if (waited < 100) + { + continue; + } + waited = 0; + + std::vector map; + unsigned char lamps[64]; + { + std::lock_guard hold(gLock); + map = gMap; + memcpy(lamps, gLamps, sizeof(lamps)); + } + + std::vector colors(map.size()); + std::vector keys(map.size()); + int bestLevel = 0; + bool bestYellow = false; + bool changed = (lastColors.size() != map.size()); + for (size_t i = 0; i < map.size(); ++i) + { + int address = map[i].address; + int level = (address >= 0 && address < 64) + ? LampLevel(lamps[address]) : 0; + if (level > bestLevel) + { + bestLevel = level; + bestYellow = map[i].yellow; + } + colors[i] = Shade(level, map[i].yellow); + keys[i] = (VirtualKey) map[i].virtualKey; + if (!changed && !SameColor(colors[i], lastColors[i])) + { + changed = true; + } + } + + std::lock_guard hold(claimedLock); + bool freshClaim = false; + for (ClaimedArray &entry : claimed) + { + if (!entry.baseCoated) + { + freshClaim = true; + } + } + if (!changed && !freshClaim) + { + continue; + } + + Color aggregate = Shade(bestLevel, bestYellow); + for (ClaimedArray &entry : claimed) + { + try + { + if (entry.perKey) + { + if (!entry.baseCoated) + { + Color black; + black.A = 255; black.R = 0; black.G = 0; black.B = 0; + entry.array.SetColor(black); + entry.baseCoated = true; + } + if (!map.empty()) + { + entry.array.SetColorsForKeys( + array_view(colors.data(), colors.data() + colors.size()), + array_view(keys.data(), keys.data() + keys.size())); + } + } + else + { + entry.baseCoated = true; + entry.array.SetColor(aggregate); + } + } + catch (...) + { + // device wobble; the watcher handles removal + } + } + lastColors = colors; + } + + // + // Releasing the arrays hands the LEDs back to Windows + // + try + { + watcher.Stop(); + } + catch (...) + { + } + { + std::lock_guard hold(claimedLock); + claimed.clear(); + } + if (!anySeen) + { + Log("KeyLight: no Dynamic Lighting keyboard was found this session"); + } + } +} + +//######################################################################## +// The scalar interface (safe across the packing boundary) +//######################################################################## + +void + KeyLight_SetLogger(void (*logger)(const char *line)) +{ + std::lock_guard hold(gLock); + gLogger = logger; +} + +void + KeyLight_SetMap( + const int *virtual_keys, + const int *addresses, + const unsigned char *yellow, + int count + ) +{ + std::lock_guard hold(gLock); + gMap.clear(); + gMap.reserve(count); + for (int i = 0; i < count; ++i) + { + KeyEntry entry; + entry.virtualKey = virtual_keys[i]; + entry.address = addresses[i]; + entry.yellow = (yellow[i] != 0); + gMap.push_back(entry); + } +} + +void + KeyLight_UpdateLamps(const unsigned char *lamp_state, int count) +{ + if (count > 64) + { + count = 64; + } + std::lock_guard hold(gLock); + memcpy(gLamps, lamp_state, count); +} + +void + KeyLight_Start() +{ + if (gRunning.exchange(true)) + { + return; + } + gWorker = std::thread(Worker); +} + +void + KeyLight_Stop() +{ + if (!gRunning.exchange(false)) + { + return; + } + if (gWorker.joinable()) + { + gWorker.join(); + } +} diff --git a/MUNGA_L4/L4KEYLIGHT.h b/MUNGA_L4/L4KEYLIGHT.h new file mode 100644 index 0000000..2476d0d --- /dev/null +++ b/MUNGA_L4/L4KEYLIGHT.h @@ -0,0 +1,53 @@ +//===========================================================================// +// File: l4keylight.h // +// Project: MUNGA Brick: RGB keyboard lamp mirror // +// Contents: Windows Dynamic Lighting bridge (vRIO's KeyboardLampMirror) // +//---------------------------------------------------------------------------// +// Copyright (C) 1994-1995, Virtual World Entertainment, Inc. // +// PROPRIETARY AND CONFIDENTIAL // +//===========================================================================// + +#pragma once + +//######################################################################## +// Mirrors the game-commanded RIO lamp states onto per-key RGB keyboards +// through Windows Dynamic Lighting (the WinRT LampArray API), following +// vRIO's KeyboardLampMirror: keys bound to lamp addresses glow with the +// panel palette (red for the banks, yellow for the Secondary/Screen +// columns), flash modes blink at the panel's rates, other keys are +// blacked out so the keyboard reads as the button field. Zone-lit +// keyboards mirror the strongest lamp board-wide. Dynamic Lighting +// grants LED control to the FOREGROUND app - which is the game itself, +// so no Windows settings dance is needed. +// +// The implementation is C++/WinRT on a private worker thread, compiled +// with default struct packing (the engine builds /Zp1, which would +// break the WinRT ABI) - hence this interface is scalars only. All +// functions are safe to call when Dynamic Lighting is unavailable; +// failures log once and the mirror stays dormant. +//######################################################################## + +// Where the mirror's status lines go (the caller owns the sink). +void + KeyLight_SetLogger(void (*logger)(const char *line)); + +// The key map: parallel arrays of virtual keys, their RIO lamp +// addresses (0x00-0x47), and whether each paints yellow (Secondary / +// Screen columns) instead of red. +void + KeyLight_SetMap( + const int *virtual_keys, + const int *addresses, + const unsigned char *yellow, + int count + ); + +// Latest game-commanded lamp bytes (indexed by RIO lamp number). +void + KeyLight_UpdateLamps(const unsigned char *lamp_state, int count); + +// Claim keyboards and start painting / release the LEDs to Windows. +void + KeyLight_Start(); +void + KeyLight_Stop(); diff --git a/MUNGA_L4/L4PADRIO.cpp b/MUNGA_L4/L4PADRIO.cpp index fb70749..47d3b34 100644 --- a/MUNGA_L4/L4PADRIO.cpp +++ b/MUNGA_L4/L4PADRIO.cpp @@ -2,6 +2,7 @@ #pragma hdrstop #include "l4padrio.h" +#include "l4keylight.h" #include #pragma comment(lib, "xinput9_1_0.lib") @@ -39,6 +40,11 @@ namespace { return (GetAsyncKeyState(virtual_key) & 0x8000) != 0; } + + void KeyLightLog(const char *line) + { + DEBUG_STREAM << line << "\n" << std::flush; + } } //######################################################################## @@ -89,6 +95,55 @@ PadRIO::PadRIO() PadBindings_Load(&profile); + // + // RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound + // to lamp addresses glow with the panel. Yellow = the Secondary / + // Screen columns (0x10-0x1F), red = everything else, exactly like + // the physical panel and vRIO. RP412KEYLIGHT=0 opts out. + // + keyLightActive = False; + const char *keylight = getenv("RP412KEYLIGHT"); + if (keylight == NULL || atoi(keylight) != 0) + { + int light_keys[PadBindingProfile::maxKeyButtons]; + int light_addresses[PadBindingProfile::maxKeyButtons]; + unsigned char light_yellow[PadBindingProfile::maxKeyButtons]; + int light_count = 0; + for (int i = 0; i < profile.keyButtonCount; ++i) + { + int address = profile.keyButtons[i].address; + if (address >= buttonUnits) + { + continue; // keypads have no lamps + } + Logical duplicate = False; + for (int j = 0; j < light_count; ++j) + { + if (light_keys[j] == profile.keyButtons[i].virtualKey) + { + duplicate = True; // first binding wins + break; + } + } + if (duplicate) + { + continue; + } + light_keys[light_count] = profile.keyButtons[i].virtualKey; + light_addresses[light_count] = address; + light_yellow[light_count] = + (address >= 0x10 && address <= 0x1F) ? 1 : 0; + ++light_count; + } + if (light_count > 0) + { + KeyLight_SetLogger(&KeyLightLog); + KeyLight_SetMap(light_keys, light_addresses, light_yellow, light_count); + KeyLight_Start(); + keyLightActive = True; + } + } + invertX = False; invertY = False; const char *flip = getenv("L4PADFLIP"); @@ -116,6 +171,11 @@ PadRIO::PadRIO() PadRIO::~PadRIO() { Check_Pointer(this); + if (keyLightActive) + { + KeyLight_Stop(); + keyLightActive = False; + } if (activeInstance == this) { activeInstance = NULL; @@ -169,6 +229,10 @@ void JoystickY = (Scalar) 0; analogRequested = True; memset(lampState, 0, sizeof(lampState)); + if (keyLightActive) + { + KeyLight_UpdateLamps(lampState, lampCount); + } memset(keypadDown, 0, sizeof(keypadDown)); for (int i = 0; i < profile.keyButtonCount; ++i) { @@ -198,6 +262,10 @@ void if (lampNumber >= 0 && lampNumber < lampCount) { lampState[lampNumber] = (unsigned char) state; + if (keyLightActive) + { + KeyLight_UpdateLamps(lampState, lampCount); + } } } diff --git a/MUNGA_L4/L4PADRIO.h b/MUNGA_L4/L4PADRIO.h index 06ec8b1..955d355 100644 --- a/MUNGA_L4/L4PADRIO.h +++ b/MUNGA_L4/L4PADRIO.h @@ -113,6 +113,8 @@ protected: throttleAccum; Logical invertX, invertY; + Logical + keyLightActive; // Dynamic Lighting keyboard mirror running Scalar sentThrottle, sentLeftPedal, sentRightPedal, diff --git a/MUNGA_L4/Munga_L4.vcxproj b/MUNGA_L4/Munga_L4.vcxproj index 806314f..30ccc86 100644 --- a/MUNGA_L4/Munga_L4.vcxproj +++ b/MUNGA_L4/Munga_L4.vcxproj @@ -254,6 +254,14 @@ + + + stdcpp17 + Default + true + @@ -444,6 +452,7 @@ +