diff --git a/emulator/VDB-NOTES.md b/emulator/VDB-NOTES.md new file mode 100644 index 0000000..4312402 --- /dev/null +++ b/emulator/VDB-NOTES.md @@ -0,0 +1,134 @@ +# 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=` +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 OPEN decode problem (2026-07-10) + +Same board, same driver. Captured live for RP (VDB_PALDUMP): framebuffer +identical (mode 0x111, 640x480); **Secondary/pal0 loads ALL ZEROS** (BT drives +it as the dynamic color radar) -> our radar head is black; Aux1=green, +Aux2=RGB are driven; the RP MFD heads decode as SPARSE fragments in the top +strip. So under the shared mechanism, RP simply puts NO display in the low +byte (Secondary empty is legitimate), and its high-byte MFD content is not +landing where our BT-tuned head/channel split expects. + +**Operator's steer: the card is dumb, so the fix is glaringly simple** — not +a driver-logic difference. Leading suspects to check next (empirically, like +the first BT pass): (a) the framebuffer PAGE/`real_start` — is RP double- +buffering so we read a stale/wrong SVGA page? (`SVGASetPage` exists); (b) the +device DUMP path stopped writing win*.bmp for RP (fires in pal_draw) — worth +finding why, it may reveal the head-render path isn't running the same; +(c) which bit groups/channels RP's displays actually claim (dump a BT vs RP +Aux palette side-by-side; the ramp/channel structure shows the bit layout); +(d) confirm the low/high byte order and the 640x480 vs a smaller gauge region +(top-strip content hints RP may draw gauges in fewer rows). + +## Physical-sample verification checklist (have the board + Cirrus card) + +Photograph and record for the archive: +- **Splitter board**: the port-decoder PAL/GAL (mark/part#) at 0x300-0x31A; + the THREE RAMDAC chips (part#, are they real Bt476/Bt478-class or a Cirrus + DAC?) and how their pixel-mask/addr/data pins map to 0x302/0x30A/0x312; the + high-color-divider logic tied to 0x319/0x31A; the feature-connector tap. +- **Octopus cable**: pinout — which of the 3 head outputs feeds which of the + 6 cockpit displays (confirms the pentapus win5-9 + radar mapping). +- **Companion Cirrus Logic card**: exact chip (CL-GD54xx?) + BIOS, to pin the + 640x480x16 mode/pitch and the feature-connector timing the divider assumes. +- Any onboard ROM/PAL dump (like the RIO EPROM) for full preservation. + +Related: [[vdb-three-vga-head-decode]], [[tesla-cockpit-emulator-state]].