sanyo-verlet88

verlets
org 100h
cpu 8086

COLS       equ 80              ; visible cell columns
ROWS       equ 50              ; visible cell rows
GREEN      equ 0x3c00          ; RGB plane segment for green

POINTS     equ 12              ; chain point count
LINK_DIST  equ 4               ; rest length in cells
FP_SHIFT   equ 4               ; fixed-point fractional bits
FP_ONE     equ 1 << FP_SHIFT   ; 1.0 in fixed-point
GRAVITY_FP equ 4               ; gravity strength in fixed-point
LINK_DIST_FP equ LINK_DIST << FP_SHIFT ; rest length in fixed-point
CONSTRAINT_STEP equ 4          ; constraint correction per pass
COLS_FP    equ COLS << FP_SHIFT ; screen width in fixed-point
ROWS_FP    equ ROWS << FP_SHIFT ; screen height in fixed-point
ITERATIONS equ 4               ; constraint iterations per frame
START_ROW  equ 1               ; initial chain row
START_COL  equ 18              ; initial chain start column
PIN_START_ROW equ 0            ; pinned point row
PRE_FRAMES equ 150            ; buffered animation frames
DELAY      equ 0x800           ; playback busy-wait

setup:
    push cs
    pop ds

    call clear_green_plane
    call init_chain
    call warm_up_chain
    call copy_screen_positions

    call prerender_frames
    call clear_green_plane

    mov word [play_frame], 0
    mov word [draw_word], 0xffff
    xor ax, ax
    call draw_buffered_chain

main_loop:
    call set_next_frame
    call update_green_chain
    mov ax, [next_frame]
    mov [play_frame], ax
    call frame_delay
    jmp main_loop

simulate_frame:
    call move_pin
    call verlet_step

    mov cl, ITERATIONS
.solve_again:
    push cx
    call pin_first
    call solve_links
    call keep_inside_screen
    pop cx
    loop .solve_again

    call pin_first
    ret

prerender_frames:
    mov word [render_frame], 0
.next:
    call simulate_frame
    mov ax, [render_frame]
    call store_frame_positions
    inc word [render_frame]
    cmp word [render_frame], PRE_FRAMES
    jb .next
    ret

warm_up_chain:
    mov bl, 8
.next:
    call simulate_frame
    dec bl
    jnz .next
    ret

set_next_frame:
    mov ax, [play_frame]
    inc ax
    cmp ax, PRE_FRAMES
    jb .done
    xor ax, ax
.done:
    mov [next_frame], ax
    ret

set_frame_offset:
    mov bx, POINTS
    mul bx
    mov [frame_offset], ax
    ret

store_frame_positions:
    call set_frame_offset
    mov bx, [frame_offset]
    xor si, si
    xor di, di
.next:
    mov cl, FP_SHIFT
    mov ax, [rows + si]
    shr ax, cl
    mov [frame_rows + bx + di], al
    mov ax, [cols + si]
    shr ax, cl
    mov [frame_cols + bx + di], al
    add si, 2
    inc di
    cmp di, POINTS
    jb .next
    ret

load_frame_positions:
    call set_frame_offset
    mov bx, [frame_offset]
    xor di, di
.next:
    mov al, [frame_rows + bx + di]
    mov [screen_rows + di], al
    mov al, [frame_cols + bx + di]
    mov [screen_cols + di], al
    inc di
    cmp di, POINTS
    jb .next
    ret

load_prev_frame_positions:
    call set_frame_offset
    mov bx, [frame_offset]
    xor di, di
.next:
    mov al, [frame_rows + bx + di]
    mov [prev_screen_rows + di], al
    mov al, [frame_cols + bx + di]
    mov [prev_screen_cols + di], al
    inc di
    cmp di, POINTS
    jb .next
    ret

load_next_frame_positions:
    call set_frame_offset
    mov bx, [frame_offset]
    xor di, di
.next:
    mov al, [frame_rows + bx + di]
    mov [next_screen_rows + di], al
    mov al, [frame_cols + bx + di]
    mov [next_screen_cols + di], al
    inc di
    cmp di, POINTS
    jb .next
    ret

draw_buffered_chain:
    call load_frame_positions
    call draw_screen_chain
    ret

update_green_chain:
    mov ax, GREEN
    mov es, ax
    mov ax, [play_frame]
    call load_prev_frame_positions
    mov ax, [next_frame]
    call load_next_frame_positions
    call draw_transition_chain
    ret

draw_transition_chain:
    mov byte [draw_phase], 0
    xor si, si
.next:
    mov word [draw_word], 0x0000
    mov bh, [prev_screen_rows + si]
    mov bl, [prev_screen_cols + si]
    mov dh, [prev_screen_rows + si + 1]
    mov dl, [prev_screen_cols + si + 1]
    call draw_link

    mov word [draw_word], 0xffff
    mov bh, [next_screen_rows + si]
    mov bl, [next_screen_cols + si]
    mov dh, [next_screen_rows + si + 1]
    mov dl, [next_screen_cols + si + 1]
    call draw_link

    inc si
    cmp si, POINTS - 1
    jb .next

    mov word [draw_word], 0x0000
    mov bh, [prev_screen_rows + POINTS - 1]
    mov bl, [prev_screen_cols + POINTS - 1]
    call draw_point

    mov word [draw_word], 0xffff
    mov bh, [next_screen_rows + POINTS - 1]
    mov bl, [next_screen_cols + POINTS - 1]
    call draw_point
    ret

; Initialize a vertical chain.
; rows/cols hold current positions; old_rows/old_cols hold previous positions.
init_chain:
    xor si, si
    mov ax, START_ROW << FP_SHIFT
.next:
    mov [rows + si], ax
    mov [old_rows + si], ax
    mov word [cols + si], START_COL << FP_SHIFT
    mov word [old_cols + si], START_COL << FP_SHIFT
    add ax, LINK_DIST_FP
    add si, 2
    cmp si, POINTS * 2
    jb .next
    ret

copy_screen_positions:
    xor si, si
    xor di, di
.next:
    mov cl, FP_SHIFT
    mov ax, [rows + si]
    shr ax, cl
    mov [screen_rows + di], al
    mov ax, [cols + si]
    shr ax, cl
    mov [screen_cols + di], al
    add si, 2
    inc di
    cmp di, POINTS
    jb .next
    ret

clear_screen:
    xor di, di
    xor ax, ax
    mov cx, COLS * ROWS * 2    ; 80*50 cells, 2 words per cell
    rep stosw
    ret

clear_green_plane:
    mov ax, GREEN
    mov es, ax
    call clear_screen
    ret

frame_delay:
%if DELAY
    mov cx, DELAY
.wait:
    loop .wait
%endif
    ret

; Move the pinned endpoint in a full-width horizontal triangle wave.
move_pin:
    inc byte [frame]
    mov al, [frame]
    cmp al, 158
    jb .phase_ok
    sub al, 158
    mov [frame], al
.phase_ok:
    cmp al, 79
    jbe .rising
    mov bl, 157
    sub bl, al
    mov al, bl
.rising:
    mov ah, 0
    mov cl, FP_SHIFT
    shl ax, cl
    mov [pin_col], ax
    mov word [pin_row], PIN_START_ROW << FP_SHIFT
    ret

; Pin point 0.
pin_first:
    mov ax, [pin_row]
    mov [rows], ax
    mov [old_rows], ax
    mov ax, [pin_col]
    mov [cols], ax
    mov [old_cols], ax
    ret

; Verlet step for points 1..POINTS-1.
; new = current + (current - old) + gravity(row only)
verlet_step:
    mov si, 2
.next:
    mov ax, [rows + si]
    mov bx, ax
    sub ax, [old_rows + si]    ; row velocity
    mov [old_rows + si], bx
    add ax, bx
    add ax, GRAVITY_FP         ; gravity
    mov [rows + si], ax

    mov ax, [cols + si]
    mov bx, ax
    sub ax, [old_cols + si]    ; col velocity
    mov [old_cols + si], bx
    add ax, bx
    mov [cols + si], ax

    add si, 2
    cmp si, POINTS * 2
    jb .next
    ret

; Keep all non-pinned points in the visible 80x50 cell grid.
keep_inside_screen:
    mov si, 2
.next:
    cmp word [rows + si], 0
    jge .row_low_ok
    neg word [rows + si]
    neg word [old_rows + si]
    jmp .col
.row_low_ok:
    cmp word [rows + si], ROWS_FP
    jb .col
    mov ax, (ROWS - 1) << FP_SHIFT
    shl ax, 1
    sub ax, [rows + si]
    mov [rows + si], ax
    mov ax, (ROWS - 1) << FP_SHIFT
    shl ax, 1
    sub ax, [old_rows + si]
    mov [old_rows + si], ax
.col:
    cmp word [cols + si], 0
    jge .col_low_ok
    neg word [cols + si]
    neg word [old_cols + si]
    jmp .done
.col_low_ok:
    cmp word [cols + si], COLS_FP
    jb .done
    mov ax, (COLS - 1) << FP_SHIFT
    shl ax, 1
    sub ax, [cols + si]
    mov [cols + si], ax
    mov ax, (COLS - 1) << FP_SHIFT
    shl ax, 1
    sub ax, [old_cols + si]
    mov [old_cols + si], ax
.done:
    add si, 2
    cmp si, POINTS * 2
    jb .next
    ret

; Constraint pass over adjacent points.
; Processing uses sqrt(dx*dx + dy*dy). This port uses an octagonal distance
; approximation, max(abs(dx),abs(dy)) + min(abs(dx),abs(dy))/2, then corrects
; along the link vector. That avoids the axis/diagonal locking of independent
; row/col nudges without paying for an integer square root.
solve_links:
    xor si, si
.next:
    mov ax, [cols + si + 2]
    sub ax, [cols + si]        ; signed col gap: next - current
    mov [link_dx], ax
    mov bx, ax
    cmp bx, 0
    jge .abs_dx_ready
    neg bx
.abs_dx_ready:
    mov [abs_dx], bx

    mov ax, [rows + si + 2]
    sub ax, [rows + si]        ; signed row gap: next - current
    mov [link_dy], ax
    mov bx, ax
    cmp bx, 0
    jge .abs_dy_ready
    neg bx
.abs_dy_ready:
    mov [abs_dy], bx

    mov ax, [abs_dx]
    mov bx, [abs_dy]
    cmp ax, bx
    jae .have_max_min
    xchg ax, bx
.have_max_min:
    shr bx, 1
    add ax, bx
    cmp ax, 0
    je .done_link
    mov [link_dist], ax
    sub ax, LINK_DIST_FP
    mov [link_error], ax

    imul word [link_dx]        ; DX:AX = error * dx
    idiv word [link_dist]
    mov [move_col], ax

    mov ax, [link_error]
    imul word [link_dy]        ; DX:AX = error * dy
    idiv word [link_dist]
    mov [move_row], ax

    cmp si, 0
    je .pinned_parent

    mov ax, [move_col]
    sar ax, 1
    add [cols + si], ax
    mov bx, [move_col]
    sub bx, ax
    sub [cols + si + 2], bx

    mov ax, [move_row]
    sar ax, 1
    add [rows + si], ax
    mov bx, [move_row]
    sub bx, ax
    sub [rows + si + 2], bx
    jmp .done_link

.pinned_parent:
    mov ax, [move_col]
    sub [cols + si + 2], ax
    mov ax, [move_row]
    sub [rows + si + 2], ax

.done_link:
    add si, 2
    cmp si, (POINTS - 1) * 2
    jb .next
    ret

draw_screen_chain:
    mov byte [draw_phase], 0
    xor si, si
.next:
    mov bh, [screen_rows + si]
    mov bl, [screen_cols + si]
    mov dh, [screen_rows + si + 1]
    mov dl, [screen_cols + si + 1]
    call draw_link

    inc si
    cmp si, POINTS - 1
    jb .next

    mov bh, [screen_rows + POINTS - 1]
    mov bl, [screen_cols + POINTS - 1]
    call draw_point
    ret

draw_chain:
    mov byte [draw_phase], 0
    xor si, si
.next:
    mov cl, FP_SHIFT
    mov ax, [rows + si]
    shr ax, cl
    mov bh, al
    mov ax, [cols + si]
    shr ax, cl
    mov bl, al
    mov ax, [rows + si + 2]
    shr ax, cl
    mov dh, al
    mov ax, [cols + si + 2]
    shr ax, cl
    mov dl, al
    call draw_link

    add si, 2
    cmp si, (POINTS - 1) * 2
    jb .next

    mov cl, FP_SHIFT
    mov ax, [rows + (POINTS - 1) * 2]
    shr ax, cl
    mov bh, al
    mov ax, [cols + (POINTS - 1) * 2]
    shr ax, cl
    mov bl, al
    call draw_point
    ret

; input: BH/BL=start row/col, DH/DL=end row/col
draw_link:
    mov [line_row], bh
    mov [line_col], bl
    mov [line_end_row], dh
    mov [line_end_col], dl

    mov al, [line_end_col]
    sub al, [line_col]
    cbw
    mov byte [line_sx], 1
    cmp ax, 0
    jge .dx_ready
    neg ax
    mov byte [line_sx], -1
.dx_ready:
    mov [line_dx], ax

    mov al, [line_end_row]
    sub al, [line_row]
    cbw
    mov byte [line_sy], 1
    cmp ax, 0
    jge .dy_ready
    neg ax
    mov byte [line_sy], -1
.dy_ready:
    neg ax
    mov [line_dy], ax

    add ax, [line_dx]
    mov [line_err], ax

.next:
    mov bh, [line_row]
    mov bl, [line_col]
    call draw_point

    mov al, [line_col]
    cmp al, [line_end_col]
    jne .step
    mov al, [line_row]
    cmp al, [line_end_row]
    je .done

.step:
    mov ax, [line_err]
    add ax, ax                 ; e2 = 2 * err

    cmp ax, [line_dy]
    jl .skip_col
    mov bx, [line_err]
    add bx, [line_dy]
    mov [line_err], bx
    mov bl, [line_sx]
    add [line_col], bl
.skip_col:
    cmp ax, [line_dx]
    jg .next
    mov bx, [line_err]
    add bx, [line_dx]
    mov [line_err], bx
    mov bl, [line_sy]
    add [line_row], bl
    jmp .next
.done:
    ret

; input: BH=row, BL=col
draw_point:
    call cell_offset
    mov ax, [draw_word]
    cmp ax, 0
    je .erase

    mov al, [draw_phase]
    inc byte [draw_phase]
    cmp byte [draw_phase], 5
    jb .phase_ready
    mov byte [draw_phase], 0
.phase_ready:
    cmp al, 0
    je .full

    mov ax, 0x0800             ; bytes: 00 08
    or word [es:di], ax
    add di, 2
    mov ax, 0x0800             ; bytes: 00 08
    or word [es:di], ax
    ret
.full:
    mov ax, 0xffff
    or word [es:di], ax
    add di, 2
    or word [es:di], ax
    ret
.erase:
    stosw
    stosw
    ret

; input:  BH=row, BL=col
; output: DI=byte offset in active color plane
cell_offset:
    xor di, di

    mov al, bh
    xor ah, ah
    mov cx, COLS * 4           ; 4 bytes per cell, 80 cells per row
    mul cx                     ; AX = row * (COLS*4)
    mov di, ax

    xor ax, ax
    mov al, bl
    shl ax, 1                  ; col * 2
    shl ax, 1                  ; col * 4
    add di, ax
    ret

frame    db 0
render_frame dw 0
play_frame dw 0
next_frame dw 0
frame_offset dw 0
pin_row  dw PIN_START_ROW << FP_SHIFT
pin_col  dw START_COL << FP_SHIFT
draw_word dw 0xffff
draw_phase db 0
rows     times POINTS dw 0
cols     times POINTS dw 0
old_rows times POINTS dw 0
old_cols times POINTS dw 0
screen_rows times POINTS db 0
screen_cols times POINTS db 0
prev_screen_rows times POINTS db 0
prev_screen_cols times POINTS db 0
next_screen_rows times POINTS db 0
next_screen_cols times POINTS db 0
line_row     db 0
line_col     db 0
line_end_row db 0
line_end_col db 0
line_sx      db 0
line_sy      db 0
line_dx      dw 0
line_dy      dw 0
line_err     dw 0
link_dx      dw 0
link_dy      dw 0
abs_dx       dw 0
abs_dy       dw 0
link_dist    dw 0
link_error   dw 0
move_col     dw 0
move_row     dw 0
frame_rows   times PRE_FRAMES * POINTS db 0
frame_cols   times PRE_FRAMES * POINTS db 0