VDO — RATVID Streaming Video (.VDO)¶
Video frames for mission briefing sequences. Found in FA_7.LIB (Disc 1) —
355 files, each a 4-character stem (e.g. AACA). Every .VDO has exactly
one same-stem .FBC frame-size index. Audio is shared per 3-character
briefing-group prefix: AAC.11K narrates the whole AACA…AACE variant
group (the 4th character A–J is the angle/variant). 104 of the 105 groups
carry a .11K; one group (IQC) is silent — so a .VDO is not guaranteed a
same-stem audio track. All are 320×200, magic RATV. Pairing verified across
the full corpus (#137).
Tools¶
fx¶
fx vdo info F16C.VDO [F16C.FBC] # header (+ frame count with the FBC)
fx vdo export AACA.VDO AACA.FBC -o out/ # decode every frame to PNG
fx_lib decodes read-only (the engine has no encoder). vdo_open needs the
paired .FBC — it supplies the frame boundaries the .VDO itself omits.
Decoding is sequential (frames are inter-coded); the decoder replays from frame
0 when asked for an earlier frame.
File Layout¶
All multi-byte integers are little-endian.
| Offset | Size | Description |
|---|---|---|
0x00 |
816 | Header (see below) |
0x330 |
var | Frame data blocks, one per frame (sizes given by paired .FBC) |
Header (816 bytes)¶
| Offset | Size | Type | Description |
|---|---|---|---|
0x00 |
6 | char[6] | Magic RATVID (ASCII, no null terminator) |
0x06 |
1 | u8 | Major version = 1 |
0x07 |
1 | u8 | Minor version = 2 |
0x08 |
4 | u32 | Frame rate (observed: 15 fps) |
0x0C |
4 | u32 | Unknown (observed: 0) |
0x10 |
2 | u16 | Frame count (N) |
0x12 |
2 | u16 | Width in pixels (observed: 320) |
0x14 |
2 | u16 | Height in pixels (observed: 200) |
0x16 |
2 | u16 | Palette entry count (always 256) |
0x18 |
2 | u16 | Audio channel count (always 1 = mono) |
0x1A |
2 | u16 | Audio sample rate in Hz (observed: 8000) |
0x1C |
4 | u32 | Unknown |
0x20 |
16 | u8[16] | Zeroed |
0x30 |
768 | u8[256×3] | VGA palette: 256 × 3 bytes (R, G, B each 6-bit, range 0–63) |
The 768-byte palette at offset 48 is a standard VGA DAC palette — 256 entries
of 3 bytes each, all values in 0–63 (6-bit per channel, matching VGA register
format). Entry 0 is always black (00 00 00). The palette is per-video; each
.VDO file carries its own.
Field at offset 22 is confirmed to be the palette entry count (always 256, matching the 768-byte palette block at +48). Field at offset 24 is confirmed to be the audio channel count (always 1 = mono).
Frame Data¶
Frame data begins at offset 816. Frames are packed back-to-back with no
delimiters. Frame N starts at offset 816 + sum(FBC[0..N-1]).
Frame data is palettized (8-bit palette indices into the header palette at
+48). Each frame is decoded by GetVDOFrame (VA 0x4AF510) into the VDO
struct's decode buffers.
The
.VDOcodec is not the CobraDecodeFramecluster. The shipped 320×200 8bpp movies decode through a small, self-contained path —GetVDOFrame→UnRLE→DecompressVideo(three functions at0x4C8AA4–0x4C8DBB, ~460 bytes) — and never touch theDecodeFramedispatcher (0x442370) or the ~45 Cobra/CB8 leaves at0x456300–0x45D090. Those are the CB8 player. "Cobra" is the shared FMV umbrella; the two codecs are distinct. (Reconciles the earlier note in video-decode.md that submode-6 served.VDO.)
Per-frame stream structure¶
GetVDOFrame reads the whole frame (FBC[n] bytes) and parses it as a leading
u16 tag followed by one or two RLE-or-raw sub-streams:
Tag (u16) |
Meaning |
|---|---|
0 |
Reuse the current codebook; decode the index stream only (delta frame) |
1 |
Color-table refresh (RLE) — no pixel blit this frame |
2 |
Image keyframe → DecompressVideoImage; a second u16 gives the sub-stream length |
| other | Codebook sub-stream: bit 0x8000 set ⇒ RLE-compressed, low 15 bits = its byte length (UnRLE expands it); raw otherwise |
After the codebook sub-stream a u16 marker separates the index sub-stream:
0 = end, 0xFFFF = an RLE-compressed index stream follows (UnRLE). The
frame is then rendered by DecompressVideo(colortable, codebook, index, width,
height). Verified against the corpus: frame 0 of AACA.VDO begins BE 8B
(tag = 0x8BBE, bit 0x8000 set ⇒ a 0x0BBE-byte RLE codebook block).
UnRLE (0x4C8AFC)¶
Byte-oriented RLE, prefixed by a u16 output-pixel count. Each control byte:
bit 0x80 set ⇒ run — length (b & 0x7f) + 1 (or, if 0x7f, a following
u16 + 1) of the next byte repeated; clear ⇒ literal — copy b bytes
verbatim. Decoding stops when the output count is exhausted.
DecompressVideo (0x4C8AA4) — the blit dispatch¶
Walks the index stream in 8-pixel groups (width × height / 8): a 0 byte
skips the group (leaves the prior frame's pixels — the inter-frame delta), and a
nonzero byte indexes a span-emitter jump table at 0x50E5CE whose handler
writes that group's 8 pixels. DecompressVideoImage (0x4C8CD8) is the keyframe
variant, adding a row-replication tail (each output row copied to the next — the
320×200 → 640×480 vertical doubling).
The index byte is an 8-bit copy mask (#139)¶
The 0x50E5CE handlers are generated x86, built once at startup by
BuildSelfModifyCode (0x4C8BEC, via VDOInit 0x405490) into a code buffer
at 0x50E9CE; the table statically holds only zeros. Each handler decodes one
index byte, and that byte is a per-pixel copy mask over the group's 8
pixels: a set bit copies the next source pixel; a clear bit keeps the previous
frame's pixel (advances the output pointer without reading source). So the
source stream carries only the pixels that changed, and the mask says where.
BuildSelfModifyCode emits, per index value:
0x00→add edi, 8; ret— skip the whole group (the0fast-path inDecompressVideo).0xFF→movsd; movsd; ret— copy all 8 pixels (A5 A5).- everything else → two
DoNibble(0x4C8C60) calls (high nibble → pixels 0–3, low nibble → pixels 4–7) +ret.
DoNibble emits the x86 for one 4-bit nibble = 4 pixels, MSB first:
| Nibble / 2-bit pair | Emitted | Effect |
|---|---|---|
nibble 0xF |
A5 (movsd) |
copy 4 |
pair 11 |
66 A5 (movsw) |
copy 2 |
pair 10 |
A4 47 (movsb; inc edi) |
copy, keep |
pair 01 |
47 A4 (inc edi; movsb) |
keep, copy |
pair 00 |
47 47 (inc edi; inc edi) |
keep 2 |
So the codec is a per-pixel delta: mask bit set ⇒ take a new pixel from the source stream; clear ⇒ retain the prior frame's pixel — plus the RLE that compresses the mask and source streams.
Worked example — AACA.VDO frame 0 (FBC[0] = 35,546 B, tag = 0x8BBE): the
mask sub-stream is a 0x0BBE-byte RLE block that expands to exactly 8,000
mask bytes (= 320×200/8, one per group); the 0xFFFF marker then introduces
an RLE source stream declaring 32,534 pixels. Reconciling the mask's set-bit
total against that source count byte-for-byte is the one detail the fx_lib
reference decoder pins down (#140).
Audio¶
Audio is stored separately in the paired .11K file (raw PCM, 8000 Hz mono
8-bit). It is not embedded in the .VDO.
File Inventory¶
| File | Frames | Width | Height | FPS | Audio Hz |
|---|---|---|---|---|---|
| AACA.VDO | 123 | 320 | 200 | 15 | 8000 |
| AACB.VDO | 260 | 320 | 200 | 15 | 8000 |
| IPCA.VDO | 1685 | 320 | 200 | 15 | 8000 |
Engine Notes¶
Confirmed functions (FA.SMS names), in load → decode order:
| VA | Symbol | Role |
|---|---|---|
0x4AF1E0 |
OpenVDOFile |
Open the .VDO file, begin streaming |
0x4AF200 |
ReadVDOHeader |
Parse the 816-byte file header into a VDOHEADER |
0x4AF230 |
ReadFrameSizesFile |
Read the paired .FBC frame-size index |
0x4AF2D0 |
ReadVDOPalette |
Load the 768-byte VGA palette into T_RGB[] |
0x4AF320 |
VDOfromVDOHEADER |
Build the runtime VDO struct from the header |
0x4AF3A0 |
AllocVDO |
Allocate the frame decode buffers (codebook / index / color) |
0x4AF070 |
StartVDOAudio |
Start the paired .11K narration track |
0x4AF510 |
GetVDOFrame |
Read + parse one frame, dispatch to the decoder |
0x4C8AFC |
UnRLE |
RLE decompress a sub-stream (see § UnRLE) |
0x4C8AA4 |
DecompressVideo |
Blit the index stream via the 0x50E5CE table |
0x4C8CD8 |
DecompressVideoImage |
Keyframe/image blit + row replication |
0x405490 |
VDOInit |
One-time init; calls BuildSelfModifyCode |
0x4C8BEC |
BuildSelfModifyCode |
Generate the 256 copy-mask handlers into 0x50E9CE |
0x4C8C60 |
DoNibble |
Emit the x86 for one nibble (4 pixels) of a handler |
0x4AF6B0 |
VDO_320x200_to_640x480 |
2× upscale for the SVGA present path |
All operate on the VDO struct, whose fields correspond to the header layout
above. These live in the 0x4AExxx/0x4C8xxx range — distinct from the
0x456300 Cobra/CB8 cluster.
Open Questions¶
1. tag-2 image-keyframe path¶
The fx_lib decoder (#140) decodes every frame of all 355 stock .VDO files —
the mask/RLE/copy-mask path is validated end to end (frame 0 of AACA.VDO
renders the briefing image pixel-for-pixel; the mask set-bit count matches the
source-pixel count exactly). The one unexercised branch is tag 2
(DecompressVideoImage, the row-replicated image keyframe): no shipped file
uses it, so the decoder treats it as a prior-frame passthrough. Confirming that
path needs a .VDO that carries it (none in the corpus) or the running engine.
Status: open — re-static (#55; only the corpus-absent tag-2 path remains)
Related¶
Formats: FBC — the paired frame-size index; 11K — the paired audio track; CB8 — the other (fully decoded) FMV format.