Narrative on-ramp before the register-level RE: two independent video systems, the 16bpp byte-lane split (low=radar / high=5 mono MFDs), the three Bt477 heads, and the octopus fan-out to the six cockpit displays. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
249 lines
14 KiB
Markdown
249 lines
14 KiB
Markdown
# The VDB — VWE Video Display/Splitter Board
|
||
|
||
Reverse-engineered from the shared MUNGA_L4 driver (`CODE/RP/MUNGA_L4/
|
||
L4SVGA16.ASM` + `L4VB16.CPP`/`.HPP`) and the pod emulator's HLE
|
||
(`emulator/vpx-device/vpxlog.cpp`). **A physical VDB sample + its companion
|
||
Cirrus Logic SVGA card exist** (operator, 2026-07-10) — see the hardware
|
||
verification checklist at the end. This board is IDENTICAL between BattleTech
|
||
and Red Planet (shared driver); only the game-side display layout differs.
|
||
|
||
## What it is
|
||
|
||
An ISA card that **taps the companion Cirrus Logic SVGA card's pixel output**
|
||
off the VGA feature connector and fans that ONE framebuffer out to the six
|
||
secondary cockpit displays (5 mono MFDs + 1 color radar) via three RAMDAC-like
|
||
palette groups. It is a DUMB splitter: no framebuffer of its own, no
|
||
compute — it re-clocks the SVGA pixel stream and routes byte-lanes through
|
||
palette CLUTs to separate VGA heads. "Adam's port decoder design" (Adam G.,
|
||
VWE hardware) per the driver comments.
|
||
|
||
The game renders cockpit gauges to the Cirrus SVGA in a **640x480x16bpp**
|
||
mode (VBE mode 0x111; `L4GAUGE=640x480x16`). The Division/VPX card is separate
|
||
(the out-the-window 3D). So the SVGA framebuffer the VDB taps = GAUGES ONLY.
|
||
|
||
## Data-flow overview: one canvas → six displays
|
||
|
||
*(Narrative on-ramp; the register-level detail is in the sections below.)*
|
||
|
||
The pod has **two independent video systems**, which are easy to conflate:
|
||
|
||
- **Out-the-window 3D** comes from the **Division VelociRender** card (i860 +
|
||
PXPL IGC/EMC rasterizer) on its own monitor — nothing to do with the VDB.
|
||
See [[division-card-hardware]].
|
||
- **The six cockpit gauge displays** — 5 monochrome MFDs + 1 color radar — all
|
||
come from the VDB, fanned out from a **single framebuffer**.
|
||
|
||
The game never renders "six displays." The MUNGA_L4 gauge renderer draws every
|
||
instrument into **one 640×480×16bpp image** on a commodity Cirrus SVGA card.
|
||
All the intelligence is in how that one canvas is *packed*; the VDB itself does
|
||
no computing — it is a dumb byte-lane splitter.
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
G["Gauge renderer (MUNGA_L4)<br/>draws ALL instruments"] --> FB["Cirrus CL-GD5434 SVGA<br/>640x480x16bpp framebuffer<br/>VBE mode 0x111 - one packed image"]
|
||
FB -->|"26-pin VGA feature-connector ribbon"| VDB{{"VDB splitter - MACH130 CPLD<br/>high-color clock divider ON<br/>(dumb: no framebuffer, no compute)"}}
|
||
|
||
VDB -->|"LOW byte - bits 0-7"| SEC["Secondary palette<br/>I/O 0x302 - Bt477 #1<br/>full-color CLUT"]
|
||
VDB -->|"HIGH byte - bits 8-15"| AUX1["Aux1 palette<br/>I/O 0x30A - Bt477 #2<br/>brightness-ramp CLUT"]
|
||
VDB -->|"HIGH byte - bits 8-15"| AUX2["Aux2 palette<br/>I/O 0x312 - Bt477 #3<br/>brightness-ramp CLUT"]
|
||
|
||
SEC --> HA["Head A - full RGB"]
|
||
AUX1 --> HB["Head B"]
|
||
AUX2 --> HC["Head C"]
|
||
|
||
HA -->|"octopus / DB25 cable"| R1["Color RADAR"]
|
||
HB -->|"R wire"| M1["Lower-left MFD"]
|
||
HB -->|"G wire"| M2["Lower-right MFD"]
|
||
HC -->|"R wire"| M3["Upper-left MFD"]
|
||
HC -->|"G wire"| M4["Upper-center MFD"]
|
||
HC -->|"B wire"| M5["Upper-right MFD"]
|
||
```
|
||
|
||
**How one 16-bit pixel becomes six displays:**
|
||
|
||
1. **The two bytes are two index streams.** In 16bpp mode the VDB's high-color
|
||
clock divider (I/O `0x31A`) clocks the **low byte** and **high byte** of
|
||
every pixel into *separate* palette lanes. Low byte → the color radar;
|
||
high byte → the five mono MFDs.
|
||
2. **Five mono MFDs share the high byte** by owning **disjoint bit groups**
|
||
(`bitMask`) and driving **different DAC color channels** (`channelEnable` =
|
||
R/G/B). A mono MFD only needs a brightness ramp, so `BuildAuxiliaryPalette`
|
||
writes a linear intensity ramp into that display's channel of the shared
|
||
256-entry CLUT. The radar keeps true colors via `BuildSecondaryPalette`.
|
||
3. **Three RAMDACs → three heads.** Each palette group is a real Brooktree
|
||
Bt477 (6-bit/channel — hence the driver's `shr al,2` on writes).
|
||
Secondary→Head A (radar), Aux1→Head B, Aux2→Head C.
|
||
4. **The octopus cable splits heads into wires.** A mono MFD needs only one
|
||
color channel, so the DB25 "octopus/pentapus" fan-out routes each head's
|
||
individual R/G/B wire to its own monitor: Head B → 2 lower MFDs (R,G; no
|
||
blue), Head C → 3 upper MFDs (R,G,B). Head A drives the radar as full RGB.
|
||
**2 + 3 MFDs + 1 radar = the six cockpit displays.**
|
||
|
||
**Why it was built this way:** in 1994 you couldn't cheaply drive six arcade
|
||
monitors from one PC. This lets a **single commodity SVGA card render one
|
||
image**, and a **cheap dumb splitter** (one CPLD + three RAMDACs, no memory)
|
||
reconstruct six independent displays — with per-gauge brightness ramps, blink,
|
||
and radar fade all handled "for free" by loading palettes (see *Palette flash +
|
||
fade* below). The register/CLUT detail follows.
|
||
|
||
## Register map (definitive, from L4SVGA16.ASM)
|
||
|
||
I/O 0x300-0x31A. Three palette groups, each a 6-bit VGA-DAC-style CLUT. The
|
||
driver's port table (`L4VB16.CPP` ~3685) passes the group base as **base+2**:
|
||
|
||
```
|
||
Group base(+2) mask read-addr write-addr data
|
||
NativePalette 0x3C6 (the Cirrus's own VGA DAC — 0x3C6/0x3C8/0x3C9)
|
||
SecondaryPalette 0x302 0x302 0x303 0x304 0x305
|
||
AuxiliaryPal1 0x30A 0x30A 0x30B 0x30C 0x30D
|
||
AuxiliaryPal2 0x312 0x312 0x313 0x314 0x315
|
||
|
||
Clock divider: 0x319 write = high-color divider OFF (VWE_HC_OFF)
|
||
0x31A write = high-color divider ON (VWE_HC_ON) (any value)
|
||
```
|
||
|
||
Per-group offsets (VGA-DAC layout): **+0 pixel-mask, +1 read-address,
|
||
+2 write-address, +3 data** (auto-incrementing R,G,B triplets). DAC is
|
||
**6-bit**: `SVGAWriteFullPalette` does `shr al,2` (8→6) on write,
|
||
`SVGAReadFullPalette` does `shl al,2` (6→8) on read. Palette load =
|
||
set write-addr to 0, then `rep outsb` 256*3 bytes. Mask write = one `out`
|
||
to base+0. The game only WRITES the VDB (fire-and-forget, no status/ACK);
|
||
reads return DAC read-back.
|
||
|
||
## How the split works (the dumb part)
|
||
|
||
**High-color clock divider (0x31A on):** in 16bpp mode each pixel is two
|
||
bytes. The divider clocks the LOW byte and HIGH byte of each pixel into
|
||
SEPARATE palette lanes → separate heads. So:
|
||
- **LOW byte (bits 0-7) -> Secondary palette (0x302 group) -> one head**
|
||
- **HIGH byte (bits 8-15) -> Aux1/Aux2 palettes -> the other heads**
|
||
|
||
Each head is a physical VGA output off the splitter; an "octopus" cable fans
|
||
the (up to) three head signals to the six cockpit displays. Decode confirmed
|
||
live for BT: radar = low byte via Secondary; the 5 mono MFDs = high byte via
|
||
Aux1/Aux2 color channels. [[vdb-three-vga-head-decode]]
|
||
|
||
## How the game packs multiple displays into one framebuffer
|
||
|
||
The card is dumb, but the DRIVER is clever about loading the CLUTs so several
|
||
displays share the same pixels. Each cockpit display (`L4GraphicsPort`) has:
|
||
- **`bitMask`** — which framebuffer pixel BITS belong to this display.
|
||
- **`channelEnable`** — which DAC color channel it drives: Red / Green / Blue
|
||
/ AllChannels (+ ...TransparentZero variants that leave index 0 undefined).
|
||
|
||
`BuildSecondaryPalette` (low byte = `bitMask & 0xFF`) and
|
||
`BuildAuxiliaryPalette` (high byte = `(bitMask>>8) & 0xFF`) walk the display's
|
||
bits with a `BitWrangler` and write that display's color/ramp into its channel
|
||
of the shared 256-entry CLUT. So N mono displays coexist in one byte-lane by
|
||
owning disjoint bit groups and different color channels; the CLUT decodes each
|
||
combination back to the right per-display brightness. `BuildAuxiliaryPalette`
|
||
generates a linear brightness RAMP across the display's bit values (mono
|
||
MFD = beam intensity); `BuildSecondaryPalette` copies source colors (color
|
||
radar).
|
||
|
||
## Palette flash + fade (the pixel-mask cycling)
|
||
|
||
`SVGA16::UpdatePalette` (`L4VB16.CPP` ~4196): each palette can FLASH by
|
||
cycling its **pixel-mask** through `mask[]` states at `flashRate` (this is the
|
||
`0x302` mask writes seen cycling e.g. 0x3F/0x7F/0xBF/0xFF — a blink, NOT index
|
||
decode). **Only the Secondary palette is allowed to FADE** (its RGB scaled
|
||
over time). So the pixel-mask register is used as a per-display blink control,
|
||
not (usually) as a decode gate.
|
||
|
||
## Our emulation (vpxlog.cpp)
|
||
|
||
`vdb_write`/`vdb_read` mirror the register map (`VDB_BASE=0x300`,
|
||
`vdb_group_of`/`vdb_group_base`); `vdb_pal[0/1/2]` = Secondary/Aux1/Aux2.
|
||
`pal_draw` reads the live Cirrus framebuffer (`vga.mem.linear` at
|
||
`vga.config.real_start`, 640x480, stride 1280) and for each pixel looks up
|
||
the low byte via pal0 and high byte via pal1/pal2 (one color channel per mono
|
||
head, pentapus split). 6-bit DAC expanded to 8-bit. `VDB_PALDUMP=<prefix>`
|
||
dumps each group's 768-byte CLUT; pixel-mask writes now log their value; masks
|
||
default 0xFF; `VDB_APPLYMASK=1` ANDs the index with the mask (correct DAC
|
||
emulation, default off = BT-identical).
|
||
|
||
## RP vs BT — the decode problem, SOLVED (2026-07-10)
|
||
|
||
**ROOT CAUSE: an SVGA bank-switch GRANULARITY mismatch, NOT a VDB decode bug.**
|
||
RP's `REL410/RP/GAUGE/L4GAUGE.INI` `[640x480x16]` mode was byte-identical to
|
||
BT's EXCEPT `granularityInKB=4` (RP) vs `=64` (BT) -- same card (STB Horizon+
|
||
= CL-GD5434). The MUNGA gauge renderer (L4GREND.CPP reads L4GAUGE ->
|
||
L4GAUGE.INI -> SVGA16 ctor with that granularity) uses it to compute VESA
|
||
bank/page numbers for its paged writes down the gauge framebuffer. DOSBox-X's
|
||
CL-GD5434 bank emulation handles 64KB granularity correctly (BT gauges full)
|
||
but MISHANDLES the 4KB-granular paging, so RP's gauge writes scattered/landed
|
||
off-screen -- symptom: the visible page held only a top ~51-row strip while a
|
||
copy sat at VRAM 0x100000, and the heads decoded near-empty. Everything else
|
||
(low/high byte split, 3 palettes, stride, the whole VDB model) was ALREADY
|
||
CORRECT -- proven by BT and by the fix.
|
||
|
||
**FIX (config, no rebuild): set RP's L4GAUGE.INI `[640x480x16]`
|
||
`granularityInKB=64` to match BT.** Safe because it's the same card BT drives
|
||
at 64KB. VERIFIED live 2026-07-10: VRAM content scan went from `0x0 + 0x100000`
|
||
(top-strip + off-screen) to the FULL framebuffer `0x0..0x90000` all populated;
|
||
**operator confirmed the radar AND all MFDs render correctly** (checked vs
|
||
reference). RP cockpit displays now decode fully like BT's — RP VDB DONE.
|
||
|
||
The tooling that found it (all in vpxlog.cpp, default-off diagnostics, kept):
|
||
`VDB_PALDUMP` (dump the 3 CLUTs), pixel-mask value logging, `VDB_REALSTRIDE`
|
||
+ live scan_len/addr_add/real_start/mode logging, `VDB_SCAN` (VRAM
|
||
content-region locator -- THE decisive tool), `VDB_BASE` (read-base override),
|
||
`VDB_APPLYMASK` (honor the DAC pixel-mask). Note: proper long-term fix could
|
||
instead be DOSBox-X honoring 4KB CL-GD5434 bank granularity, but the INI match
|
||
is correct and sufficient.
|
||
|
||
## Physical hardware (documented from the operator's sample, 2026-07-10)
|
||
|
||
CONFIRMS the reverse-engineering below the chip level:
|
||
|
||
- **Companion video card**: **Jaton KY2-JAX-CVGA54PCI** — a legacy PCI Local
|
||
Bus VGA card, **Cirrus Logic CL-GD5434** chipset. Renders the cockpit gauges
|
||
(640x480x16, VBE 0x111) and feeds the pixel stream to the VDB via a **26-pin
|
||
ribbon off the VGA Feature Connector**.
|
||
- **The splitter brain**: **AMD/Lattice MACH130-15JC** CPLD — EE-CMOS, 64
|
||
macrocells, 15 ns max prop delay, 84-pin PLCC (2nd-sourced by Rochester
|
||
Electronics). This is "Adam's port decoder": it CONSUMES the feature-connector
|
||
video stream AND takes instructions from the **ISA bus** (the 0x300-0x31A
|
||
register file — palette loads, masks, and the 0x319/0x31A high-color clock
|
||
divider). It does the byte-lane split in hardwired logic (same for BT and RP).
|
||
- **The three DACs**: **3x Brooktree Bt477KPJ80** RAMDACs (256-entry, 6-bit/
|
||
channel, 80 MHz) = the three palette groups Secondary/Aux1/Aux2. The Bt477's
|
||
6-bit DAC is exactly why the driver does `shr al,2` (8->6) on palette writes.
|
||
Output routing (confirms the head map): **one Bt477 -> a VGA stream to the
|
||
color RADAR display** (= Secondary/0x302); the **other two Bt477s -> a DB25
|
||
connector that fans out to the 5 monochrome MFDs** (= Aux1/0x30A R,G = 2
|
||
lower MFDs, Aux2/0x312 R,G,B = 3 upper MFDs; 5 mono wires total, one channel
|
||
unused). The DB25 fan-out is the "octopus/pentapus" cable.
|
||
- **Host hard-reset circuit (remote reboot)**: an **HSSR-8060** — a 6-pin
|
||
opto-isolated solid-state relay (MOSFET output, Agilent/Broadcom) — with its
|
||
**output switch (pins 4 & 6) wired to a 2-pin header silk-screened
|
||
`RESET_OUT`**, which is in parallel with the PC's front-panel RESET switch.
|
||
So energizing the SSR's input LED = "pressing reset" = a hardware reboot of
|
||
the pod's host PC. **What drives the SSR input is still unknown (operator
|
||
researching).** This is almost certainly a REMOTE/AUTOMATED REBOOT path: an
|
||
arcade operator (or the ops console over the net) could hard-reset a wedged
|
||
pod without physically touching it. Candidate input drivers to trace: (a) the
|
||
MACH130 CPLD decoding a "reset" write in the 0x300-0x31A I/O space (a soft
|
||
command -> hard reset, e.g. a watchdog or console-commanded reboot); (b) a
|
||
discrete line from the network/console board; (c) a watchdog timer. NOTE for
|
||
the emulator: our HLE has no reset-out port yet; if the driver ever writes a
|
||
reset trigger it'd appear as an unhandled 0x300-0x31A write — worth watching
|
||
the vpxresp log for writes outside the known palette/clock offsets.
|
||
**RESOLVED (operator, 2026-07-10): the SSR INPUT is driven from a spare pair
|
||
on the RADAR display's 9-pin video cable.** So it is NOT software/CPLD-driven
|
||
— it's a COCKPIT-LOCAL reset button: the reset signal is carried UP the radar
|
||
cable's unused conductors from a button in the cockpit down to the SSR, which
|
||
closes RESET_OUT across the PC front-panel reset. Lets the operator hard-
|
||
reboot a wedged pod PC from the cockpit without opening the chassis. Pure
|
||
hardware; nothing for the emulator to model.
|
||
|
||
### Still worth capturing for the archive
|
||
- Photos of each chip + the board silkscreen; the DB25 pinout (which pin ->
|
||
which MFD) and the 26-pin feature-connector ribbon pinout.
|
||
- Whether the MACH130 JEDEC fuse-map is readable (a MACH130 can often be
|
||
read back unless secured) -- that IS the splitter logic, the ultimate
|
||
ground truth, and preservable like the RIO EPROM.
|
||
- The reset-tie circuit trace (operator's follow-up).
|
||
|
||
Related: [[vdb-three-vga-head-decode]], [[tesla-cockpit-emulator-state]].
|