RGB keyboard lamp mirror + the shipped controls reference

KEYBOARD LAMP MIRROR (L4KEYLIGHT).  Ported from RP412, itself a port of vRIO's
KeyboardLampMirror.  Keys bound to a lamp address in bindings.txt glow with the
panel palette through Windows Dynamic Lighting -- yellow for the map's side
columns (0x10-0x1F), red for the rest -- flashing in step with the on-screen
buttons (its LampLevel copy matches the FIXED BTLampBrightnessOf).  Per-key
boards light each bound key; zone-lit boards mirror the strongest lamp
board-wide.  All WinRT runs on a private worker thread: watcher, claim, and a
100 ms paint loop that repaints only on change.

RP412's packing hazard does NOT transfer: it forces default struct packing there
because its engine is /Zp1, which would break the WinRT ABI.  BT411's BT_OPTS
carries no /Zp, so only the dialect flags are needed -- /std:c++17
/permissive- per-file, since the project otherwise builds C++14 /permissive.
The scalars-only interface is kept anyway so the isolation survives if packing
is ever added.

Wired in PadRIO: map built from bindings.keyBindings (ActionButton binds only,
first-binding-wins per key), fed from PadRIO::SetLamp, stopped in the dtor
(which hands the LEDs back to Windows).  BT_KEYLIGHT=0 opts out; a machine
without Dynamic Lighting logs once and stays dormant.

Verified live: claimed this machine's 24-zone keyboard and mirrors 25 bound
keys; BT_KEYLIGHT=0 and the pod profile produce zero keylight lines and run
clean.

SHIPPED CONTROLS REFERENCE.  docs/CONTROLS.md carries the keyboard table plus
the full 72-BUTTON POD MAP, grouped by the display each bank surrounds, with the
coolant-valve detent warning (1-5-50-CLOSED, one press past max shuts the loop)
and the jam/eject procedure.  mkdist flattens it to ASCII as CONTROLS.txt at the
zip root, the README's own idiom.  players/README.txt gained pointers to it, to
environ.ini, to -fit, and to the RGB mirror.

KB: context/glass-cockpit.md and docs/GLASS_COCKPIT.md updated for the whole
branch -- layout modes, L4RIOBANK and the under-glass rule, the letterbox fit
and its ordering trap, player-tunable displays, the measured legend grid, the
closed dead-button backlog, and this mirror.

Verified: five-mode regression (surround/exploded/dock/pod/dev) boots and
simulates with zero faults; mkdist writes a 5149-byte pure-ASCII CONTROLS.txt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-26 02:24:30 -05:00
co-authored by Claude Opus 5
parent 1debbeb240
commit 02b1b50e45
10 changed files with 1167 additions and 7 deletions
+90
View File
@@ -12,6 +12,7 @@
#include "l4glasswin.h"
#include "l4ctrl.h"
#include "l4joy.h"
#include "l4keylight.h" // RGB keyboard lamp mirror (scalars-only interface)
#include <windows.h>
#include <xinput.h>
@@ -63,6 +64,17 @@ int
// XInput normalization: thumbs to -1..1 past the stock deadzone, triggers
// to 0..1 past the stock threshold.
//
//
// The lamp mirror's log sink. It cannot use DEBUG_STREAM itself: its TU
// takes no engine headers (l4keylight.h is scalars only), so it hands us
// finished lines instead.
//
static void
KeyLightLog(const char *line)
{
DEBUG_STREAM << "[keylight] " << (line ? line : "") << "\n" << std::flush;
}
static float
NormalizeThumb(int value, int dead_zone)
{
@@ -170,6 +182,67 @@ PadRIO::PadRIO():
flipStickAxes =
(getenv("L4PADFLIP") != NULL && *getenv("L4PADFLIP") != '0');
//
// RGB KEYBOARD LAMP MIRROR (Windows Dynamic Lighting, l4keylight.h):
// every key bound to a lamp ADDRESS glows with the panel palette --
// yellow for the Secondary/Screen columns (0x10-0x1F), red for the rest,
// exactly like the on-screen buttons. Keypad binds have no lamp, so
// they are skipped; a key bound twice takes its FIRST binding.
// BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once
// and stays dormant.
//
keyLightActive = False;
{
const char *gate = getenv("BT_KEYLIGHT");
if (gate == NULL || atoi(gate) != 0)
{
static int lightKeys[192];
static int lightAddresses[192];
static unsigned char lightYellow[192];
int count = 0;
for (int k = 0; k < bindings.keyBindingCount && count < 192; ++k)
{
const PadBindingProfile::KeyBinding &binding = bindings.keyBindings[k];
if (binding.action.kind != PadBindingProfile::ActionButton)
{
continue; // axes/keypads carry no lamp
}
int address = binding.action.address;
if (address < 0 || address >= LampCount)
{
continue;
}
Logical duplicate = False;
for (int j = 0; j < count; ++j)
{
if (lightKeys[j] == binding.virtualKey)
{
duplicate = True; // first binding wins
break;
}
}
if (duplicate)
{
continue;
}
lightKeys[count] = binding.virtualKey;
lightAddresses[count] = address;
lightYellow[count] = (unsigned char)
((address >= 0x10 && address <= 0x1F) ? 1 : 0);
++count;
}
if (count > 0)
{
KeyLight_SetLogger(KeyLightLog);
KeyLight_SetMap(lightKeys, lightAddresses, lightYellow, count);
KeyLight_Start();
keyLightActive = True;
DEBUG_STREAM << "[keylight] mirroring " << count
<< " bound key(s) onto RGB keyboards\n" << std::flush;
}
}
}
//
// Never revision 0.0 -- some diagnostics print it; give the synthetic
// board a recognizable version.
@@ -199,6 +272,11 @@ PadRIO::PadRIO():
PadRIO::~PadRIO()
{
if (keyLightActive)
{
KeyLight_Stop(); // hands the LEDs back to Windows
keyLightActive = False;
}
BTGlassPanels_Destroy(); // safe no-op if the glass windows were never created
BTPadPanel_Destroy();
if (activeInstance == this)
@@ -1038,6 +1116,18 @@ void
if (lampNumber >= 0 && lampNumber < LampCount)
{
lampState[lampNumber] = state;
if (keyLightActive)
{
// The mirror keeps its own copy (its paint loop runs on a private
// thread and must not reach into the device); the first 64
// addresses are the whole lamp field.
unsigned char bytes[64];
for (int i = 0; i < 64; ++i)
{
bytes[i] = (unsigned char) lampState[i];
}
KeyLight_UpdateLamps(bytes, 64);
}
}
}