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:
- Hornet 1990 demo index: https://files.scene.org/get/mirrors/hornet/demos/1990/00_index.txt
- Scene.org archive: https://files.scene.org/get/mirrors/hornet/demos/1990/vega.zip
- Hornet 1990 archive directory: https://files.scene.org/browse/mirrors/hornet/demos/1990/
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:
- a PKLITE-packed COM-style MZ executable
- a custom
.PICfile that is really header + DAC + raw mode-13h pixels - an optional Creative Labs CT-VOICE driver path
- an 8-row bottom scroller implemented as a 328-byte-wide software strip
- an 8x8 font expander that turns one character into a colored glyph block
- an 8-step shift-and-blit loop per character
- retrace-synchronized 2,560-byte updates into
A000:F000
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:
- The picture path is raw mode-13h data: fixed header, fixed DAC block, fixed 64,000-byte read.
- Sound is delegated to a vendor driver loaded at runtime.
- The scroller is a software strip with an 8-byte hidden edge, not a VGA register trick.
- The glyph renderer converts 1-bit font rows into colored 8-bit pixels before the scroll loop.
- The hot pixel step is just
rep movsb, retrace wait,rep movsw.
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.