From 34521143c0c2d473b01c58c8ad1be84bb935afae Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Sat, 25 Apr 2026 17:53:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 9 +- Makefile | 3 + cleonos/CMakeLists.txt | 2 +- cleonos/c/apps/help_main.c | 7 +- cleonos/c/apps/shell/shell_cmd.c | 2 + cleonos/c/apps/uwm_main.c | 734 ++++++++++++++++++++++++++++ cleonos/c/include/cleonos_syscall.h | 42 +- cleonos/c/src/syscall.c | 10 +- clks | 2 +- docs/README.md | 3 +- docs/stage30.md | 52 ++ docs/syscall.md | 16 +- kit | 2 +- wine | 2 +- 14 files changed, 855 insertions(+), 31 deletions(-) create mode 100644 cleonos/c/apps/uwm_main.c create mode 100644 docs/stage30.md diff --git a/CMakeLists.txt b/CMakeLists.txt index cfe3713..79df1c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,11 @@ set(CLEONOS_QEMU_ACCEL_ARGS "") if(CLEONOS_QEMU_ENABLE_KVM) list(APPEND CLEONOS_QEMU_ACCEL_ARGS -enable-kvm -cpu host) endif() +set(CLEONOS_QEMU_ENABLE_USB_TABLET ON CACHE BOOL "Enable QEMU USB tablet for absolute mouse pointer") +set(CLEONOS_QEMU_INPUT_ARGS "") +if(CLEONOS_QEMU_ENABLE_USB_TABLET) + list(APPEND CLEONOS_QEMU_INPUT_ARGS -usb -device usb-tablet) +endif() set(CLEONOS_SOURCE_DIR "${CMAKE_SOURCE_DIR}/cleonos") if(EXISTS "${CLEONOS_SOURCE_DIR}/CMakeLists.txt") @@ -381,7 +386,7 @@ if(CLEONOS_ENABLE) "-DLOG_LEVEL=STEP" "-DLOG_TEXT=launching qemu run" -P "${CL_LOG_EMIT_SCRIPT}" - COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio + COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} ${CLEONOS_QEMU_INPUT_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio DEPENDS iso disk-image USES_TERMINAL ) @@ -392,7 +397,7 @@ if(CLEONOS_ENABLE) "-DLOG_LEVEL=STEP" "-DLOG_TEXT=launching qemu debug (-s -S)" -P "${CL_LOG_EMIT_SCRIPT}" - COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio -s -S + COMMAND ${QEMU_X86_64} ${CLEONOS_QEMU_ACCEL_ARGS} ${CLEONOS_QEMU_INPUT_ARGS} -M pc -m 1024M -boot order=d -cdrom "${ISO_IMAGE}" -drive "file=${DISK_IMAGE},format=raw,if=none,id=clksdisk,media=disk" -device "ide-hd,drive=clksdisk,bus=ide.0" -netdev "user,id=clksnet0" -device "e1000,netdev=clksnet0" -serial stdio -s -S DEPENDS iso disk-image USES_TERMINAL ) diff --git a/Makefile b/Makefile index 45f48c6..6ffe99f 100644 --- a/Makefile +++ b/Makefile @@ -239,6 +239,9 @@ help: > $(Q)echo "QEMU KVM toggle:" > $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_KVM=ON' # force on" > $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_KVM=OFF' # force off" +> $(Q)echo "QEMU USB tablet toggle:" +> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_USB_TABLET=ON' # absolute pointer" +> $(Q)echo " make run CMAKE_EXTRA_ARGS='-DCLEONOS_QEMU_ENABLE_USB_TABLET=OFF' # disable tablet" > $(Q)echo "" > $(Q)echo "Pass custom CMake cache args via:" > $(Q)echo " make configure CMAKE_EXTRA_ARGS='-DLIMINE_SKIP_CONFIGURE=1 -DOBJCOPY_FOR_TARGET=objcopy'" diff --git a/cleonos/CMakeLists.txt b/cleonos/CMakeLists.txt index 97f0043..aea5320 100644 --- a/cleonos/CMakeLists.txt +++ b/cleonos/CMakeLists.txt @@ -171,7 +171,7 @@ set(USER_SHELL_COMMAND_APPS diskinfo mkfsfat32 mount partctl shutdown restart exit clear ansi ansitest wavplay fastfetch memstat fsstat taskstat userstat shstat stats tty dmesg kbdstat mkdir touch write append cp mv rm kdbg bmpview qrcode browser - vim + vim uwm ) foreach(SRC IN LISTS USER_APP_MAIN_SOURCES) diff --git a/cleonos/c/apps/help_main.c b/cleonos/c/apps/help_main.c index e6b653d..1d6f919 100644 --- a/cleonos/c/apps/help_main.c +++ b/cleonos/c/apps/help_main.c @@ -14,9 +14,10 @@ static int ush_cmd_help(void) { ush_writeln(" clear"); ush_writeln(" ansi / ansitest / color"); ush_writeln(" bmpview [cols]"); - ush_writeln(" qrcode [--ecc ] "); - ush_writeln(" vim [file] (vim-like editor: normal/insert/:w/:q/:wq)"); - ush_writeln(" wavplay [steps] [ticks] / wavplay --stop"); + ush_writeln(" qrcode [--ecc ] "); + ush_writeln(" vim [file] (vim-like editor: normal/insert/:w/:q/:wq)"); + ush_writeln(" uwm (user-space window manager demo)"); + ush_writeln(" wavplay [steps] [ticks] / wavplay --stop"); ush_writeln(" fastfetch [--plain]"); ush_writeln(" doom [wad_path] (framebuffer bootstrap renderer)"); ush_writeln(" memstat / fsstat / taskstat / userstat / shstat / stats"); diff --git a/cleonos/c/apps/shell/shell_cmd.c b/cleonos/c/apps/shell/shell_cmd.c index 896ac0a..810f046 100644 --- a/cleonos/c/apps/shell/shell_cmd.c +++ b/cleonos/c/apps/shell/shell_cmd.c @@ -137,6 +137,8 @@ static int ush_cmd_help(void) { ush_writeln(" ansi / ansitest / color"); ush_writeln(" bmpview [cols]"); ush_writeln(" qrcode [--ecc ] "); + ush_writeln(" vim [file] (vim-like editor)"); + ush_writeln(" uwm (user-space window manager demo)"); ush_writeln(" wavplay [steps] [ticks] / wavplay --stop"); ush_writeln(" fastfetch [--plain]"); ush_writeln(" doom [wad_path] (framebuffer bootstrap renderer)"); diff --git a/cleonos/c/apps/uwm_main.c b/cleonos/c/apps/uwm_main.c new file mode 100644 index 0000000..ba86d11 --- /dev/null +++ b/cleonos/c/apps/uwm_main.c @@ -0,0 +1,734 @@ +#include "cmd_runtime.h" + +#define USH_UWM_WINDOW_COUNT 3ULL +#define USH_UWM_TITLE_HEIGHT 18 +#define USH_UWM_BORDER_THICKNESS 2 +#define USH_UWM_WINDOW_MIN_W 120 +#define USH_UWM_WINDOW_MIN_H 80 +#define USH_UWM_STARTUP_KEY_DRAIN_MAX 256ULL +#define USH_UWM_BLIT_MAX_FAIL_STREAK 30ULL + +#define USH_UWM_KEY_LEFT 1 +#define USH_UWM_KEY_RIGHT 2 +#define USH_UWM_KEY_UP 3 +#define USH_UWM_KEY_DOWN 4 + +#define USH_UWM_COLOR_BG 0x00161D2BUL +#define USH_UWM_COLOR_PANEL 0x00263352UL +#define USH_UWM_COLOR_CURSOR 0x00FFFFFFUL +#define USH_UWM_COLOR_CURSOR_SHADOW 0x00000000UL +#define USH_UWM_COLOR_BORDER_ACTIVE 0x00FFD166UL +#define USH_UWM_COLOR_BORDER_INACTIVE 0x004A566EL +#define USH_UWM_COLOR_TITLE_ACTIVE 0x005A77B7UL +#define USH_UWM_COLOR_TITLE_INACTIVE 0x00394C78UL + +typedef unsigned int ush_uwm_u32; + +typedef struct ush_uwm_window { + int x; + int y; + int w; + int h; + ush_uwm_u32 body_color; +} ush_uwm_window; + +typedef struct ush_uwm_runtime { + cleonos_fb_info fb; + ush_uwm_u32 *canvas; + u64 canvas_pixels; + int screen_w; + int screen_h; + int mouse_x; + int mouse_y; + u64 mouse_buttons; + int mouse_ready; +} ush_uwm_runtime; + +static int ush_uwm_window_index_valid(int index) { + return (index >= 0 && index < (int)USH_UWM_WINDOW_COUNT) ? 1 : 0; +} + +static void ush_uwm_z_order_sanitize(int *z_order) { + int seen[USH_UWM_WINDOW_COUNT]; + int fill_index = 0; + u64 i; + + if (z_order == (int *)0) { + return; + } + + for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) { + seen[i] = 0; + } + + for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) { + int value = z_order[i]; + + if (ush_uwm_window_index_valid(value) != 0 && seen[(u64)value] == 0) { + seen[(u64)value] = 1; + continue; + } + + while (fill_index < (int)USH_UWM_WINDOW_COUNT && seen[(u64)fill_index] != 0) { + fill_index++; + } + + if (fill_index >= (int)USH_UWM_WINDOW_COUNT) { + fill_index = (int)USH_UWM_WINDOW_COUNT - 1; + } + + z_order[i] = fill_index; + seen[(u64)fill_index] = 1; + } +} + +static int ush_uwm_runtime_sane(const ush_uwm_runtime *rt) { + u64 min_pixels; + + if (rt == (const ush_uwm_runtime *)0 || rt->canvas == (ush_uwm_u32 *)0) { + return 0; + } + + if (rt->screen_w <= 0 || rt->screen_h <= 0 || rt->screen_w > 4096 || rt->screen_h > 4096) { + return 0; + } + + min_pixels = (u64)(unsigned int)rt->screen_w * (u64)(unsigned int)rt->screen_h; + if (rt->canvas_pixels < min_pixels) { + return 0; + } + + return 1; +} + +static int ush_uwm_clampi(int value, int min_value, int max_value) { + if (value < min_value) { + return min_value; + } + + if (value > max_value) { + return max_value; + } + + return value; +} + +static int ush_uwm_u64_to_i32_sat(u64 value) { + if (value > 0x7FFFFFFFULL) { + return 0x7FFFFFFF; + } + + return (int)value; +} + +static void ush_uwm_fill_rect(ush_uwm_runtime *rt, int x, int y, int w, int h, ush_uwm_u32 color) { + int left; + int top; + int right; + int bottom; + int row; + + if (ush_uwm_runtime_sane(rt) == 0 || w <= 0 || h <= 0) { + return; + } + + left = x; + top = y; + right = x + w; + bottom = y + h; + + if (left < 0) { + left = 0; + } + if (top < 0) { + top = 0; + } + if (right > rt->screen_w) { + right = rt->screen_w; + } + if (bottom > rt->screen_h) { + bottom = rt->screen_h; + } + + if (left >= right || top >= bottom) { + return; + } + + for (row = top; row < bottom; row++) { + u64 offset = ((u64)(unsigned int)row * (u64)(unsigned int)rt->screen_w) + (u64)(unsigned int)left; + u64 max_pixels = rt->canvas_pixels; + int col; + + if (offset >= max_pixels) { + break; + } + + for (col = left; col < right; col++) { + if (offset >= max_pixels) { + break; + } + rt->canvas[offset++] = color; + } + } +} + +static void ush_uwm_draw_window(ush_uwm_runtime *rt, const ush_uwm_window *win, int active) { + ush_uwm_u32 border_color = (active != 0) ? USH_UWM_COLOR_BORDER_ACTIVE : USH_UWM_COLOR_BORDER_INACTIVE; + ush_uwm_u32 title_color = (active != 0) ? USH_UWM_COLOR_TITLE_ACTIVE : USH_UWM_COLOR_TITLE_INACTIVE; + int title_h; + + if (rt == (ush_uwm_runtime *)0 || win == (const ush_uwm_window *)0) { + return; + } + + title_h = USH_UWM_TITLE_HEIGHT; + if (title_h > win->h) { + title_h = win->h; + } + + ush_uwm_fill_rect(rt, win->x, win->y, win->w, win->h, border_color); + ush_uwm_fill_rect(rt, win->x + USH_UWM_BORDER_THICKNESS, win->y + USH_UWM_BORDER_THICKNESS, + win->w - (USH_UWM_BORDER_THICKNESS * 2), title_h - USH_UWM_BORDER_THICKNESS, title_color); + ush_uwm_fill_rect(rt, win->x + USH_UWM_BORDER_THICKNESS, win->y + title_h, win->w - (USH_UWM_BORDER_THICKNESS * 2), + win->h - title_h - USH_UWM_BORDER_THICKNESS, win->body_color); + + ush_uwm_fill_rect(rt, win->x + win->w - 16, win->y + 4, 10, 10, 0x00E76F51UL); +} + +static void ush_uwm_draw_cursor(ush_uwm_runtime *rt, int x, int y) { + int i; + + if (rt == (ush_uwm_runtime *)0) { + return; + } + + for (i = -6; i <= 6; i++) { + ush_uwm_fill_rect(rt, x + i, y, 1, 1, (i == 0) ? USH_UWM_COLOR_CURSOR : USH_UWM_COLOR_CURSOR_SHADOW); + ush_uwm_fill_rect(rt, x, y + i, 1, 1, (i == 0) ? USH_UWM_COLOR_CURSOR : USH_UWM_COLOR_CURSOR_SHADOW); + } + + ush_uwm_fill_rect(rt, x - 1, y - 1, 3, 3, USH_UWM_COLOR_CURSOR); +} + +static void ush_uwm_draw_background(ush_uwm_runtime *rt) { + int y; + + if (rt == (ush_uwm_runtime *)0) { + return; + } + + ush_uwm_fill_rect(rt, 0, 0, rt->screen_w, rt->screen_h, USH_UWM_COLOR_BG); + ush_uwm_fill_rect(rt, 0, 0, rt->screen_w, 30, USH_UWM_COLOR_PANEL); + + for (y = 60; y < rt->screen_h; y += 40) { + ush_uwm_fill_rect(rt, 0, y, rt->screen_w, 1, 0x001E283BUL); + } +} + +static int ush_uwm_window_hit_title(const ush_uwm_window *win, int x, int y) { + int title_h; + + if (win == (const ush_uwm_window *)0) { + return 0; + } + + title_h = USH_UWM_TITLE_HEIGHT; + if (title_h > win->h) { + title_h = win->h; + } + + if (x < win->x || y < win->y) { + return 0; + } + + if (x >= win->x + win->w || y >= win->y + title_h) { + return 0; + } + + return 1; +} + +static void ush_uwm_bring_to_front(int *z_order, u64 count, int window_index) { + u64 i; + u64 pos = count; + + if (z_order == (int *)0 || count == 0ULL || ush_uwm_window_index_valid(window_index) == 0) { + return; + } + + for (i = 0ULL; i < count; i++) { + if (z_order[i] == window_index) { + pos = i; + break; + } + } + + if (pos >= count || pos + 1ULL >= count) { + return; + } + + for (i = pos; i + 1ULL < count; i++) { + z_order[i] = z_order[i + 1ULL]; + } + z_order[count - 1ULL] = window_index; +} + +static int ush_uwm_cycle_focus(const int *z_order, u64 count, int active_window) { + u64 i; + u64 pos = 0ULL; + + if (z_order == (const int *)0 || count == 0ULL) { + return 0; + } + + if (ush_uwm_window_index_valid(active_window) == 0) { + return z_order[count - 1ULL]; + } + + for (i = 0ULL; i < count; i++) { + if (z_order[i] == active_window) { + pos = i; + break; + } + } + + return z_order[(pos + 1ULL) % count]; +} + +static int ush_uwm_poll_mouse(ush_uwm_runtime *rt) { + cleonos_mouse_state ms; + int max_x; + int max_y; + + if (rt == (ush_uwm_runtime *)0) { + return 0; + } + + ush_zero(&ms, (u64)sizeof(ms)); + if (cleonos_sys_mouse_state(&ms) == 0ULL) { + return 0; + } + + rt->mouse_buttons = ms.buttons; + rt->mouse_ready = (ms.ready != 0ULL) ? 1 : 0; + + if (rt->screen_w <= 0 || rt->screen_h <= 0) { + return rt->mouse_ready; + } + + max_x = rt->screen_w - 1; + max_y = rt->screen_h - 1; + + rt->mouse_x = ush_uwm_clampi(ush_uwm_u64_to_i32_sat(ms.x), 0, max_x); + rt->mouse_y = ush_uwm_clampi(ush_uwm_u64_to_i32_sat(ms.y), 0, max_y); + return rt->mouse_ready; +} + +static int ush_uwm_present(ush_uwm_runtime *rt) { + cleonos_fb_blit_req req; + + if (ush_uwm_runtime_sane(rt) == 0) { + return 0; + } + + req.pixels_ptr = (u64)(usize)rt->canvas; + req.src_width = (u64)(unsigned int)rt->screen_w; + req.src_height = (u64)(unsigned int)rt->screen_h; + req.src_pitch_bytes = (u64)(unsigned int)rt->screen_w * 4ULL; + req.dst_x = 0ULL; + req.dst_y = 0ULL; + req.scale = 1ULL; + return (cleonos_sys_fb_blit(&req) != 0ULL) ? 1 : 0; +} + +static u64 ush_uwm_drain_startup_keys(void) { + u64 drained = 0ULL; + + while (drained < USH_UWM_STARTUP_KEY_DRAIN_MAX) { + u64 key = cleonos_sys_kbd_get_char(); + if (key == (u64)-1) { + break; + } + drained++; + } + + return drained; +} + +/* return: 0 fail, 1 ok, 2 help */ +static int ush_uwm_parse_args(const char *arg) { + char first[USH_PATH_MAX]; + const char *rest = ""; + + if (arg == (const char *)0 || arg[0] == '\0') { + return 1; + } + + if (ush_split_first_and_rest(arg, first, (u64)sizeof(first), &rest) == 0) { + return 0; + } + + if (rest != (const char *)0 && rest[0] != '\0') { + return 0; + } + + if (ush_streq(first, "--help") != 0 || ush_streq(first, "-h") != 0) { + return 2; + } + + return 0; +} + +static void ush_uwm_usage(void) { + ush_writeln("usage: uwm"); + ush_writeln("keys: q quit, tab cycle focus, wasd move active window"); + ush_writeln("mouse: drag window by title bar"); +} + +static int ush_cmd_uwm(const char *arg) { + ush_uwm_runtime rt; + ush_uwm_window windows[USH_UWM_WINDOW_COUNT]; + int z_order[USH_UWM_WINDOW_COUNT]; + int active_window = 0; + int dragging = 0; + int drag_window = 0; + int drag_offset_x = 0; + int drag_offset_y = 0; + u64 prev_buttons = 0ULL; + u64 blit_fail_streak = 0ULL; + int running = 1; + int parse_result; + u64 drained_keys; + + parse_result = ush_uwm_parse_args(arg); + if (parse_result == 2) { + ush_uwm_usage(); + return 1; + } + + if (parse_result == 0) { + ush_uwm_usage(); + return 0; + } + + ush_zero(&rt, (u64)sizeof(rt)); + + if (cleonos_sys_fb_info(&rt.fb) == 0ULL || rt.fb.width == 0ULL || rt.fb.height == 0ULL || rt.fb.bpp != 32ULL) { + ush_writeln("uwm: framebuffer unavailable"); + return 0; + } + + if (rt.fb.width > 4096ULL || rt.fb.height > 4096ULL) { + ush_writeln("uwm: framebuffer too large"); + return 0; + } + + rt.screen_w = (int)rt.fb.width; + rt.screen_h = (int)rt.fb.height; + rt.canvas_pixels = rt.fb.width * rt.fb.height; + rt.canvas = (ush_uwm_u32 *)malloc((size_t)(rt.canvas_pixels * 4ULL)); + if (rt.canvas == (ush_uwm_u32 *)0) { + ush_writeln("uwm: framebuffer buffer alloc failed"); + return 0; + } + + windows[0].x = 50; + windows[0].y = 70; + windows[0].w = rt.screen_w / 3; + windows[0].h = rt.screen_h / 3; + windows[0].body_color = 0x002C3E6BUL; + + windows[1].x = 180; + windows[1].y = 140; + windows[1].w = rt.screen_w / 3; + windows[1].h = rt.screen_h / 3; + windows[1].body_color = 0x003F5B7EUL; + + windows[2].x = 310; + windows[2].y = 210; + windows[2].w = rt.screen_w / 3; + windows[2].h = rt.screen_h / 3; + windows[2].body_color = 0x00506F89UL; + + z_order[0] = 0; + z_order[1] = 1; + z_order[2] = 2; + active_window = 2; + + { + u64 i; + for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) { + int max_w = rt.screen_w - 20; + int max_h = rt.screen_h - 40; + if (max_w < USH_UWM_WINDOW_MIN_W) { + max_w = USH_UWM_WINDOW_MIN_W; + } + if (max_h < USH_UWM_WINDOW_MIN_H) { + max_h = USH_UWM_WINDOW_MIN_H; + } + + if (windows[i].w < USH_UWM_WINDOW_MIN_W) { + windows[i].w = USH_UWM_WINDOW_MIN_W; + } + if (windows[i].h < USH_UWM_WINDOW_MIN_H) { + windows[i].h = USH_UWM_WINDOW_MIN_H; + } + if (windows[i].w > max_w) { + windows[i].w = max_w; + } + if (windows[i].h > max_h) { + windows[i].h = max_h; + } + } + } + + rt.mouse_x = rt.screen_w / 2; + rt.mouse_y = rt.screen_h / 2; + rt.mouse_buttons = 0ULL; + rt.mouse_ready = 0; + + ush_writeln("uwm: enter user window manager"); + ush_writeln("uwm: q quit, tab focus, wasd move, drag title bar with mouse"); + + (void)cleonos_sys_fb_clear(USH_UWM_COLOR_BG); + drained_keys = ush_uwm_drain_startup_keys(); + if (drained_keys > 0ULL) { + ush_writeln("uwm: stale keyboard input discarded"); + } + + while (running != 0) { + u64 key; + int left_down; + int left_press; + int left_release; + + if (ush_uwm_runtime_sane(&rt) == 0) { + free(rt.canvas); + ush_writeln("uwm: runtime state corrupted"); + return 0; + } + + ush_uwm_z_order_sanitize(z_order); + if (ush_uwm_window_index_valid(active_window) == 0) { + active_window = z_order[USH_UWM_WINDOW_COUNT - 1ULL]; + } + if (ush_uwm_window_index_valid(drag_window) == 0) { + dragging = 0; + drag_window = active_window; + } + + (void)ush_uwm_poll_mouse(&rt); + + for (;;) { + key = cleonos_sys_kbd_get_char(); + if (key == (u64)-1) { + break; + } + + if (key == (u64)'q' || key == (u64)'Q') { + running = 0; + break; + } + + if (key == 27ULL) { + continue; + } + + if (key == (u64)'\t') { + active_window = ush_uwm_cycle_focus(z_order, USH_UWM_WINDOW_COUNT, active_window); + if (ush_uwm_window_index_valid(active_window) != 0) { + ush_uwm_bring_to_front(z_order, USH_UWM_WINDOW_COUNT, active_window); + } + continue; + } + + if (ush_uwm_window_index_valid(active_window) == 0) { + continue; + } + + if (key == (u64)'a' || key == (u64)'A') { + windows[active_window].x -= 8; + } else if (key == (u64)'d' || key == (u64)'D') { + windows[active_window].x += 8; + } else if (key == (u64)'w' || key == (u64)'W') { + windows[active_window].y -= 8; + } else if (key == (u64)'s' || key == (u64)'S') { + windows[active_window].y += 8; + } else if (key == USH_UWM_KEY_LEFT) { + rt.mouse_x -= 10; + } else if (key == USH_UWM_KEY_RIGHT) { + rt.mouse_x += 10; + } else if (key == USH_UWM_KEY_UP) { + rt.mouse_y -= 10; + } else if (key == USH_UWM_KEY_DOWN) { + rt.mouse_y += 10; + } + } + + if (running == 0) { + break; + } + + left_down = ((rt.mouse_buttons & 0x01ULL) != 0ULL) ? 1 : 0; + left_press = (left_down != 0 && (prev_buttons & 0x01ULL) == 0ULL) ? 1 : 0; + left_release = (left_down == 0 && (prev_buttons & 0x01ULL) != 0ULL) ? 1 : 0; + + if (left_press != 0) { + int hit = -1; + i64 z; + + for (z = (i64)USH_UWM_WINDOW_COUNT - 1LL; z >= 0LL; z--) { + int win_index = z_order[(u64)z]; + if (ush_uwm_window_index_valid(win_index) == 0) { + continue; + } + if (ush_uwm_window_hit_title(&windows[win_index], rt.mouse_x, rt.mouse_y) != 0) { + hit = win_index; + break; + } + } + + if (hit >= 0) { + active_window = hit; + ush_uwm_bring_to_front(z_order, USH_UWM_WINDOW_COUNT, hit); + dragging = 1; + drag_window = hit; + drag_offset_x = rt.mouse_x - windows[hit].x; + drag_offset_y = rt.mouse_y - windows[hit].y; + } + } + + if (left_release != 0) { + dragging = 0; + } + + if (dragging != 0 && left_down != 0 && ush_uwm_window_index_valid(drag_window) != 0) { + int new_x = rt.mouse_x - drag_offset_x; + int new_y = rt.mouse_y - drag_offset_y; + int max_x = rt.screen_w - windows[drag_window].w; + int max_y = rt.screen_h - windows[drag_window].h; + int min_y = (max_y >= 30) ? 30 : 0; + + if (max_x < 0) { + max_x = 0; + } + if (max_y < 0) { + max_y = 0; + } + + windows[drag_window].x = ush_uwm_clampi(new_x, 0, max_x); + windows[drag_window].y = ush_uwm_clampi(new_y, min_y, max_y); + } + + if (ush_uwm_window_index_valid(active_window) != 0) { + int max_x = rt.screen_w - windows[active_window].w; + int max_y = rt.screen_h - windows[active_window].h; + int min_y = (max_y >= 30) ? 30 : 0; + + if (max_x < 0) { + max_x = 0; + } + if (max_y < 0) { + max_y = 0; + } + + windows[active_window].x = ush_uwm_clampi(windows[active_window].x, 0, max_x); + windows[active_window].y = ush_uwm_clampi(windows[active_window].y, min_y, max_y); + } + rt.mouse_x = ush_uwm_clampi(rt.mouse_x, 0, rt.screen_w - 1); + rt.mouse_y = ush_uwm_clampi(rt.mouse_y, 0, rt.screen_h - 1); + + ush_uwm_draw_background(&rt); + { + u64 i; + for (i = 0ULL; i < USH_UWM_WINDOW_COUNT; i++) { + int win_index = z_order[i]; + if (ush_uwm_window_index_valid(win_index) == 0) { + continue; + } + int is_active = (win_index == active_window) ? 1 : 0; + ush_uwm_draw_window(&rt, &windows[win_index], is_active); + } + } + ush_uwm_draw_cursor(&rt, rt.mouse_x, rt.mouse_y); + + if (ush_uwm_present(&rt) == 0) { + blit_fail_streak++; + if (blit_fail_streak == 1ULL) { + ush_writeln("uwm: framebuffer blit failed, retrying"); + } + + if (blit_fail_streak >= USH_UWM_BLIT_MAX_FAIL_STREAK) { + free(rt.canvas); + ush_writeln("uwm: framebuffer blit failed too many times"); + return 0; + } + + (void)cleonos_sys_sleep_ticks(1ULL); + continue; + } + + blit_fail_streak = 0ULL; + + prev_buttons = rt.mouse_buttons; + (void)cleonos_sys_sleep_ticks(1ULL); + } + + (void)cleonos_sys_fb_clear(0x00000000ULL); + free(rt.canvas); + ush_writeln("uwm: exit"); + return 1; +} + +int cleonos_app_main(int argc, char **argv, char **envp) { + ush_cmd_ctx ctx; + ush_cmd_ret ret; + ush_state sh; + char initial_cwd[USH_PATH_MAX]; + char arg_buf[USH_ARG_MAX]; + int has_context = 0; + int success = 0; + const char *arg = ""; + + (void)envp; + + ush_zero(&ctx, (u64)sizeof(ctx)); + ush_zero(&ret, (u64)sizeof(ret)); + ush_zero(arg_buf, (u64)sizeof(arg_buf)); + ush_init_state(&sh); + ush_copy(initial_cwd, (u64)sizeof(initial_cwd), sh.cwd); + + if (ush_command_ctx_read(&ctx) != 0) { + if (ctx.cmd[0] != '\0' && ush_streq(ctx.cmd, "uwm") != 0) { + has_context = 1; + arg = ctx.arg; + if (ctx.cwd[0] == '/') { + ush_copy(sh.cwd, (u64)sizeof(sh.cwd), ctx.cwd); + ush_copy(initial_cwd, (u64)sizeof(initial_cwd), sh.cwd); + } + } + } + + if (has_context == 0 && argc > 1 && argv != (char **)0 && argv[1] != (char *)0) { + ush_copy(arg_buf, (u64)sizeof(arg_buf), argv[1]); + arg = arg_buf; + } + + success = ush_cmd_uwm(arg); + + if (has_context != 0) { + if (ush_streq(sh.cwd, initial_cwd) == 0) { + ret.flags |= USH_CMD_RET_FLAG_CWD; + ush_copy(ret.cwd, (u64)sizeof(ret.cwd), sh.cwd); + } + + if (sh.exit_requested != 0) { + ret.flags |= USH_CMD_RET_FLAG_EXIT; + ret.exit_code = sh.exit_code; + } + + (void)ush_command_ret_write(&ret); + } + + return (success != 0) ? 0 : 1; +} diff --git a/cleonos/c/include/cleonos_syscall.h b/cleonos/c/include/cleonos_syscall.h index 4c33d98..ad3b804 100644 --- a/cleonos/c/include/cleonos_syscall.h +++ b/cleonos/c/include/cleonos_syscall.h @@ -43,12 +43,20 @@ typedef struct cleonos_proc_snapshot { char path[CLEONOS_PROC_PATH_MAX]; } cleonos_proc_snapshot; -typedef struct cleonos_fb_info { - u64 width; - u64 height; - u64 pitch; - u64 bpp; -} cleonos_fb_info; +typedef struct cleonos_fb_info { + u64 width; + u64 height; + u64 pitch; + u64 bpp; +} cleonos_fb_info; + +typedef struct cleonos_mouse_state { + u64 x; + u64 y; + u64 buttons; + u64 packet_count; + u64 ready; +} cleonos_mouse_state; typedef struct cleonos_fb_blit_req { u64 pixels_ptr; @@ -198,10 +206,11 @@ typedef struct cleonos_net_tcp_recv_req { #define CLEONOS_SYSCALL_NET_NETMASK 100ULL #define CLEONOS_SYSCALL_NET_GATEWAY 101ULL #define CLEONOS_SYSCALL_NET_DNS_SERVER 102ULL -#define CLEONOS_SYSCALL_NET_TCP_CONNECT 103ULL -#define CLEONOS_SYSCALL_NET_TCP_SEND 104ULL -#define CLEONOS_SYSCALL_NET_TCP_RECV 105ULL -#define CLEONOS_SYSCALL_NET_TCP_CLOSE 106ULL +#define CLEONOS_SYSCALL_NET_TCP_CONNECT 103ULL +#define CLEONOS_SYSCALL_NET_TCP_SEND 104ULL +#define CLEONOS_SYSCALL_NET_TCP_RECV 105ULL +#define CLEONOS_SYSCALL_NET_TCP_CLOSE 106ULL +#define CLEONOS_SYSCALL_MOUSE_STATE 107ULL u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); u64 cleonos_sys_log_write(const char *message, u64 length); @@ -307,9 +316,10 @@ u64 cleonos_sys_net_dns_server(void); u64 cleonos_sys_net_ping(u64 dst_ipv4_be, u64 poll_budget); u64 cleonos_sys_net_udp_send(const cleonos_net_udp_send_req *req); u64 cleonos_sys_net_udp_recv(cleonos_net_udp_recv_req *req); -u64 cleonos_sys_net_tcp_connect(const cleonos_net_tcp_connect_req *req); -u64 cleonos_sys_net_tcp_send(const cleonos_net_tcp_send_req *req); -u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req); -u64 cleonos_sys_net_tcp_close(u64 poll_budget); - -#endif +u64 cleonos_sys_net_tcp_connect(const cleonos_net_tcp_connect_req *req); +u64 cleonos_sys_net_tcp_send(const cleonos_net_tcp_send_req *req); +u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req); +u64 cleonos_sys_net_tcp_close(u64 poll_budget); +u64 cleonos_sys_mouse_state(cleonos_mouse_state *out_state); + +#endif diff --git a/cleonos/c/src/syscall.c b/cleonos/c/src/syscall.c index a714cb6..9b456b7 100644 --- a/cleonos/c/src/syscall.c +++ b/cleonos/c/src/syscall.c @@ -456,6 +456,10 @@ u64 cleonos_sys_net_tcp_recv(cleonos_net_tcp_recv_req *req) { return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_RECV, (u64)req, 0ULL, 0ULL); } -u64 cleonos_sys_net_tcp_close(u64 poll_budget) { - return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_CLOSE, poll_budget, 0ULL, 0ULL); -} +u64 cleonos_sys_net_tcp_close(u64 poll_budget) { + return cleonos_syscall(CLEONOS_SYSCALL_NET_TCP_CLOSE, poll_budget, 0ULL, 0ULL); +} + +u64 cleonos_sys_mouse_state(cleonos_mouse_state *out_state) { + return cleonos_syscall(CLEONOS_SYSCALL_MOUSE_STATE, (u64)out_state, 0ULL, 0ULL); +} diff --git a/clks b/clks index 6ac3f4e..96a0da2 160000 --- a/clks +++ b/clks @@ -1 +1 @@ -Subproject commit 6ac3f4e6968aaf7d6009cafcc4d621f092bb2da5 +Subproject commit 96a0da2b1dae9682f265a26caff9d7d6558cd031 diff --git a/docs/README.md b/docs/README.md index d5bf84e..cf41b93 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,9 +26,10 @@ - `stage27.md` - `stage28.md` - `stage29.md` +- `stage30.md` - `syscall.md` (syscall ABI reference) ## Notes - Stage docs use a fixed template: goal, implementation, acceptance criteria, build targets, QEMU command, and debugging notes. -- Stages 16~19 are currently not documented in this folder; add them later using the same template to keep history continuous. +- Stages 16~19 are currently not documented in this folder; add them later using the same template to keep history continuous. diff --git a/docs/stage30.md b/docs/stage30.md new file mode 100644 index 0000000..35a3709 --- /dev/null +++ b/docs/stage30.md @@ -0,0 +1,52 @@ +# Stage 30 - User-Space Window Manager (UWM) + Mouse Syscall + +## Goal +- Add a minimal user-space desktop environment entrypoint. +- Provide a simple window manager demo (`uwm`) with focus, dragging, and rendering in framebuffer. +- Expose mouse snapshot data to user-space through a dedicated syscall. + +## Implementation +- New syscall: + - Added `MOUSE_STATE` (`id=107`) in kernel/user syscall headers. + - Kernel side (`clks/kernel/runtime/syscall.c`) now exports mouse snapshot: + - fields: `x`, `y`, `buttons`, `packet_count`, `ready`. + - User wrapper added: `cleonos_sys_mouse_state()`. +- New user app: + - Added `cleonos/c/apps/uwm_main.c`. + - Features: + - Desktop background rendering. + - Multiple windows with z-order. + - Active window focus + border highlight. + - Drag by title bar (left mouse). + - Keyboard fallback controls (`Tab`, `W/A/S/D`, `Q/Esc`). +- Build integration: + - Added `uwm` into shell command app set in `cleonos/CMakeLists.txt`. + - Updated shell help text and standalone `help` app output. +- QEMU run/debug update: + - Added optional absolute pointer device by default: + - `-usb -device usb-tablet` + - New CMake option: + - `CLEONOS_QEMU_ENABLE_USB_TABLET=ON` (default). + +## Acceptance Criteria +- `uwm` can be started from user shell. +- Framebuffer window demo draws multiple windows and supports dragging. +- Mouse state is retrievable from user-space via syscall `107`. +- `make run`/`make debug` use USB tablet by default for smoother pointer interaction. + +## Build Targets +- `make userapps` +- `make iso` +- `make run` + +## QEMU Command +- `make run` + +## Debug Notes +- If `uwm` reports framebuffer unavailable: + - verify `FB_INFO` syscall is enabled and framebuffer bpp is `32`. +- If pointer behavior is relative/jumpy: + - keep `CLEONOS_QEMU_ENABLE_USB_TABLET=ON`. + - for manual QEMU invocation, include `-usb -device usb-tablet`. +- If mouse syscall returns `ready=0`: + - check boot log for PS/2 mouse init status under `[INFO][MOUSE]`. diff --git a/docs/syscall.md b/docs/syscall.md index 79b006e..680eda6 100644 --- a/docs/syscall.md +++ b/docs/syscall.md @@ -83,7 +83,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); - `/proc/`:指定 PID 快照文本 - `/proc` 为只读;写入类 syscall 不支持。 -## 4. Syscall 列表(0~106) +## 4. Syscall 列表(0~107) ### 0 `CLEONOS_SYSCALL_LOG_WRITE` @@ -808,6 +808,16 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); - `arg0`: `u64 poll_budget` - 返回:关闭成功返回 `1`,失败返回 `0`。 - 说明:最小关闭流程支持 FIN/ACK 轮询等待,超时会强制回收连接状态。 + +### 107 `CLEONOS_SYSCALL_MOUSE_STATE` + +- 参数: +- `arg0`: `struct { u64 x; u64 y; u64 buttons; u64 packet_count; u64 ready; } *out_state` +- 返回:成功返回 `1`,失败返回 `0`。 +- 说明: +- 读取当前鼠标状态快照,坐标为 framebuffer 像素坐标系(左上角为原点)。 +- `buttons` 位掩码:`bit0=left`、`bit1=right`、`bit2=middle`。 +- `ready=1` 表示鼠标设备已上线;`ready=0` 时坐标/按键可能为默认值。 ## 5. 用户态封装函数 @@ -844,6 +854,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); - `cleonos_sys_net_available()` / `cleonos_sys_net_ipv4_addr()` / `cleonos_sys_net_netmask()` / `cleonos_sys_net_gateway()` / `cleonos_sys_net_dns_server()` / `cleonos_sys_net_ping()` - `cleonos_sys_net_udp_send()` / `cleonos_sys_net_udp_recv()` - `cleonos_sys_net_tcp_connect()` / `cleonos_sys_net_tcp_send()` / `cleonos_sys_net_tcp_recv()` / `cleonos_sys_net_tcp_close()` +- `cleonos_sys_mouse_state()` ## 6. 开发注意事项 @@ -854,7 +865,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); ## 7. Wine 兼容说明 -- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..106`(含 `DL_*`、`FB_*`、`KERNEL_VERSION`、`DISK_*`、`NET_*`)。 +- `wine/cleonos_wine_lib/runner.py` 当前已覆盖到 `0..107`(含 `DL_*`、`FB_*`、`KERNEL_VERSION`、`DISK_*`、`NET_*`、`MOUSE_STATE`)。 - `DL_*`(`77..79`)在 Wine 中为“可运行兼容”实现: - `DL_OPEN`:加载 guest ELF 到当前 Unicorn 地址空间,返回稳定 `handle`,并做引用计数。 - `DL_SYM`:解析 ELF `SYMTAB/DYNSYM` 并返回 guest 可调用地址。 @@ -872,6 +883,7 @@ u64 cleonos_syscall(u64 id, u64 arg0, u64 arg1, u64 arg2); - `DISK_MOUNT`/`DISK_MOUNT_PATH` 支持挂载点管理,并与 `FS_MKDIR/WRITE/APPEND/REMOVE` 的路径规则联动。 - `DISK_READ_SECTOR`/`DISK_WRITE_SECTOR`(`93..94`)在 Wine 中已实现为 512B 原始扇区读写(host 文件后端)。 - 网络 syscall(`95..106`)在 Wine 当前为兼容占位实现(统一返回 `0`);即 Wine 运行模式下不会提供真实网络收发。 +- `MOUSE_STATE`(`107`)在 Wine 中为基础兼容实现:可返回指针数据结构;未启用窗口鼠标事件时 `ready` 可能为 `0`。 - Wine 在运行时崩溃场景下会生成与内核一致格式的“信号编码退出状态”,可通过 `WAITPID` 读取。 - Wine 当前音频 syscall 为占位实现:`AUDIO_AVAILABLE=0`,`AUDIO_PLAY_TONE=0`,`AUDIO_STOP=1`。 - Wine 版本号策略固定为 `85.0.0-wine`(历史兼容号;不会随 syscall 扩展继续增长)。 diff --git a/kit b/kit index 6be9056..1e7b5aa 160000 --- a/kit +++ b/kit @@ -1 +1 @@ -Subproject commit 6be905609a078737c345dd1520f4bb03e73b780e +Subproject commit 1e7b5aa8d2c23ed1a2fee53fa74ce71e1b93825e diff --git a/wine b/wine index abf4a53..6ffb354 160000 --- a/wine +++ b/wine @@ -1 +1 @@ -Subproject commit abf4a53e783698514dfea9a45df5b9aa39af5da7 +Subproject commit 6ffb3542657aedaf6745a9520b69562ccafc7c70