emulator: RIO panel click-to-press + lamp flash blink

Make the in-fork glass cockpit fully interactive and finish the lamp model.

- Click-to-press: clicking a button's region in a VDB head window presses that
  RIO address into the game (lights white-hot while held, releases on mouse-up).
  vpxlog rt_wndproc hit-tests the click against the bezel geometry (MFD 4 top /
  4 bottom, radar 6 left / 6 right; the radar bottom indicators are display-only,
  not clickable); head windows are tagged via GWLP_USERDATA. Clicks arrive on the
  VPX render thread, so the seam serialrio RIO_HostButton() queues them under a
  mutex and pollInput() drains them on the emu tick through the same incHold/
  decHold path as the keyboard/pad -- so a click also lights the bezel.
- Flash blink: decode the lamp byte's flash bits (RioLampState 1 slow / 2 med /
  3 fast) -- an unpressed flashing lamp now toggles between its brightness level
  and off (half-periods 500/250/125ms); solid lamps unchanged, a press still wins.
- Focus: head windows are WS_EX_NOACTIVATE so a button click never pulls
  foreground off DOSBox (which would demote the emu thread and stop SDL polling
  the pad); a click uses MA_NOACTIVATE too. Do NOT enable SDL background joystick
  events -- that raced the emu-thread controller poll against SDL's main-thread
  pump and crashed the process.

Explode layout only, and only when a serial=rio port is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-24 11:50:53 -05:00
co-authored by Claude Opus 4.8
parent f2424e9fdc
commit 9dd0aa8702
4 changed files with 125 additions and 5 deletions
+6 -2
View File
@@ -12,8 +12,12 @@ version control because the DOSBox-X source tree itself
bottom, tucked under a 640x500 display so a 10px lip shows) and the radar
with the amber Secondary/Screen side columns + a centered bottom indicator
strip, lit from `serialrio`'s `RIO_GetPanelState` (off/dim/bright + white-hot
press). No labels -- the display shows each button's function. No-op when no
`serial=rio` port is present.
press; flashing lamps blink at the RioLampState rate). No labels -- the
display shows each button's function. **Click-to-press:** `rt_wndproc`
hit-tests a click against the bezel and presses that RIO address (thread-safe
via serialrio's queued `RIO_HostButton`); head windows are `WS_EX_NOACTIVATE`
so a click never pulls foreground off DOSBox (which would demote the emu
thread and stop the SDL pad poll). No-op when no `serial=rio` port is present.
- **`vweawe.cpp`** — the dual-AWE32 sound device: two vendored EMU8000
cores at 0x620/0x640 (+0x400/+0x800 triplets), rear-card DSP/mixer stub
at 0x240, autonomous render thread with direct winmm output. Needs the
+26
View File
@@ -217,6 +217,10 @@ bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]) {
return true;
}
void RIO_HostButton(int addr, bool down) {
if (rio_instance) rio_instance->hostButton(addr, down);
}
// ============================================================================
CSerialRio::CSerialRio(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
@@ -584,6 +588,13 @@ void CSerialRio::getPanelState(uint8_t lampsOut[72], uint8_t pressedOut[72]) {
}
}
// panel click from the VPX render thread -- queue it; pollInput() (emu thread)
// drains and applies it, so it composes with the keyboard/pad hold counts.
void CSerialRio::hostButton(int addr, bool down) {
std::lock_guard<std::mutex> lk(panelMx);
panelQ.push_back(std::make_pair(addr, down));
}
void CSerialRio::releaseAllKeys() {
std::set<int> keys = heldKeys;
heldKeys.clear();
@@ -781,6 +792,11 @@ void CSerialRio::applyReset(uint8_t target) {
// ---- gamepad + axis input ----------------------------------------------------
void CSerialRio::openPad() {
// NOTE: do NOT enable SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS here -- it
// makes SDL's main-thread event pump update the joystick concurrently with
// our emu-thread SDL_GameControllerUpdate (not thread-safe -> process crash,
// 2026-07-24). Keeping DOSBox focused (the head windows are WS_EX_NOACTIVATE
// so a click can't steal foreground) is what keeps the pad polling.
SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);
int n = SDL_NumJoysticks();
for (int i = 0; i < n; i++) {
@@ -815,6 +831,16 @@ void CSerialRio::pollInput() {
if (dt < 0.0) dt = 0.0;
if (dt > 0.1) dt = 0.1;
// apply panel clicks queued by the VPX window thread (RIO_HostButton) --
// same hold-count path as the keyboard/pad, so a held click lights the bezel
std::vector<std::pair<int,bool>> clicks;
{ std::lock_guard<std::mutex> lk(panelMx); clicks.swap(panelQ); }
for (size_t i = 0; i < clicks.size(); i++) {
if (clicks[i].second) incHold(clicks[i].first);
else decHold(clicks[i].first);
logf("panel %s 0x%02X", clicks[i].second ? "click" : "release", clicks[i].first);
}
// gamepad snapshot; everything rests at zero when absent/detached, so a
// detach releases whatever the pad held (button edges below see btns=0)
float src[6] = { 0, 0, 0, 0, 0, 0 };
+10
View File
@@ -60,8 +60,10 @@
#include <deque>
#include <map>
#include <mutex>
#include <set>
#include <string>
#include <utility>
#include <vector>
// ---- host-side hooks (called from sdlmain's SDL2 event loop) ----------------
@@ -78,6 +80,11 @@ void RIO_HostFocusLost(void);
// serial<n>=rio port is installed, so the VDB heads render normally.
bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]);
// A panel button was clicked (down=true) or released in a VDB head window.
// Called from the VPX render thread (rt_wndproc), so it is thread-safe: the
// event is queued and applied on the emulator's next tick. No-op if no rio port.
void RIO_HostButton(int addr, bool down);
// ---- binding profile (transcribed from vRIO BindingProfile) -----------------
// key-axis modes: DEFLECT holds the axis at value while the key is down and
// springs back; RATE ("slew" in bindings files) walks the axis by value/second
@@ -106,6 +113,7 @@ public:
bool hostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
void hostFocusLost();
void getPanelState(uint8_t lampsOut[72], uint8_t pressedOut[72]);
void hostButton(int addr, bool down); // panel click (VPX thread -> queue)
private:
// ---- board -> game byte delivery (UART RX pacing; mirrors directserial) --
@@ -168,6 +176,8 @@ private:
int last_sent[5] = {};
bool last_sent_valid[5] = {};
std::map<int,int> holdCounts; // held count per RIO address
std::mutex panelMx; // guards panelQ (VPX thread <-> emu)
std::vector<std::pair<int,bool>> panelQ; // pending panel clicks (addr, down)
std::set<int> heldKeys; // scancodes routed to the panel
std::set<int> toggled; // latched (toggle) addresses
uint16_t prevPadButtons = 0;
+83 -3
View File
@@ -1343,14 +1343,25 @@ static int mfd_hi_for_g(int g) {
return -1;
}
/* Flash phase for a flashing lamp (RioLampState bits 0-1: 1 slow / 2 med / 3
* fast; 0 = solid = always on). Toggles on/off at half-periods 500/250/125ms
* off the wall clock, so a flashing lamp blinks between its level and off. */
static bool rio_flash_on(int flash) {
if (flash == 0) return true;
unsigned int hp = flash == 1 ? 500u : flash == 2 ? 250u : 125u;
return ((GetTickCount() / hp) & 1u) == 0u;
}
/* Panel button color at the host lamp level (vRIO RioLampState: brighter of the
* two brightness fields, bits 2-3 = 0x0C bright/0x04 dim, bits 4-5 = 0x30 bright/
* 0x10 dim). MFD buttons are red, radar Secondary/Screen buttons are amber. A
* press shows white-hot so it's visible over any level. */
* 0x10 dim; bits 0-1 = flash mode). MFD buttons are red, radar Secondary/Screen
* buttons are amber. A press shows white-hot so it's visible over any level. */
static void rio_button_color(uint8_t ls, bool pr, bool amber, float *r, float *g, float *b) {
int f1 = ls & 0x0C, f2 = ls & 0x30;
int level = (f1 == 0x0C || f2 == 0x30) ? 2 : (f1 || f2) ? 1 : 0;
if (pr) { *r = 1.00f; *g = 0.90f; *b = 0.62f; return; } /* pressed white-hot */
int flash = ls & 0x03;
if (flash != 0 && !rio_flash_on(flash)) level = 0; /* flashing, off phase */
if (pr) { *r = 1.00f; *g = 0.90f; *b = 0.62f; return; } /* pressed white-hot (press wins) */
if (amber) {
if (level == 2) { *r = 1.00f; *g = 0.72f; *b = 0.05f; } /* bright amber */
else if (level == 1) { *r = 0.50f; *g = 0.36f; *b = 0.02f; } /* dim amber */
@@ -1717,8 +1728,69 @@ static void pal_draw(HDC dc, int g, int cw, int ch, bool dump) {
SwapBuffers(dc);
}
/* ---- click-to-press: map a click in a head window to a RIO button ----------
* Mirrors the bezel geometry (draw_mfd_bezel / draw_radar_bezel): returns the
* RIO address under (mx,my) in client coords, or -1 for a non-button click.
* The radar bottom indicators (spares) are display-only, not clickable. */
extern void RIO_HostButton(int addr, bool down);
static int rio_hit_test(int g, int cw, int ch, int mx, int my) {
int hi = mfd_hi_for_g(g);
if (hi >= 0) { /* MFD head: 4 top / 4 bottom */
int col = (cw > 0) ? mx * 4 / cw : 0;
if (col < 0) col = 0;
else if (col > 3) col = 3;
if (my >= 0 && my < RIO_BEZEL_BTN) return hi - col; /* top row (window top) */
if (my >= ch - RIO_BEZEL_BTN && my < ch) return hi - 4 - col; /* bottom row */
return -1;
}
if (g == 0) { /* radar: 6 left / 6 right */
const int BTN_H = 104, GAP = 3, N = 6, BTN_W = 100;
int side = -1;
if (mx >= 0 && mx < BTN_W) side = 0; /* left = Secondary 0x10.. */
else if (mx >= cw - BTN_W && mx < cw) side = 1; /* right = Screen 0x18.. */
if (side < 0) return -1;
int stack = N * BTN_H + (N - 1) * GAP;
int margin = ((ch - RIO_RADAR_BOT) - stack) / 2;
int rel = my - margin;
if (rel < 0) return -1;
int i = rel / (BTN_H + GAP);
if (i < 0 || i >= N) return -1;
if (rel - i * (BTN_H + GAP) >= BTN_H) return -1; /* landed in the 3px gap */
return (side == 0 ? 0x10 : 0x18) + i;
}
return -1;
}
static int g_click_addr[10] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; /* held addr per head */
static LRESULT CALLBACK rt_wndproc(HWND w, UINT msg, WPARAM wp, LPARAM lp) {
if (msg == WM_CLOSE) { ShowWindow(w, SW_MINIMIZE); return 0; }
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE; /* click a button, don't steal DOSBox focus */
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONUP) {
int tag = (int)(LONG_PTR)GetWindowLongPtrA(w, GWLP_USERDATA); /* head windows store g+1 */
int g = tag - 1;
if (g >= 0 && g < 10 && pal_radar_cw) { /* bezel exists only in explode layout */
if (msg == WM_LBUTTONDOWN) {
RECT cr; GetClientRect(w, &cr);
int addr = rio_hit_test(g, cr.right, cr.bottom,
(short)LOWORD(lp), (short)HIWORD(lp));
if (addr >= 0) {
if (g_click_addr[g] >= 0) RIO_HostButton(g_click_addr[g], false);
g_click_addr[g] = addr;
RIO_HostButton(addr, true);
SetCapture(w);
}
} else { /* WM_LBUTTONUP */
if (g_click_addr[g] >= 0) {
RIO_HostButton(g_click_addr[g], false);
g_click_addr[g] = -1;
ReleaseCapture();
}
}
return 0;
}
}
return DefWindowProcA(w, msg, wp, lp);
}
@@ -1860,6 +1932,14 @@ static DWORD WINAPI rt_main(LPVOID) {
env_rect(en, &x, &y, &w, &h);
phave[g] = make_gl_window(pal_titles[g], w, h, x, y, cockpit,
&pwnd[g], &pdc[g], &pgl[g]);
if (phave[g]) {
/* tag with g+1 so rt_wndproc maps a click to this head (click-to-press) */
SetWindowLongPtrA(pwnd[g], GWLP_USERDATA, (LONG_PTR)(g + 1));
/* WS_EX_NOACTIVATE: clicking a button never pulls foreground off
* DOSBox (else the emu thread demotes and SDL stops polling the pad). */
SetWindowLongPtrA(pwnd[g], GWL_EXSTYLE,
GetWindowLongPtrA(pwnd[g], GWL_EXSTYLE) | WS_EX_NOACTIVATE);
}
}
VFrame cur;