Files
TeslaRel410/emulator/VDB-NOTES.md
T
CydandClaude Fable 5 6a595cc565 RP VDB SOLVED: gauge bank granularity 4KB->64KB (L4GAUGE.INI) -- full displays
RP's cockpit displays decoded near-empty (top-strip only, a copy off-screen at
VRAM 0x100000) while BT's were full. Root cause was NOT the VDB decode (byte
split/palettes/stride all correct, proven by BT) but an SVGA bank-switch
GRANULARITY mismatch: RP's REL410/RP/GAUGE/L4GAUGE.INI [640x480x16] used
granularityInKB=4 vs BT's 64 on the same STB Horizon+ (CL-GD5434). DOSBox-X's
CL-GD5434 bank emulation handles 64KB granularity (BT full) but mishandles the
4KB-granular paged gauge writes, scattering them off-screen.

FIX (config, no rebuild): RP L4GAUGE.INI granularityInKB 4->64 (matches BT,
same card). VERIFIED live: VRAM content scan went from 0x0+0x100000 (top-strip
+ off-screen) to the FULL framebuffer 0x0..0x90000 populated; radar +
upper-center MFD confirmed correct, rest under reference check.

Also commits the VDB diagnostic tooling that found it (vpxlog.cpp, all
default-off): VDB_SCAN (VRAM content locator -- decisive), VDB_PALDUMP,
pixel-mask value logging, VDB_REALSTRIDE + live mode/stride/start logging,
VDB_BASE read-base override, VDB_APPLYMASK DAC-mask honoring. Full writeup in
VDB-NOTES.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:57:12 -05:00

10 KiB

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 + upper-center MFD render correctly (others being checked vs reference). RP cockpit displays now decode like BT's.

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.