Skip to content

Instantly share code, notes, and snippets.

@kajott
Created December 6, 2020 16:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kajott/2613c7792aa96025a3bd83bbe3a3521e to your computer and use it in GitHub Desktop.
Save kajott/2613c7792aa96025a3bd83bbe3a3521e to your computer and use it in GitHub Desktop.
clock screensaver for DOS
; clock screensaver in 670 bytes (DOS, 186+)
;
; features:
; - VGA mode 13h
; - 24-hour clock in pixellated LED clock style
; - animated screen position
; - smooth alpha-blended transition between seconds
; - fade-in on startup, fade-out on exit
; - smoothly changing colors
;
; assemble and test with:
; yasm -fbin -oclock.com clock.asm && dosbox clock.com
cpu 186
bits 16
org 100h
section .text
; set video mode
mov ax, 13h
int 10h
; get "random" start position
call @getticks ; ax = timer
mov cx, ax ; save into cx
and ax, 127 ; only use 7 bits for Y position
mov bx, 320 ; multiply with 320
mul bx
xchg cl, ch ; use other bits for X position
and cx, 127 ; use 7 bits there too
add ax, cx ; add X position to Y position
mov [@pos], ax ; store position
@mainloop:
; ----- ONCE-PER-SECOND TIME UPDATE AND BITMAP PREPARATION -----
; get absolute system time
mov ah, 2Ch
int 21h
; new second?
cmp dh, [@lastsec]
je @nonewtime
mov [@lastsec], dh
; toggle phase and set di to appropriate text address
mov al, [@phase]
xor al, 1
mov [@phase], al
xor ah, ah
mov di, @text
shl ax, 4 ; translate phase to text offset
add di, ax ; add phase
; decode system time into digits
mov bl, 10 ; set divisor
mov al, ch ; convert hour
xor ah, ah
div bl
stosb
shr ax, 8 ; also resets ah to zero
stosb
mov al, bl ; store separator
stosb
mov al, cl ; convert minute
div bl
stosb
shr ax, 8 ; also resets ah to zero
stosb
mov al, bl ; store separator
stosb
mov al, dh ; convert second
div bl
stosb
mov al, ah
stosb
not al ; store terminator
stosb
; ensure that there are two copies of the text
; (because in the first frame, text0 is still not valid)
cmp byte [@text], 128
jne @textok
mov di, @text
mov si, @text+16
mov cx, 9
rep movsb
@textok:
; clear the bitmap
mov di, @bitmap
mov cx, 32*7
xor al, al
rep stosb
; render the bitmap
mov si, @text ; render text0 into bit 0
mov cl, 0
call @render
mov si, @text+16 ; render text1 into bit 1
mov cl, 1
call @render
; reset alpha
mov al, 32
mov byte [@alpha], al
@nonewtime:
; ----- PALETTE UPDATE AND SCREEN SCALING -----
; set colors using the sine table
mov di, @color
call @getticks ; load time (only MSBs are relevant)
mov dl, al ; save time
call @sin ; R = sin(t)
stosb
add dl, 55h ; t += (1/3) * 256
mov al, dl
call @sin ; G = sin(t)
stosb
add dl, 55h ; t += (1/3) * 256
mov al, dl
call @sin ; B = sin(t)
stosb
; load alphas (bl = alpha, bh = inverse alpha)
mov bl, byte [@alpha]
mov bh, 32
sub bh, bl
; swap alphas depending on phase
mov al, byte [@phase]
or al, al
jnz @noswap
xchg bl, bh
@noswap:
; wait for VSync
mov dx, 3DAh
@vsync1: ; wait until we *leave* VSync
in al, dx ; (required if everything else runs too quickly)
test al, 8
jnz @vsync1
@vsync0: ; wait until we enter VSync again
in al, dx
test al, 8
jz @vsync0
; configure palette
mov dx, 3C8h ; port 3C8h
mov al, 1 ; start with index 1 (0 stays black)
out dx, al
inc dx ; port 3C9h
call @setcolor ; set color 1
mov bl, bh ; set color 2
call @setcolor
mov bl, 32 ; set "always-on" base color (3)
call @setcolor
; scale to screen
push es ; save data segment
mov ax, 0A000h ; load screen segment
mov es, ax
mov di, word [@pos] ; load screen position
sub di, 320*5+6 ; adjust screen position
mov si, @bitmap
mov dh, 7 ; dh = outer line counter
@scalelinea:
mov dl, 4 ; dl = inner line counter
@scalelineb:
push si ; save source pointer
mov cx, 29 ; cx = number of pixels
@scalepixel:
lodsb ; load pixel
times 5 stosb ; scale x5
xor al, al ; add another black pixel
stosb
loop @scalepixel
; end of scaler loops
add di, 320-29*6 ; advance to next screen line
pop si ; restore source pointer
dec dl ; repeat inner line loop
jnz @scalelineb
add si, 32 ; advance to next bitmap line
mov cx, 320 ; clear the following line
rep stosb
dec dh ; repeat outer line loop
jnz @scalelinea
pop es ; restore data segment
; ----- PER-FRAME STATE UPDATES -----
; adjust alpha
mov al, byte [@alpha] ; alpha -= 1
dec al
jns @alphaok ; if (alpha < 0) alpha = 0
xor al, al
@alphaok:
mov [@alpha], al
; adjust position
mov ax, word [@pos] ; load old position and apply delta
mov cx, word [@delta]
add ax, cx
mov word [@pos], ax
mov bx, 320 ; split position into X/Y pair (ax = Y, dx = X)
xor dx, dx
div bx
or dx, dx ; if X=0, set direction right
jnz @x0ok
add cx, 2
@x0ok:
cmp dx, 320-27*6+1 ; if X=max, set direction left
jne @x1ok
sub cx, 2
@x1ok:
or ax, ax ; if Y=0, set direction down
jnz @y0ok
add cx, 640
@y0ok:
cmp ax, 200-5*5+1 ; if Y=max, set direction up
jne @y1ok
sub cx, 640
@y1ok:
mov word [@delta], cx ; store new direction
; update fading
mov al, byte [@fade] ; fade += fdelta
add al, byte [@fdelta]
or al, al ; if fade < 0 then quit
js @exit
mov dl, 64 ; fade = min(fade, 64)
cmp al, dl
jl @fadeok
mov al, dl
@fadeok:
mov byte [@fade], al ; store new fade value
; exit handling
mov ah, 1 ; check for keystroke
int 16h
jz @mainloop ; if no key pressed, resume with frame loop
xor ah, ah ; consume keystroke
int 16h
mov al, byte [@fdelta] ; fdelta -= 2 (if it was +1, makes it -1;
sub al, 2 ; if it already was negative, this speeds up
mov byte [@fdelta], al ; the fade-out operation)
jmp @mainloop ; new loop
@exit:
mov ax, 3
int 10h
ret
; -------- FUNCTIONS --------
; FUNCTION @render(si = text pointer, cl = output bit select)
@render:
xor ch,ch ; ch = current line number
@renderline:
push si ; create a backup of the text pointer
; set DI to bitmap pointer: @bitmap + 33 + ch * 32
mov di, @bitmap+33
xor bh, bh
mov bl, ch
shl bl, 5
add di, bx
; set glyph base pointer: @glyphs + ch * 12
mov dl, bl ; backup ch*32
shr bl, 2 ; bl = ch*8
shr dl, 3 ; dl = ch*4
add bl, dl ; bl = ch*8 + ch*4 = ch*12
add bx, @glyphs ; bx = @glyphs + ch*12
@renderglyph:
lodsb ; load glyph index
or al, al ; check for end marker
js @renderendline
xlatb ; translate glyph index into bit pattern
mov ah, al ; save bit pattern
@renderpixel:
mov al, ah ; restore bit pattern
and al, 1 ; mask LSB
shl al, cl ; move to appropriate bit position
or al, [es:di]
stosb ; OR into bitmap
shr ah, 1 ; go to next bit
cmp ah, 1 ; end marker reached? if not, render next pixel
jne @renderpixel
jmp @renderglyph ; render next glyph
@renderendline:
pop si ; restore the text pointer backup
inc ch ; move to next line
cmp ch, 5 ; go to next line
jne @renderline
ret
; FUNCTION @setcolor(bl = alpha)
@setcolor:
mov al, bl
mul byte [@fade] ; multiply with fade factor to get final alpha
shr ax, 5
mov bl, al ; save final alpha
mul byte [@color+0] ; multiply with R and set palette
shr ax, 6
out dx, al
mov al, bl ; restore final alpha
mul byte [@color+1] ; multiply with G and set palette
shr ax, 6
out dx, al
mov al, bl ; restore final alpha
mul byte [@color+2] ; multiply with B and set palette
shr ax, 6
out dx, al
ret
; FUNCTION @sin(al) -> al
@sin:
mov ah, al ; save argument
and al, 63 ; clip sub-quadrant index
test ah, 64 ; check for quadrants 1 and 3
jz @quadxok
xor al, 63 ; reverse table in affected quadrants
@quadxok:
mov bx, @sintab ; lookup base sin
xlatb
test ah, 128 ; check for quadrants 2 and 3
jz @quadyok
xor al, 63 ; reverse signal in affected quadrants
@quadyok:
ret ; done
; FUNCTION @getticks -> ax
; get 18.2 Hz tick counter from 0:046Ch
@getticks:
push ds
xor ax, ax
mov ds, ax
mov ax, [046Ch]
pop ds
ret
;------- DATA -------
section .data ; constants and initialized variables
@delta: dw 321 ; position increment
; python -c 'import math;print([int((math.sin((i+.5)/128*math.pi)/2+.5)*63+.5) for i in range(64)])'
@sintab:
db 32, 33, 33, 34, 35, 36, 37, 37, 38, 39, 40, 40, 41, 42, 42, 43
db 44, 45, 45, 46, 47, 47, 48, 49, 49, 50, 51, 51, 52, 52, 53, 53
db 54, 55, 55, 56, 56, 57, 57, 57, 58, 58, 59, 59, 59, 60, 60, 60
db 61, 61, 61, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 63
@glyphs: ; glyph bitmaps, intermixed with initialized variables
; [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [:]
db 10111b, 10100b, 10111b, 10111b, 10101b, 10111b, 10111b, 10111b, 10111b, 10111b, 100b
@lastsec: db 44h ; second where the last phase change occurred
db 10101b, 10100b, 10100b, 10100b, 10101b, 10001b, 10001b, 10100b, 10101b, 10101b, 101b
@phase: db 0 ; current phase: 0/1
db 10101b, 10100b, 10111b, 10111b, 10111b, 10111b, 10111b, 10100b, 10111b, 10111b, 100b
@fade: db 0 ; current fade-in/-out state
db 10101b, 10100b, 10001b, 10100b, 10100b, 10100b, 10101b, 10100b, 10101b, 10100b, 101b
@fdelta: db 1 ; current fade direction
db 10111b, 10100b, 10111b, 10111b, 10100b, 10111b, 10111b, 10100b, 10111b, 10111b, 100b
@text: db 128 ; first text line (starts with an "invalid" marker)
section .bss ; uninitialized variables follow
resb 31 ; remainder of the text lines
@alpha: resb 1 ; alpha blending factor
@pos: resw 1 ; screen position (linear offset in video memory)
@color: resb 3 ; current RGB color
@bitmap: resb 32*7 ; unscaled bitmap (with some borders)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment