Analysis model: gpt-5.5 xhigh
Psychic Link / JUICE by Psychic Link - Technical Dissection
Scope
This is a static binary dissection of JUICE v1.1 (the second act) by Psychic
Link, the fixed post-party version of the demo that placed second in the
Assembly 1995 PC demo competition. The Assembly archive lists the production as
Psychic link by Juice; the shipped NFO and executable identify the demo as
JUICE by Psychic Link.
Useful public references:
- Assembly archive entry: https://archive.assembly.org/1995/pc-demo/psychic-link-by-juice
- Pouet entry: https://www.pouet.net/prod.php?which=69
- Scene.org archive: https://files.scene.org/view/parties/1995/assembly95/demo/juice11.zip
- Scene.org bugfix archive: https://files.scene.org/view/parties/1995/assembly95/demo/juicefix.zip
This is not a source reconstruction. The notes below come from the shipped archive, the NFOs, UNP expansion of the executable, MZ/overlay carving, FLIB resource-table parsing, string inspection, and targeted disassembly of the runtime and hot loops. Old BBS/contact details in the NFOs are intentionally not reproduced.
Offsets in code sections are offsets in the UNP-expanded MZ load image unless
explicitly marked as full-file offsets. The appended FLIB resource offsets are
absolute offsets in the released packed JUICE.EXE.
Examined Files
Primary archive:
290961e9a724ccd59a0a555ec2684074e96336bef855da60fc1f9e53d6ca116e juice11.zip
Archive contents:
JUICE.NFO 10787 bytes
JUICE.EXE 1867968 bytes
PL95.NFO 10713 bytes
JUICIER.BAT 137 bytes
Hashes of extracted files:
c81db33e1c27cb3d290acb8939b383804c50443ccec5c89537f513398d0a60fa JUICE.EXE
e4866f160083e2724814cef4eae339514bf418d78d843e8138b91ac799a89747 JUICE.NFO
e280490f10b22b261ae104c85d056c16860b5f74e0bfa40bfd47564e7e7dd973 PL95.NFO
3f26bd6ad438828c9cfd8506ba4d5eeb950181d8165ad36b4354a88ba842bb7d JUICIER.BAT
The later bugfix archive is useful for provenance:
995f6b7ba90b9c6189ce2a14b53d216318be853366188630f863c83f72241e84 juicefix.zip
It contains a patcher plus BUGFIX.DAT. The first 186492 bytes of the
juice11.zip JUICE.EXE are byte-identical to BUGFIX.DAT:
e7b406498a7e2004b5e2ad3d0b2e4dd3c2853c27378ea49d03cb6b9dbe9ba390 BUGFIX.DAT
e7b406498a7e2004b5e2ad3d0b2e4dd3c2853c27378ea49d03cb6b9dbe9ba390 JUICE.EXE first 186492 bytes
So the v1.1 archive already contains the fixed executable image. The separate bugfix archive proves what changed, but it is not a different analysis target.
JUICIER.BAT is just an infinite-loop wrapper:
@echo off
cls
echo This batch file runs JUICE by Psychic Link on an infinite loop.
pause
:herewego
juice l
goto herewego
The l command-line parameter matters: the executable treats any parameter as
permission to suppress interactive setup and autodetect from the environment.
NFO Facts
JUICE.NFO identifies the release as:
J U I C E v1.1 (the second act)
Copyright 1995 Psychic Link
Released 23 Oct 1995
It states that the party version came second at Assembly 1995 and that v1.1 was
released to fix bugs, improve setup, improve compatibility, and improve
synchronization. BUGFIX.NFO is more specific: the fix addresses a no-sound
crash after the tunnel and restores/improves VGA synchronization that had been
removed for the Assembly projector.
The requirements are:
VGA
386/486/Pentium
4 MB RAM
The credited division of work is unusually useful for mapping binary sections:
Statix:
Utilities: polygon routines for all screens, player/tracker, PMODE
Screens: intro, credits, ducks, torus, rasters, vast, juice pillar
S-Cubed:
Utilities: object designer for the terrain screen
Screens: plasmas, greets, terrain, tunnel, Mandelbrot zoom
Skywalker:
Music, written with DisorderTracker 2.0
Tran:
PMODE interface thanks
The executable strings match those statements: it contains sound setup strings
for no sound, Sound Blaster, Sound Blaster 16, and Gravis UltraSound, plus
resource names for juice.plm, greetext.raw, terrpath.bin, starcrds.bin,
ducko.bin, 2faceo.bin, raster1.raw, realtime.raw, and the terrain
.plo object set.
Executable Shape
Released JUICE.EXE:
file size 1867968 bytes
MZ image 186492 bytes
MZ header 512 bytes
load image 185980 bytes
overlay 1681476 bytes
entry 2d4b:0010, linear 0x2d4c0
relocations 0
The entry at 0x2d4c0 is a packed executable stub. It relocates and expands the
actual image, and contains the fallback text:
Packed file is corrupted
UNP expands the MZ code while preserving the appended FLIB resources:
file size 1969444 bytes
MZ image 287968 bytes
MZ header 272 bytes
load image 287696 bytes
overlay 1681476 bytes
entry 0000:040c, linear 0x040c
relocations 61
The expanded executable hash used for disassembly:
ca8cf26bc6b16ec7fab90078e20eae30a8855694860b9323b200c83050fe2cbf JUICE.EXE expanded by UNP
The appended overlay hash is identical before and after UNP:
2e3b1add84f031a86b4bac1241c479a8ef7a8e0e5a04ac33cb1f5349bc169275 overlay
So the packer only wraps the executable image. The resource container at the end is already in its final form.
FLIB Container
The executable ends with:
uint32 count = 0x24
ASCII magic = FLIB
The directory starts at full-file offset 0x1c7c38. Each record is 32 bytes:
struct flib_record {
char name[16]; /* NUL-padded uppercase filename */
uint32_t file_offset; /* absolute offset in the released JUICE.EXE */
uint32_t size;
uint32_t checksum_or_hash;
uint32_t flags_or_reserved; /* observed as zero */
};
The important detail is that file_offset is absolute in the full packed
JUICE.EXE, not relative to the overlay. Treating it as overlay-relative makes
late records look invalid.
Parsed entries:
00 MAIN.EXE off 00000000 size 0002d87c
01 PL.RAW off 0002d87c size 00005300
02 PR.RAW off 00032b7c size 000031e0
03 PHONG.RAW off 00035d5c size 00010300
04 INTRO2.FAD off 0004605c size 00010000
05 GREETEXT.RAW off 0005605c size 00011760
06 FONT3.RAW off 000677bc size 0000b840
07 FACE.RLE off 00072ffc size 00003694
08 JUICE.RLE off 00076690 size 00004154
09 1995END.RLE off 0007a7e4 size 00000d38
10 1995END2.RLE off 0007b51c size 0000169c
11 MAPS2.RAW off 0007cbb8 size 00010300
12 VAST.FAD off 0008ceb8 size 00010000
13 RASTERS.PAL off 0009ceb8 size 00000300
14 PHONG2.RAW off 0009d1b8 size 00010300
15 TERRPATH.BIN off 000ad4b8 size 00000540
16 POST1.PLO off 000ad9f8 size 00000834
17 POST2.PLO off 000ae22c size 00000844
18 POST3.PLO off 000aea70 size 00000be4
19 RADAR.PLO off 000af654 size 0000133c
20 WSIDE1.PLO off 000b0990 size 00000134
21 WSIDE2.PLO off 000b0ac4 size 00000134
22 WCORNER1.PLO off 000b0bf8 size 00000174
23 WCORNER2.PLO off 000b0d6c size 00000174
24 WCORNER3.PLO off 000b0ee0 size 00000174
25 WCORNER4.PLO off 000b1054 size 00000174
26 HATCH.PLO off 000b11c8 size 00000544
27 TERRTEXT.RAW off 000b170c size 00010300
28 TERRAIN.FAD off 000c1a0c size 00010000
29 STARCRDS.BIN off 000d1a0c size 000041a0
30 JUICE.PLM off 000d5bac size 000c0e7c
31 2FACEO.BIN off 00196a28 size 0000e51c
32 DUCKO.BIN off 001a4f44 size 0000495c
33 ENDSCRO.BIN off 001a98a0 size 0000ce18
34 RASTER1.RAW off 001b66b8 size 00010000
35 REALTIME.RAW off 001c66b8 size 00001580
MAIN.EXE is the packed MZ image at the beginning of the file, matching the
patched BUGFIX.DAT content. The demo therefore ships as a self-contained
FLIB archive whose first member is the executable itself.
FLIB Loader
The resource loader is small and practical. It uses DOS file services through the PMODE interface rather than memory-mapping the appended data.
At 0x4892 the loader opens the library file whose name pointer is passed in
ESI:
mov word [0x10f4], 0x3d00 ; DOS open
mov word [0x10ec], si ; filename pointer
mov word [0x10fc], 0x00b2 ; segment/interface selector
mov al, 0x21
int 0x33 ; PMODE DOS-call gate
mov [0x3a74], ax ; file handle
It then seeks to -8 from EOF and reads the trailer:
seek EOF - 8
read 8 bytes at linear 0
cmp dword [0x0004], 'FLIB'
If the magic is absent, it jumps to the fatal StatixLoader: LibFile corrupt!
path. If valid, it reads the directory:
count = dword [0]
dir_bytes = count << 5
dir_offset = -(dir_bytes + 8) from EOF
read dir_bytes to directory buffer
[0x3a70] = directory buffer
[0x3a76] = count
[0x3a86] = 2 ; library is open and indexed
The filename lookup at 0x45ce compares the requested name against each
16-byte directory name. The compare is partly unrolled, uppercases ASCII
a..z on the fly, and stops early on NUL:
for each record:
for i in 0..11 unrolled:
al = request[i]
if 'a' <= al <= 'z': al -= 0x20
if al != record.name[i]: try next record
if al == 0: match
The main load path at 0x4754 calls that lookup, seeks to the file offset, and
then returns the size in ECX:
record = find(name)
file_offset = record.offset
size = record.size
seek file_offset from BOF
return ECX = size
Callers then pass ECX, destination EBX, and call 0x4806. That routine
reads through a tiny 0x800-byte buffered reader. If the buffer is empty, it asks
DOS for another 0x800 bytes, then copies bytes to the caller's destination:
while bytes_left:
if buffer_index == 0:
read 0x800 bytes into [buffer + 0x800]
*dest++ = buffer[0x800 + buffer_index]
buffer_index = (buffer_index + 1) & 0x7ff
This loader explains the repeated per-part pattern:
call 0x4754 ; find/seek named resource
call 0x4733 ; get resource size
call 0x3b04 ; allocate
call 0x4806 ; read
call 0x45a7 ; close/end loader operation
Protected-Mode Bootstrap
The expanded entry at 0x040c enters a Tran/PMODE-style bootstrap. The strings
and I/O shape identify the responsibilities:
Sorry - Get a 386!
No low memory!
Where's the VCPI server bub?
No high memory!
Couldn't enable A20 gate...
The 8259 vectors have been remapped.
The bootstrap:
- checks for a 386-class CPU;
- enables A20 using port
0x92, keyboard-controller ports0x64/0x60, and a memory/PIT fallback test; - checks VCPI through
int 2fh, ax=4300h; - uses VCPI
int 67hcalls during protected-mode setup; - remaps the 8259 PIC vectors;
- builds the runtime call gate used later as
int 33hwithAL=21h.
The application entry is around 0x5128. It enables interrupts, initializes
keyboard/timer/sync, prints the loading/setup messages, installs the audio
callback pointer, loads juice.plm, and then calls the screen sequence.
The main effect sequence visible in the dispatcher is:
0x212ae intro / two-image fade-scaler setup and loop
0x22ebe greets text pre-render
0x21fbb RLE sprite / logo screen loop
0x28496 combined plasma/object sequence
0x22dc1 free precomputed tunnel/plasma table
0x22fb9 free greets strip
0x28bce free object/polygon buffers
0x34dd3 terrain resource sequence
0x2618c free shared plasma buffers
0x37428 object-heavy sequence using star/3D data
0x38673 "chimmer.mod" / scanline motion sequence
0x45dd4 later end/final sequence
0x2e532 later resource/effect sequence
0x2b208 later resource/effect sequence
0x2e5d8 final cleanup/free path
Only some of these names are grounded by visible resource names or NFO credits. Where the binary does not carry a clear title, I am treating the label as a mechanical description of the code, not as a claimed scene name.
Main Scheduler And Memory Guard
The dispatcher has a useful self-check around each effect. It snapshots a memory
state using 0x3b48, runs the effect, snapshots again, and reports a failure if
the value changed:
call 0x3b48
mov [0x44d0], eax
mov dword [0x1e9c], 0x0000b22e
call effect
mov dword [0x1e9c], 0x0000b22e
call 0x3b48
cmp eax, [0x44d0]
jne memory_error_path
cmp byte [0x1a3b], 0
jne cleanup
The error path computes an A0000h mapping delta, calls a diagnostic routine,
uses PIT channel 2 to beep/delay, waits for keyboard input several times, and
then jumps to global cleanup. This is not a debug symbol artifact; it is shipped
runtime leak detection around the demo parts.
The keyboard abort flag is byte [0x1a3b]. The timer callback pointer is
[0x1e9c].
Timer, Sync, And Frame Copy
The IRQ handler at 0x3e86 increments the global tick counter and calls the
current music/timer callback:
pusha
push ds
push es
inc dword [0x1e31] ; global tick
sti
wait/poll 0x3da ; VGA status timing
out 0x43, 0x36 ; PIT mode
out 0x40, low([0x1e71])
out 0x40, high([0x1e71])
send EOI to PIC
if [0x1e9c] != 0: call [0x1e9c]
iret
The setup routine at 0x3eda measures the display against vertical retrace:
wait until 0x3da bit 3 is low
wait until 0x3da bit 3 is high
program PIT
sample counter
dx = -sample
dx -= dx >> 7
[0x1e71] = dx
That divisor is later used by the IRQ handler. The bugfix NFO's synchronization comment is consistent with this code: the demo really does derive timing from VGA retrace/PIT behavior rather than treating the timer as an abstract clock.
The common backbuffer-to-VGA copy is small and appears in several places:
edi = 0xa0000 - [0x1079] ; PMODE linear address for VGA memory
esi = [0x1622c] ; current 320x200 backbuffer
ecx = 0x3e80 ; 16000 dwords = 64000 bytes
rep movsd
The companion clear is:
edi = [0x1622c]
eax = 0
ecx = 0x3e80
rep stosd
0x57cd writes the 768-byte DAC palette in the normal way:
dx = 0x3c8
al = 0
out dx, al
inc dl ; 0x3c9
ecx = 0x300
loop:
al = *esi++
out dx, al
Memory Manager
The demo has a local allocator rather than using C library heap calls:
0x3b04 allocate
0x3b10 free
0x3b48 memory-state query
The allocator at 0x3be4 is a best-fit freelist followed by a bump allocator.
It scans 8-byte free records and chooses the smallest block that satisfies the
request:
best = none;
for each free_record:
if record.size >= request && record.size < best.size:
best = record;
if exact:
remove record by shifting later 8-byte records down;
else:
result = record.start;
record.start += request;
record.size -= request;
if no free record:
allocate from bump pointer below a reserved top margin;
The free path at 0x3c67 inserts ranges back into the freelist and coalesces
adjacent blocks. If a free reaches the current heap top, the bump pointer is
rewound.
The move helper at 0x3b9a is overlap-safe and copies backward when needed:
if esi > edi:
rep movsw/movsb forward
else:
esi += ecx - 1
edi += ecx - 1
std
rep movsb
cld
That allocator matters because the main dispatcher checks that every screen gives back exactly what it took.
Inner Loop: RLE Sprite And Logo Blitter
The clearest classic inner loop is at 0x4ff4. It draws .RLE resources such
as FACE.RLE, JUICE.RLE, 1995END.RLE, and 1995END2.RLE into the 320x200
line-pointer surface rooted at [0x1622c].
Call shape:
ESI = RLE stream
ECX = signed x position
EDX = signed y position
Line setup:
ebx = ecx
if edx < 0:
skip source lines until y reaches 0
edi = dword [0x1622c + edx * 4] + ebx
ebp = ebx ; current x, signed for clipping
The stream is word-based:
0xfffe end of object
0xffff next scanline
word transparent skip count
word visible run length
bytes visible pixels
Unclipped positive-x path:
read ax
if ax == 0xfffe: return
if ax == 0xffff:
edx++
if edx == 200: return
goto next_line
edi += ax ; transparent pixels
ebp += ax
if ebp >= 320:
skip rest of source line
read ax ; visible length
ebp += ax
if ebp >= 320:
goto right_clip
ecx = eax & 3
rep movsb
ecx = eax >> 2
rep movsd
goto read_next_run
Left clipping path:
edi += transparent_skip
ebp += transparent_skip
if ebp >= 0:
join normal path
read visible_length
ebp += visible_length
if ebp <= 0:
edi += visible_length
esi += visible_length
continue
edi += visible_length
esi += visible_length
edi -= ebp ; rewind to visible left edge
esi -= ebp
eax = ebp ; clipped visible length
ebp = 0
copy eax bytes/dwords
Right clipping path:
eax = visible_length - ebp + 320
copy eax bytes/dwords
esi += ebp
esi -= 320 ; skip off-screen tail
goto read_next_run
Negative-y clipping is also stream-aware. It consumes whole source scanlines
until EDX reaches zero:
while edx < 0:
read word
if 0xfffe: return
if 0xffff:
edx++
continue
read visible_length
esi += visible_length
The important quality of this loop is that it never draws per pixel in the
transparent regions. It treats the RLE as alternating skip and copy spans, uses
rep movsd for the bulk of visible runs, and clips by adjusting ESI/EDI
rather than branching for every pixel.
0x21fbb is one caller. It first forces the DAC to white, loads the resources,
and then uses the RLE blitter three times per frame:
call 0x220f2 ; gray clear/load
[0x1e9c] = 0x213b7 ; timer/music callback
draw sprite A at (x, y)
draw sprite A again at (x + 320, y)
draw sprite B at (x2, y2)
call 0x217cd ; palette/frame update
loop until keyboard or timeline flag
The double draw at x and x + 320 suggests a wraparound or split-screen logo
movement. The routine is mechanically a clipped RLE compositor, regardless of
which logo/frame is on screen at that moment.
Inner Loop: Intro Two-Image Fade Scaler
The first visible effect path begins near 0x212ae. It loads two .FAD/raw
sources and a palette, builds a color translation structure, and animates two
independent source images into the same 320x200 backbuffer.
The per-pixel scaler/mixer is at 0x21053. Its setup computes fixed-point
steps from source and destination sizes:
source_width_step = (source_width << 7) / dest_width
source_height_step = (source_height << 8) / dest_height
row_advance = (source_height_step >> 8) * source_width
The hot part draws two destination pixels at a time. The source pointer is advanced by a fractional accumulator:
for each row:
save esi/edi/width
cl = 0
for x in 0..width-1 step 2:
al = [esi]
bh = table[al]
bl = [edi]
dl = table[ebx]
cl += x_fraction_step
esi += carry + integer_x_step
al = [esi]
bh = table[al]
bl = [edi+1]
dh = table[ebx]
cl += x_fraction_step
esi += carry + integer_x_step
word [edi] = dx
edi += 2
The interesting thing here is the indexed two-stage palette operation:
source pixel -> table source component
destination pixel -> table destination component
combined pair -> output word
This lets the effect fade or combine a scaled image over the existing
backbuffer without a multiply per pixel. 0x212c7 drives it with timeline
values:
width_1 = [0x2077c]
height_1 = [0x20780]
alpha_1 = [0x20784] >> 2
source_1 = [0x20110]
width_2 = [0x20785]
height_2 = [0x20789]
alpha_2 = [0x2078d] >> 2
source_2 = [0x20114]
It centers each scaled image by converting width/height into an (x, y) offset:
x = 160 - width
y = 100 - height/2
edi = line_ptr[y] + x
Then 0x20f6d copies a banded staging buffer to VGA and clears the backbuffer.
That routine uses full-screen rep stosd/rep movsd, but the real visual work
is the scaler's two-pixel inner loop.
Inner Loop: Palette Quantizer / Translation Table
The setup at 0x21508 builds a 64-level translation table used by the intro
scaler. It allocates 0x20000 bytes, aligns a 64 KiB block, and then iterates
over possible RGB offsets.
The key search loop is a brute-force closest-color pass over 256 palette
entries. For each candidate output color it computes an error from three color
component differences using a precomputed square table at 0x153a0:
best_error = 0x7fffffff
for palette_index in 0..255:
dr = palette_r - target_r
dg = palette_g - target_g
db = palette_b - target_b
error = sqr[dr] + sqr[dg] + sqr[db]
if error < best_error:
best_error = error
best_index = palette_index
The output byte is stored in the aligned translation block. This is slow setup work, but it pays for itself by making the frame loop use indexed byte lookups instead of RGB math.
The same setup also builds a 64x256 multiplication table:
for ch in 0..63:
for cl in 0..255:
al = high_byte(cl * ch)
*dest++ = al
That is a classic demoscene trade: spend memory to turn per-pixel multiplies into byte fetches.
Inner Loop: Proportional Greets Pre-Renderer
The greets setup at 0x22ebe loads GREETEXT.RAW and creates a variable-width
strip before the actual greets animation runs.
First it measures the text:
ecx = 0
for char in text:
if char == ' ':
ecx += 0x2d
else:
glyph = char - 'a'
ecx += glyph_width[glyph] + 8
[0x21737] = ecx ; total strip width
[0x21733] = ecx * 60 ; strip bytes, 60 rows
Then it allocates and clears the strip:
size = total_width * 60
buffer = alloc(size)
memset(buffer, 0, size)
The glyph compositor copies each glyph column block out of the raw sheet:
for char in text:
if char == ' ':
dest += 0x2d
continue
src = GREETEXT.RAW + glyph_offset[char]
width = glyph_width[char]
for row in 0..59:
rep movsb width bytes
dest += total_width - width
src += 0x4a8 - width
dest += width + 8
After pre-rendering, it frees the raw GREETEXT.RAW source. The actual effect
can then scroll a single long strip instead of re-reading proportional glyph
metadata every frame.
Inner Loop: Plasma / Byte-Phase Generator
The plasma-like generator starts at 0x22fca. It writes to the buffer pointed
to by [0x2171f] and reads three phase streams using byte registers as wrapping
indices:
edi = [0x2171f]
ebx = [0x2171b] ; table base
ecx = ebx
edx = ebx + 0x10000
bl = [0x2173d]
bh = [0x21741]
cl = [0x21745]
ch = [0x21749]
dl = [0x2174d]
dh = [0x21751]
esi = [0x21753] + 0xff0000
ebp = 0x8080
One unrolled output word is:
eax = esi
al += [ebx]
dl++
al += [ecx]
bl++
al += [edx]
cl--
ah += [ebx]
dl++
ah += [ecx]
bl++
ah += [edx]
cl--
eax |= 0x8080
stosw
The loop is not written as a compact loop body. The same block is repeated many
times so a row can be generated with very few taken branches. Each stosw
emits two pixels, one in AL and one in AH. The two halves are similar but
use staggered phase updates, giving horizontal motion without address
calculation per pixel.
This is the classic core idea:
for many pixels, unrolled:
p0 = base + waveA[i0] + waveB[i1] + waveC[i2];
advance byte phase counters;
p1 = base + waveA[i0] + waveB[i1] + waveC[i2];
advance byte phase counters;
*(uint16_t *)dst = (p1 << 8) | p0 | 0x8080;
The 0x8080 OR biases output into the upper half of the palette. The table base
and phase bytes are updated by surrounding code, so the hot loop only does byte
adds and stosw.
Inner Loop: Tunnel Lookup Precompute
The setup at 0x22dd1 allocates 0x8000 bytes and builds a 64x64 lookup table
with mirrored quadrants. It appears to support a tunnel or radial remap effect.
The math uses fixed-point distance and divides to turn (x, y) into texture
coordinates:
for y in 0..63:
for x in 0..63:
r2 = 0x10000000 - (x << 8)^2 - (y << 8)^2
if r2 <= 0x800000:
u = v = 0x80
else:
r = sqrt_like(r2) ; call 0x15e90
u = -((x << 14) / r)
v = -((y << 14) / r)
if u >= 127 or v >= 99:
u = v = 0x80
store u, v
After the first quadrant, the routine mirrors horizontally:
for 64 rows:
read word u/v forward
if u != 0x80: u = -u
write word backward
Then it mirrors vertically by negating the other coordinate:
for 64 rows:
copy 128 bytes from bottom/top partner
negate high byte in each coordinate word
The important observation is that the division-heavy polar mapping happens once in setup. The animation code can later use table lookups.
Inner Loop: Shared Fade / Row Synthesizer
The routine starting at 0x261d7 writes a repeated gradient/plasma structure
into the current destination. Its inner section is another unrolled word writer:
ecx = ([0x21763] + 0x40) << 8
eax = [0x2176b] << 16
eax /= ecx
step = eax
phase = step + (0x100 - [0x2176b])
line_step = (-160 * step) + phase
It then repeatedly samples a byte table using the high byte of an accumulator:
bl = high(accumulator)
al = table[ebx]
accumulator += phase
bl = high(accumulator)
ah = table[ebx]
stosw
The disassembly is heavily unrolled, so a row is a chain of:
add ecx, esi
mov bl, ch
mov al, [ebx]
add ecx, esi
mov bl, ch
mov ah, [ebx]
stosw
Again, the pattern is fixed-point phase accumulation plus two-pixel stosw
output. The code spends setup time computing phase values so the hot path is
almost entirely byte table reads and stores.
Terrain Sequence
The terrain sequence starts at 0x34dd3 and is strongly identified by resource
names:
terrpath.bin
mapbase.bin
starcrds.bin
post1.plo
post2.plo
post3.plo
radar.plo
wside1.plo
wside2.plo
wcorner1.plo
wcorner2.plo
wcorner3.plo
wcorner4.plo
hatch.plo
terrtext.raw
terrain.fad
The entry code:
- reads the current DAC palette from index 0x80 into a 0x180-byte buffer;
- loads a new 0x300-byte palette from local data;
- initializes terrain resources;
- installs timer callback
0x3413d; - enters a frame loop that updates path/camera state, renders, copies the 320x200 backbuffer to VGA, and exits based on timeline flags.
The frame skeleton is:
call 0x27169 ; sync/wait/update
eax = [0x32a40] >> 2
ebx = [0x32a44] >> 10
call 0x33693 ; terrain/camera position
call 0x34b63
call 0x33735
call 0x34869 ; draw
call 0x34dba ; copy backbuffer to VGA
if key abort: exit
if music_position >= 0x2a8: [0x340fe] = 1
if [0x2de33] > 0x1e000: [0x340fe] = 2
loop until [0x340fe] >= 2
The actual terrain renderer is spread across the helper calls, so I am not
claiming one simple "terrain inner loop" from this pass. What is certain is the
resource architecture and the frame scheduler: path data plus .PLO objects
feed a per-frame renderer into the same 320x200 backbuffer and synchronized VGA
copy.
Object / Polygon Sequences
The sequence around 0x28496 ties the plasma buffer, object/polygon helpers,
and palette transitions together:
[0x1e9c] = 0x21b56
loop:
call 0x22fca ; generate plasma/texture buffer
edi = [0x1622c]
call 0x261d7 ; draw shared gradient/texture rows
call 0x28b72 ; object pass
call 0x28ba0 ; object pass
call 0x261ad ; copy to VGA and wait tick
continue until timeline flag
Later in the same section it builds a 0x600-byte palette interpolation table by
reading the current DAC palette from 0x3c7/0x3c9 and mixing it with computed
ramps. The frame loop then blends between the saved and target palette:
weight_a = [0x2175f]
weight_b = 0x100 - weight_a
for 0x300 DAC components:
out = (saved_component * weight_a + target_component * weight_b) >> 8
out 0x3c9, out
The polygon/object helper calls underneath use bucketed lists. Code around
0x28723 splits records into four groups by low two bits:
bucket = record.key & 3
record.key >>= 2
append record to bucket[bucket]
The draw routines then iterate buckets and call the shared polygon routine at
0x1da48. That is consistent with the NFO credit: polygon routines are shared
utilities used by several screens.
Star / 3D Data Sequence
0x37428 is a resource-heavy scene using STARCRDS.BIN and a large set of
object/model resources. It allocates:
0x41a0 bytes star coordinates
0x40000 bytes working/render buffer
0x0c00 bytes helper table
0x0240 bytes helper table
0x0708 bytes path/state table
It loads many resources through the same FLIB path, sets callback 0x3524b,
clears the backbuffer, then loops:
call 0x3716b
call 0x3685a
call 0x36892
call 0x3647f
call 0x36247
call 0x36a92
copy [0x1622c] to VGA
update interactive/timeline variables from keyboard/timer state
loop until music position 0x8f8 or abort
The visible renderer helpers include many matrix/vector operations and calls to the same polygon draw backend. Again, the code is more of a small object engine than a single isolated effect loop.
"Chimmer" / Scanline Motion Sequence
The later sequence at 0x38673 references the embedded string:
chimmer.mod
It allocates a 320-byte table, initializes a 32-entry scanline/state array, then fills the backbuffer with a single palette index:
al = [0x36ebc]
ah = al
eax = al | (al << 8) | (al << 16) | (al << 24)
ecx = 0x3e80
rep stosd
The loop calls:
call 0x384bd
call 0x381e4
copy backbuffer to VGA
loop until music position 0x9f0 or key abort
0x384bd is tick-gated:
ecx = [0x1e31] - [0x36eb8]
if ecx == 0: wait
[0x36eb8] = [0x1e31]
Then it advances 32 scanline state records by two units each tick. When a head
record crosses a threshold, it shifts a block of records backward with std; rep movsd, inserts new phase/amplitude values from scripted tables, and writes
sine-derived offsets using the table at 0x14230.
The practical result is a scanline/strip motion system:
for each tick:
for each active strip:
strip.y -= 2;
if top strip enters visible range:
shift strip records;
insert new scripted amplitude/phase;
for each strip:
strip.x_offset = sin_table[(phase + 0x100) & 0x3ff] >> 4;
strip.y_offset = sin_table[(phase2 + 0x100) & 0x3ff] >> 4;
The renderer then consumes those records to distort or place strips. The binary does not carry a friendly screen title here, but the inner mechanics are clear: scripted rows plus sine-table offsets plus synchronized full-frame copies.
Sound Setup And Music Callback
The startup code contains setup strings for:
No sound card
SoundBlaster 16
SoundBlaster
Gravis UltraSound
BLASTER=
ULTRASND=
The NFO states that a command-line parameter triggers autodetection from
ULTRASND and BLASTER. The main entry checks the command line early, calls
sound setup around 0xb148/0xb163, then assigns:
[0x1e9c] = 0x0000b22e
That pointer is called from the timer IRQ path, so the player is frame/timer
driven. The large JUICE.PLM resource is the music payload:
JUICE.PLM off 000d5bac size 000c0e7c
The NFO says the music was written with DisorderTracker 2.0. The executable string says:
Loading music.... Prepare for JUICE!
What The Demo Is Doing As A Whole
Mechanically, JUICE is a packed DOS MZ executable followed by a custom FLIB
archive. After unpacking, it is a PMODE/VCPI protected-mode demo runtime with a
small allocator, a file/resource loader, keyboard/timer/sync services, sound
setup, and a long sequence of independent screens.
The code is table-heavy:
- palettes are read, stored, and interpolated as 768-byte DAC blocks;
- scaled images use translation tables rather than per-pixel RGB math;
- RLE sprites are rendered by skip/copy spans and
rep movsd; - plasma/tunnel-style effects precompute or stream through byte-indexed tables;
- object-heavy scenes bucket and pass records into a shared polygon backend.
The "fancy" part is not one magic trick. It is the amount of infrastructure: runtime, loader, resource archive, music callback, allocator leak checks, synchronized frame loop, and multiple specialized inner loops tied together by the music position. That is why v1.1's bugfix note talks about setup, compatibility, no-sound crashes, and VGA syncing rather than just changing art assets.
Remaining Unknowns
Some scene names can be mapped from the NFO and resource names, but not every effect call has a unique self-identifying string. The safest current mapping is:
- RLE logo/face/end blitter: certain, based on
.RLEresources and0x4ff4. - Greets strip builder: certain, based on
GREETEXT.RAWand text/glyph tables. - Terrain sequence: certain at the resource/scheduler level; inner renderer helper functions need a separate pass to label every sub-loop.
- Plasma/tunnel helpers: certain mechanically; exact visual screen labels are inferred from NFO categories and call position.
- Object/polygon sequences: certain as shared polygon/object machinery; exact screen names need video-capture correlation or deeper tracing.
That is a deliberate boundary: the writeup favors exact code mechanics over assigning confident names to every routine when the binary only gives resource and control-flow evidence.