Analysis model: gpt-5.5 xhigh

Vega by Ultra Force - Technical Dissection

Scope

This is a binary-level pass over Vega by Ultra Force, released for MS-DOS in 1990. The Hornet 1990 demo index lists vega.zip as **+ under the title Vega by Ultraforce; the main executable in the archive is timestamped 1990-10-02.

Useful public references:

The package includes old contact text in the executable, document, and scroller. Those details are intentionally not reproduced here. This article focuses on the DOS, VGA, scroller, and Sound Blaster-driver mechanics.

The interesting shape:

This is not a big engine. It is a picture viewer with music and a very direct software scroller, which is exactly why the inner loop is easy to see.

Archive And Expanded Executable

The examined archive:

a811ac5702126f6ce0e21fa036c0c1b7239ab62325cef1375adcba2b5ca4df27  vega.zip

Archive contents:

ct-voice.drv     2377  1989-11-27 10:26
demosite.com     1104  1992-10-21 18:43
vega.doc          644  1991-05-05 19:53
vega.exe        36320  1990-10-02 23:00
vega.pic        64817  1980-01-03 02:30

Important file hashes:

57cdd1e9888dbc5d3dc9d7abf3325a1fb78dc47d78bc7abcce119f618fe3c31a  vega.exe
1be150f82b8155724c6d83948b989605dfb7bcb447ec42b112f5c25ec32fe036  vega.pic
63b9ee3aa25d2d2fcc9113cc83b7f1e8c63d95dbaebcf547537fbbdfddee3ad0  ct-voice.drv

The original VEGA.EXE is PKLITE-packed:

size=36320  mzsize=36320  header=112  load=36208  relocs=0
entry=fff0:0100  stack=08df:0400  min=0728  max=ffff

After expansion with UNP, the useful executable is:

b4d69f08b4684605b3367b8f8f7ceabdfc6250b34b660aaa20a8bdd20e83a57f  VEGA.EXE
size=65149  mzsize=65149  header=96  load=65053  relocs=0
entry=fff0:0100  stack=fff0:fffe  min=001e  max=ffff

CS:IP = FFF0:0100 is the classic COM-style EXE trick. DOS loads the image after the PSP, but the wrapped entry executes as if the image were a .COM program with ORG 100h. The disassembly below is therefore shown with a 100h base. Absolute operands such as [025Ah] are PSP-relative logical offsets inside the loaded image.

Top-Level Flow

The main entry is compact:

0100  xor  ah, ah
0102  call 0163h           ; set/check mode 13h
0105  call 064eh           ; parse command tail
0108  call 0826h           ; handle /?, /nosb, default sound flag
010b  call 025eh           ; load VEGA.PIC
010e  cmp  byte [0825h], 1
0113  jne  0118h
0115  call 06d9h           ; load/init CT-VOICE path
0118  mov  word [025ah], f000h
011e  mov  si, 1b69h       ; scroller text pointer

Then it loops over one character at a time:

0121  lodsb
0122  cmp  al, 00h
0124  je   011eh           ; wrap to start on NUL
0126  push si
0127  call 0918h           ; build 8x8 colored glyph
012a  call 060bh           ; insert glyph at right edge of strip
012d  mov  cx, 0008h       ; 8 pixel columns per character

0130  push cx
0131  call 06cdh           ; shift source strip left by one byte
0134  call 0622h           ; blit 8 rows to A000:F000
0137  pop  cx
0138  loop 0130h

013a  cmp  word [07f7h], 0
013f  jne  0144h
0141  call 07d7h           ; feed/play next audio block if needed
0144  pop  si
0145  mov  ah, 01h
0147  int  16h             ; exit on a key
0149  je   0121h

So the visual cadence is:

for each character:
  build an 8x8 glyph
  insert it just beyond the right edge of the strip
  repeat 8 times:
    shift the whole 8-row strip left by one byte
    copy the visible 320-byte part of each row to VRAM

The scroller does not use hardware panning. It is an ordinary byte scroller, but the buffer layout makes it cheap.

Mode 13h Check

The mode setup is plain BIOS:

0163  mov  ah, 00h
0165  mov  al, 13h
0167  int  10h
0169  mov  ah, 0fh
016b  int  10h
016d  cmp  al, 13h
016f  je   017ch
0171  mov  dx, 017dh
0174  mov  ah, 09h
0176  int  21h
0178  mov  ah, 4ch
017a  int  21h
017c  ret

There are no VGA register tweaks here. Vega is a mode-13h demo: one 64,000 byte linear screen, one byte per pixel.

Picture File Format

VEGA.PIC is exactly 64,817 bytes:

0031h bytes   ASCII signature/header
0300h bytes   256 DAC entries, 3 bytes each
FA00h bytes   320x200 raw pixels

The header says it is an Ultra Force VGA 320x200 256-color screen file. The loader does not parse fields; it relies on fixed offsets:

025e  mov  dx, 02d1h       ; "VEGA.pic"
0261  mov  ax, 3d00h
0264  int  21h             ; open
0279  mov  bx, ax
027b  mov  dx, 02dah
027e  mov  cx, 0331h
0282  mov  ah, 3fh
0284  int  21h             ; read header + palette

The palette starts at buffer offset 030Bh, because:

030Bh - 02DAh = 0031h

The palette upload is BIOS function AX=1012h:

0286  push bx
0287  mov  ax, 1012h
028a  mov  bx, 0000h
028d  mov  cx, 0100h       ; 256 DAC entries
0290  mov  dx, 030bh       ; palette data
0293  int  10h
0295  pop  bx

Then the pixel data is read directly into VGA memory:

0296  mov  ax, 0a000h
0299  mov  ds, ax
029b  xor  dx, dx
029d  mov  ah, 3fh
029f  mov  cx, fa00h       ; 64,000 bytes
02a2  int  21h
02a4  mov  ax, cs
02a6  mov  ds, ax
02a8  ret

That is the whole image loader:

open VEGA.PIC
read 817 bytes into program memory
upload 768 palette bytes from inside that read buffer
read 64,000 bytes to A000:0000
restore DS = CS

There is no compression in the picture path.

Command Tail And Sound Flag

Routine 064Eh copies the first command-line token from the PSP command tail at 0080h to buffer 067Dh:

064e  mov  si, 0080h
0651  mov  di, 067dh
0654  cld
0655  lodsb               ; command-tail length
0656  or   al, al
0658  je   067bh
065a  lodsb
065b  cmp  al, 0ah
065d  je   067bh
065f  cmp  al, 20h
0661  je   065ah          ; skip spaces
0663  stosb
0664  lodsb
0665  cmp  al, 20h
0667  je   0673h
0669  cmp  al, 0dh
066b  je   0673h
066d  cmp  al, 00h
066f  je   0673h
0671  jmp  0663h
0673  xor  al, al
0675  stosb
0676  mov  si, 067dh
0679  clc
067a  ret
067b  stc
067c  ret

Routine 0826h compares that token with /nosb in two cases and sets the sound-enable byte at 0825h:

082f  mov  si, 067dh
0832  mov  di, 0819h       ; lowercase option text
0835  mov  cx, 0005h
0838  repe cmpsb
083a  cmp  cx, 0
083d  je   0855h
083f  mov  si, 067dh
0842  mov  di, 081eh       ; uppercase option text
0845  mov  cx, 0005h
0848  repe cmpsb
084a  cmp  cx, 0
084d  je   0855h
084f  mov  byte [0825h], 1 ; default: sound enabled
0854  ret
0855  mov  byte [0825h], 0 ; no sound
085a  ret

The help text lives nearby, but it is not needed to understand the runtime.

CT-VOICE Driver Loading

If sound is enabled, routine 06D9h loads CT-VOICE.DRV into a segment 1800h paragraphs above CS:

06e9  push ds
06ea  push es
06eb  mov  ax, cs
06ed  mov  ds, ax
06ef  add  ax, 1800h
06f2  mov  [07fbh], ax     ; far-call segment
06f5  mov  es, ax
06f7  mov  dx, 10dch       ; "CT-VOICE.DRV"
06fa  mov  ah, 3dh
06fc  mov  al, 00h
06fe  int  21h
0702  mov  [0817h], ax
0705  mov  ah, 3fh
0707  mov  bx, [0817h]
070b  mov  cx, 7fffh
070e  xor  dx, dx
0712  mov  si, [07fbh]
0716  mov  ds, si
0718  int  21h             ; read driver to loaded segment

The far pointer used for driver calls is at 07F9h:

07F9 = offset 0000h
07FB = segment CS + 1800h

The init path calls the driver through that pointer with small command numbers in BX. The visible pattern is:

0727  mov  bx, 0003h
072a  mov  bp, 07f9h
072d  call far [bp+00h]
0730  cmp  ax, 0000h
0733  je   0798h

0735  mov  bx, 0002h
0738  mov  ax, 0003h
073b  mov  bp, 07f9h
073e  call far [bp+00h]
0741  mov  bx, 0003h
0744  mov  bp, 07f9h
0747  call far [bp+00h]
074a  cmp  ax, 0000h
074d  je   0798h

It repeats the BX=2 setup call with AX=5 and AX=7, checking BX=3 after each try. Interpreting the CT-VOICE API from the outside, this looks like probing several Sound Blaster interrupt choices until the status call succeeds.

When setup is complete, it calls:

0798  mov  bp, 07f9h
079b  mov  bx, 0005h
079e  mov  di, 07f7h
07a1  call far [bp+00h]

The word at 07F7h is then polled from the main loop. When it reaches zero, the demo feeds the next audio block:

07d8  cmp  byte [0825h], 1
07dc  je   07dfh
07de  ret
07df  push es
07e0  push bp
07e1  mov  ax, cs
07e3  mov  es, ax
07e5  mov  bp, 07f9h
07e8  mov  bx, 0006h
07eb  mov  di, 1d2bh
07ee  add  di, 001ah
07f1  call far [bp+00h]
07f4  pop  bp
07f5  pop  es
07f6  ret

The executable embeds a Creative Voice data stream at 1D2Bh; playback starts after the 26-byte voice-file header. On exit, routine 07A8h calls the driver with BX=9, presumably shutdown/stop:

07a8  cmp  byte [0825h], 1
07ad  je   07b0h
07af  ret
07b0  mov  bp, 07f9h
07b3  mov  bx, 0009h
07b6  call far [bp+00h]
07b9  ret

So audio is not mixed by this program. The demo loads an external driver, patches a far pointer, and delegates playback.

Glyph Builder

The scroller's glyph builder starts at 0918h. The character byte is still in AL from the main loop. It multiplies that by 8 and indexes an 8-byte font at 097Ch:

0918  xor  ah, ah
091a  add  ax, ax
091c  add  ax, ax
091e  add  ax, ax          ; AX = character * 8
0920  add  ax, 097ch       ; font base
0923  mov  di, 10e9h       ; 8x8 scratch glyph
0926  mov  si, ax
0928  mov  bl, 08h         ; 8 font rows

For each font byte, it shifts the byte left and emits eight pixels:

092a  lodsb
092b  mov  ah, al
092d  mov  cx, 0008h
0930  xor  al, al
0932  shl  ah, 1
0934  jb   0940h
0936  mov  al, 00h
0938  stosb
0939  loop 0930h
093b  dec  bl
093d  jne  092ah
093f  ret

For a clear font bit, it stores zero. For a set bit, it chooses a color based on which glyph row is being drawn:

0940  mov  al, 05h
0942  cmp  bl, 08h
0945  jne  0949h
0947  mov  al, 18h
0949  cmp  bl, 07h
094c  jne  0950h
094e  mov  al, 19h
0950  cmp  bl, 06h
0953  jne  0957h
0955  mov  al, 1ah
...
0973  cmp  bl, 01h
0976  jne  097ah
0978  mov  al, 1fh
097a  jmp  0938h

The row colors are:

row 0 -> 18h
row 1 -> 19h
row 2 -> 1Ah
row 3 -> 1Bh
row 4 -> 1Ch
row 5 -> 1Dh
row 6 -> 1Eh
row 7 -> 1Fh

The scratch glyph at 10E9h is therefore a linear 8x8, 64-byte block:

for y in 0..7:
  font_byte = font[char * 8 + y]
  for x in 0..7:
    if bit set: glyph[y*8+x] = 18h + y
    else:       glyph[y*8+x] = 00h

The row-gradient colors are baked at glyph-build time, not during the screen blit.

Right-Edge Glyph Insert

After building the glyph, routine 060Bh inserts it into the source strip:

060b  mov  si, 10e9h       ; scratch 8x8 glyph
060e  mov  di, 1269h       ; right edge of scroll strip
0611  mov  ah, 08h         ; 8 rows
0613  cld
0614  mov  cx, 0008h
0617  rep  movsb           ; copy one 8-pixel glyph row
0619  add  di, 0140h       ; advance to next strip row
061d  dec  ah
061f  jne  0614h
0621  ret

The destination is not the visible screen. It is a software strip beginning at 1129h. 1269h equals:

1129h + 0140h = 1129h + 320

The strip row is actually 328 bytes wide. rep movsb writes 8 bytes, then add di,0140h advances another 320 bytes, so the next row starts 328 bytes after the previous row:

row stride = 8 copied bytes + 0140h skipped bytes = 0148h = 328

The glyph is inserted just beyond the 320-byte visible area. The next eight one-byte shifts move it into view.

One-Byte Strip Shift

Routine 06CDh shifts the entire 8-row strip left by one byte:

06cd  mov  si, 112ah
06d0  mov  di, 1129h
06d3  mov  cx, 0a40h
06d6  rep  movsb
06d8  ret

0A40h is 2,624 bytes:

8 rows * 328 bytes per row = 2624 = 0A40h

Because SI = DI + 1 and the direction flag is clear, the copy performs:

strip[i] = strip[i + 1]

for the whole strip. This is the classic byte-scroller inner loop. It costs one rep movsb over 2,624 bytes per pixel step.

VBlank Blit To A000:F000

Routine 0622h copies the visible 320 bytes of each strip row to the bottom of the mode-13h screen:

0622  push es
0623  mov  ax, 0a000h
0626  mov  es, ax
0628  mov  si, 1129h
062b  mov  di, [025ah]     ; F000h
062f  mov  ah, 08h         ; 8 rows
0631  cli
0632  mov  dx, 03dah
0635  in   al, dx
0636  test al, 08h
0638  jne  0635h           ; wait outside retrace
063a  in   al, dx
063b  test al, 08h
063d  je   063ah           ; wait inside retrace

Then it copies eight screen rows:

063f  mov  cx, 00a0h
0642  rep  movsw           ; 320 bytes
0644  add  si, 0008h       ; skip hidden right-edge bytes
0647  dec  ah
0649  jne  063fh
064b  pop  es
064c  sti
064d  ret

The copy per frame is:

8 rows * 320 bytes = 2,560 bytes

The source stride is again 328 bytes:

copy 320 bytes with MOVSW
skip 8 bytes of offscreen/right-edge storage

The destination starts at A000:F000h, which is byte offset 61,440:

61440 / 320 = row 192

So the scroller occupies the bottom 8 rows of the 320x200 screen.

Per-Character Timing

Putting the three scroller routines together:

build glyph once:
  8 font rows * 8 bits = 64 stores into glyph scratch

insert glyph once:
  8 rows * 8 bytes = 64 copied bytes into strip right edge

for each of 8 pixel steps:
  shift 2624 bytes inside the strip
  wait for vertical retrace
  copy 2560 bytes to A000:F000

A complete character advance therefore performs:

8 * (2624 + 2560) = 41,472 byte copies

plus the 128 bytes of glyph build/insert work. That is a lot of memory traffic for an 8-pixel character, but it is predictable, small enough for a 1990 VGA PC, and keeps the visible blit synchronized to retrace.

Why It Matters

Vega is useful because it shows a very early PC demo pattern in its simplest form:

It is much simpler than the 1991 planar demos, but that makes it historically useful: you can see the transition point where PC demos are still mostly mode-13h memory copies, with music and presentation structure wrapped around one very clear inner loop.