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.

Anti-svd recovered runtime map

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

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:

  1. Opens tu.dat.
  2. Seeks to the supplied offset.
  3. Reads a 30h-byte header to 7369h.
  4. Reads 3E0h bytes to 5398h.
  5. Reads 80h bytes to 5798h.
  6. Seeks to original offset + 0490h.
  7. Reads [738Ah] << 10 bytes into segment 074Dh.
  8. Streams F000h-byte chunks into the caller's DS, advancing DS by 0F00h paragraphs after every full chunk.
  9. 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.