Analysis model: gpt-5.5 xhigh
Anti-svd by The Untouchables - Technical Dissection
Anti-svd is a 1991 MS-DOS VGA demo by The Untouchables. The Hornet 1991
index lists it as tudemo.zip 144334 *** Anti-svd by The Untouchables.
Release year: 1991
This is an early, packed, data-file-driven VGA demo. The restored executable is
not just a linear mode-13h picture viewer: it switches between chained and
unchained VGA access, uses external tu.dat resource blocks, contains two
bitstream image decoders, does retrace-paced DAC fades, stages masked image
blocks through scratch segments, scrolls large screen windows with CRTC start
addresses, and finishes in a tight four-plane strip loop at 57FFCh.

The image above is a static-analysis map, not a literal runtime screenshot. It summarizes the recovered loader and inner-loop topology from the unpacked MZ code.
Sources
- Hornet 1991 demo index: https://ftp.scene.org/mirrors/hornet/demos/1991/00_index.txt
- Scene.org archive: https://ftp.scene.org/mirrors/hornet/demos/1991/tudemo.zip
- Scene.org file page: https://files.scene.org/view/mirrors/hornet/demos/1991/tudemo.zip
Archive
The examined archive is tudemo.zip from the Hornet mirror:
tudemo.zip 144334 bytes
ZIP contents:
demosite.com 1104 bytes 1992-10-21 18:43
tu.dat 196435 bytes 1991-06-30 03:02
tu.exe 25723 bytes 1991-06-30 03:19
Hashes:
2bed2d64b303691a9c7e160567b522135db97df5f6af44d8ba8d7c327c9664de tudemo.zip
ae4377ea5ad7ace3149992f111f8384a81a71b1d658cd2fe453291ab14b26d6f demosite.com
94015ffafcd1ea2167dccb7f2a9be2d5e291852cd9f6e2ef03a0bda06345d020 tu.dat
5e1e424cf2df1bdb3f0dda0a853bfee7c988093c8b087bba7199deba0d6ffe9a tu.exe
21213584a29d7b319a8b26a2249c4b5a612fb963c6aee5b875931dfb94809df3 tu-unpacked.exe
demosite.com is a TheDraw-style demo-site advertisement screen. It is not
part of the demo runtime.
Runtime Capture Status
This article does not contain exact observed playback timestamps. DOSBox-X was available for unpacking/probing, but I do not yet have a trustworthy timed capture of this production's visible sequence. The sequence below is therefore code-derived:
setup text user paced, before graphics
three intro resource fades after speed/sound setup
block/masked image stages scripted, retrace paced
sliding screen windows scripted, retrace paced
main strip loop repeated until keyboard flag
final resource screen after leaving the main loop
The code uses VGA status port 03DAh and a timer mailbox for pacing, so
wall-clock timing depends on the chosen speed setting, host emulation, and
whether the sound backend is installed. Exact effect timestamps should be added
from a video or DOSBox capture later.
Packing And MZ Layout
The distributed tu.exe is a 25723-byte MZ executable with an EXEPACK-family
stub. The packed file contains the familiar failure text:
Packed file is corrupt
UNP restored an expanded MZ image of 363056 bytes:
original file size 25723 bytes
original header 512 bytes
original load image 25211 bytes
original entry 060A:0012
original stack 58AD:0080
unpacked file size 363056 bytes
unpacked header 320 bytes
unpacked load image 362736 bytes
unpacked relocations 71
unpacked entry 5758:0EBE
unpacked stack 5658:1000
entry file offset 5857Eh
The expanded code still contains relocation records. Instructions such as
mov ax,5040h, call 417D:013Ch, and call 0000:73A3h are relocation
targets; DOS adds the program load segment at runtime. In this note, code
labels such as 57FFCh are raw file offsets in the restored executable.
There is one important recovery caveat. Some runtime data references, especially
the resource-offset pointers at 5040:EBFE through 5040:EC22, lie beyond the
static restored MZ image. The code clearly uses them as four-byte seek offsets
for tu.dat, but the recovered file does not contain their initialized values.
Those entries are treated here as runtime-populated resource pointers, not as
known static constants. The control flow and inner loops are still recoverable.
Setup Flow
The entry point starts at 5857Eh. It parses the PSP command tail for:
/speed:n
/sound:p
If either switch is missing, the demo asks interactively:
1= 6 Mhz AT
2= 8 Mhz AT
3=10 Mhz AT
4=12 Mhz AT
5=16 Mhz 386
6=20 Mhz 386
7=25 Mhz 386
1=Sound Blaster
2=Covox on LPT1
3=Covox on LPT2
4=Nosound
The speed selection indexes a table at CS:0C4A; the selected word minus
0500h is stored at CS:0C48. Later the program divides that value by 60 and
stores the per-frame pacing threshold at CS:0C46.
The sound choice is stored at CS:0C58:
0 Sound Blaster path
1 Covox on LPT1
2 Covox on LPT2
3 no sound
The keyboard handler is installed by a far call to 5758:0045, raw file
offset 57705h. The handler at 576D2h reads scancodes from port 60h,
sets a byte at CS:0004, acknowledges the keyboard controller through port
61h, sends EOI to PIC port 20h, and returns with iret:
space scancode 39h -> CS:0004 = 1
escape scancode 01h -> CS:0004 = 2
key 1 scancode 02h -> CS:0004 = 3
The main loop only tests for value 1, so Space is the normal "continue/exit
this loop" key in the recovered path.
After setup the code switches to INT 10h, AX=0013h, clears the DAC to black,
puts VGA into a planar/unchained variant, clears A000, flips back to the other
VGA mode variant, and begins loading resources.
VGA Mode Helpers
The two VGA setup helpers are a good sign that this is not plain mode 13h code.
57F74h modifies Sequencer, Graphics Controller, and CRTC bits:
Sequencer 04h: clear bit 3, set bit 2
GC mode 05h: clear bit 4
GC misc 06h: clear bit 1
CRTC 14h: clear bit 6
CRTC 17h: set bit 6
57FAAh applies the complementary settings:
Sequencer 04h: set bit 3, clear bit 2
GC mode 05h: set bit 4
GC misc 06h: set bit 1
CRTC 14h: set bit 6
CRTC 17h: clear bit 6
The clear routines then rely on the Sequencer map mask:
; 57F59h - clear A000 through all planes
mov dx,03C4h
mov al,02h
out dx,al
inc dx
mov al,0Fh
out dx,al
mov ax,0A000h
mov es,ax
xor di,di
xor ax,ax
mov cx,8000h
rep stosw
57F3Eh is the same idea for segment A400h with 6000h words.
tu.dat Resource Loader
There are two close loader entry points in segment 417Dh.
417D:00E5, raw file offset 419F5h, opens a filename supplied by DS:SI,
reads EA60h bytes into CS:019Ch, closes the file, then decodes the block.
417D:013C, raw file offset 41A4Ch, opens hardcoded tu.DAT, seeks to the
32-bit offset stored at caller DS:SI, reads EA60h bytes into CS:019Ch,
closes the file, then decodes the block.
The seek/read path is:
mov dx,0135h ; "tu.DAT" in the loader segment
mov ah,3Dh
int 21h ; open
mov dx,[si] ; low word of resource offset
mov cx,[si+2] ; high word
mov ax,4200h
int 21h ; seek absolute
mov cx,EA60h
mov dx,019Ch
mov ah,3Fh
int 21h ; read packed block
After the read, byte CS:00E2 selects the decoder:
0 -> 417D:0040 linear byte output
1 -> 417D:008F planar pixel output
The wrapper at 57F1Ch calls 417D:013C with ES=A000, then copies a
768-byte palette from the caller's data area into CS:00B6:
push di
mov ax,0A000h
mov es,ax
call 417D:013Ch
mov ax,5040h
mov ds,ax
pop di
mov si,di
mov di,00B6h
mov cx,0180h
rep movsw ; 0300h palette bytes
This explains the repeated call pattern near startup:
SI=EC0E DI=0300 load resource, palette at 5040:0300
SI=EC16 DI=0600 load resource, palette at 5040:0600
SI=EC1A DI=0900 load resource, palette at 5040:0900
SI=EBFE DI=0000 load resource, then carve blocks
SI=EC02 DI=0000 load resource, fade it in
The four-byte ECxx resource offsets are the unresolved runtime-populated
values mentioned above.
Bitstream Decoder
The linear decoder starts at raw file offset 41950h, corresponding to
417D:0040. It emits exactly FA00h bytes, a full 320x200 indexed image.
The bit cursor is stored in CL. The compressed stream starts at CS:019Eh,
so the first two bytes of the read block are not emitted as pixels. Each symbol
uses a decision bit extracted by loading two source bytes into AX and rotating
through carry:
mov si,019Eh
mov di,0000h
xor cx,cx
loop:
cmp di,FA00h
jae done
cmp cl,8
jne have_bit_pos
xor cl,cl
inc si
have_bit_pos:
lodsb
mov ah,al
lodsb
rcl ax,cl
push cx
mov cl,9
rcl ax,cl
pop cx
jae literal
If carry is clear, the low byte is a literal:
literal:
inc cl
stosb
dec si
jmp loop
If carry is set, the decoder takes AL as the repeated value, extracts a
second bitfield for the count, adds one, and writes a run:
run:
mov dh,al ; repeated value
dec si
lodsb
mov ah,al
lodsb
rcl ax,cl
push cx
mov cl,0Ah
rcl ax,cl
mov cl,al ; count byte
mov al,dh
xor ch,ch
inc cx ; run length = count + 1
rep stosb
pop cx
dec si
inc cl
jmp loop
417D:008F, raw file offset 4199Fh, uses the same token grammar, but replaces
each stosb with a planar pixel plotter at 4192Bh.
The planar plotter computes the VGA map mask from the low two bits of the linear pixel index:
; 4192Bh
push cx
push di
push dx
push ax
mov dx,03C4h
mov al,02h
out dx,al
inc dx
mov cx,di
and cl,03h
mov al,01h
shl al,cl
out dx,al
pop ax
pop dx
shr di,1
shr di,1
add di,cs:[0000h]
stosb
pop di
inc di
pop cx
ret
So the decoder walks a logical chunky pixel index while physically writing one byte into one of four VGA planes.
Palette Machinery
The fade routines use real DAC reads/writes rather than just copying prepared tables.
57E90h clears the complete 256-color DAC:
mov dx,03C8h
xor al,al
out dx,al
inc dx
mov cx,0300h
out_loop:
out dx,al
loop out_loop
57D7Ch fades the current palette down. It first reads the DAC through
03C7h/03C9h into CS:00B6, then performs 64 outer fade steps. For every step
it writes the palette in four chunks of C0h bytes, decrementing nonzero
components and clamping values above 3Fh:
outer fade steps 40h
chunks per step 4
bytes per chunk C0h
total DAC bytes/step 300h
sync 03DAh wait between chunks/steps
57DFEh is the inverse-ish fade-in helper. It builds a temporary palette at
CS:03B9 from the saved palette at CS:00B6, using the current outer value as
a threshold, then outputs the full 300h bytes in four retrace-paced chunks.
The result is expensive but visually smooth for 1991: the code does not merely swap one palette into place; it repeatedly writes the DAC during vertical timing windows.
Block Carving And Masked Compositing
After loading one full image resource, 5817Ch carves eight 72x61 blocks from
A000 into segment 174Dh. The source positions come from a word table at
CS:0AACh, but the table is runtime-populated in the recovered image.
The loop structure is simple and fixed:
for block in 0..7:
SI = source_offset_table[block]
for row in 0..60:
copy 72 bytes from A000:SI to 174D:DI
SI += 320
The paired compositor at 581BAh treats the selected block pair in 174Dh as
two images: an AND mask followed by OR data. CX selects the block pair:
block pair stride 1128h bytes
mask size 72 * 61 = 1128h
image size 72 * 61 = 1128h
destination segment 26EDh
row stride 320 bytes
The first pass applies the mask:
lodsb
and es:[di],al
inc di
The second pass ORs in the image bytes:
lodsb
or es:[di],al
inc di
The second block family uses 5820Ch and 5824Ch. It carves four 83x59 blocks
and composites them with the same mask-then-image idea:
block pair stride 1321h bytes
mask size 83 * 59 = 1321h
image size 83 * 59 = 1321h
destination stride B6h bytes in the visible window
This is a classic early VGA trick: precompute or decode rectangular sprites as mask/data pairs, then update the background by ANDing away pixels and ORing in the new shape.
Sliding Window Copies
The routines at 584C9h and 58544h implement a large vertical-window reveal.
58544h copies a 72-row, 91-word-wide window from segment 174Dh into A000.
The destination is:
DI = BX * 320 + 005Ah
Each row copies 5Bh words, then advances both source and destination by
8Ah bytes:
mov ax,bx
mov cx,0140h
mul cx
mov di,ax
add di,005Ah
mov cx,0048h
row:
push cx
mov cx,005Bh
rep movsw
pop cx
add si,008Ah
add di,008Ah
loop row
584C9h restores only the changed rows from segment 588Fh back to A000. It
compares the new BX against CS:0DDE, chooses forward or backward movement,
and copies only the delta. That makes the slide cheaper than redrawing the
whole 320x200 page every tick.
Later, a separate section also writes CRTC start-address registers 0Ch/0Dh
while moving BX, giving a hardware-scroll component to the reveal.
Small Point/History Effect
5835Ah is a compact 64-point update fed by low-memory bytes at 0000:729E.
It waits until 0000:72DE >= 3Fh, then walks 64 entries:
old y = CS:0C5A[i]
clear A000[i + old_y * 320]
new y = low_memory_stream[i] >> 3
CS:0C5A[i] = new_y
plot A000[i + new_y * 320] = FFh
The address multiplication is done as y*64 + y*256, i.e. y*320:
mov al,cs:[bx]
xor ah,ah
mov cl,06h
shl ax,cl ; y * 64
add di,ax
shl ax,1
shl ax,1 ; y * 256
add di,ax ; total y * 320
After updating all 64 columns it resets 0000:72DE to zero. The data producer
is an interrupt-side path, so this looks like a small sampled/history-driven
visual element rather than a standalone random starfield.
Main CRTC Strip Loop At 57FFCh
The most distinctive inner loop is called once per main-loop tick from
589A0h. It draws into a planar A000 area and scrolls it with CRTC start
address changes.
State variables in segment 5040h:
13E8h frame/tick counter
13EAh ring offset, wraps at 0A00h
13ECh phase within current strip source
13EEh derived strip selector byte
13EFh source-frame index
The first block updates the source-frame index. It uses 13EEh to index a
threshold table at 0F6Ch, divides the selected threshold by four, and compares
it against the current phase:
mov dx,[13ECh]
mov bl,[13EEh]
xor bh,bh
add bx,0F6Ch
mov al,[bx]
shr al,1
shr al,1
xor ah,ah
cmp dx,ax
jb no_advance
inc word [13EFh]
...
mov word [13ECh],0
When the frame index reaches CS:093A, it wraps to zero and clears two history
bytes at CS:0936 and CS:0937.
Next it clears the old strip in all planes:
mov dx,03C4h
mov al,02h
out dx,al
inc dx
mov al,0Fh
out dx,al
mov di,[13EAh]
add di,7D00h
mov cx,20h
xor al,al
clear_loop:
stosb
add di,4Fh
loop clear_loop
This clears 32 bytes separated by 4Fh bytes. In the unchained layout that is
a vertical-ish strip in VGA memory, not 32 contiguous screen pixels.
Then it scrolls the display by changing CRTC start address registers:
start = [13EAh] + 7D00h + 2
out 03D4h,0Dh
out 03D5h,low(start)
out 03D4h,0Ch
out 03D5h,high(start)
The next selector byte is generated from the source stream at
[CS:0938 + 13EFh] and two one-byte history values:
al = stream[13EFh]
CS:0937 = al
al ^= CS:0936
rol al,4
al ^= 3Eh
al -= 20h
[13EEh] = al
That byte is used to choose a source column in segment 174Dh. The source
offset is:
SI = (selector * 4) + (phase * 4)
The four-plane draw is then explicit. For plane CL = 0..3, it writes map mask
1 << CL, copies 32 source bytes with source stride 1Fh and destination
stride 4Fh, then repeats the same strip 0A00h bytes lower:
for plane in 0..3:
set sequencer map mask = 1 << plane
copy 32 bytes:
A000:DI = 174D:SI
SI += 1Fh
DI += 4Fh
copy duplicate 32 bytes at DI + 0A00h:
A000:DI = 174D:SI
SI += 1Fh
DI += 4Fh
if DI >= 9100h:
DI -= 1400h
SI = original_SI + 1
Finally:
inc word [13EAh]
if [13EAh] == 0A00h:
[13EAh] = 0
inc word [13ECh]
ret
So the effect is a hardware-assisted ring scroller/strip animator. CRTC start
address moves the viewport through a 0A00h-byte circular region while the CPU
refreshes only one new vertical strip per tick. The duplicate write at +0A00h
keeps the wrap area valid so the CRTC scroll can cross the ring boundary
without exposing stale data.
Main Loop And Timing
The main loop at 5898Ah sets:
DS = 5040h
ES = A000h
CX = 0A82h
PIC mask = FCh
FCh leaves timer and keyboard enabled while masking the other IRQs. Every
iteration:
inc word [13E8h]
call 57FFCh
Then it waits around VGA status port 03DAh and a timer counter at
0000:735E:
wait until not in vertical retrace
wait until 0000:735E > CS:0C46 or retrace starts
clear 0000:735E
unmask PIC
if CS:0004 != 1:
loop
The check for CS:0004 == 1 is why the IRQ 09 handler matters. The visual loop
has no DOS keyboard polling inside it; keyboard input is reduced to one byte by
the interrupt handler.
On exit it fades down, restores a normal mode-13h state, mutes/restores the
sound backend, loads a final resource through the same 57F1Ch path, fades it
in, waits for another keyboard flag, restores IRQ 09, switches to text mode
03h, and exits through INT 21h, AX=4C00h.
Bulk Loader At 73A3h
The far call at 58915h goes through 0000:73A3, raw file offset 74E3h.
This is a larger tu.dat loader used after the earlier picture stages.
Input:
CX:DX absolute offset inside tu.dat
DS caller destination segment
The loader:
- Opens
tu.dat. - Seeks to the supplied offset.
- Reads a
30h-byte header to7369h. - Reads
3E0hbytes to5398h. - Reads
80hbytes to5798h. - Seeks to original offset +
0490h. - Reads
[738Ah] << 10bytes into segment074Dh. - Streams
F000h-byte chunks into the caller's DS, advancing DS by0F00hparagraphs after every full chunk. - Applies a small relocation/adjustment table at
53A6h, up to 31 entries.
The repeated F000h chunk size is deliberate: it is just below a 64 KB segment
limit, so the loader can stream a large resource without crossing a segment
boundary in one DOS read.
Sound Path
The startup code branches on CS:0C58.
For Sound Blaster it calls the sound setup path at 0000:7345 and then
0000:72FA. For Covox it reads the BIOS LPT base address from 0040:0008 or
0040:000A, stores it in DX, and calls 0000:7316. The no-sound setting
skips this setup and lets the visual loop run with only timer/keyboard pacing.
The visible setup text says no sound "will improve the speed but not the demo", which matches the code structure: audio installs interrupt-side work, while the visuals still run from the same retrace/timer loop.
The sound code is larger than the surrounding VGA helpers and includes sample
or pattern state tables around 5798h, 581Ah, and 68C7h. I have not treated
it as a full music-driver dissection here because the interesting demo-specific
part is the graphics/resource pipeline; the audio path is a reused driver-style
subsystem.
What The Demo Is Doing
At a high level, Anti-svd is built around a compact data pipeline:
packed MZ -> setup -> VGA mode helpers -> tu.dat resource blocks
-> bitstream picture decode -> palette fade
-> block carve/masked composite/slide
-> planar CRTC strip loop
The early resource stages are full-screen or large-window effects, decoded from
tu.dat and made presentable with palette fades. The middle stages carve and
composite rectangular elements through scratch segments. The final loop is more
engine-like: it maintains a scrollable unchained VGA ring and draws only the
new strip needed for the next frame.
The main unresolved piece is the initialized tu.dat offset table above the
static restored EXE image. A proper runtime memory dump from the original packed
program would let this analysis name the exact file offsets for each picture and
probably produce literal screenshots from the decoded resources. The recovered
code is already enough to describe the inner loops, but not enough to label
every asset by offset without guessing.