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:

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:

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:

  1. reads the current DAC palette from index 0x80 into a 0x180-byte buffer;
  2. loads a new 0x300-byte palette from local data;
  3. initializes terrain resources;
  4. installs timer callback 0x3413d;
  5. 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:

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:

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.