184 lines
10 KiB
Markdown
184 lines
10 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.
|
|
|
|
## 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]].
|