Analysis model: gpt-5.5 xhigh

Physical by Physical Crew - Technical Dissection

physical.zip appears in the Hornet 1991 demo index as Physical by Physical Crew : ,,DAC,. The archive timestamps are from 1993 and there is no bundled NFO that settles the conflict, so the safest metadata is:

Release year: uncertain; Hornet catalog entry is 1991, archive evidence is 1993.

This is a compact real-mode DOS/VGA intro built around three external .MRH payload files. The visible show is one strong effect rather than a multipart demo: a yellow/blue perspective checker floor, a huge white outline scroller, and optional 8-bit sample playback through a resistor-ladder D/A converter on the parallel port.

Private contact details shown at the end of the demo are deliberately omitted. The public screenshot sheet below excludes that final page.

Physical DOSBox-X runtime contact sheet

The front-page card uses this public-safe contact sheet directly. I did not convert it into a GIF because the published visual evidence here is one composite sheet and the omitted final page should stay omitted.

Sources

Archive

The examined archive is physical.zip from the Hornet mirror:

physical.zip  172477 bytes

ZIP contents:

DATA1.MRH   209822 bytes  1993-12-27 13:59
DATA2.MRH    35840 bytes  1993-12-27 13:59
FONT.MRH     50400 bytes  1993-12-27 13:59
PCDEMO.EXE   13872 bytes  1993-12-27 13:59

Hashes:

25abba2ea91a7dac4b06a9812df3819fc61e19d62da033b6452bbced42e5b1d1  physical.zip
72e501fcc64c60090d9166ae0ee0cc6a43b7efb6c3404671068c6da19c8fbdf5  DATA1.MRH
0858fcb3f37c1caa50c63df6c36aa8550bb52e7e6b506bc961b3200bfe51c4c2  DATA2.MRH
ef52c74f2d2ab3115169a043e0370d5770f5b50f25bb03f764863bf09c7b7ec3  FONT.MRH
dbbacdbbf7a4d96767e45bc839f84016cb907054865290e3160b38a8986c7379  PCDEMO.EXE

The payload division is unusually clean:

PCDEMO.EXE  code, strings, BIOS wrappers, file/runtime layer, VGA/DAC loops
DATA1.MRH   six XOR-obfuscated sample chunks for the optional DAC music path
DATA2.MRH   4-plane 80x112-byte planar image block, used for the floor
FONT.MRH    30 glyph cells, 1680 bytes each, used by the huge scroller

The arithmetic is exact:

DATA1 chunks   DF5Ch + DEAEh + 6F22h + 5FDEh + 46BAh + 5FDAh = 3339Eh
DATA1 size     3339Eh = 209822 bytes
DATA2 size     8C00h  = 80 * 112 * 4 bytes
FONT size      C4E0h  = 30 * 690h = 30 * 1680 bytes

Runtime Capture And Timestamps

The runtime frames were captured with DOSBox-X:

DOSBox-X version       2026.01.02 Commit 59915c1
capture command        dx-capture /v PCDEMO.EXE
selected sound option  0, forget the music
capture format         MPEGTS H.264 + AAC
video stream           720x400, avg_frame_rate 7500/107, about 70.09 fps
capture duration       32.981333 seconds
timing zero            start of the capture command

Observed frame points:

00:00.500  black startup/transition
00:02.000  floor initialized, no large letters yet
00:05.000  small white block glyphs appear over the floor
00:10.000  huge outline letters, "VERY" visible
00:15.000  huge scroller continues, "YOU" and following letters visible
00:20.000  huge scroller continues, "HAVE" visible
00:25.000  huge scroller continues, "NEW Y..." visible
00:30.000  final private contact page appears; not included in public sheet
00:32.981  capture ends after the demo exits

The no-music path skips the timer/LPT playback setup, but it still exercises the full VGA visual sequence. The program exits on its own after the final text page.

Runtime-To-Code Concordance

The public contact sheet maps to the visual path of the no-music run. The 00:02.000 floor-only frame is after the BIOS mode 10h switch and the DATA2.MRH four-plane upload: 05D3h reads 80 bytes per row for 112 rows per plane, sets sequencer map masks 0100h, 0200h, 0400h, and 0800h, and leaves the checker/floor bitmap resident in VGA memory.

The 00:05.000 frame, where small white block glyphs begin to appear, maps to the first visible use of the huge-font streaming path. FONT.MRH is loaded by 04CEh, each glyph is addressed as glyph_index * 0690h, and the column loop at 1133h..1184h writes 112 rows into A800h twice: once at the current column and once at the wrapped duplicate +50h. That duplicate write is why the scroller can slide continuously without a full redraw.

The 00:10.000, 00:15.000, 00:20.000, and 00:25.000 frames are later states of the same CRTC-scrolled surface. The frame loop computes [0DF2h] from the 8000h..8050h base plus the table at 0CA0h, waits for retrace at 11BBh..11D1h, then writes CRTC start-address registers 0Ch and 0Dh at 11D3h..11F1h. The visible "VERY", "YOU", "HAVE", and "NEW Y..." fragments are therefore not separate screens; they are successive hardware-scroll positions over the same planar floor plus streamed font columns.

The omitted 00:30.000 contact page is after the visual sequence and is not part of the public contact sheet. The no-music capture also leaves the optional parallel-port DAC ISR unused, but the code path is still documented separately: choosing ports 0378h, 0278h, or 03BCh installs the 03CEh timer handler and streams decoded DATA1.MRH bytes to the selected LPT base.

MZ Layout

PCDEMO.EXE is a normal linked MZ executable, not an obvious packed EXE:

file size       13872 bytes
MZ image size   13872 bytes
header size       688 bytes = 002Bh paragraphs
relocations        164
min alloc        043Bh paragraphs
max alloc        A43Bh paragraphs
initial SS:SP    0373h:4000h
initial CS:IP    0000h:0BB7h
entry raw        0E67h
load image size  3380h bytes

The high relocation count and the Runtime error string point to a compiled high-level DOS runtime, but the important graphics and DAC work is direct 8086 code. Offsets below are load-image offsets, meaning:

load offset = raw file offset - 02B0h

The entry at 0BB7h calls two runtime initializers, then prints the DAC schematic/menu, accepts one key, initializes video/assets, and enters the visual loop:

0bb7  9a 00 00 aa 01        call far 01AA:0000  ; runtime/init
0bbc  9a 00 00 48 01        call far 0148:0000  ; screen/runtime init
...
0dcc  9a fa 02 48 01        call far 0148:02FA  ; key pending?
0dd7  9a 0c 03 48 01        call far 0148:030C  ; read key
0ddc  a2 d6 0e              mov [0ED6],al

The sound option maps directly to the DAC output port variable at 0DF0h:

0ddf  cmp byte [0ED6],30h
0de6  mov word [0DF0],0000h   ; 0, no music

0ded  cmp byte [0ED6],31h
0df4  mov word [0DF0],0378h   ; 1, LPT1

0dfc  cmp byte [0ED6],32h
0e03  mov word [0DF0],0278h   ; 2, alternate LPT base

0e0b  cmp byte [0ED6],33h
0e12  mov word [0DF0],03BCh   ; 3, LPT2-style base

If the input is not 0..3, the program exits through the runtime termination path. The visible menu also has an escape option for users who want to build the parallel-port D/A converter first.

BIOS Interrupt Wrapper

The local code uses a generic interrupt-call wrapper rather than inlining every BIOS call. The wrapper entry at 13EBh takes:

argument 1  interrupt number
argument 2  pointer to a register image

It fetches the interrupt vector from the IVT, loads registers from the supplied buffer, then far-returns into the interrupt handler:

13eb  push bp
13ec  push ds
13ef  pushf
13f0  mov  bx,0045h
13f3  push cs
13f4  push bx              ; return target after interrupt
13f5  xor  bx,bx
13f7  mov  ds,bx
13f9  mov  bl,[bp+0Ch]     ; interrupt number
13fc  shl  bx,1
13fe  shl  bx,1
1400  lds  bx,[bx]         ; DS:BX = interrupt vector
1402  push ds
1403  push bx
1404  lds  si,[bp+08h]     ; register block
1408  lodsw                ; AX
140a  lodsw -> BX
140d  lodsw -> CX
1410  lodsw -> DX
1413  lodsw -> BP
1416  lodsw -> saved SI
1419  lodsw -> DI
141d  lodsw -> ES
1423  cli
1424  retf                 ; execute selected interrupt handler

This is why many mode changes appear in the main code as register-buffer writes followed by call far 013E:000B. For example:

0e82  mov byte [0D8F],00h
0e87  mov byte [0D8E],10h
0e8c  push 10h
0e8f  push ds:0D8E
0e94  call far 013E:000B

That builds AX=0010h and calls int 10h, switching to VGA/EGA graphics mode 10h before the planar setup.

DATA1 Loader: XOR-Decoded Sample Chunks

The loader opens DATA1.MRH, allocates six blocks, reads each chunk, and XOR-decodes it in place with 55h.

The six chunk sizes are hardcoded in the caller:

02b7  load resource 1, size DF5Ch
02c3  load resource 2, size DEAEh
02cf  load resource 3, size 6F22h
02db  load resource 4, size 5FDEh
02e7  load resource 5, size 46BAh
02f3  load resource 6, size 5FDAh

The decode loop is inside the generic load resource routine:

01e8  mov ax,[bp+08h]
01eb  dec ax
01ec  mov [bp-04h],ax       ; last byte index
01ef  xor ax,ax

decode_loop:
01fb  inc word [bp-02h]
01fe  mov ax,[bp-02h]
0203  mov al,[bp+0Ah]       ; resource number
0208  shl di,1
020a  shl di,1
020e  les di,[0DC2h+di]     ; resource pointer
0212  add di,dx
0214  mov al,es:[di]
0217  xor al,55h
0219  mov cl,al
022b  les di,[0DC2h+di]
022f  add di,dx
0231  mov es:[di],cl
0234  mov ax,[bp-02h]
0237  cmp ax,[bp-04h]
023a  jne decode_loop

The pointer table starts at 0DC2h and stores far pointers by resource number. The length table starts at 0EBEh and stores the loaded chunk length. This table is later consumed by the DAC sequence builder.

FONT.MRH: Huge Glyph Stream

FONT.MRH is loaded into one contiguous block:

04ce  function load_font
050c  allocate CB70h bytes
0526  ES:DI = allocated block -> stored at [0ED2]
053d  open "Font.Mrh"
055a  read C4E0h bytes into [0ED2]

The file size is C4E0h, and the main loop uses a glyph stride of 0690h:

10f8  mov cx,0690h
10fb  imul cx
10fd  mov [0DACh],ax        ; glyph_index * 1680

That means the file holds:

30 glyphs * 1680 bytes per glyph

The frame loop streams one vertical byte-column from the current glyph per video frame. The visible font is very tall because each glyph column spans 112 rows:

1133  mov word [0DA8],0001h ; row counter starts at 1

column_loop:
113f  mov ax,[0DACh]
1142  les di,[0ED2h]        ; FONT.MRH buffer
1146  add di,ax
1148  mov dl,es:[di]        ; source byte-column row

114b  mov ax,0A800h
114f  mov di,[0DB0h]
1154  mov es:[di],dl        ; write column into circular page

1157  mov ax,[0DACh]
115a  les di,[0ED2h]
115e  add di,ax
1160  mov dl,es:[di]

1163  mov ax,0A800h
1167  mov di,[0DB2h]
116c  mov es:[di],dl        ; duplicate 80 bytes later for wrap

116f  add word [0DB0h],00A4h
1175  add word [0DB2h],00A4h
117b  inc word [0DACh]
117f  cmp word [0DA8h],0070h
1184  jne column_loop

The row pitch is 00A4h bytes. The second write, at [0DB2h], is initialized to the same x position plus 50h bytes and wraps at A2h. That is a standard circular scrolling trick: write the new column both in the visible column and in the wrapped duplicate so the CRTC start address can slide without needing a full-screen memory copy.

The next glyph is selected every 15-ish frames:

10e7  cmp word [0DB4h],000Eh
10ee  mov di,[0DB6h]
10f2  mov al,[di+005Ah]     ; next glyph index from text/script table
10f8  mov cx,0690h
10fb  imul cx               ; glyph_index * glyph_stride
1100  mov word [0DB4h],0
1105  inc word [0DB6h]
1109  cmp word [0DB6h],0C44h
1111  mov word [0DB6h],0    ; wrap text pointer

So the huge lettering is not a conventional byte-shifted text-mode scroller. It is a streaming font-column renderer: the script selects glyphs, each glyph contributes columns over several frames, and the CRTC scroll position moves the display across the circular buffer.

DATA2.MRH: Four-Plane Floor Upload

DATA2.MRH is exactly 80 * 112 * 4 bytes. The loader at 05D3h opens the file, then uploads it one VGA plane at a time:

0606  mov word [bp-84h],0100h ; first map mask value
0614  inc word [bp-86h]       ; plane counter

plane_loop:
0618  mov ax,1803h
061b  mov dx,03CEh
061e  out dx,ax              ; graphics controller setup

062d  mov ax,[bp-84h]
0631  add ax,0002h
0634  mov dx,03C4h
0637  out dx,ax              ; sequencer index 2, map mask in high byte

0640  mov word [bp-88h],0     ; row = 0

row_loop:
064c  read 80 bytes from Data2.Mrh into stack buffer
0666  mov word [bp-8Ah],0     ; x byte = 0

byte_loop:
0672  mov ax,0A000h
0676  mov ax,[bp-88h]
067a  mov cx,00A4h
067d  mul cx
067f  add ax,[bp-8Ah]
0683  mov di,ax
0686  mov al,es:[di]         ; latch/read existing byte
068d  mov di,[bp-8Ah]
0691  mov bl,[bp+di-00E0h]   ; DATA2 byte
0695  mov ax,0A000h
0699  row * 00A4h + x
06a9  mov es:[di],bl
06ac  cmp word [bp-8Ah],004Fh
06b3  cmp word [bp-88h],006Fh
06d6  cmp word [bp-86h],0003h
06dd  jmp plane_loop

The upload writes 80 bytes per row and 112 rows per plane. The code doubles the sequencer map mask each pass (0100h, 0200h, 0400h, 0800h before adding the index byte), so the file is stored as four consecutive plane images.

After loading all four planes, the routine restores normal write behavior:

06ba  mov ax,0F02h
06bd  mov dx,03C4h
06c0  out dx,ax              ; enable all four planes

06c1  mov ax,0003h
06c4  mov dx,03CEh
06c7  out dx,ax

06c8  mov ax,0005h
06cb  mov dx,03CEh
06ce  out dx,ax

06cf  mov ax,0FF08h
06d2  mov dx,03CEh
06d5  out dx,ax

This floor is not raycast every frame. It is a planar bitmap loaded once, then hardware-scrolled and column-overwritten by the text renderer.

CRTC Scroll Loop

The visible movement is synchronized to vertical retrace and driven by CRTC start-address updates:

11bb  mov dx,03DAh
11be  in  al,dx
11bf  and al,08h
11c3  jne 11c7h
11c5  jmp 11bbh             ; wait until in retrace

11c7  mov dx,03DAh
11ca  in  al,dx
11cb  and al,08h
11cf  je  11d3h
11d1  jmp 11c7h             ; wait until retrace ends

11d3  mov ax,[0DF2h]
11d6  and ax,0FF00h
11d9  add ax,000Ch
11dc  mov dx,03D4h
11df  out dx,ax             ; CRTC start address high byte

11e0  mov ax,[0DF2h]
11e3  and ax,00FFh
11e6  shl ax,8
11eb  add ax,000Dh
11ee  mov dx,03D4h
11f1  out dx,ax             ; CRTC start address low byte

The scroll address is computed from a base in the 8000h..8050h range plus a small table at 0CA0h:

1186  mov ax,[0DAEh]
1189  mov di,[0DBAh]
118d  shl di,1
118f  add ax,[0CA0h+di]
1193  sub ax,3340h
1196  mov [0DF2h],ax

121e  inc word [0DAEh]
1222  cmp word [0DAEh],8050h
122a  mov word [0DAEh],8000h

The table perturbs the start address while the base increments, which gives the floor/text surface a more elastic movement than a perfectly linear horizontal pan.

Optional DAC Timer ISR

If the selected output port is nonzero, the code builds a sample sequence from the decoded DATA1.MRH chunks, installs an interrupt-8 handler, programs the PIT, and outputs bytes to the chosen parallel port.

The ISR is at 03CEh:

03ce  push bp
03d8  pushf
03da  push ax
03e4  mov ds,cs:[0080h]     ; restore program data segment

03e9  mov bx,[0DE8h]
03ed  dec bx
03ee  jne output_sample

03f0  mov bx,[0DEAh]
03f4  dec bx
03f5  mov [0DEAh],bx
03f9  jne load_current

03fb  mov bx,001Fh
03fe  mov [0DEAh],bx
0402  mov bx,[0DECh]
0406  mov [0DEEh],bx        ; restart sequence pointer

load_current:
040a  mov si,[0DEEh]
040e  lodsw
040f  mov [0DE6h],ax        ; sample segment
0412  lodsw
0413  mov [0DEEh],si
0417  mov bx,ax             ; sample offset/count

output_sample:
0419  mov [0DE8h],bx
041d  mov es,[0DE6h]
0421  mov al,es:[bx]
0424  mov dx,[0DF0h]
0428  out dx,al             ; DAC byte to LPT port
042d  mov al,20h
042f  out 20h,al            ; PIC EOI
0433  sti
0437  iret

The timer setup around 1082h..10AFh does three things:

108d  set interrupt vector 08h to CS:03CEh
1093  enable speaker/PIT gate bits through port 61h
109d  program PIT channel 0 through ports 43h/40h
10a9  unmask IRQ0 through PIC mask port 21h

The shutdown path at 1237h..1267h restores the old timer vector and a slower PIT divisor before returning to text/contact output.

The no-music capture path leaves this handler unused, but the binary code is direct and unambiguous: the demo is prepared to stream 8-bit sample bytes to a home-built parallel-port DAC.

Overall Frame Model

The main visual loop is small:

startup:
    print DAC schematic/menu in text mode
    read key
    choose output port 0000h, 0378h, 0278h, or 03BCh
    switch to mode 10h through BIOS int 10h
    adjust CRTC registers 07h, 09h, and 13h
    load FONT.MRH into heap
    upload DATA2.MRH into four VGA planes
    load/decode DATA1.MRH chunks
    optionally install timer/DAC ISR

per visible frame:
    if current glyph column budget exhausted:
        fetch next glyph index from script
        set source offset = glyph_index * 0690h
    for 112 rows:
        read one font byte from FONT.MRH
        write it to A800:current_column
        write the same byte to A800:current_column+50h
        advance both destinations by 00A4h
        advance font source by one byte
    compute CRTC start address from 8000h..8050h base plus wobble table
    wait for retrace high then retrace low
    write CRTC start address high/low registers
    advance wobble/base counters

The result looks more expensive than it is. The floor is a preloaded planar bitmap. The huge text is streamed as byte columns. The smooth motion comes from VGA start-address hardware plus a small table-driven wobble, not from redrawing the whole screen each frame.