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.

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
- 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/physical.zip
- Scene.org file page: https://files.scene.org/view/mirrors/hornet/demos/1991/physical.zip
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.