03_fb_bouncing_ball
Example 03: Framebuffer Bouncing Ball
Real-time physics and high-speed memory-mapped graphics rendering.
This tutorial demonstrates how to build a real-time, high-frame-rate animation using raw /dev/fb0 memory mapping (mmap()). We will implement basic physics (gravity, boundary collisions, velocity) to animate a colored ball bouncing off the screen edges, while ensuring safe terminal recovery using standard signal handling.
📝 Concepts Introduced
- Direct zero-copy display access via memory mapping (
mmap()). - High-speed frame clearance and pixel-drawing functions.
- 2D coordinate-to-1D index translation using display stride (line length).
- Basic physics loop: gravity ($g$), velocity ($v_x, v_y$), and elastic boundary collisions.
- Handling termination signals (
SIGINT,SIGTERM) to guarantee TTY console recovery.
The Code (fb_bouncing_ball.c)
// BOREDOS_APP_DESC: Framebuffer Bouncing Ball - real-time graphics and physics simulation via /dev/fb0.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/kd.h>
#include <sys/mman.h>
#include <stdint.h>
// Global structures to coordinate signal handler recovery
int g_fb_fd = -1;
uint32_t *g_fb_mmap = MAP_FAILED;
uint32_t g_screen_size = 0;
// Cleanup callback to restore TTY and unmap memory
void clean_exit(int sig) {
if (g_fb_mmap != MAP_FAILED && g_screen_size > 0) {
munmap(g_fb_mmap, g_screen_size);
}
if (g_fb_fd >= 0) {
close(g_fb_fd);
}
// Crucial: Restore standard text console blitting
ioctl(0, KDSETMODE, (void*)KD_TEXT);
printf("\n[Cleanup] Graphics closed and console restored cleanly (Signal %d).\n", sig);
exit(0);
}
int main(void) {
// 1. Open the framebuffer device
g_fb_fd = open("/dev/fb0", O_RDWR);
if (g_fb_fd < 0) {
printf("Error: cannot open /dev/fb0. Are you running as root?\n");
return 1;
}
// 2. Query display dimensions and pitch stride
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
if (ioctl(g_fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0 ||
ioctl(g_fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) {
printf("Error: failed to query framebuffer layout.\n");
close(g_fb_fd);
return 1;
}
g_screen_size = finfo.line_length * vinfo.yres;
uint32_t stride_pixels = finfo.line_length / 4; // Width in pixels (including padding)
// 3. Register signal handlers to guarantee terminal restoration
signal(SIGINT, clean_exit);
signal(SIGTERM, clean_exit);
// 4. Disable kernel TTY text blitting
if (ioctl(0, KDSETMODE, (void*)KD_GRAPHICS) < 0) {
printf("Warning: failed to set TTY console to graphics mode.\n");
}
// 5. Memory-map physical display memory directly to process address space
g_fb_mmap = (uint32_t *)mmap(NULL, g_screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, g_fb_fd, 0);
if (g_fb_mmap == MAP_FAILED) {
ioctl(0, KDSETMODE, (void*)KD_TEXT); // Recover console before exit
printf("Error: failed to map framebuffer device to memory!\n");
close(g_fb_fd);
return 1;
}
// 6. Allocate a local backbuffer for double-buffering
uint32_t *backbuffer = (uint32_t *)malloc(g_screen_size);
if (!backbuffer) {
ioctl(0, KDSETMODE, (void*)KD_TEXT);
printf("Error: failed to allocate heap backbuffer!\n");
munmap(g_fb_mmap, g_screen_size);
close(g_fb_fd);
return 1;
}
// 7. Physics and Rendering Loop Setup
double ball_x = vinfo.xres / 2.0;
double ball_y = vinfo.yres / 4.0;
double vel_x = 5.0; // Initial horizontal speed
double vel_y = 0.0; // Initial vertical speed
const double gravity = 0.35; // Downward acceleration constant
const double elasticity = 0.88; // Bounce bounce conservation coefficient
const double radius = 35.0; // Size of the bouncing ball
const uint32_t ball_color = 0xFFFF3333; // Bright Red (BGRA: 0xAARRGGBB)
const uint32_t bg_color = 0xFF121212; // Sleek Dark Grey
printf("Starting bouncing physics loop... Press Ctrl+C inside terminal to exit.\n");
// Run the real-time graphics loop for 600 frames (approx 10 seconds at 60 FPS)
for (int frame = 0; frame < 600; frame++) {
// --- PHYSICS UPDATE ---
vel_y += gravity; // Apply gravity
ball_x += vel_x; // Apply horizontal velocity
ball_y += vel_y; // Apply vertical velocity
// Handle Horizontal boundary collisions (Left / Right edges)
if (ball_x - radius < 0) {
ball_x = radius;
vel_x = -vel_x * elasticity;
} else if (ball_x + radius >= vinfo.xres) {
ball_x = vinfo.xres - radius - 1;
vel_x = -vel_x * elasticity;
}
// Handle Vertical boundary collisions (Top / Bottom edges)
if (ball_y - radius < 0) {
ball_y = radius;
vel_y = -vel_y * elasticity;
} else if (ball_y + radius >= vinfo.yres) {
ball_y = vinfo.yres - radius - 1;
vel_y = -vel_y * elasticity; // Bounce up with loss of energy
}
// --- DRAWING / RENDERING ---
// A. Clear Screen (Fill entire backbuffer with Dark Grey)
for (uint32_t y = 0; y < vinfo.yres; y++) {
uint32_t *row = backbuffer + y * stride_pixels;
for (uint32_t x = 0; x < vinfo.xres; x++) {
row[x] = bg_color;
}
}
// B. Render the Ball (Draw a solid rasterized circle)
int start_y = (int)(ball_y - radius);
int end_y = (int)(ball_y + radius);
int start_x = (int)(ball_x - radius);
int end_x = (int)(ball_x + radius);
for (int y = start_y; y <= end_y; y++) {
if (y < 0 || (uint32_t)y >= vinfo.yres) continue;
uint32_t *row = backbuffer + y * stride_pixels;
double dy = y - ball_y;
for (int x = start_x; x <= end_x; x++) {
if (x < 0 || (uint32_t)x >= vinfo.xres) continue;
double dx = x - ball_x;
// Circle equation: dx^2 + dy^2 <= r^2
if ((dx * dx) + (dy * dy) <= (radius * radius)) {
row[x] = ball_color;
}
}
}
// C. Copy the finished frame to mapped physical screen memory (Double-Buffering flush)
memcpy(g_fb_mmap, backbuffer, g_screen_size);
// Throttle to 60 FPS (16.6 milliseconds)
sleep(16);
}
// 8. Restore text mode and unmap resources upon clean exit
free(backbuffer);
clean_exit(0);
return 0;
}
How it Works
1. Zero-Copy mmap() Mechanics
mmap() links physical screen coordinates directly into userspace address ranges. Under the hood, this sets up page tables pointing to the graphics memory address range finfo.smem_start. Doing this means you do not have to copy pixel rows repeatedly using slow write() system calls. Modifying g_fb_mmap[offset] updates the display instantly.
2. Resolution vs. Stride Offset Calculation
When physical screen memory is configured, hardware often aligns rows to boundaries (like 128-byte multiples). The stride length (in bytes) is returned as finfo.line_length.
- We divide
finfo.line_lengthby4(bytes per 32-bit pixel) to find the stride in pixels:stride_pixels. - A coordinate
(x, y)maps to: $$\text{pixel_ptr} = \text{g_fb_mmap} + (y \times \text{stride_pixels}) + x$$
3. Physics Equations
On every frame refresh, the ball updates its physics coordinates using Euler integration:
- Gravity Acceleration: $v_y \leftarrow v_y + g$. Gravity constantly adds downward speed.
- Elastic Collision: When hitting an edge, velocity is inverted and multiplied by a loss coefficient ($\text{elasticity} = 0.88$). This simulates bouncing energy loss, causing the ball to settle down over time.
Running It
- Compile the bouncing ball client natively:
tcc fb_bouncing_ball.c -o /bin/fb_bouncing_ball.elf - Type
fb_bouncing_balland press Enter. - The display will enter graphics mode and render a bright red bouncing ball accelerating under gravity. The terminal will be cleanly restored after 10 seconds or immediately if you press
Ctrl+C.