From 2c2e2d536e582e9daf012e498198b0ea5c863f0a Mon Sep 17 00:00:00 2001 From: subhobhai943 Date: Tue, 25 Aug 2026 23:05:44 +0530 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Fh5z6V7nGPbHvqVG5p4CXX --- gui/gui_cursor.c | 48 +++++++++++++++++++++++++++++ gui/gui_desktop.c | 65 ++++++++++++++++++++++++++++++++-------- gui/gui_wm.c | 57 +++++++++++++++++++++++++++++++++++ include/gui/gui_cursor.h | 6 ++++ include/gui/gui_wm.h | 12 ++++++++ include/kernel/ktime.h | 4 +++ kernel/main.c | 2 +- kernel/time/ktime.c | 19 ++++++++++++ 8 files changed, 200 insertions(+), 13 deletions(-) diff --git a/gui/gui_cursor.c b/gui/gui_cursor.c index f654483..1ff5279 100644 --- a/gui/gui_cursor.c +++ b/gui/gui_cursor.c @@ -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(); +} diff --git a/gui/gui_desktop.c b/gui/gui_desktop.c index e79c1e7..6e7e1c7 100644 --- a/gui/gui_desktop.c +++ b/gui/gui_desktop.c @@ -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; } diff --git a/gui/gui_wm.c b/gui/gui_wm.c index 7f4a12b..bf787cd 100644 --- a/gui/gui_wm.c +++ b/gui/gui_wm.c @@ -10,6 +10,9 @@ #include #include #include +#include + +#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; diff --git a/include/gui/gui_cursor.h b/include/gui/gui_cursor.h index a0d3262..21bbc63 100644 --- a/include/gui/gui_cursor.h +++ b/include/gui/gui_cursor.h @@ -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 diff --git a/include/gui/gui_wm.h b/include/gui/gui_wm.h index e3aa234..4f4bee3 100644 --- a/include/gui/gui_wm.h +++ b/include/gui/gui_wm.h @@ -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); diff --git a/include/kernel/ktime.h b/include/kernel/ktime.h index a41cc29..5589def 100644 --- a/include/kernel/ktime.h +++ b/include/kernel/ktime.h @@ -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 diff --git a/kernel/main.c b/kernel/main.c index 5f0a1df..948c2b6 100644 --- a/kernel/main.c +++ b/kernel/main.c @@ -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... "); diff --git a/kernel/time/ktime.c b/kernel/time/ktime.c index 1cb3028..af3b5c4 100644 --- a/kernel/time/ktime.c +++ b/kernel/time/ktime.c @@ -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 */ + } +}