emulator: B2 RIO lamp/button bezel on the VDB heads + explode-view polish

Draw the cockpit RIO buttons around the mono-MFD and radar heads (explode
layout), lit by the host-commanded lamp state -- the display side of the
in-fork glass cockpit. Live-validated 2026-07-24 (operator).

- vpxlog.cpp: per-head button bezel in pal_draw, reading serialrio's new
  RIO_GetPanelState seam. The 5 MFD heads get red buttons (4 top / 4 bottom,
  100px tall tucked under a grown 640x500 display so a 10px lip shows); the
  radar gets amber Secondary/Screen side columns (6x 104px each) plus a
  centered bottom indicator strip (the 4 spares). Lamp byte decoded to
  off/dim/bright (vRIO RioLampState, brighter of the two brightness fields);
  a press shows white-hot. No labels -- the MFD shows each button's function.
  Also swapped the two upper-outer MFD window NAMES to match their (already
  position-swapped) desktop locations.
- serialrio.{cpp,h}: RIO_GetPanelState(lamps, pressed) accessor -- returns
  false when no rio port, so non-rio configs and other heads render normally.
- pod-launch: in explode (dev) layout the Division bridge is a normal, freely
  movable window -- not pinned topmost (Focus.cs) and nudged to 8,40 so its
  title bar clears the top of the screen (its client was at 0,0, pushing the
  frame off-screen). Cockpit/kiosk keeps the topmost + 0,0 borderless look.

Only affects the explode layout 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 10:43:41 -05:00
co-authored by Claude Opus 4.8
parent 11f0a082e3
commit 86d6b950e5
7 changed files with 234 additions and 17 deletions
+8 -4
View File
@@ -15,16 +15,19 @@ namespace VwePod
{
internal static class Focus
{
public static string Apply(int x, int y)
public static string Apply(int x, int y, bool pinRenderer)
{
var s = new StringBuilder();
// 1) renderer -> topmost
// 1) renderer Z-order. Kiosk/cockpit: pin it always-on-top so a head
// window never covers the out-the-window view. Explode (dev): leave
// it a normal window so the operator can freely move/restack it.
IntPtr r = FindRenderer();
if (r != IntPtr.Zero)
{
SetWindowPos(r, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
s.Append("renderer topmost; ");
SetWindowPos(r, pinRenderer ? HWND_TOPMOST : HWND_NOTOPMOST,
0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
s.Append(pinRenderer ? "renderer topmost; " : "renderer movable; ");
}
else s.Append("renderer NOT found; ");
@@ -69,6 +72,7 @@ namespace VwePod
// ---- native ----
private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
private const uint SWP_NOSIZE = 0x0001, SWP_NOMOVE = 0x0002, SWP_NOZORDER = 0x0004;
private const int SW_RESTORE = 9;
+8 -2
View File
@@ -51,7 +51,7 @@ namespace VwePod
var o = new Options();
string modeName = null, confArg = null, rootArg = null, dosboxArg = null,
bridgeScriptArg = null, rendererArg = null, aweArg = null;
bool dosboxXySet = false;
bool dosboxXySet = false, bridgePosSet = false;
for (int i = 0; i < args.Length; i++)
{
@@ -73,7 +73,7 @@ namespace VwePod
case "--renderer": rendererArg = Next(a); break;
case "--awe-rom": aweArg = Next(a); break;
case "--layout": o.Layout = Next(a).Equals("explode", StringComparison.OrdinalIgnoreCase) ? LayoutMode.Explode : LayoutMode.Cockpit; break;
case "--bridge-pos": o.BridgePos = Next(a); break;
case "--bridge-pos": o.BridgePos = Next(a); bridgePosSet = true; break;
case "--bridge-size": ParseWH(Next(a), out o.BridgeW, out o.BridgeH); break;
case "--dosbox-xy": ParseWH(Next(a), out o.DosBoxX, out o.DosBoxY); dosboxXySet = true; break;
case "--no-bridge": o.NoBridge = true; break;
@@ -101,6 +101,12 @@ namespace VwePod
// under the render window unless the caller placed it explicitly.
if (!dosboxXySet && o.Mode.Name == "test") { o.DosBoxX = 900; o.DosBoxY = 600; }
// explode (dev) layout: the bridge is a normal framed window, but at
// 0,0 SDL puts the CLIENT at 0,0 so the title bar sits off the top of
// the screen (unmovable). Nudge it on-screen (frame needs y >= 31)
// unless the caller placed it. Cockpit keeps 0,0 (kiosk, borderless-look).
if (!bridgePosSet && o.Layout == LayoutMode.Explode) o.BridgePos = "8,40";
o.DosBox = dosboxArg ?? FirstExisting(
Path.Combine(o.Root, "dosbox-x.exe"),
Path.Combine(o.Root, "src", "src", "dosbox-x.exe"))
+7 -5
View File
@@ -142,14 +142,16 @@ namespace VwePod
// TerminateProcess skips this, but the job still closes with us.)
Console.CancelKeyPress += (s, e) => { e.Cancel = false; job.Dispose(); };
// Window layout LAST so it wins over the child windows: renderer
// -> always-on-top; DOSBox -> parked + foreground (keeps the emu
// thread at foreground priority so the RIO ACK deadline isn't
// missed). Give the windows a moment to exist first.
// Window layout LAST so it wins over the child windows: DOSBox ->
// parked + foreground (keeps the emu thread at foreground priority
// so the RIO ACK deadline isn't missed). The renderer is pinned
// topmost only in cockpit (kiosk) layout; in explode (dev) it stays
// a normal, freely movable window. Give the windows a moment first.
if (!opt.NoFocus)
{
Thread.Sleep(3000);
Console.WriteLine("focus: " + Focus.Apply(opt.DosBoxX, opt.DosBoxY));
Console.WriteLine("focus: " + Focus.Apply(
opt.DosBoxX, opt.DosBoxY, opt.Layout == LayoutMode.Cockpit));
}
// Authoritative for the whole session: block until DOSBox exits.
+7 -1
View File
@@ -7,7 +7,13 @@ version control because the DOSBox-X source tree itself
- **`vpxlog.cpp`** — the Division VPX link adapter (INMOS C012 at I/O base
`0x150`), grown through Phase 3: iserver handshake responder, FIFO scene
decode, GL Division renderer (gallery shading model), VDB video-head
splitter windows.
splitter windows. **RIO lamp/button bezel (B2, 2026-07-24, explode layout):**
`pal_draw` rings each mono-MFD head with its 8 RIO buttons (red, 4 top / 4
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.
- **`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
+14
View File
@@ -211,6 +211,12 @@ void RIO_HostFocusLost(void) {
if (rio_instance) rio_instance->hostFocusLost();
}
bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]) {
if (!rio_instance) return false;
rio_instance->getPanelState(lamps, pressed);
return true;
}
// ============================================================================
CSerialRio::CSerialRio(Bitu id, CommandLine* cmd) : CSerial(id, cmd) {
@@ -570,6 +576,14 @@ void CSerialRio::hostFocusLost() {
releaseAllKeys();
}
// panel snapshot for the in-fork lamp bezel (vpxlog reads this each frame)
void CSerialRio::getPanelState(uint8_t lampsOut[72], uint8_t pressedOut[72]) {
for (int i = 0; i < 72; i++) {
lampsOut[i] = lamps[i];
pressedOut[i] = holdCounts.count(i) ? 1 : 0; // addr held right now
}
}
void CSerialRio::releaseAllKeys() {
std::set<int> keys = heldKeys;
heldKeys.clear();
+7
View File
@@ -72,6 +72,12 @@
bool RIO_HostKeyEvent(int sdl_scancode, bool pressed, bool repeat, unsigned int sdl_mods);
void RIO_HostFocusLost(void);
// Current panel state for the in-fork lamp/button bezel (B2): fills lamps[]
// with the host-commanded LampRequest byte and pressed[] with 1 where a button
// address is currently held. Returns false (and touches nothing) when no
// serial<n>=rio port is installed, so the VDB heads render normally.
bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]);
// ---- 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
@@ -99,6 +105,7 @@ public:
// host keyboard (forwarded by the RIO_Host* free functions)
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]);
private:
// ---- board -> game byte delivery (UART RX pacing; mirrors directserial) --
+183 -5
View File
@@ -1323,6 +1323,156 @@ static void write_bmp(const char *path, const unsigned char *rgb, int W, int H)
* encoded stream the VDB splits): win0 = bits 0-7 via pal0 = color radar;
* win3/win4 = bits 8-15 via pal1/pal2 = the two MFD heads (mono MFDs ride
* the individual R/G/B channel wires). g=1/2 (exploratory) no longer used. */
/* ---- in-fork RIO lamp/button bezel (B2) ---------------------------------
* The five mono-MFD heads (g=5..9, explode layout) render the RIO buttons that
* physically ring each MFD -- 4 across the top, 4 across the bottom -- lit by
* the host-commanded lamp state (and highlighted while pressed). Address map =
* vRIO CockpitLayout: each MFD is a 4x2 block whose top-left is 'hi', addresses
* descending row-major (top row hi..hi-3, bottom row hi-4..hi-7). The seam is
* serialrio's RIO_GetPanelState (no-op / returns false when no rio port). */
extern bool RIO_GetPanelState(uint8_t lamps[72], uint8_t pressed[72]);
static int mfd_hi_for_g(int g) {
switch (g) {
case 7: return 0x2F; /* upper-left MFD (AuxUpperLeft) */
case 8: return 0x27; /* upper-mid MFD (AuxUpperCenter) */
case 9: return 0x37; /* upper-right MFD (AuxUpperRight = target hotbox) */
case 5: return 0x0F; /* lower-left MFD (AuxLowerLeft) */
case 6: return 0x07; /* lower-right MFD (AuxLowerRight) */
}
return -1;
}
/* 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. */
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 */
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 */
else { *r = 0.16f; *g = 0.12f; *b = 0.02f; } /* off */
} else {
if (level == 2) { *r = 1.00f; *g = 0.12f; *b = 0.08f; } /* bright red */
else if (level == 1) { *r = 0.52f; *g = 0.05f; *b = 0.04f; } /* dim red */
else { *r = 0.16f; *g = 0.03f; *b = 0.03f; } /* off */
}
}
/* The buttons are drawn UNDER the display image, which is inset by RIO_BEZEL_LIP
* on the button edges -- so only the outer LIP of each button "sticks out" past
* the display edge (the lamp tip you can see), while the rest tucks behind the
* display's soft-key region (the future click zone). MFDs grow in height (480 +
* 2*LIP = 500), the radar in width (480 + 2*LIP = 500). */
#define RIO_BEZEL_LIP 10 /* protruding lip px, each side */
#define RIO_BEZEL_BTN 100 /* full MFD button height px (mostly hidden) */
#define RIO_RADAR_BOT 10 /* radar bottom indicator strip height px */
static void draw_mfd_bezel(int hi, int cw, int ch,
const uint8_t *lamps, const uint8_t *pressed) {
glViewport(0, 0, cw, ch);
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
glOrtho(0, cw, 0, ch, -1, 1); /* pixel space, y up */
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
glDisable(GL_TEXTURE_2D); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING);
const float bw = (float)cw / 4.0f;
for (int row = 0; row < 2; row++) {
/* top row butts up under the display top; bottom row under the bottom */
float y0 = (row == 0) ? (float)(ch - RIO_BEZEL_BTN) : 0.0f;
float y1 = (row == 0) ? (float)ch : (float)RIO_BEZEL_BTN;
for (int col = 0; col < 4; col++) {
int addr = hi - (row * 4 + col);
uint8_t ls = (addr >= 0 && addr < 72) ? lamps[addr] : 0;
bool pr = (addr >= 0 && addr < 72) && pressed[addr];
/* No labels -- the MFD display shows each button's function. */
float r, g_, b; rio_button_color(ls, pr, false /*red*/, &r, &g_, &b);
float x0 = col * bw + 3, x1 = (col + 1) * bw - 3;
float yy0 = y0, yy1 = y1; /* full height; lip is what shows */
glColor3f(r, g_, b);
glBegin(GL_QUADS);
glVertex2f(x0, yy0); glVertex2f(x1, yy0);
glVertex2f(x1, yy1); glVertex2f(x0, yy1);
glEnd();
glColor3f(0.40f, 0.10f, 0.10f); /* bezel outline */
glBegin(GL_LINE_LOOP);
glVertex2f(x0, yy0); glVertex2f(x1, yy0);
glVertex2f(x1, yy1); glVertex2f(x0, yy1);
glEnd();
}
}
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
}
/* Radar/secondary-screen bezel (explode only), amber buttons. The Secondary
* column (0x10-0x15) runs down the LEFT, the Screen column (0x18-0x1D) down the
* RIGHT -- 6 buttons each, 104px tall / 3px apart, 100px wide with 90px tucked
* behind the display so a 10px lip shows on the outer edge; the columns center
* in the display region above the bottom strip. The 4 spares (Secondary
* 0x16/0x17, Screen 0x1E/0x1F) are INDICATOR-only: a 10px strip along the
* bottom of the window. */
static void draw_amber_quad(float x0, float y0, float x1, float y1,
uint8_t ls, bool pr) {
float r, g_, b; rio_button_color(ls, pr, true /*amber*/, &r, &g_, &b);
glColor3f(r, g_, b);
glBegin(GL_QUADS);
glVertex2f(x0, y0); glVertex2f(x1, y0);
glVertex2f(x1, y1); glVertex2f(x0, y1);
glEnd();
glColor3f(0.42f, 0.30f, 0.05f);
glBegin(GL_LINE_LOOP);
glVertex2f(x0, y0); glVertex2f(x1, y0);
glVertex2f(x1, y1); glVertex2f(x0, y1);
glEnd();
}
static void draw_radar_bezel(int cw, int ch, const uint8_t *lamps, const uint8_t *pressed) {
static const int leftCol[6] = { 0x10, 0x11, 0x12, 0x13, 0x14, 0x15 }; /* Secondary top->bottom */
static const int rightCol[6] = { 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D }; /* Screen top->bottom */
static const int bottomInd[4] = { 0x16, 0x17, 0x1E, 0x1F }; /* spares (indicators) */
const int BTN_H = 104, GAP = 3, N = 6, BTN_W = 100;
float stack = (float)(N * BTN_H + (N - 1) * GAP);
float regionH = (float)(ch - RIO_RADAR_BOT); /* display region (above the strip) */
float margin = (regionH - stack) / 2.0f; /* center the stack in the display */
glViewport(0, 0, cw, ch);
glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity();
glOrtho(0, cw, 0, ch, -1, 1);
glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity();
glDisable(GL_TEXTURE_2D); glDisable(GL_DEPTH_TEST); glDisable(GL_LIGHTING);
/* side columns */
for (int side = 0; side < 2; side++) {
const int *col = side == 0 ? leftCol : rightCol;
float x0 = side == 0 ? 0.0f : (float)(cw - BTN_W); /* outer edge flush to window */
float x1 = x0 + (float)BTN_W;
for (int i = 0; i < N; i++) {
int addr = col[i];
uint8_t ls = (addr >= 0 && addr < 72) ? lamps[addr] : 0;
bool pr = (addr >= 0 && addr < 72) && pressed[addr];
float yy1 = (float)ch - (margin + (float)(i * (BTN_H + GAP))); /* GL top (y up) */
float yy0 = yy1 - (float)BTN_H;
draw_amber_quad(x0, yy0, x1, yy1, ls, pr);
}
}
/* bottom indicator strip: 4 spares, ~25% narrower than full width and
* centered (indicators only -- 0x16/0x17 Secondary, 0x1E/0x1F Screen). */
float rowW = 0.75f * (float)cw;
float rowX = ((float)cw - rowW) / 2.0f;
float igap = 8.0f;
float iw = (rowW - 3.0f * igap) / 4.0f;
for (int i = 0; i < 4; i++) {
int addr = bottomInd[i];
uint8_t ls = (addr >= 0 && addr < 72) ? lamps[addr] : 0;
bool pr = (addr >= 0 && addr < 72) && pressed[addr];
float x0 = rowX + (float)i * (iw + igap);
draw_amber_quad(x0, 0.0f, x0 + iw, (float)RIO_RADAR_BOT, ls, pr);
}
glMatrixMode(GL_PROJECTION); glPopMatrix();
glMatrixMode(GL_MODELVIEW); glPopMatrix();
}
static void pal_draw(HDC dc, int g, int cw, int ch, bool dump) {
const int W = 640, H = 480;
static unsigned char img[640 * 480 * 3];
@@ -1530,10 +1680,38 @@ static void pal_draw(HDC dc, int g, int cw, int ch, bool dump) {
snprintf(p, sizeof p, "%s/win%d.bmp", pal_dump_dir, g);
write_bmp(p, out, DW, DH);
}
/* B2: the mono-MFD heads reserve a top+bottom band for the RIO button
* bezel (only when a serial=rio port is present); the display insets into
* the middle. Other heads and non-rio configs render full-window as before. */
/* B2 (explode layout only): buttons draw FIRST (under), then the opaque
* display insets over their inner part -- only the outer LIP stays visible.
* MFD heads inset top/bottom; the radar insets left/right. */
int hi = mfd_hi_for_g(g);
uint8_t lamps[72], pressed[72];
bool have = RIO_GetPanelState(lamps, pressed);
bool mfd_bz = (hi >= 0) && have; /* g5..9 exist only in explode */
bool radar_bz = (g == 0) && pal_radar_cw && have; /* radar bezel = explode only */
glViewport(0, 0, cw, ch);
glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT);
glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); glDisable(GL_LIGHTING);
glPixelZoom((float)cw / DW, -(float)ch / DH); /* scale + flip y */
if (mfd_bz) {
draw_mfd_bezel(hi, cw, ch, lamps, pressed);
int bh = ch - 2 * RIO_BEZEL_LIP; /* display band (= DH when ch=500) */
if (bh < 1) bh = 1;
glViewport(0, RIO_BEZEL_LIP, cw, bh); /* GL origin bottom-left */
glPixelZoom((float)cw / DW, -(float)bh / DH);
} else if (radar_bz) {
draw_radar_bezel(cw, ch, lamps, pressed);
int bw2 = cw - 2 * RIO_BEZEL_LIP; /* display band (= DW when cw=500) */
int bh2 = ch - RIO_RADAR_BOT; /* leave the bottom indicator strip */
if (bw2 < 1) bw2 = 1;
if (bh2 < 1) bh2 = 1;
glViewport(RIO_BEZEL_LIP, RIO_RADAR_BOT, bw2, bh2);
glPixelZoom((float)bw2 / DW, -(float)bh2 / DH);
} else {
glPixelZoom((float)cw / DW, -(float)ch / DH); /* scale + flip y */
}
glRasterPos2f(-1.0f, 1.0f);
glDrawPixels(DW, DH, GL_RGB, GL_UNSIGNED_BYTE, out);
SwapBuffers(dc);
@@ -1644,9 +1822,9 @@ static DWORD WINAPI rt_main(LPVOID) {
"MFD head B, 3 upper (bits 8-15 via pal2)",
"MFD lower-left (HEAD A / red wire)",
"MFD lower-right (HEAD A / green wire)",
"MFD upper-left (HEAD B / red wire)",
"MFD upper-right (HEAD B / red wire)", /* name swapped to match its screen position (sits on the right, x=1340) */
"MFD upper-center (HEAD B / green wire)",
"MFD upper-right (HEAD B / blue wire)" };
"MFD upper-left (HEAD B / blue wire)" }; /* name swapped to match its screen position (sits on the left, x=20) */
static const int ck_x[5] = { 800, 0, 0, 1440, 2080 };
/* explode grid, all displays at native 640x480 (radar 480x640
* portrait, rotated upright): upper MFD row on top; below it the two
@@ -1673,8 +1851,8 @@ static DWORD WINAPI rt_main(LPVOID) {
int x, y, w, h;
if (cockpit) { x = ck_x[g]; y = 0; w = 640; h = 480; }
else if (explode) { x = ex_x[g]; y = ex_y[g];
if (g == 0) { w = 480; h = 640; } /* portrait radar */
else { w = 640; h = 480; } }
if (g == 0) { w = 500; h = 650; } /* portrait radar: +20 wide (side lips) +10 tall (bottom strip) */
else { w = 640; h = 500; } } /* +20 tall: RIO top/bottom lips (B2) */
else { x = 700 + (slot % 3) * 500; y = 20 + (slot / 3) * 400;
w = 480; h = 360; }
slot++;