perf(gui): scene-dirty compositor + cursor overlay + 60 FPS pacer + animations

Rework the compositor so it stops repainting the whole screen every frame,
tripling the frame rate (17 -> 54 FPS at HD) and adding smooth animation.

- Scene-dirty skipping (gui/gui_desktop.c): the full scene is recomposited only
  when something other than the bare pointer changed -- input, an open animation,
  or a ~10 Hz heartbeat for live content (clock, graphs). Idle and pointer-only
  frames skip the recomposite entirely.
- Cursor save-under (gui/gui_cursor.c): the pointer is an overlay that stashes
  and restores the pixels beneath it, so moving the mouse never forces a full
  repaint -- pointer motion runs at the frame cap.
- 60 FPS frame pacer: each iteration is timed against the monotonic clock and
  padded to a 16.666 ms budget with the new ktime_delay_us() (HPET-precise
  busy-wait), giving consistent pacing instead of a free-running spin.
- Window open animation (gui/gui_wm.c): new windows grow from 60% to full size
  with a cubic ease-out over ~160 ms; gui_wm_update() drives it and reports
  while any window is still animating so the compositor keeps redrawing.
- Default display mode set to 1280x720 (HD) -- the sweet spot where the
  software compositor sustains a near-60 FPS cursor with headroom for effects.

Kernel: kernel/time/ktime.c gains ktime_delay_us(), a precise HPET-based delay
(bounded PIT fallback) used by the pacer.

Verified live (screenshotted): boots to 1280x720, logs in, tray reads ~54 FPS
(green) while the pointer moves, no cursor ghosting, windows render cleanly.
Clean build on x86_64/aarch64/armv8i; 0 panics / 0 OOM.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX
This commit is contained in:
subhobhai943andClaude Opus 4.8 committed 2026-08-25 23:05:44 +05:30
1 parent 4ecf119377
commit 2c2e2d536e
8 files changed
+200 -13

No files matched your search

+48
View File
@@ -81,3 +81,51 @@ void gui_cursor_draw(void) {
}
}
}
// ---------------------------------------------------------------------------
// Save-under overlay: lets the compositor move the cursor without repainting
// the whole scene. The block under the cursor (body + drop shadow) is stashed
// before drawing and restored on the next frame.
// ---------------------------------------------------------------------------
#define CUR_SAVE_W (CURSOR_W + 3)
#define CUR_SAVE_H (CURSOR_H + 3)
static uint32_t g_save[CUR_SAVE_H * CUR_SAVE_W];
static int g_save_x = -1, g_save_y = -1, g_save_w = 0, g_save_h = 0;
// Put back whatever was under the cursor last frame (erasing it).
void gui_cursor_restore_under(void) {
if (g_save_x < 0) return;
uint32_t* bb = gui_gfx_get_backbuffer();
if (!bb) { g_save_x = -1; return; }
int W = gui_gfx_get_width();
for (int j = 0; j < g_save_h; j++) {
uint32_t* dst = &bb[(g_save_y + j) * W + g_save_x];
const uint32_t* src = &g_save[j * g_save_w];
for (int i = 0; i < g_save_w; i++) dst[i] = src[i];
}
g_save_x = -1;
}
// Stash the block under the current cursor position, then draw the cursor.
void gui_cursor_composite(void) {
uint32_t* bb = gui_gfx_get_backbuffer();
if (!bb) return;
int W = gui_gfx_get_width();
int H = gui_gfx_get_height();
int x = g_cur_x, y = g_cur_y;
int w = CUR_SAVE_W, h = CUR_SAVE_H;
if (x + w > W) w = W - x;
if (y + h > H) h = H - y;
if (w <= 0 || h <= 0) { g_save_x = -1; gui_cursor_draw(); return; }
for (int j = 0; j < h; j++) {
const uint32_t* srow = &bb[(y + j) * W + x];
uint32_t* d = &g_save[j * w];
for (int i = 0; i < w; i++) d[i] = srow[i];
}
g_save_x = x; g_save_y = y; g_save_w = w; g_save_h = h;
gui_cursor_draw();
}
+53 -12
View File
@@ -86,6 +86,14 @@ static uint64_t g_frames = 0;
static uint32_t g_fps = 0; // frames per second, sampled from ktime
static uint64_t g_fps_last_ms = 0;
static uint64_t g_fps_last_frames = 0;
static uint64_t g_last_scene_ms = 0; // last full scene recomposite (for heartbeat)
static int g_prev_mx = -1, g_prev_my = -1;
// Target frame budget: 60 FPS == 16666 us. The compositor only repaints the
// full scene when it is dirty; between times it just moves the cursor overlay,
// so idle and pointer-only frames comfortably hit the cap.
#define FRAME_BUDGET_US 16666
#define SCENE_HEARTBEAT_MS 100 // ~10 Hz refresh for live content (clock, graphs)
/*
* Cached wallpaper.
@@ -730,6 +738,8 @@ int gui_desktop_run(void) {
printk(KERN_INFO "GUI: Desktop session started (ESC returns to the kernel TTY)\n");
while (g_desktop_running) {
uint64_t frame_start_us = ktime_us();
// --- 1. Sample input -----------------------------------------------
const mouse_state_t* ms = mouse_get_state();
int mx = ms ? ms->x : gui_gfx_get_width() / 2;
@@ -785,28 +795,59 @@ int gui_desktop_run(void) {
}
}
// --- 4. Composite the scene, back to front --------------------------
// --- 4. Decide what needs repainting --------------------------------
// Advancing open animations mutates window geometry, so this both drives
// the effect and reports whether it is still running.
bool wm_animating = gui_wm_update();
uint64_t now_ms = ktime_ms();
bool heartbeat = (now_ms - g_last_scene_ms) >= SCENE_HEARTBEAT_MS;
bool interacting = left || right || key ||
g_start_menu_open || g_context_menu_open ||
gui_dialog_is_open();
// A full recomposite is needed when anything but the bare cursor moved;
// otherwise we keep the retained scene and just slide the cursor overlay.
bool scene_dirty = wm_animating || interacting || heartbeat || (g_frames == 0);
if ((g_frames & 0x1F) == 0) sample_cpu_history();
gui_desktop_render_background();
gui_desktop_render_icons();
gui_wm_render();
gui_desktop_render_taskbar();
gui_desktop_render_start_menu();
gui_desktop_render_context_menu();
gui_dialog_render();
gui_cursor_draw();
if (scene_dirty) {
gui_desktop_render_background();
gui_desktop_render_icons();
gui_wm_render();
gui_desktop_render_taskbar();
gui_desktop_render_start_menu();
gui_desktop_render_context_menu();
gui_dialog_render();
g_last_scene_ms = now_ms;
} else {
// Retained scene: erase the previous cursor from it before redrawing.
gui_cursor_restore_under();
}
// The cursor is an overlay drawn on top of the (retained or fresh) scene,
// so pointer motion never forces a full recomposite.
gui_cursor_composite();
gui_gfx_present();
g_frames++;
g_pushed_pixels += gui_gfx_last_present_pixels();
g_prev_mx = mx;
g_prev_my = my;
// --- 5. Pace to the 60 FPS budget -----------------------------------
uint64_t elapsed_us = ktime_us() - frame_start_us;
if (elapsed_us < FRAME_BUDGET_US) {
ktime_delay_us(FRAME_BUDGET_US - elapsed_us);
}
// Sample the frame rate roughly twice a second from the monotonic clock.
uint64_t now_ms = ktime_ms();
uint64_t dt = now_ms - g_fps_last_ms;
uint64_t fps_now_ms = ktime_ms();
uint64_t dt = fps_now_ms - g_fps_last_ms;
if (dt >= 500) {
g_fps = (uint32_t)(((g_frames - g_fps_last_frames) * 1000ULL) / dt);
g_fps_last_ms = now_ms;
g_fps_last_ms = fps_now_ms;
g_fps_last_frames = g_frames;
}
+57
View File
@@ -10,6 +10,9 @@
#include <drivers/keyboard.h>
#include <lib/string.h>
#include <mm/kmalloc.h>
#include <kernel/ktime.h>
#define WM_OPEN_ANIM_MS 160
#define BTN_SIZE 14
#define BTN_GAP 4
@@ -85,9 +88,63 @@ gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h
g_window_order[g_num_windows++] = win->id;
gui_wm_focus_window(win->id);
// Kick off the grow-open animation: remember the final geometry and centre,
// then start small. gui_wm_update() eases it back to full over ~160 ms.
win->target_w = win->width;
win->target_h = win->height;
win->center_x = win->x + win->width / 2;
win->center_y = win->y + win->height / 2;
win->anim_open_ns = ktime_ns();
win->width = win->target_w * 6 / 10;
win->height = win->target_h * 6 / 10;
win->x = win->center_x - win->width / 2;
win->y = win->center_y - win->height / 2;
if (win->x < 0) win->x = 0;
if (win->y < 0) win->y = 0;
return win;
}
// Advance open animations. Returns true while any window is still animating.
bool gui_wm_update(void) {
bool animating = false;
uint64_t now = ktime_ns();
for (int i = 0; i < g_num_windows; i++) {
gui_window_t* win = gui_wm_get_window(g_window_order[i]);
if (!win || win->anim_open_ns == 0) continue;
uint64_t elapsed_ms = (now - win->anim_open_ns) / 1000000ULL;
if (elapsed_ms >= WM_OPEN_ANIM_MS) {
// Snap to the final geometry and stop animating.
win->width = win->target_w;
win->height = win->target_h;
win->x = win->center_x - win->target_w / 2;
win->y = win->center_y - win->target_h / 2;
win->anim_open_ns = 0;
} else {
animating = true;
int p = (int)(elapsed_ms * 1000 / WM_OPEN_ANIM_MS); // 0..1000
int inv = 1000 - p;
int ease = 1000 - (inv * inv / 1000) * inv / 1000; // cubic ease-out
int scale = 600 + 400 * ease / 1000; // 600..1000 permille
win->width = win->target_w * scale / 1000;
win->height = win->target_h * scale / 1000;
win->x = win->center_x - win->width / 2;
win->y = win->center_y - win->height / 2;
}
// Keep the animating window on screen.
if (win->x < 0) win->x = 0;
if (win->y < 0) win->y = 0;
if (win->x + win->width > g_work_w) win->x = g_work_w - win->width;
if (win->y + win->height > g_work_h) win->y = g_work_h - win->height;
if (win->x < 0) win->x = 0;
if (win->y < 0) win->y = 0;
}
return animating;
}
static int order_index_of(int win_id) {
for (int i = 0; i < g_num_windows; i++) {
if (g_window_order[i] == win_id) return i;
+6
View File
@@ -10,4 +10,10 @@ int gui_cursor_get_x(void);
int gui_cursor_get_y(void);
void gui_cursor_draw(void);
// Overlay compositing: restore the pixels under the previous cursor position,
// then stash-and-draw the cursor at the current one. Lets the compositor move
// the pointer at full frame rate without repainting the whole scene.
void gui_cursor_restore_under(void);
void gui_cursor_composite(void);
#endif // _GUI_CURSOR_H
+12
View File
@@ -70,6 +70,14 @@ typedef struct gui_window {
void* user_data;
// Open animation: the window grows from a fraction of its final size to
// full over a short interval. anim_open_ns == 0 means no animation running.
uint64_t anim_open_ns; // ktime_ns() when the open animation began
int target_w; // final geometry the animation resolves to
int target_h;
int center_x; // fixed centre the growth expands around
int center_y;
void (*paint)(struct gui_window* win);
void (*handle_event)(struct gui_window* win, const gui_event_t* ev);
} gui_window_t;
@@ -80,6 +88,10 @@ void gui_wm_init(void);
void gui_wm_set_workarea(int width, int height);
gui_window_t* gui_wm_create_window(const char* title, int x, int y, int w, int h);
// Advance any running open animations; returns true while at least one window
// is still animating (so the compositor keeps redrawing during the effect).
bool gui_wm_update(void);
void gui_wm_destroy_window(int win_id);
void gui_wm_focus_window(int win_id);
gui_window_t* gui_wm_get_window(int win_id);
+4
View File
@@ -14,4 +14,8 @@ uint64_t ktime_ms(void);
const char* ktime_source(void); // "HPET" or "PIT"
bool ktime_is_highres(void); // true when backed by the HPET
// Busy-wait for the given number of microseconds against the monotonic clock.
// Precise when HPET-backed; used by the compositor's 60 FPS frame pacer.
void ktime_delay_us(uint64_t us);
#endif // _KERNEL_KTIME_H
+1 -1
View File
@@ -332,7 +332,7 @@ void kernel_main(void* memory_map, uint64_t memory_map_count) {
printk(ANSI_BRIGHT_CYAN " SUB-OS 64-Bit [%s] Modular Monolithic Kernel %s\n" ANSI_RESET, arch_get_name(), kernel_get_version());
printk(ANSI_BRIGHT_CYAN "=================================================================\n\n" ANSI_RESET);
init_early("root=/dev/sda console=tty1 init=/bin/lazybox video=1920x1080 quiet");
init_early("root=/dev/sda console=tty1 init=/bin/lazybox video=1280x720 quiet");
rtc_init();
printk(KERN_INFO "[1/14] Initializing 64-Bit GDT, IDT, PIC & PIT Timer... ");
+19
View File
@@ -37,3 +37,22 @@ uint64_t ktime_ms(void) { return ktime_ns() / 1000000ULL; }
const char* ktime_source(void) { return g_use_hpet ? "HPET" : "PIT"; }
bool ktime_is_highres(void) { return g_use_hpet; }
void ktime_delay_us(uint64_t us) {
if (us == 0) return;
// With the HPET we can spin precisely; on the PIT the 10 ms tick is far
// coarser than a microsecond, so cap the spin so we never wedge the CPU.
if (!g_use_hpet) {
uint64_t deadline_ms = ktime_ms() + (us + 999) / 1000;
uint64_t guard = 0;
while (ktime_ms() < deadline_ms && guard++ < 100000000ULL) {
/* spin */
}
return;
}
uint64_t start = ktime_ns();
uint64_t target = start + us * 1000ULL;
while (ktime_ns() < target) {
/* spin on the HPET counter */
}
}