RGB keyboard lamp mirror: vRIO Dynamic Lighting port, live in-game
The polish-backlog item, implemented from vRIO KeyboardLampMirror: game-commanded RIO lamp states paint per-key RGB keyboards through Windows Dynamic Lighting (WinRT LampArray). Keys bound to lamp addresses in the active bindings profile glow with the panel palette (red banks, yellow Secondary/Screen columns), flash modes use the exact L4MFDVIEW formula so keyboard and on-screen buttons blink in step, unbound keys are blacked out so the board reads as the button field, and zone-lit keyboards fall back to a board-wide mirror of the strongest lamp. Advantage over vRIO: Dynamic Lighting grants LEDs to the FOREGROUND app - which is the game - so no Windows settings dance. Isolation: L4KEYLIGHT.cpp compiles /std:c++17 + DEFAULT packing + conformance (per-file vcxproj settings; the engine /Zp1 would break the WinRT ABI) with a scalars-only interface, and all WinRT work runs on a private worker thread (watcher, claiming, 100ms paint loop). On by default with a bindings map present; RP412KEYLIGHT=0 opts out; missing Dynamic Lighting logs once and stays dormant. Verified live on the dev laptop: claimed its 24-zone keyboard (board-wide mirror) during a race; race cycling with per-race start/stop of the mirror thread stays green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
#include <winrt/base.h>
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.Devices.Enumeration.h>
|
||||
#include <winrt/Windows.Devices.Lights.h>
|
||||
#include <winrt/Windows.System.h>
|
||||
#include <winrt/Windows.UI.h>
|
||||
|
||||
#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<KeyEntry> gMap;
|
||||
unsigned char gLamps[64] = {};
|
||||
void (*gLogger)(const char *) = nullptr;
|
||||
|
||||
std::atomic<bool> gRunning{ false };
|
||||
std::thread gWorker;
|
||||
|
||||
void Log(const char *line)
|
||||
{
|
||||
void (*logger)(const char *);
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<ClaimedArray> 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<std::mutex> 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<std::mutex> 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<Color> lastColors;
|
||||
int waited = 0;
|
||||
while (gRunning.load())
|
||||
{
|
||||
Sleep(50);
|
||||
waited += 50;
|
||||
if (waited < 100)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
waited = 0;
|
||||
|
||||
std::vector<KeyEntry> map;
|
||||
unsigned char lamps[64];
|
||||
{
|
||||
std::lock_guard<std::mutex> hold(gLock);
|
||||
map = gMap;
|
||||
memcpy(lamps, gLamps, sizeof(lamps));
|
||||
}
|
||||
|
||||
std::vector<Color> colors(map.size());
|
||||
std::vector<VirtualKey> 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<std::mutex> 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<Color const>(colors.data(), colors.data() + colors.size()),
|
||||
array_view<VirtualKey const>(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<std::mutex> 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<std::mutex> hold(gLock);
|
||||
gLogger = logger;
|
||||
}
|
||||
|
||||
void
|
||||
KeyLight_SetMap(
|
||||
const int *virtual_keys,
|
||||
const int *addresses,
|
||||
const unsigned char *yellow,
|
||||
int count
|
||||
)
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<std::mutex> 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma hdrstop
|
||||
|
||||
#include "l4padrio.h"
|
||||
#include "l4keylight.h"
|
||||
|
||||
#include <XInput.h>
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,8 @@ protected:
|
||||
throttleAccum;
|
||||
Logical
|
||||
invertX, invertY;
|
||||
Logical
|
||||
keyLightActive; // Dynamic Lighting keyboard mirror running
|
||||
|
||||
Scalar
|
||||
sentThrottle, sentLeftPedal, sentRightPedal,
|
||||
|
||||
@@ -254,6 +254,14 @@
|
||||
<ClCompile Include=".\L4STEAMTRANSPORT.cpp" />
|
||||
<ClCompile Include=".\L4PARTICLES.cpp" />
|
||||
<ClCompile Include=".\L4MFDVIEW.cpp" />
|
||||
<!-- Dynamic Lighting mirror: C++/WinRT needs C++17, conformance mode,
|
||||
and DEFAULT packing (the engine's /Zp1 would break the WinRT ABI).
|
||||
Its l4keylight.h interface is scalars-only for exactly that reason. -->
|
||||
<ClCompile Include=".\L4KEYLIGHT.cpp">
|
||||
<LanguageStandard>stdcpp17</LanguageStandard>
|
||||
<StructMemberAlignment>Default</StructMemberAlignment>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
</ClCompile>
|
||||
<ClCompile Include=".\L4PADBINDINGS.cpp" />
|
||||
<ClCompile Include=".\L4PADRIO.cpp" />
|
||||
<ClCompile Include=".\L4PCSPAK.cpp" />
|
||||
@@ -444,6 +452,7 @@
|
||||
<ClInclude Include=".\L4MPPR.h" />
|
||||
<ClInclude Include=".\L4NET.H" />
|
||||
<ClInclude Include=".\L4NETTRANSPORT.h" />
|
||||
<ClInclude Include=".\L4KEYLIGHT.h" />
|
||||
<ClInclude Include=".\L4PADBINDINGS.h" />
|
||||
<ClInclude Include=".\L4STEAMTRANSPORT.h" />
|
||||
<ClInclude Include=".\L4PARTICLES.h" />
|
||||
|
||||
Reference in New Issue
Block a user