; Je BoterHAM Eet: x86 software rendering into Lisa HAM8.
;
; The frame path renders a 160x128 working image, encodes two HAM pixels
; per working pixel, and uses Lisa row doubling for 320x256.
; The SID player owns its 300-second loop and receives only one start.
; Read data layout, initialisation, frame path and table generation in order.

%macro mix_colour_inline 0
        mov edx, eax
        mov ebp, ebx
        and eax, 0xff00ff
        and ebx, 0xff00ff
        sub ebx, eax
        imul ebx, ecx
        shl eax, 8
        add eax, ebx
        shr eax, 8
        and eax, 0xff00ff
        and edx, 0x00ff00
        and ebp, 0x00ff00
        sub ebp, edx
        imul ebp, ecx
        shl edx, 8
        add edx, ebp
        shr edx, 8
        and edx, 0x00ff00
        or eax, edx
%endmacro

%macro rotate_vector_inline 0
        mov edx, eax
        imul edx, [cos_a]
        mov ebp, ecx
        imul ebp, [sin_a]
        add edx, ebp
        sar edx, 14
        imul ecx, [cos_a]
        imul eax, [sin_a]
        sub ecx, eax
        sar ecx, 14
        mov eax, edx
        mov edx, ebx
        imul edx, [cos_b]
        mov ebp, ecx
        imul ebp, [sin_b]
        sub edx, ebp
        sar edx, 14
        imul ecx, [cos_b]
        imul ebx, [sin_b]
        add ecx, ebx
        sar ecx, 14
        mov ebx, edx
%endmacro

bits 32
org 0
%include "ie86.inc"

; ============================================================================
; DATA LAYOUT
; ============================================================================
; Buffers are fixed, clear of the image, MMIO, and the descending stack.
CHUNKY equ 0x1400000                 ; 160x128 packed 00RRGGBB words
PLANES equ 0x1420000                 ; eight contiguous planes, 128 rows of 44 bytes
DEPTH equ 0x1440000                  ; 160x128 signed depth words
COMMANDS equ 0x1460000               ; 352x128 commands, six leading and 26 trailing padding pixels
VERTICES equ 0x1480000               ; transformed fixed mesh
NEAREST equ 0x1490000                ; 4096-byte nearest-palette table
ENCODE_CACHE equ 0x1600000          ; 65536 entries: target, held, command, result
PAIR_CACHE equ 0x1900000            ; exact two-command results, 65536 entries
DIAG equ 0x1500000                   ; diagnostic words, documented beside main
PERIODIC_FIELDS equ 0x800000         ; 128 logo fields of 45056 bytes
LOADING_SPARKS equ 0x1310000         ; 16 moving opening fields, below CHUNKY
TUNNEL_BACKGROUNDS equ 0x2000000     ; 128 full-RGB frames, ending at 0x2a00000
TRANSITION_FRAMES equ 0xe00000        ; 16 RGB key frames for each of four scenes

; ============================================================================
; INITIALISATION
; ============================================================================
start:
        mov esp, 0x1800000
        cld
        mov edi, DIAG
        xor eax, eax
        mov ecx, 32
        rep stosd
        mov esi, nearest_table
        mov edi, NEAREST
        mov ecx, 4096/4
        rep movsd
        mov edi, 0x1510000
        xor eax, eax
        mov ecx, 1024
        rep stosd
        mov edi, ENCODE_CACHE
        mov ecx, 0x100000/4
        rep stosd
        mov edi, PAIR_CACHE
        mov ecx, 0x100000/4
        rep stosd
        mov edi, 0x1c00000
        mov ecx, 0x40000/4
        rep stosd
        mov eax, [SYSINFO_FEATURES]
        and eax, SYSINFO_FEATURE_WAIT
        mov [can_wait], eax
        xor eax, eax
        cmp dword [SYSINFO_LOW_WINDOW_LO], 0x2a00000 ; system low-RAM window, in bytes
        setae al
        mov [tunnel_cache_enabled], eax
        call build_spread_pairs
        call init_lisa
        mov eax, [RTC_MONO_USEC_LO]
        mov [clock_usec], eax
        call cache_openings
        call cache_loading_sparks
        call cache_transition_motion
        mov dword [DIAG+8], 0
        jmp initialised
initialised:
        call render_frame
        call cache_periodic_fields
        mov byte [SID_PLUS_CTRL], 0
        mov byte [SID_SUBSONG], 1
        mov dword [SID_PLAY_PTR], sid_data
        mov dword [SID_PLAY_LEN], sid_data_end-sid_data
        mov dword [SID_PLAY_CTRL], 5
        mov eax, [RTC_MONO_USEC_LO]
        add eax, 33333
        mov [frame_deadline], eax
        jmp main_loop
main_loop:
        call poll_clock
        call select_cue
        mov dword [paced_render], 1
        call render_frame
        mov dword [paced_render], 0
        jmp main_loop

; Prepare the next field immediately after publishing the previous one.
; Waiting here, after rendering, keeps variable shader cost out of the
; publication cadence. Diagnostic render_frame calls remain unpaced.
wait_frame:
        call poll_clock
        mov eax, [DIAG+44]
        sub eax, [last_frame_edge]
        mov edx, [clock_sample]
        sub edx, [frame_deadline]
        call frame_due
        test eax, eax
        jnz .ready
        mov eax, [frame_deadline]
        call idle_until_frame
        jmp wait_frame
.ready:
        mov eax, [DIAG+44]
        mov [last_frame_edge], eax
        mov eax, [clock_sample]
        mov edx, [frame_deadline]
        call advance_frame_deadline
        mov [frame_deadline], eax
        ret

; Vblank can release a frame within 500 microseconds of its 30 Hz deadline.
; A missed edge cannot extend the wait. EDX is the signed time relative to
; that deadline; subtraction remains valid across the RTC low-word wrap.
frame_due:
        test edx, edx
        jns .ready
        cmp edx, -500
        jl .wait
        cmp eax, 2
        jae .ready
.wait:
        xor eax, eax
        ret
.ready:
        mov eax, 1
        ret

; EAX is now, EDX the previous deadline. Preserve the cadence through a
; modest overrun, but discard a stale backlog after a pause or long stall.
advance_frame_deadline:
        add edx, 33333
        mov ecx, eax
        sub ecx, edx
        js .keep
        lea edx, [eax+33333]
.keep:
        mov eax, edx
        ret

; Use the existing bounded host wait while there is useful idle time.
; Its five-millisecond sleep granularity needs a six-millisecond margin.
; Engines without the advertised facility retain the polling fallback.
; EAX names the low word of the next deadline, at most one frame ahead.
idle_until_frame:
        cmp dword [can_wait], 0
        je .done
        mov ebx, eax
.clock:
        mov ecx, [RTC_MONO_USEC_HI]
        mov eax, [RTC_MONO_USEC_LO]
        cmp ecx, [RTC_MONO_USEC_HI]
        jne .clock
        mov edx, ebx
        sub edx, eax
        cmp edx, 7000
        jl .done
        sub ebx, 6000
        cmp ebx, eax
        jae .high
        inc ecx
.high:
        mov [CPU_WAIT_UNTIL_LO], ebx
        mov [CPU_WAIT_UNTIL_HI], ecx
        mov dword [CPU_WAIT_UNTIL_GO], 1
.done:
        ret

render_frame:
        call prepare_frame
        jmp frame_prepared

prepare_frame:
        mov eax, [RTC_MONO_USEC_LO]
        mov [stage_start], eax
        call load_periodic_field
        test eax, eax
        jz .draw
        mov eax, [RTC_MONO_USEC_LO]
        sub eax, [stage_start]
        mov [DIAG+64], eax
        xor eax, eax
        mov [DIAG+68], eax
        mov [DIAG+72], eax
        ret
.draw:
        call render_scene
        mov eax, [RTC_MONO_USEC_LO]
        mov ebx, eax
        sub eax, [stage_start]
        mov [DIAG+64], eax
        mov [stage_start], ebx
        call encode_image
        mov eax, [RTC_MONO_USEC_LO]
        mov ebx, eax
        sub eax, [stage_start]
        mov [DIAG+68], eax
        mov [stage_start], ebx
        call pack_planes
        mov eax, [RTC_MONO_USEC_LO]
        mov ebx, eax
        sub eax, [stage_start]
        mov [DIAG+72], eax
        ret
frame_prepared:
        cmp dword [paced_render], 0
        je .publish
        call wait_frame
.publish:
        mov eax, [RTC_MONO_USEC_LO]
        mov [stage_start], eax
        call publish_field
        mov eax, [RTC_MONO_USEC_LO]
        sub eax, [stage_start]
        mov [DIAG+76], eax
        cmp dword [DIAG+48], 0
        je .done
        mov edi, [DIAG+52]
        and edi, 65535
        shl edi, 5
        add edi, 0x1d00000
        mov eax, [RTC_MONO_USEC_LO]
        stosd
        mov esi, DIAG+64
        mov ecx, 4
        rep movsd
        mov eax, [DIAG]
        stosd
        mov eax, [DIAG+4]
        stosd
        mov eax, [DIAG+16]
        stosd
        inc dword [DIAG+52]
.done:
        ret

; Diagnostic block: scene, local animation frame, absolute 60 Hz tick count,
; completed fields, cycles, cycle tick, state (0 loading, 1 playing, 2 error),
; observed openings, fallback wraps, scene override (1..4), fixed-time flag.
; The compositor can make VIDEO_STATUS low for less than a guest polling
; interval. The existing monotonic RTC supplies missed ticks; no engine clock
; or interface is added. Unsigned subtraction also handles the RTC low-word
; wrap. One tick is 16667 microseconds (6 ms excess over a five-minute cycle).
poll_clock:
        pushfd
        pushad
        mov eax, [VGA_STATUS]
        and eax, 8
        cmp eax, [last_blank]
        je .clock
        mov [last_blank], eax
        test eax, eax
        jz .clock
        inc dword [DIAG+44]
.clock:
        mov eax, [RTC_MONO_USEC_LO]
        mov [clock_sample], eax
        sub eax, [clock_usec]
        cmp eax, 16667
        jb .done
        mov ecx, 4222040210
        mul ecx
        mov eax, edx
        shr eax, 14
        mov ecx, eax
        imul eax, 16667
        add [clock_usec], eax
.tick:
        push ecx
        call advance_tick
        pop ecx
        loop .tick
        call observe_music
.done:
        popad
        popfd
        ret

advance_tick:
        inc dword [DIAG+8]
        cmp dword [DIAG+24], 1
        jne .done
        inc dword [DIAG+20]
        cmp dword [fallback_pending], 0
        je .arm
        cmp dword [DIAG+20], 60
        jb .arm
        mov dword [marker_armed], 0
        mov dword [fallback_pending], 0
.arm:
        cmp dword [DIAG+20], 16200
        jb .wrap
        mov dword [marker_armed], 1
.wrap:
        cmp dword [DIAG+20], 18000
        jb .done
        mov eax, [DIAG+20]
        call advance_visual_offset
        mov dword [DIAG+20], 0
        inc dword [DIAG+16]
        inc dword [DIAG+32]
        mov dword [fallback_pending], 1
.done:
        ret

; The ADSR combination plus zero voice-two frequency identifies one 59 ms
; window per player stream, including a warm wrap. Keep observing for one
; second after the calibrated wrap, so modest audio-clock drift can be
; corrected without mistaking the later introductory notes for the opening.
observe_music:
        cmp dword [cache_building], 0
        jne .done
        cmp dword [DIAG+24], 2
        je .done
        movzx eax, byte [SID_PLAY_STATUS]
        test eax, 2
        jnz music_error
        cmp dword [DIAG+24], 0
        jne .playing
        cmp dword [DIAG+8], 1800
        jae music_error
        jmp .match
.playing:
        cmp dword [marker_armed], 0
        je .done
.match:
%macro match_byte 2
        cmp byte [SID_BASE+%1], %2
        jne .done
%endmacro
        match_byte 7, 0
        match_byte 8, 0
        match_byte 5, 0x0d
        match_byte 6, 0xfb
        match_byte 12, 0x0f
        match_byte 13, 0xc4
        match_byte 19, 9
        match_byte 20, 0x9f
        cmp dword [DIAG+24], 0
        je .onset
        cmp dword [fallback_pending], 0
        jne .onset
        inc dword [DIAG+16]
.onset:
        cmp dword [DIAG+24], 0
        je .first_onset
        mov eax, [DIAG+20]
        call advance_visual_offset
.first_onset:
        mov dword [DIAG+24], 1
        mov dword [DIAG+20], 0
        mov dword [marker_armed], 0
        mov dword [fallback_pending], 0
        inc dword [DIAG+28]
.done:
        ret

music_error:
        mov dword [SID_PLAY_CTRL], 2
        mov dword [DIAG+24], 2
        ret

select_cue:
        cmp dword [DIAG+40], 0
        jne .done
        xor ebx, ebx
        xor edx, edx
        mov eax, [DIAG+20]
        add eax, [visual_offset]
        cmp dword [DIAG+24], 1
        je .select
        xor eax, eax
.select:
        cmp eax, 6600
        jb .visual_tick
        sub eax, 6600
        jmp .select
.visual_tick:
        cmp eax, [cue_ends+ebx*4]
        jb .found
        mov edx, [cue_ends+ebx*4]
        inc ebx
        cmp ebx, 3
        jbe .select
        xor ebx, ebx
        xor edx, edx
        xor eax, eax
.found:
        cmp ebx, 2
        jb .scene_phase
        mov dword [outro_latched], 1
.scene_phase:
        sub eax, edx
        shr eax, 1
        mov [DIAG+4], eax
        cmp dword [DIAG+36], 0
        je .store
        mov ebx, [DIAG+36]
        dec ebx
        cmp ebx, 3
        jbe .store
        xor ebx, ebx
.store:
        mov [DIAG], ebx
        mov eax, [scene_phase_offsets+ebx*4]
        mov [phase_offset], eax
.done:
        ret

; Preserve the visual cycle when the SID marker or calibrated fallback
; resets the player's 300-second tick counter.
advance_visual_offset:
        add eax, [visual_offset]
.reduce:
        cmp eax, 6600
        jb .store
        sub eax, 6600
        jmp .reduce
.store:
        mov [visual_offset], eax
        ret

init_lisa:
        ; Lisa supplies the complete picture; avoid copying an unused framebuffer.
        mov dword [VIDEO_CTRL], 0
        mov word [LISA_OUT_CTRL], LISA_OUT_PUBLISH | LISA_OUT_DOUBLED
        mov word [LISA_OUT_FIRST_LINE], 44
        mov word [LISA_DIWSTRT], 0x2c80
        mov word [LISA_DIWSTOP], 0xac20
        mov word [LISA_DIWHIGH], 0x2000
        mov word [LISA_LINE_LEN], 144
        mov word [LISA_BPLCON0], 0x8810
        mov word [LISA_BPLCON1], 0
        mov word [LISA_BPLCON2], 0
        mov word [LISA_BPLCON3], 0
        mov word [LISA_BPLCON4], 0
        mov word [ALICE_CTRL], 0
        mov dword [ALICE_DMA_MASK], 0xffffffff
        mov word [ALICE_DMACON], ALICE_DMACON_BPLEN | ALICE_DMACON_DMAEN
        mov word [ALICE_BPL1MOD], 0
        mov word [ALICE_BPL2MOD], 0
        mov word [ALICE_DDFSTRT], 0x3a
        mov word [ALICE_DDFSTOP], 0x8a
        mov word [ALICE_FMODE], 1
        mov esi, palette
        mov edi, LISA_PAL_BASE
        mov ecx, 64
.palette:
        lodsd
        mov [edi], eax
        add edi, 4
        loop .palette
        ret

; ============================================================================
; FIELD PATH
; ============================================================================
; The mesh is a closed trefoil tube. Positions use 8 fractional bits and
; normals use 14. Orthographic projection keeps depth and environment
; coordinates affine, so all three interpolate linearly across triangles.
render_scene:
        call draw_scene
        mov eax, [DIAG]
        mov ebx, [scene_lengths+eax*4]
        mov ecx, 60
        cmp eax, 3
        jne .duration
        mov ecx, 150
.duration:
        sub ebx, ecx
        mov eax, [DIAG+4]
        sub eax, ebx
        js .credits
        mov [transition_progress], eax
        dec ecx
        cmp eax, ecx
        cmova eax, ecx
        shl eax, 8
        mov ebx, 72796056
        cmp ecx, 59
        je .reciprocal
        mov ebx, 28825284
.reciprocal:
        mul ebx
        mov eax, edx
        cmp eax, 256
        jbe .fraction
        mov eax, 256
.fraction:
        mov [blend_fraction], eax
        mov eax, [DIAG]
        inc eax
        cmp eax, 4
        jb .next
        xor eax, eax
.next:
        imul eax, 16*160*128*4
        lea esi, [TRANSITION_FRAMES+eax]
        mov eax, [transition_progress]
        cmp dword [DIAG], 3
        jne .short_transition
        movzx ebx, byte [transition_spark_index+eax]
        jmp .transition_index
.short_transition:
        movzx ebx, byte [transition_short_index+eax]
.transition_index:
        imul ebx, 160*128*4
        add esi, ebx
        mov edi, CHUNKY
        mov dword [blend_count], 160*128/8
.blend:
%rep 8
        lodsd
        mov ebx, [edi]
        mov ecx, 256
        sub ecx, [blend_fraction]
        mix_colour_inline
        stosd
%endrep
        mov eax, [blend_count]
        dec eax
        mov [blend_count], eax
        test eax, eax
        jz .credits
        test eax, 127
        jnz .blend
        call poll_clock
        jmp .blend
.credits:
        mov dword [text_opacity], 256
        cmp dword [DIAG], 2
        jae .persistent_outro
        cmp dword [outro_latched], 0
        jne .persistent_outro
        cmp dword [DIAG], 1
        jne .song_credit
        cmp dword [DIAG+4], 1140
        jb .song_credit
        mov eax, 256
        sub eax, [blend_fraction]
        mov [text_opacity], eax
.song_credit:
        call draw_song_credit
        mov dword [text_opacity], 256
        jmp .failure
.persistent_outro:
        cmp dword [outro_latched], 0
        je .failure
        call draw_outro_text
.failure:
        cmp dword [DIAG+24], 2
        jne .done
        mov esi, failure_text
        mov dword [text_x], 47
        mov dword [text_y], 62
        call draw_text
.done:
        ret

draw_song_credit:
        mov dword [text_scale], 1
        mov esi, song_credit_one
        mov dword [text_x], 29
        mov dword [text_y], 84
        call draw_text
        mov esi, song_credit_two
        mov dword [text_x], 44
        mov dword [text_y], 94
        call draw_text
        ret

; Retain raw scene openings for fixed-time diagnostics. Moving transition
; pictures are prepared separately before SID playback starts.
cache_openings:
        mov dword [DIAG+24], 2       ; suppress playback observation during setup
        mov dword [DIAG], 0
        mov dword [DIAG+4], 0
.scene:
        call draw_scene
        mov edi, [DIAG]
        imul edi, 160*128*4
        add edi, 0x1b00000
        mov esi, CHUNKY
        mov ecx, 160*128
        rep movsd
        inc dword [DIAG]
        cmp dword [DIAG], 4
        jb .scene
        mov dword [DIAG], 0
        mov dword [DIAG+24], 0
        ret

; Keep Lisa changing while the remaining fixed pictures are prepared.
; These 16 spark fields ping-pong over 30 publications and are never rebuilt
; at a visual or musical loop boundary.
cache_loading_sparks:
        mov dword [DIAG], 0
        mov dword [DIAG+24], 0
        mov dword [phase_offset], 150
        mov dword [loading_spark_index], 0
.frame:
        mov eax, [loading_spark_index]
        lea eax, [eax+eax*2]
        shl eax, 1
        mov [DIAG+4], eax
        call render_scene
        call encode_image
        call pack_planes
        mov edi, [loading_spark_index]
        imul edi, 45056
        add edi, LOADING_SPARKS
        mov esi, PLANES
        mov ecx, 45056/4
        rep movsd
        call publish_field
        inc dword [loading_spark_index]
        cmp dword [loading_spark_index], 16
        jb .frame
        mov dword [loading_spark_index], 0
        mov dword [DIAG+4], 0
        mov dword [phase_offset], 0
        ret

load_loading_spark:
        mov eax, [loading_spark_index]
        inc dword [loading_spark_index]
        cmp dword [loading_spark_index], 30
        jb .phase
        mov dword [loading_spark_index], 0
.phase:
        cmp eax, 15
        jbe .copy
        mov edx, 30
        sub edx, eax
        mov eax, edx
.copy:
        imul esi, eax, 45056
        add esi, LOADING_SPARKS
        mov edi, PLANES
        mov ecx, 45056/4
        rep movsd
        ret

publish_loading_spark:
        call load_loading_spark
        call publish_field
        ret

; Sixteen key pictures per incoming scene move throughout a dissolve. The
; spark spans 150 output frames; the other scenes span 60. Reusing each
; picture for a few frames keeps one RGB blend per output pixel.
cache_transition_motion:
        mov dword [DIAG+24], 2
        mov dword [DIAG], 0
.scene:
        mov dword [transition_key], 0
.frame:
        mov eax, [transition_key]
        cmp dword [DIAG], 0
        jne .short
        cmp eax, 15
        je .spark_end
        imul eax, 10
        jmp .phase
.spark_end:
        mov eax, 150
        jmp .phase
.short:
        cmp eax, 15
        je .short_end
        shl eax, 2
        jmp .phase
.short_end:
        mov eax, 60
.phase:
        mov [DIAG+4], eax
        call draw_scene
        mov edi, [DIAG]
        shl edi, 4
        add edi, [transition_key]
        imul edi, 160*128*4
        add edi, TRANSITION_FRAMES
        mov esi, CHUNKY
        mov ecx, 160*128
        rep movsd
        call publish_loading_spark
        inc dword [transition_key]
        cmp dword [transition_key], 16
        jb .frame
        inc dword [DIAG]
        cmp dword [DIAG], 4
        jb .scene
        mov dword [DIAG], 0
        mov dword [DIAG+4], 0
        mov dword [DIAG+24], 0
        ret

; Tunnel coordinates repeat after 128 frames. Retain their full RGB backgrounds
; for the knot, and the logo's exact bitplanes in RAM. Initialise all slots
; before starting music, with moving spark fields visible throughout. Dissolves
; and player errors always use the live renderer. Fixed-time diagnostics
; may skip this warm-up, then populate individual slots through the normal
; preparation and store routines.
cache_periodic_fields:
        cmp dword [DIAG+40], 0
        jne .done
        mov dword [cache_building], 1
        mov dword [phase_offset], 60
        mov dword [DIAG], 3
        mov dword [DIAG+4], 0
.frame:
        call render_tunnel
        cmp dword [tunnel_cache_enabled], 0
        je .next
        mov edi, [DIAG+4]
        imul edi, 160*128*4
        add edi, TUNNEL_BACKGROUNDS
        mov esi, CHUNKY
        mov ecx, 160*128
        rep movsd
.next:
        call prepare_frame
        call store_periodic_field
        inc dword [DIAG+4]
        call publish_loading_spark
.advance:
        cmp dword [DIAG+4], 128
        jb .frame
.ready:
        mov eax, [tunnel_cache_enabled]
        mov [tunnel_cache_ready], eax
        mov dword [DIAG], 0
        mov dword [DIAG+4], 0
        mov dword [DIAG+8], 0
        mov dword [phase_offset], 150
        mov dword [cache_building], 0
        mov eax, [RTC_MONO_USEC_LO]
        mov [clock_usec], eax
.done:
        ret

periodic_field_address:
        mov eax, [DIAG+4]
        and eax, 127
        imul edi, eax, 45056
        add edi, PERIODIC_FIELDS
        ret

store_periodic_field:
        call periodic_field_address
        mov esi, PLANES
        mov ecx, 45056/4
        rep movsd
        mov byte [cached_valid+eax], 1
        ret

load_periodic_field:
        cmp dword [DIAG+24], 1
        jne .miss
        mov eax, [DIAG]
        cmp eax, 3
        jne .miss
        cmp dword [DIAG+4], 450    ; logo: last 150 frames dissolve live
        jae .miss
        call periodic_field_address
        cmp byte [cached_valid+eax], 0
        je .miss
        mov esi, edi
        mov edi, PLANES
        mov ecx, 45056/4
        rep movsd
        mov eax, 1
        ret
.miss:
        xor eax, eax
        ret

draw_scene:
        mov eax, [DIAG]
        cmp eax, 1
        je render_knot
        cmp eax, 2
        je render_reactor
        cmp eax, 3
        je render_logo
        jmp render_spark

render_spark:
        mov esi, radial
        mov edi, CHUNKY
        mov ecx, 160*128
.pixel:
        lodsd
        mov edx, eax
        shr edx, 16
        mov ebx, [glow+edx*4]
        and eax, 255
        imul eax, 7
        add eax, [DIAG+4]
        add eax, [phase_offset]
        and eax, 255
        mov eax, [sine+eax*4]
        sar eax, 7
        add eax, 200
        imul eax, ebx
        shr eax, 8
        cmp eax, 255
        jbe .colour
        mov eax, 255
.colour:
        mov eax, [heat+eax*4]
        stosd
        test edi, 4095
        jnz .next
        call poll_clock
.next:
        loop .pixel
        ret

render_tunnel:
        mov esi, radial
        mov edi, CHUNKY
        mov ecx, 160*128
.pixel:
        lodsd
        mov ebx, eax
        and eax, 255
        shr eax, 1
        add eax, [DIAG+4]
        add eax, [phase_offset]
        and eax, 127
        mov edx, ebx
        shr edx, 8
        and edx, 255
        add edx, [DIAG+4]
        add edx, [DIAG+4]
        add edx, [phase_offset]
        add edx, [phase_offset]
        and edx, 127
        mov ebp, eax
        xor ebp, edx
        shl edx, 7
        or eax, edx
        mov eax, [environment+eax*4]
        test ebp, 16
        jz .bright
        shr eax, 1
        and eax, 0x7f7f7f
.bright:
        shr ebx, 16
        cmp ebx, 24
        jae .store
        shr eax, 2
        and eax, 0x3f3f3f
.store:
        stosd
        test edi, 4095
        jnz .next
        call poll_clock
.next:
        loop .pixel
        ret

; Three inverse-square scalar sources sampled on a 41x33 grid. Finite
; differences select the shared environment map; RGB shading is bilinearly
; interpolated over four-pixel cells, with no per-frame table generation.
render_reactor:
%assign k 0
%rep 3
        mov eax, [DIAG+4]
        add eax, [phase_offset]
        add eax, k*85
        and eax, 255
        mov ebx, [sine+eax*4]
        sar ebx, 11
        add ebx, 20
        mov [centres+k*8], ebx
        add eax, 64
        and eax, 255
        mov ebx, [sine+eax*4]
        sar ebx, 11
        add ebx, 16
        mov [centres+k*8+4], ebx
%assign k k+1
%endrep
        mov edi, 0x14a0000
        mov dword [grid_y], 0
.row:
        mov dword [grid_x], 0
.node:
        xor ebp, ebp
%assign k 0
%rep 3
        mov eax, [grid_x]
        sub eax, [centres+k*8]
        imul eax, eax
        mov ebx, [grid_y]
        sub ebx, [centres+k*8+4]
        imul ebx, ebx
        add eax, ebx
        add ebp, [inverse+eax*4]
%assign k k+1
%endrep
        mov eax, ebp
        stosd
        inc dword [grid_x]
        cmp dword [grid_x], 41
        jb .node
        call poll_clock
        inc dword [grid_y]
        cmp dword [grid_y], 33
        jb .row
        mov esi, 0x14a0000
        mov edi, 0x14a2000
        mov ecx, 41*33
.shade:
        mov eax, 0x02060f
        cmp ecx, 42
        jb .shade_store
        mov edx, [esi]
        cmp edx, 100
        jb .shade_store
        mov eax, edx
        sub eax, [esi+4]
        sar eax, 2
        add eax, 64
        xor ebx, ebx
        cmp eax, 0
        cmovl eax, ebx
        mov ebx, 127
        cmp eax, 127
        cmovg eax, ebx
        sub edx, [esi+41*4]
        sar edx, 2
        add edx, 64
        xor ebx, ebx
        cmp edx, 0
        cmovl edx, ebx
        mov ebx, 127
        cmp edx, 127
        cmovg edx, ebx
        shl edx, 7
        or eax, edx
        mov eax, [environment+eax*4]
.shade_store:
        stosd
        add esi, 4
        loop .shade
        call interpolate_reactor
        mov edi, CHUNKY
        mov ecx, 160*128
.dim_for_text:
        mov eax, [edi]
        shr eax, 1
        and eax, 0x7f7f7f
        stosd
        loop .dim_for_text
        call draw_outro_text
        ret

; Horizontal samples are shared by four output rows. Keep their rounded
; RGB values in RAM, then interpolate vertically with the original weights.
; This preserves the two-stage rounding at every four-pixel cell boundary.
interpolate_reactor:
        mov esi, 0x14a2000
        mov edi, 0x14a4000
        mov dword [grid_y], 33
.horizontal_row:
        mov dword [grid_x], 40
.horizontal_cell:
%assign fraction 0
%rep 4
        mov eax, [esi]
        mov ebx, [esi+4]
        mov ecx, fraction*64
        mix_colour_inline
        stosd
%assign fraction fraction+1
%endrep
        add esi, 4
        mov eax, [grid_x]
        dec eax
        mov [grid_x], eax
        test eax, eax
        jnz .horizontal_cell
        add esi, 4
        call poll_clock
        mov eax, [grid_y]
        dec eax
        mov [grid_y], eax
        test eax, eax
        jnz .horizontal_row
        mov edi, CHUNKY
        mov dword [grid_y], 0
.vertical_row:
        mov esi, [grid_y]
        shr esi, 2
        imul esi, 640
        add esi, 0x14a4000
        mov ecx, [grid_y]
        and ecx, 3
        shl ecx, 6
        mov dword [grid_x], 160/4
.vertical_pixels:
%rep 4
        mov eax, [esi]
        mov ebx, [esi+640]
        mix_colour_inline
        stosd
        add esi, 4
%endrep
        mov eax, [grid_x]
        dec eax
        mov [grid_x], eax
        test eax, eax
        jnz .vertical_pixels
        call poll_clock
        inc dword [grid_y]
        cmp dword [grid_y], 128
        jb .vertical_row
        ret

render_logo:
        call render_tunnel
        mov edi, CHUNKY
        mov ecx, 160*128
.dim:
        mov eax, [edi]
        shr eax, 2
        and eax, 0x3f3f3f
        stosd
        loop .dim
        call draw_outro_text
        ret

draw_outro_text:
        mov dword [text_opacity], 256
        mov dword [text_scale], 1
        mov dword [text_x], 35
        mov dword [text_y], 7
        mov esi, title_one
        call draw_text
        mov dword [text_x], 5
        mov dword [text_y], 32
        mov esi, outro_detail_one
        call draw_text
        mov dword [text_x], 14
        mov dword [text_y], 48
        mov esi, outro_detail_two
        call draw_text
        mov dword [text_x], 35
        mov dword [text_y], 64
        mov esi, outro_detail_three
        call draw_text
        mov dword [text_x], 2
        mov dword [text_y], 82
        mov esi, subtitle
        call draw_text
        ret

draw_text:
.char:
        lodsb
        test al, al
        jz .done
        movzx eax, al
        sub eax, 32
        lea ebx, [font+eax*8]
        mov [glyph_ptr], ebx
        mov dword [glyph_y], 0
.row:
        mov ebx, [glyph_ptr]
        mov eax, [glyph_y]
        movzx ebx, byte [ebx+eax]
        mov [glyph_bits], ebx
        mov eax, [text_inks+eax*4]
        mov [glyph_colour], eax
        mov dword [glyph_x], 0
.pixel:
        test dword [glyph_bits], 16
        jz .skip
        mov eax, [glyph_y]
        imul eax, [text_scale]
        add eax, [text_y]
        imul eax, 160
        mov edx, [glyph_x]
        imul edx, [text_scale]
        add edx, [text_x]
        add eax, edx
        lea edi, [CHUNKY+eax*4]
        mov eax, [glyph_colour]
        cmp dword [text_opacity], 256
        je .ink_ready
        mov ebx, eax
        mov eax, [edi]
        mov ecx, [text_opacity]
        mix_colour_inline
.ink_ready:
        mov [edi], eax
        cmp dword [text_scale], 2
        jne .skip
        mov [edi+4], eax
        mov [edi+640], eax
        mov [edi+644], eax
.skip:
        shl dword [glyph_bits], 1
        inc dword [glyph_x]
        cmp dword [glyph_x], 5
        jb .pixel
        inc dword [glyph_y]
        cmp dword [glyph_y], 7
        jb .row
        mov eax, [text_scale]
        imul eax, 6
        add [text_x], eax
        jmp .char
.done:
        ret

render_knot:
        cmp dword [tunnel_cache_ready], 0
        je .live_background
        mov esi, [DIAG+4]
        and esi, 127
        imul esi, 160*128*4
        add esi, TUNNEL_BACKGROUNDS
        mov edi, CHUNKY
        mov ecx, 160*128
        rep movsd
        jmp .background_ready
.live_background:
        call render_tunnel
.background_ready:
        call transform_mesh
        mov edi, DEPTH
        mov eax, 0x80000000
        mov ecx, 160*128
        rep stosd
        mov dword [triangle_index], 0
knot_triangle:
        mov esi, [triangle_index]
        imul esi, 12
%assign vertex 0
%rep 3
        mov eax, [triangles+esi+vertex*4]
        imul eax, 20
%assign component 0
%rep 5
        mov ebx, [VERTICES+eax+component*4]
        mov [triangle_data+vertex*20+component*4], ebx
%assign component component+1
%endrep
%assign vertex vertex+1
%endrep
        jmp raster_triangle
knot_triangle_done:
        inc dword [triangle_index]
        test dword [triangle_index], 31
        jnz .next
        call poll_clock
.next:
        cmp dword [triangle_index], TRIANGLE_COUNT
        jb knot_triangle
        ret

transform_mesh:
        mov eax, [DIAG+4]
        add eax, [phase_offset]
        and eax, 255
        mov edx, [sine+eax*4]
        mov [sin_a], edx
        add eax, 64
        and eax, 255
        mov edx, [sine+eax*4]
        mov [cos_a], edx
        mov eax, [DIAG+4]
        add eax, [phase_offset]
        shr eax, 1
        and eax, 255
        mov edx, [sine+eax*4]
        mov [sin_b], edx
        add eax, 64
        and eax, 255
        mov edx, [sine+eax*4]
        mov [cos_b], edx
        mov esi, mesh
        mov edi, VERTICES
        mov dword [vertex_count], MESH_COUNT
.vertex:
        mov eax, [esi]
        mov ebx, [esi+4]
        mov ecx, [esi+8]
        rotate_vector_inline
        sar eax, 8
        add eax, 80
        mov [edi], eax
        sar ebx, 8
        add ebx, 64
        mov [edi+4], ebx
        sar ecx, 8
        mov [edi+8], ecx
        mov eax, [esi+12]
        mov ebx, [esi+16]
        mov ecx, [esi+20]
        rotate_vector_inline
        sar eax, 8
        add eax, 64
        cmp eax, 127
        jle .u
        mov eax, 127
.u:
        mov [edi+12], eax
        sar ebx, 8
        add ebx, 64
        cmp ebx, 127
        jle .v
        mov ebx, 127
.v:
        mov [edi+16], ebx
        add edi, 20
        add esi, 24
        mov eax, [vertex_count]
        dec eax
        mov [vertex_count], eax
        test eax, eax
        jnz .vertex
        ret

rotate_vector:
        rotate_vector_inline
        ret

; Integer edge equations cover the bounding box. Environment U/V and depth
; gradients have 12 fractional bits. A signed depth buffer resolves the
; self-overlapping tube without sorting or per-frame allocation.
raster_triangle:
        mov eax, [triangle_data+20]
        sub eax, [triangle_data]
        mov ebx, [triangle_data+44]
        sub ebx, [triangle_data+4]
        imul eax, ebx
        mov ebx, [triangle_data+40]
        sub ebx, [triangle_data]
        mov ecx, [triangle_data+24]
        sub ecx, [triangle_data+4]
        imul ebx, ecx
        sub eax, ebx
        jz .return
        test eax, eax
        jns .positive
        neg eax
%assign k 0
%rep 5
        mov ebx, [triangle_data+20+k*4]
        xchg ebx, [triangle_data+40+k*4]
        mov [triangle_data+20+k*4], ebx
%assign k k+1
%endrep
.positive:
        cmp eax, 4096
        jae .return
        mov [area], eax
%macro bound 3
        mov eax, [triangle_data+%1]
        mov ebx, eax
%assign k 1
%rep 2
        mov ecx, [triangle_data+k*20+%1]
        cmp ecx, eax
        cmovl eax, ecx
        cmp ecx, ebx
        cmovg ebx, ecx
%assign k k+1
%endrep
        xor ecx, ecx
        cmp eax, ecx
        cmovl eax, ecx
        mov ecx, %2
        cmp ebx, ecx
        cmovg ebx, ecx
        mov [%3], eax
        mov [%3+4], ebx
        cmp eax, ebx
        jg .return
%endmacro
        bound 0, 159, min_x
        bound 4, 127, min_y
        mov eax, [triangle_data+20]
        sub eax, [triangle_data]
        mov [dx1], eax
        mov eax, [triangle_data+40]
        sub eax, [triangle_data]
        mov [dx2], eax
        mov eax, [triangle_data+24]
        sub eax, [triangle_data+4]
        mov [dy1], eax
        mov eax, [triangle_data+44]
        sub eax, [triangle_data+4]
        mov [dy2], eax
%macro gradient 2
        mov eax, [triangle_data+20+%1]
        sub eax, [triangle_data+%1]
        mov ebx, eax
        mov ecx, [triangle_data+40+%1]
        sub ecx, [triangle_data+%1]
        imul eax, [dy2]
        mov edx, ecx
        imul edx, [dy1]
        sub eax, edx
        mov edx, [area]
        imul dword [reciprocal+edx*4]
        shrd eax, edx, 18
        mov [%2+4], eax
        imul ecx, [dx1]
        imul ebx, [dx2]
        sub ecx, ebx
        mov eax, ecx
        mov edx, [area]
        imul dword [reciprocal+edx*4]
        shrd eax, edx, 18
        mov [%2+8], eax
        mov ebx, [min_y]
        sub ebx, [triangle_data+4]
        imul ebx, eax
        mov eax, [min_x]
        sub eax, [triangle_data]
        imul eax, [%2+4]
        add eax, ebx
        mov ebx, [triangle_data+%1]
        shl ebx, 12
        add eax, ebx
        mov [%2], eax
%endmacro
        gradient 8, grad_z
        gradient 12, grad_u
        gradient 16, grad_v
%assign k 0
%rep 3
%assign n ((k+1) % 3)
        mov eax, [triangle_data+n*20]
        sub eax, [triangle_data+k*20]
        mov [edges+k*16+8], eax
        mov ebx, [triangle_data+k*20+4]
        sub ebx, [triangle_data+n*20+4]
        mov [edges+k*16+4], ebx
        mov ecx, [min_y]
        sub ecx, [triangle_data+k*20+4]
        imul eax, ecx
        mov ecx, [min_x]
        sub ecx, [triangle_data+k*20]
        imul ebx, ecx
        add eax, ebx
        mov [edges+k*16], eax
%assign k k+1
%endrep
        mov eax, [min_y]
        mov [draw_y], eax
        imul eax, 160
        add eax, [min_x]
        lea edi, [CHUNKY+eax*4]
        lea esi, [DEPTH+eax*4]
.row:
        mov eax, [min_x]
        mov [draw_x], eax
%macro row_gradient 1
        mov eax, [%1]
        mov [%1+12], eax
%endmacro
        row_gradient grad_u
        row_gradient grad_v
        mov eax, [edges]
        mov ebx, [edges+16]
        mov ecx, [edges+32]
        mov edx, [grad_z]
.pixel:
        mov ebp, eax
        or ebp, ebx
        or ebp, ecx
        js .skip
        cmp edx, [esi]
        jle .skip
        mov [esi], edx
        mov ebp, [grad_v+12]
        sar ebp, 12
        and ebp, 127
        shl ebp, 7
        push eax
        mov eax, [grad_u+12]
        sar eax, 12
        and eax, 127
        or eax, ebp
        mov eax, [environment+eax*4]
        mov [edi], eax
        pop eax
.skip:
        add eax, [edges+4]
        add ebx, [edges+20]
        add ecx, [edges+36]
        add edx, [grad_z+4]
        mov ebp, [grad_u+4]
        add [grad_u+12], ebp
        mov ebp, [grad_v+4]
        add [grad_v+12], ebp
        add edi, 4
        add esi, 4
        inc dword [draw_x]
        mov ebp, [draw_x]
        cmp ebp, [max_x]
        jle .pixel
        sub ebp, [min_x]
        shl ebp, 2
        neg ebp
        add ebp, 640
        add edi, ebp
        add esi, ebp
%assign k 0
%rep 3
        mov eax, [edges+k*16+8]
        add [edges+k*16], eax
%assign k k+1
%endrep
%macro step_y 1
        mov eax, [%1+8]
        add [%1], eax
%endmacro
        step_y grad_z
        step_y grad_u
        step_y grad_v
        inc dword [draw_y]
        mov eax, [draw_y]
        cmp eax, [max_y]
        jle .row
.return:
        jmp knot_triangle_done

; EAX is desired RGB, EBX is the actual held RGB, EDX forces a palette load.
; Return EAX command and EBX decoded colour. ESI and EDI survive. The palette
; candidate comes from a small 4:4:4 lookup; all four candidates are compared
; against the original eight-bit channels by squared RGB error. Modify commands
; retain the held component's low two bits, exactly as Lisa decodes them.
%macro colour_error_inline 0
        movzx edx, al
        movzx ecx, byte [desired]
        sub edx, ecx
        imul edx, edx
        mov ebp, edx
        mov edx, eax
        shr edx, 8
        and edx, 255
        movzx ecx, byte [desired+1]
        sub edx, ecx
        imul edx, edx
        add ebp, edx
        mov edx, eax
        shr edx, 16
        movzx ecx, byte [desired+2]
        sub edx, ecx
        imul edx, edx
        add edx, ebp
%endmacro
; Exact memoisation includes the held colour, so retained low bits and
; tie-breaking are unchanged. Collisions replace entries, never approximate.
%macro encode_body 1
%if %1 = 0
        mov edx, eax
        imul edx, 0x9e3779b1
        xor edx, ebx
        ror edx, 11
        and edx, 0xffff
        shl edx, 4
        add edx, ENCODE_CACHE
        mov ecx, eax
        or ecx, 0x80000000
        cmp [edx], ecx
        jne %%miss
        cmp [edx+4], ebx
        jne %%miss
        mov eax, [edx+8]
        mov ebx, [edx+12]
        jmp %%return
%%miss:
        mov [edx], ecx
        mov [edx+4], ebx
        mov [cache_entry], edx
%endif
        mov [desired], eax
%if %1 = 2
        mov [force_direct], edx
%elif %1 = 0
        mov ecx, eax
        or ecx, 0x80000000
        cmp ecx, [last_palette_target]
        jne %%palette_miss
        mov edx, [last_palette_command]
        mov [palette_command], edx
        mov edx, [last_palette_error]
        jmp %%palette_ready
%%palette_miss:
        mov [last_palette_target], ecx
        mov edx, eax
        imul edx, 0x9e3779b1
        shr edx, 18
        shl edx, 4
        add edx, 0x1c00000
        cmp [edx], ecx
        jne %%palette_compute
        mov eax, [edx+4]
        mov [palette_command], eax
        mov [last_palette_command], eax
        mov edx, [edx+8]
        mov [last_palette_error], edx
        jmp %%palette_ready
%%palette_compute:
        mov [edx], ecx
        mov [palette_cache_entry], edx
%endif
        mov edx, eax
        shr edx, 12
        and edx, 0xf00
        mov ecx, eax
        shr ecx, 8
        and ecx, 0xf0
        or edx, ecx
        shr eax, 4
        and eax, 15
        or edx, eax
        movzx eax, byte [NEAREST+edx]
        shl eax, 2
        mov [palette_command], eax
        mov eax, [palette+eax]
%if %1 = 2
        cmp dword [force_direct], 0
        jne %%direct
%elif %1 = 1
        jmp %%direct
%endif
        colour_error_inline
%if %1 = 0
        mov [last_palette_error], edx
        mov eax, [palette_command]
        mov [last_palette_command], eax
        mov ecx, [palette_cache_entry]
        mov [ecx+4], eax
        mov [ecx+8], edx
%%palette_ready:
%endif
        mov ebp, edx
        xor eax, eax                ; winning control: palette, blue, red, green
%assign c 0
%rep 3
        mov edx, ebx
%if c > 0
        shr edx, c*8
%endif
        and edx, 255
        movzx ecx, byte [desired+c]
        sub edx, ecx
        imul edx, edx
        mov [held_errors+c*4], edx
%assign c c+1
%endrep
%assign control 1
%rep 3
%if control = 1
%assign component 0
%elif control = 2
%assign component 2
%else
%assign component 1
%endif
        mov edx, ebx
%if component > 0
        shr edx, component*8
%endif
        and edx, 3
        movzx ecx, byte [desired+component]
        and ecx, 3
        sub edx, ecx
        imul edx, edx
%assign c 0
%rep 3
%if c != component
        add edx, [held_errors+c*4]
%endif
%assign c c+1
%endrep
        cmp edx, ebp
        cmovb ebp, edx
        mov ecx, control
        cmovb eax, ecx
%assign control control+1
%endrep
        cmp eax, 1
        je %%blue
        cmp eax, 2
        je %%red
        cmp eax, 3
        je %%green
%%direct:
        mov eax, [palette_command]
        mov ebx, [palette+eax]
        jmp %%done
%%blue:
        movzx eax, byte [desired]
        and eax, 252
        and ebx, 0xffff03
        or ebx, eax
        or eax, 1
        jmp %%done
%%red:
        movzx eax, byte [desired+2]
        and eax, 252
        mov edx, eax
        shl edx, 16
        and ebx, 0x03ffff
        or ebx, edx
        or eax, 2
        jmp %%done
%%green:
        movzx eax, byte [desired+1]
        and eax, 252
        mov edx, eax
        shl edx, 8
        and ebx, 0xff03ff
        or ebx, edx
        or eax, 3
%%done:
%if %1 = 0
        mov edx, [cache_entry]
        mov [edx+8], eax
        mov [edx+12], ebx
%%return:
%endif
%endmacro

encode_pixel:
        nop                         ; stable listing address for diagnostics
        encode_body 2
        ret

encode_cached_pixel:
        nop                         ; stable listing address for diagnostics
        encode_body 0
        ret

; A pair shares its target, palette candidate and retained component bits.
; Re-evaluate only the changed component between commands. This preserves
; exactly the single-command error and tie ordering without a second search.
%macro pair_errors 0
        xor ebp, ebp
%assign c 0
%rep 3
        mov edx, ebx
%if c = 0
        shl edx, 8
%elif c = 2
        shr edx, 8
%endif
        and edx, 0xff00
        movzx ecx, byte [desired+c]
        or edx, ecx
        mov edx, [channel_errors+edx*4]
        mov ecx, edx
        and ecx, 15
        shr edx, 4
        add ebp, edx
        sub edx, ecx
        mov [held_errors+c*4], edx ; improvement from modifying this channel
%assign c c+1
%endrep
        mov [pair_total_error], ebp
%endmacro

%macro pair_search 0
        mov [desired], eax
        mov edx, eax
        imul edx, 0x9e3779b1
        shr edx, 18
        shl edx, 4
        add edx, 0x1c00000
        mov ecx, eax
        or ecx, 0x80000000
        cmp [edx], ecx
        jne %%palette_miss
        mov eax, [edx+4]
        mov [palette_command], eax
        mov edx, [edx+8]
        jmp %%palette_ready
%%palette_miss:
        mov [edx], ecx
        mov [palette_cache_entry], edx
        mov edx, eax
        shr edx, 12
        and edx, 0xf00
        mov ecx, eax
        shr ecx, 8
        and ecx, 0xf0
        or edx, ecx
        shr eax, 4
        and eax, 15
        or edx, eax
        movzx eax, byte [NEAREST+edx]
        shl eax, 2
        mov [palette_command], eax
        mov eax, [palette+eax]
        colour_error_inline
        mov ecx, [palette_cache_entry]
        mov eax, [palette_command]
        mov [ecx+4], eax
        mov [ecx+8], edx
%%palette_ready:
        mov [pair_palette_error], edx
        pair_errors
%assign pass 0
%rep 2
        mov ebp, [held_errors]
        mov eax, 1
        mov edx, [held_errors+8]
        cmp edx, ebp
        cmovg ebp, edx
        mov ecx, 2
        cmovg eax, ecx
        mov edx, [held_errors+4]
        cmp edx, ebp
        cmovg ebp, edx
        mov ecx, 3
        cmovg eax, ecx
        mov edx, [pair_total_error]
        sub edx, ebp
        xor ecx, ecx
        cmp edx, [pair_palette_error]
        cmovae eax, ecx             ; palette wins ties, then blue/red/green
        cmp eax, 1
        je %%blue %+ pass
        cmp eax, 2
        je %%red %+ pass
        cmp eax, 3
        je %%green %+ pass
        mov eax, [palette_command]
        mov ebx, [palette+eax]
%if pass = 0
        pair_errors
%endif
        jmp %%done %+ pass
%%blue %+ pass:
        movzx eax, byte [desired]
        and eax, 252
        and ebx, 0xffff03
        or ebx, eax
        or eax, 1
%if pass = 0
        sub [pair_total_error], ebp
        mov dword [held_errors], 0
%endif
        jmp %%done %+ pass
%%red %+ pass:
        movzx eax, byte [desired+2]
        and eax, 252
        mov edx, eax
        shl edx, 16
        and ebx, 0x03ffff
        or ebx, edx
        or eax, 2
%if pass = 0
        sub [pair_total_error], ebp
        mov dword [held_errors+8], 0
%endif
        jmp %%done %+ pass
%%green %+ pass:
        movzx eax, byte [desired+1]
        and eax, 252
        mov edx, eax
        shl edx, 8
        and ebx, 0xff03ff
        or ebx, edx
        or eax, 3
%if pass = 0
        sub [pair_total_error], ebp
        mov dword [held_errors+4], 0
%endif
%%done %+ pass:
%if pass = 0
        mov [pair_first], eax
%endif
%assign pass pass+1
%endrep
        shl eax, 8
        or eax, [pair_first]
%endmacro

%macro encode_pair_body 1
%if %1 = 1
        mov edx, [image_pair_entry]
%else
        mov edx, eax
        imul edx, 0x9e3779b1
        xor edx, ebx
        ror edx, 11
        and edx, 0xffff
        shl edx, 4
        add edx, PAIR_CACHE
%endif
        mov ecx, eax
        or ecx, 0x80000000
        cmp [edx], ecx
        jne %%miss
        cmp [edx+4], ebx
        jne %%miss
        mov eax, [edx+8]
        mov ebx, [edx+12]
        jmp %%done
%%miss:
        mov [edx], ecx
        mov [edx+4], ebx
        mov [pair_entry], edx
        pair_search
        mov edx, [pair_entry]
        mov [edx+8], eax
        mov [edx+12], ebx
%%done:
%endmacro

encode_pair:
        nop                         ; stable listing address for diagnostics
        encode_pair_body 0
        ret

encode_image:
        mov dword [image_pair_entry], PAIR_CACHE
        mov esi, CHUNKY
        mov edi, COMMANDS
        mov dword [encode_y], 128
.line:
        xor eax, eax
        mov [edi], eax
        mov [edi+4], ax
        add edi, 6
        xor ebx, ebx
        mov eax, [esi]
        encode_body 1
        stosb
        mov eax, [esi]
        encode_body 0
        stosb
        add esi, 4
        add dword [image_pair_entry], 16
        mov dword [encode_x], 159
.pixel:
        mov eax, [esi]
        encode_pair_body 1
        stosw
        add dword [image_pair_entry], 16
        add esi, 4
        mov eax, [encode_x]
        dec eax
        mov [encode_x], eax
        test eax, eax
        jnz .pixel
        xor eax, eax
        mov ecx, 26
        rep stosb
        dec dword [encode_y]
        mov eax, [encode_y]
        test eax, 3
        jnz .no_poll
        call poll_clock
.no_poll:
        test eax, eax
        jnz .line
        ret

; Generate paired spread tables in ordinary RAM once at initialisation.
; Each byte lane contains two neighbouring bits for one output plane.
build_spread_pairs:
        xor esi, esi
.pair:
        movzx eax, si
        and eax, 255
        mov edx, esi
        shr edx, 8
%assign half 0
%rep 2
%if half = 0
%define source spread_low
%else
%define source spread_high
%endif
        mov ebx, [source+eax*4]
        shl ebx, 1
        or ebx, [source+edx*4]
        mov [0x1a00000+half*0x40000+esi*4], ebx
%assign half half+1
%endrep
%undef source
        inc esi
        cmp esi, 65536
        jb .pair
        ret

; Four paired lookups transpose eight commands at once. Within each
; 32-bit lane accumulator, each byte becomes one plane's eight pixels.
pack_planes:
        mov esi, COMMANDS
        mov edi, PLANES
        mov dword [pack_groups], 128*22
.group:
        mov ebp, 1
.half:
        xor ebx, ebx
        xor edx, edx
%rep 4
        movzx eax, word [esi]
        add esi, 2
        shl ebx, 2
        or ebx, [0x1a00000+eax*4]
        shl edx, 2
        or edx, [0x1a40000+eax*4]
%endrep
%assign p 0
%rep 4
        mov [edi+ebp+p*5632], bl
        shr ebx, 8
        mov [edi+ebp+(p+4)*5632], dl
        shr edx, 8
%assign p p+1
%endrep
        dec ebp
        test ebp, ebp
        jns .half
        add edi, 2
        dec dword [pack_groups]
        mov eax, [pack_groups]
        test eax, 127
        jnz .no_poll
        call poll_clock
.no_poll:
        test eax, eax
        jnz .group
        ret

; Alice reads contiguous little-endian bitplanes from system RAM and feeds
; Lisa with plane 1 last. Its hires pipeline starts at pixel 125; six padding
; commands place the first visible colour at window pixel 128. Eleven
; 32-pixel fetches cover the 320-pixel window. Publish after all 128 rows;
; Lisa doubles each completed row to give a 320 by 256 field.
publish_field:
%assign p 0
%rep 8
        mov dword [ALICE_BPL1PT+p*4], PLANES+p*5632
%assign p p+1
%endrep
        mov word [LISA_STROBE], 2
        mov ebp, 44
.line:
        mov [ALICE_LINE], ebp
        inc ebp
        test ebp, 7
        jnz .no_poll
        call poll_clock
.no_poll:
        cmp ebp, 172
        jb .line
        mov word [LISA_STROBE], 3
        inc dword [DIAG+12]
        ret

absolute 0x1510000
image_pair_entry: resd 1
pair_entry: resd 1
pair_first: resd 1
cache_entry: resd 1
pack_groups: resd 1
palette_cache_entry: resd 1
last_palette_target: resd 1
last_palette_command: resd 1
last_palette_error: resd 1
palette_command: resd 1
desired: resd 1
force_direct: resd 1
held_errors: resd 3
pair_total_error: resd 1
pair_palette_error: resd 1
encode_x: resd 1
encode_y: resd 1
sin_a: resd 1
cos_a: resd 1
sin_b: resd 1
cos_b: resd 1
vertex_count: resd 1
triangle_index: resd 1
triangle_data: resd 15
area: resd 1
min_x: resd 1
max_x: resd 1
min_y: resd 1
max_y: resd 1
dx1: resd 1
dx2: resd 1
dy1: resd 1
dy2: resd 1
grad_z: resd 4
grad_u: resd 4
grad_v: resd 4
edges: resd 12
draw_x: resd 1
draw_y: resd 1
last_blank: resd 1
last_frame_edge: resd 1
can_wait: resd 1
paced_render: resd 1
frame_deadline: resd 1
clock_sample: resd 1
marker_armed: resd 1
fallback_pending: resd 1
clock_usec: resd 1
stage_start: resd 1
blend_fraction:
        resd 1
blend_count: resd 1
transition_progress: resd 1
transition_key: resd 1
phase_offset:
        resd 1
outro_latched:
        resd 1
visual_offset:
        resd 1
loading_spark_index:
        resd 1
grid_x: resd 1
grid_y: resd 1
centres: resd 6
text_x: resd 1
text_y: resd 1
text_scale: resd 1
text_opacity: resd 1
glyph_ptr: resd 1
glyph_x: resd 1
glyph_y: resd 1
glyph_bits: resd 1
glyph_colour: resd 1
cache_building: resd 1
tunnel_cache_enabled: resd 1
tunnel_cache_ready: resd 1
cached_valid: resb 128


section .text
cue_ends: dd 300,2700,5400,6600
scene_lengths: dd 150,1200,1350,600
scene_phase_offsets: dd 150,60,60,60
transition_spark_index:
%assign keyframe 0
%rep 150
%if keyframe = 149
        db 15
%else
        db keyframe/10
%endif
%assign keyframe keyframe+1
%endrep
transition_short_index:
%assign keyframe 0
%rep 60
%if keyframe = 59
        db 15
%else
        db keyframe/4
%endif
%assign keyframe keyframe+1
%endrep
text_inks: dd 0xe8faff,0xb0e0ff,0x70b0dc,0x386080,0xa0d0e0,0xe8ffff,0x68b0d8
title_one: db 'Je BoterHAM Eet',0
subtitle: db 'https://IntuitionEngine.io',0
song_credit_one:
        db 'Eat Your Sandwich',0
song_credit_two:
        db 'by Intuition',0
outro_detail_one:
        db 'x86 asm / Amiga AGA HAM8',0
outro_detail_two:
        db 'C64 SID Commando Music',0
outro_detail_three:
        db 'by Rob Hubbard',0
failure_text: db 'MUSIC ERROR',0

; ============================================================================
; TABLES AND MUSIC
; ============================================================================
; Keep static assets clear of the x86 low-bank compatibility I/O window.
align 0x10000
%include "chromatic_tables.inc"
sid_data:
        incbin "../assets/chromatic_reactor/Commando.sid"
sid_data_end:
