A bouncing ball
The same program in M68K, MIPS, RISC-V, Z80.
Animate a ball that turns around at the screen's edges. The program draws 600 frames, at up to 60 frames a second, so it runs for at least ten seconds. With two framebuffer pages, it finishes each picture out of view before showing it. If only one page is available, it draws directly on the visible page instead.
How a frame reaches the screen
A page is enough framebuffer memory for one whole picture. The Screen in this editor provides two pages, one after the other in memory. Only one is visible at a time. The program starts by painting both pages with the background colour, then repeats these steps:
- Choose the page to draw on and erase the rectangle around its previous ball.
- Move the ball, reverse its step if it passes an edge, and draw it at the new position.
- Ask the Screen to show the completed page, then choose the other page for the next frame.
Several values keep these jobs separate:
yresis the height of one visible picture.yres_virtualis the height of all the framebuffer memory, including lines outside that picture. Two pages need at least2 × yreslines.pageselects the page to draw on: 0 or 1.drawn_xanddrawn_yremember the last ball position separately for each page, so the program can erase the correct rectangle.yoffsetselects the first framebuffer line to display. TheFBIOPAN_DISPLAYioctl asks the Screen to display the picture starting at that line.- On this editor's Screen, that ioctl also waits for the next vertical blank, the interval between display refreshes. This supplies the frame loop's timing, including in one-page mode.
For example, the first frame draws on page 1 while page 0 is visible, then shows page 1. The second frame draws on page 0 and shows it. The third returns to page 1, which still contains the ball from the first frame. Its saved position tells the program what to erase. On the first visit to each page there is no ball yet, so erasing its initial rectangle just paints background again.
Press Build, then Run.
; @screen width=320 height=240
default rel
%include "sys/syscall.inc"
%include "fcntl.inc"
%include "sys/mman.inc"
%include "linux/fb.inc"
BALL equ 16 ; the ball's width and height
FRAMES equ 600 ; at least ten seconds, at up to 60 frames a second
BACKGROUND equ 0x00101820
BALL_COLOUR equ 0x00ffcc33
section .rodata
fb_path: db "/dev/fb0", 0
ball_rows: db 6, 10, 12, 14, 14, 16, 16, 16, 16, 16, 16, 14, 14, 12, 10, 6
section .data
x: dd 40 ; the ball's top-left corner
y: dd 30
step_x: dd 3 ; pixels a frame, + right or down, - left or up
step_y: dd 2
page: dd 1 ; the page being drawn, the one not shown
flip: dd 1 ; 1 to change pages each frame, 0 with only one
drawn_x: dd 40, 40 ; where each page last had the ball
drawn_y: dd 30, 30
section .bss
var: resb fb_var_screeninfo_size
fix: resb fb_fix_screeninfo_size
pixels: resq 1 ; the first page's top-left pixel
section .text
global _start
; fill_rect: a rectangle of colour r8d, edx pixels wide and ecx lines high, whose
; top-left corner is at column edi of line esi of the page rbx points to.
; Reads rbx and r13 (bytes a line); changes rax, rcx, rsi, rdi, r9 and r10.
fill_rect:
test edx, edx
jle .done
test ecx, ecx
jle .done
mov r9d, ecx
imul rsi, r13
lea r10, [rbx + rsi]
lea r10, [r10 + rdi*4]
mov eax, r8d
.line:
mov rdi, r10
mov ecx, edx
rep stosd
add r10, r13
dec r9d
jnz .line
.done:
ret
_start:
mov eax, SYS_open
lea rdi, [fb_path]
mov esi, O_RDWR
syscall
test rax, rax
js fail
mov r12, rax ; r12: the framebuffer's descriptor
mov eax, SYS_ioctl
mov rdi, r12
mov esi, FBIOGET_VSCREENINFO
lea rdx, [var]
syscall
mov eax, [var + fb_var_screeninfo.yres] ; is there room for a second page?
add eax, eax
cmp [var + fb_var_screeninfo.yres_virtual], eax
jae two_pages
mov dword [page], 0 ; no: draw every frame on the page shown
mov dword [flip], 0
two_pages:
mov eax, SYS_ioctl
mov rdi, r12
mov esi, FBIOGET_FSCREENINFO
lea rdx, [fix]
syscall
mov eax, SYS_mmap
xor edi, edi
mov esi, [fix + fb_fix_screeninfo.smem_len] ; every page
mov edx, PROT_READ | PROT_WRITE
mov r10d, MAP_SHARED
mov r8, r12
xor r9d, r9d
syscall
cmp rax, -4095
jae fail
mov [pixels], rax
mov r13d, [fix + fb_fix_screeninfo.line_length]
mov rbx, rax ; paint every page, yres_virtual lines
xor edi, edi
xor esi, esi
mov edx, [var + fb_var_screeninfo.xres]
mov ecx, [var + fb_var_screeninfo.yres_virtual]
mov r8d, BACKGROUND
call fill_rect
mov r15d, FRAMES ; r15: frames left
frame:
mov eax, [page] ; rbx: the top-left pixel of the page to draw
imul eax, [var + fb_var_screeninfo.yres]
imul rax, r13
add rax, [pixels]
mov rbx, rax
mov eax, [page] ; erase the rectangle at this page's saved ball position
lea rcx, [drawn_x]
mov edi, [rcx + rax*4]
lea rcx, [drawn_y]
mov esi, [rcx + rax*4]
mov edx, BALL
mov ecx, BALL
mov r8d, BACKGROUND
call fill_rect
mov eax, [x] ; move across, and turn around at an edge
add eax, [step_x]
js turn_x ; past the left edge
mov ecx, [var + fb_var_screeninfo.xres]
sub ecx, BALL
cmp eax, ecx
jle x_done ; not past the right edge
turn_x:
neg dword [step_x]
add eax, [step_x] ; back where it was
add eax, [step_x] ; and one step the other way
x_done:
mov [x], eax
mov eax, [y] ; the same vertically
add eax, [step_y]
js turn_y
mov ecx, [var + fb_var_screeninfo.yres]
sub ecx, BALL
cmp eax, ecx
jle y_done
turn_y:
neg dword [step_y]
add eax, [step_y]
add eax, [step_y]
y_done:
mov [y], eax
mov eax, [page] ; this page will have the ball here
lea rcx, [drawn_x]
mov edx, [x]
mov [rcx + rax*4], edx
lea rcx, [drawn_y]
mov edx, [y]
mov [rcx + rax*4], edx
xor r14d, r14d ; draw the ball, one row at a time
ball_row:
lea rax, [ball_rows]
movzx edx, byte [rax + r14] ; this row's width
mov edi, BALL
sub edi, edx
shr edi, 1 ; centred: (BALL - width) / 2 from the left
add edi, [x]
mov esi, [y]
add esi, r14d
mov ecx, 1
mov r8d, BALL_COLOUR
call fill_rect
inc r14d
cmp r14d, BALL
jb ball_row
mov eax, [page] ; show the page just drawn, at the vertical blank
imul eax, [var + fb_var_screeninfo.yres]
mov [var + fb_var_screeninfo.yoffset], eax
mov eax, SYS_ioctl
mov rdi, r12
mov esi, FBIOPAN_DISPLAY
lea rdx, [var]
syscall
mov eax, [flip] ; the next frame goes on the other page
xor [page], eax
dec r15d
jnz frame
mov eax, SYS_exit
xor edi, edi
syscall
fail:
mov eax, SYS_exit
mov edi, 1
syscall
Selecting and showing a page
rbx tells fill_rect where its page begins. Page 0 starts at pixels; page 1 starts
yres × line_length bytes later. The frame loop computes this address as
pixels + page × yres × line_length, so the drawing routine can use the same coordinates on
either page.
To show page 0, the program sets yoffset to 0. To show page 1, it sets yoffset to yres,
skipping the first page's lines. It then passes var to FBIOPAN_DISPLAY.
With two pages, drawing happens on the hidden page and panning shows only the finished picture.
This is double buffering: the viewer does not see the erase and redraw steps. At the end of
the frame, xor [page], eax uses the 1 in flip to switch page between 1 and 0.
A framebuffer does not always provide two pages. The program checks yres_virtual before using
the second one. If there is room for only one, it sets both page and flip to 0. XOR with 0
leaves page at 0, and yoffset stays 0 too. Each frame erases and draws on the visible page,
so the intermediate drawing may be visible. In this mode the saved position belongs to the
previous frame, rather than two frames ago.
Erasing and drawing the ball
Painting a whole 320-by-240 page every frame would take 76,800 pixel stores. Instead, the program erases the whole 16-by-16 rectangle around that page's old ball, then draws the new ball. Nothing else on the page changes, so this produces the same finished picture as a full repaint.
drawn_x and drawn_y each contain two four-byte coordinates. In [rcx + rax*4], rax is the
page number: 0 selects the first coordinate and 1 selects the second. These arrays keep the two
pages' positions separate even though x and y hold just the ball's current position.
The table ball_rows gives the width of each of the ball's 16 rows. Each row is centred inside
the 16-pixel width with (BALL - width) / 2. For the first row, the width is 6, so drawing starts
(16 - 6) / 2 = 5 pixels to the right of x. The middle rows have width 16 and start at x.
Sixteen calls to fill_rect, each one line high, produce the disc.
Frame timing
On this editor's Screen, FBIOPAN_DISPLAY waits for the next vertical blank. The Screen
refreshes 60 times a second. If drawing finishes before the next blank, the pan waits for it
and then shows the requested page. If drawing takes longer, the program misses refreshes and
the animation runs more slowly. Its maximum rate is therefore 60 frames a second.
The loop needs only draw, then pan. Adding FBIO_WAITFORVSYNC, which waits without panning,
before the pan would add another wait. Even with fast drawing, that would limit the animation
to 30 frames a second on this Screen.
After FRAMES frames the program ends, and the Screen keeps the last frame. With FRAMES set
to 600, this takes at least ten seconds. Set it to 6000 for at least 100 seconds.
Turning at an edge
x is the ball's left edge, so it fits while x is from 0 to xres - BALL. The program adds the
step; a result below 0 sets the sign flag for js, and one above xres - BALL fails jle. Either
way, neg reverses the step. Adding the reversed step once returns to the previous position;
adding it again moves one step in the other direction.
For example, the rightmost allowed x is 320 - 16 = 304. From x = 302, a step of 3 would
reach 305. The program reverses the step to -3, adds it to return to 302, then adds it again to
reach 299. The vertical movement uses the same calculation with y and yres.
Try it
- Make the ball faster: set
step_xto 5 andstep_yto 4. - Leave a trail: delete the
call fill_rectthat erases the ball. With two pages, each page then keeps the balls drawn on alternate frames. With one page, it keeps every frame's ball. - Alternate between two colours when the ball reverses direction on an axis. Add
colour: dd BALL_COLOURto.data, changemov r8d, BALL_COLOURin the ball's loop tomov r8d, [colour], and after eachneg dword [step_x]andneg dword [step_y]addxor dword [colour], 0x00ffffff. This toggles the red, green and blue bits between the original colour and its opposite. If both axes reverse in one frame, the two toggles cancel and the displayed colour stays the same.
Instructions in this lesson
testjlemovimulleastosdadddecjneretsyscalljscmpjaexorcallsubnegmovzxshrincjb