diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index dedc3a5..e3c8cbf 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 296 +#define MONTAUK_BUILD_NUMBER 302 diff --git a/kernel/src/Api/Process.hpp b/kernel/src/Api/Process.hpp index d9602ab..ea9d316 100644 --- a/kernel/src/Api/Process.hpp +++ b/kernel/src/Api/Process.hpp @@ -302,6 +302,14 @@ namespace montauk::abi { return Sched::GetCurrentTid(); } + static int Sys_FutexWait(uint64_t addr, uint32_t expected, uint64_t timeoutNs) { + return Sched::FutexWait(addr, expected, timeoutNs); + } + + static int Sys_FutexWake(uint64_t addr, uint32_t count) { + return Sched::FutexWake(addr, count); + } + static int Sys_Chdir(const char* path) { auto* proc = Sched::GetCurrentProcessPtr(); if (proc == nullptr || path == nullptr) return InvalidArgument; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 581ee2e..c994b2d 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -545,6 +545,13 @@ namespace montauk::abi { return Sys_ThreadJoin((int)frame->arg1, (int*)frame->arg2); case SYS_THREAD_SELF: return Sys_ThreadSelf(); + case SYS_FUTEX_WAIT: + // Validation demand-commits the page so the scheduler can + // read the word with its lock held without faulting. + if (!UserMemory::Readable(frame->arg1)) return BadPointer; + return Sys_FutexWait(frame->arg1, (uint32_t)frame->arg2, frame->arg3); + case SYS_FUTEX_WAKE: + return Sys_FutexWake(frame->arg1, (uint32_t)frame->arg2); case SYS_BTSCAN: if (!Sched::HasCapability(CAP_DEVICE_ADMIN)) return CapabilityDenied; if ((int64_t)frame->arg2 < 0) return InvalidArgument; diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 5cefc5e..233324c 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -364,6 +364,17 @@ namespace montauk::abi { static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status + /* Futexes: process-private wait queues keyed by a user address. WAIT + blocks only if *addr still equals `expected` (checked atomically with + enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check + only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken, + WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the + number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */ + static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status + static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status + static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL; + static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu; + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; diff --git a/kernel/src/Common/StatusCodes.hpp b/kernel/src/Common/StatusCodes.hpp index ddd707d..b217640 100644 --- a/kernel/src/Common/StatusCodes.hpp +++ b/kernel/src/Common/StatusCodes.hpp @@ -23,5 +23,7 @@ enum StatusCodes { NoSuchPath = -11, Busy = -12, BufferTooSmall = -13, - OutOfMemory = -14 + OutOfMemory = -14, + TimedOut = -15, + WouldBlock = -16 }; diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 6e17530..e52aba2 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -514,6 +514,8 @@ namespace Sched { proc.joinerSlot = -1; proc.exitCode = 0; proc.joinable = false; // main thread is reaped by BSP, not joined + proc.futexAddr = 0; + proc.futexWoken = false; proc.savedRsp = (uint64_t)sp; proc.stackBase = (uint64_t)kernelStackBase; proc.entryPoint = entry; @@ -824,6 +826,8 @@ namespace Sched { thr.exitCode = 0; thr.joinable = true; thr.threadArg = arg; + thr.futexAddr = 0; + thr.futexWoken = false; thr.savedRsp = (uint64_t)sp; thr.stackBase = (uint64_t)kernelStackBase; thr.entryPoint = entry; @@ -2116,6 +2120,129 @@ namespace Sched { } } + // Monotonic enrollment counter for FIFO futex wakeups. Only touched + // under schedLock. + static uint64_t futexNextSeq = 1; + + int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs) { + if ((uaddr & 3) != 0) return InvalidArgument; + + ExitIfKilled(); + + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu ? cpu->currentSlot : -1; + if (slot < 0) return GenericFailure; + Process& self = processTable[slot]; + + // Round up so a short timeout never turns into "check only". + uint64_t timeoutMs = 0; + if (timeoutNs != FutexWaitForever) { + if (timeoutNs == 0) { + timeoutMs = 0; + } else { + timeoutMs = timeoutNs / 1000000ULL + ((timeoutNs % 1000000ULL) ? 1 : 0); + } + } + + schedLock.Acquire(); + + // The word is read here, with schedLock held and interrupts off, so + // it must not fault: the caller validated (and demand-committed) the + // page, and the page walk below re-confirms it is still mapped. An + // aligned 32-bit word never straddles a page. + if (!Memory::VMM::Paging::IsUserRangeAccessible(self.pml4Phys, uaddr, 4, false)) { + schedLock.Release(); + return BadPointer; + } + uint32_t current = __atomic_load_n((volatile uint32_t*)uaddr, __ATOMIC_SEQ_CST); + if (current != expected) { + schedLock.Release(); + return WouldBlock; + } + if (timeoutNs == 0) { + schedLock.Release(); + return TimedOut; + } + + self.state = ProcessState::Blocked; + self.waitingForPid = -1; + self.waitingOnObject = nullptr; + self.futexAddr = uaddr; + self.futexSeq = futexNextSeq++; + self.futexWoken = false; + self.sleepUntilTick = (timeoutNs == FutexWaitForever) + ? 0 + : Timekeeping::GetTicks() + timeoutMs; + self.runningOnCpu = -1; + SwitchAwayFromBlockedCurrentLocked(); + + // Whatever made this thread runnable (FutexWake, the deadline, or a + // kill) it is no longer parked; clear the key before this thread can + // block for some other reason and be mistaken for a futex waiter. + schedLock.Acquire(); + bool woken = self.futexWoken; + self.futexAddr = 0; + self.futexWoken = false; + schedLock.Release(); + + ExitIfKilled(); + return woken ? Success : TimedOut; + } + + int FutexWake(uint64_t uaddr, uint32_t count) { + if ((uaddr & 3) != 0 || uaddr == 0) return InvalidArgument; + if (count == 0) return 0; + + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu ? cpu->currentSlot : -1; + if (slot < 0) return GenericFailure; + + schedLock.Acquire(); + int primary = processTable[slot].primarySlot; + if (primary < 0) primary = slot; + + // Gather this process's waiters on uaddr. At most MaxProcesses. + int waiters[MaxProcesses]; + int n = 0; + for (int i = 0; i < MaxProcesses; i++) { + Process& p = processTable[i]; + if (p.state != ProcessState::Blocked) continue; + if (p.futexAddr != uaddr) continue; + if (p.primarySlot != primary) continue; + waiters[n++] = i; + } + + // Waking everyone needs no ordering; otherwise pick oldest-first. + bool ordered = (uint32_t)n > count; + int woken = 0; + while (woken < n && (uint32_t)woken < count) { + if (ordered) { + int best = woken; + for (int j = woken + 1; j < n; j++) { + if (processTable[waiters[j]].futexSeq < processTable[waiters[best]].futexSeq) + best = j; + } + int tmp = waiters[woken]; waiters[woken] = waiters[best]; waiters[best] = tmp; + } + + Process& p = processTable[waiters[woken]]; + p.futexAddr = 0; + p.futexWoken = true; + p.sleepUntilTick = 0; + p.state = ProcessState::Ready; + readyCount++; + woken++; + } + schedLock.Release(); + + if (woken > 0) { + // Re-read: this thread may have migrated since `cpu` was sampled. + auto* here = Smp::GetCurrentCpuData(); + KickOneIdleCpu(here ? here->cpuIndex : -1); + } + return woken; + } + bool IsAlive(int pid) { for (int i = 0; i < MaxProcesses; i++) { if (processTable[i].pid == pid) { diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index f0b7568..eafde00 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -126,6 +126,13 @@ namespace Sched { bool joinable = true; // Sibling thread keeps Terminated state for the joiner uint64_t threadArg = 0; // Argument passed into entryPoint on first dispatch + // Futex wait state (see FutexWait). futexAddr is the user VA this + // thread is parked on, 0 when not in a futex wait. futexSeq orders + // waiters so FutexWake releases the longest-waiting first. + uint64_t futexAddr = 0; + uint64_t futexSeq = 0; + bool futexWoken = false; + // I/O redirection for GUI terminal bool redirected = false; int parentPid = -1; @@ -270,6 +277,30 @@ namespace Sched { // Wake any processes blocked on the given object. void WakeObjectWaiters(void* object); + // ==================================================================== + // Futexes + // ==================================================================== + + // Futexes are private to a process: the key is (primary slot, user VA), + // so two processes waiting on the same address never see each other. + static constexpr uint64_t FutexWaitForever = ~0ULL; + + // Block the current thread on the 32-bit word at `uaddr` if it still + // holds `expected`. The comparison and the enrollment happen atomically + // under the scheduler lock, so a FutexWake issued after the waker changed + // the word can never be lost. `timeoutNs` is rounded up to the 1 ms tick; + // FutexWaitForever waits without a deadline, 0 only performs the check. + // Returns Success when woken by FutexWake, WouldBlock when the word did + // not hold `expected`, TimedOut when the deadline passed, InvalidArgument + // for a misaligned address, BadPointer when the word is not mapped. + // Spurious Success returns are not possible, but callers must still + // re-check their condition: another thread may have run in between. + int FutexWait(uint64_t uaddr, uint32_t expected, uint64_t timeoutNs); + + // Wake up to `count` threads of the current process blocked on `uaddr`, + // longest-waiting first. Returns the number woken. + int FutexWake(uint64_t uaddr, uint32_t count); + // Kill a process by PID. If the process is running on another CPU, // sets a kill-pending flag checked on the next timer tick. // Returns 0 on success, -1 on failure. diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index a490c84..38341f1 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -277,6 +277,17 @@ namespace montauk::abi { static constexpr uint64_t SYS_GETKEY_EX = 194; // (KeyEvent*, size) -> 0; blocks like SYS_GETKEY static constexpr uint64_t SYS_CHILDIO_WRITEKEY_EX = 195; // (childPid, const KeyEvent*, size) -> status + /* Futexes: process-private wait queues keyed by a user address. WAIT + blocks only if *addr still equals `expected` (checked atomically with + enrollment). timeoutNs: FUTEX_WAIT_FOREVER = no deadline, 0 = check + only; rounded up to the 1 ms scheduler tick. WAIT returns 0 when woken, + WouldBlock on value mismatch, TimedOut on deadline. WAKE returns the + number of threads woken (count = FUTEX_WAKE_ALL wakes every waiter). */ + static constexpr uint64_t SYS_FUTEX_WAIT = 196; // (uint32_t* addr, expected, timeoutNs) -> status + static constexpr uint64_t SYS_FUTEX_WAKE = 197; // (uint32_t* addr, count) -> woken, or status + static constexpr uint64_t FUTEX_WAIT_FOREVER = ~0ULL; + static constexpr uint32_t FUTEX_WAKE_ALL = 0xFFFFFFFFu; + /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ static constexpr uint64_t CAP_PROCESS_ADMIN = 1ULL << 0; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 7f02ad6..23a705c 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -219,6 +219,8 @@ extern "C" { #define MTK_SYS_CONSENT_REPLY 193 #define MTK_SYS_GETKEY_EX 194 #define MTK_SYS_CHILDIO_WRITEKEY_EX 195 +#define MTK_SYS_FUTEX_WAIT 196 +#define MTK_SYS_FUTEX_WAKE 197 /* @SYSCALLS-END */ /* Syscall status codes (montauk/statuscodes.h for C++). */ @@ -240,6 +242,8 @@ extern "C" { #define MTK_STATUS_BUSY (-12) #define MTK_STATUS_BUFFER_TOO_SMALL (-13) #define MTK_STATUS_OUT_OF_MEMORY (-14) +#define MTK_STATUS_TIMED_OUT (-15) +#define MTK_STATUS_WOULD_BLOCK (-16) /* @STATUS-END */ #define MTK_SOCK_TCP 1 @@ -491,6 +495,21 @@ static inline void mtk_sleep_ms(unsigned long ms) { _mtk_syscall1(MTK_SYS_SLEEP_MS, (long)ms); } +/* Futexes (process-private). mtk_futex_wait blocks while *addr == expected; + returns 0 when woken, MTK_STATUS_TIMED_OUT, or MTK_STATUS_WOULD_BLOCK when + the word had already changed. timeout_ns: MTK_FUTEX_WAIT_FOREVER for no + deadline, 0 to only check. mtk_futex_wake returns the number woken. */ +#define MTK_FUTEX_WAIT_FOREVER (~0UL) +#define MTK_FUTEX_WAKE_ALL 0xFFFFFFFFu + +static inline int mtk_futex_wait(uint32_t *addr, uint32_t expected, unsigned long timeout_ns) { + return (int)_mtk_syscall3(MTK_SYS_FUTEX_WAIT, (long)addr, (long)expected, (long)timeout_ns); +} + +static inline int mtk_futex_wake(uint32_t *addr, uint32_t count) { + return (int)_mtk_syscall2(MTK_SYS_FUTEX_WAKE, (long)addr, (long)count); +} + static inline int mtk_getpid(void) { return (int)_mtk_syscall0(MTK_SYS_GETPID); } diff --git a/programs/include/montauk/statuscodes.h b/programs/include/montauk/statuscodes.h index e91813a..a078ea0 100644 --- a/programs/include/montauk/statuscodes.h +++ b/programs/include/montauk/statuscodes.h @@ -27,7 +27,9 @@ typedef enum { NoSuchPath = 11, Busy = 12, BufferTooSmall = 13, - OutOfMemory = 14 + OutOfMemory = 14, + TimedOut = 15, + WouldBlock = 16 } StatusCode; inline const char* GetStatusText(StatusCode statusCode) { @@ -47,9 +49,11 @@ inline const char* GetStatusText(StatusCode statusCode) { "Busy", // 12 "Buffer too small", // 13 "Out of memory", // 14 + "Timed out", // 15 + "Operation would block", // 16 }; - if (statusCode > 0 && statusCode <= 14) { + if (statusCode > 0 && statusCode <= 16) { return statusText[statusCode]; } else { return "Unknown error"; @@ -74,9 +78,11 @@ inline const char* GetStatusExplanationText(StatusCode statusCode) { 0, // 12 "The buffer supplied by the caller is too small.", // 13 0, // 14 + 0, // 15 + 0, // 16 }; - if (statusCode > 0 && statusCode <= 14) { + if (statusCode > 0 && statusCode <= 16) { if (explanationText[statusCode] != 0) { return explanationText[statusCode]; } diff --git a/programs/include/montauk/thread.h b/programs/include/montauk/thread.h index d45085a..99e5f2a 100644 --- a/programs/include/montauk/thread.h +++ b/programs/include/montauk/thread.h @@ -148,6 +148,28 @@ namespace montauk { return (int)syscall0(montauk::abi::SYS_THREAD_SELF); } + // Block while *addr == expected. Returns 0 when woken by futex_wake, + // WouldBlock if *addr had already changed, TimedOut when timeout_ns + // elapsed (FUTEX_WAIT_FOREVER = no deadline, 0 = check only, otherwise + // rounded up to the 1 ms tick). Futexes are private to the process. + // A 0 return is never spurious, but always re-check the condition. + inline int futex_wait(uint32_t* addr, uint32_t expected, + uint64_t timeout_ns = montauk::abi::FUTEX_WAIT_FOREVER) { + int64_t r = syscall3(montauk::abi::SYS_FUTEX_WAIT, (uint64_t)addr, + (uint64_t)expected, timeout_ns); + return r < 0 ? (int)-r : 0; + } + + // Wake up to `count` waiters on addr, longest-waiting first. Returns the + // number woken, or a negative status for a bad address. + inline int futex_wake(uint32_t* addr, uint32_t count = 1) { + return (int)syscall2(montauk::abi::SYS_FUTEX_WAKE, (uint64_t)addr, (uint64_t)count); + } + + inline int futex_wake_all(uint32_t* addr) { + return futex_wake(addr, montauk::abi::FUTEX_WAKE_ALL); + } + // Lightweight mutex backed by a single atomic word + the kernel // yield syscall. Adequate for short critical sections; a heavier // primitive can wrap mailbox_recv when blocking semantics are needed. diff --git a/programs/src/audio/manifest.toml b/programs/src/audio/manifest.toml index de3dc96..fc18b2a 100644 --- a/programs/src/audio/manifest.toml +++ b/programs/src/audio/manifest.toml @@ -4,5 +4,5 @@ binary = "audio.elf" icon = "pavucontrol.svg" [menu] -category = "System" +category = "Hardware" visible = true diff --git a/programs/src/bluetooth/manifest.toml b/programs/src/bluetooth/manifest.toml index 402d0ca..c13a2d1 100644 --- a/programs/src/bluetooth/manifest.toml +++ b/programs/src/bluetooth/manifest.toml @@ -4,5 +4,5 @@ binary = "bluetooth.elf" icon = "bluetooth.svg" [menu] -category = "System" +category = "Hardware" visible = true diff --git a/programs/src/calculator/manifest.toml b/programs/src/calculator/manifest.toml index 6d34aad..2100f2c 100644 --- a/programs/src/calculator/manifest.toml +++ b/programs/src/calculator/manifest.toml @@ -4,7 +4,7 @@ binary = "calculator.elf" icon = "accessories-calculator.svg" [menu] -category = "Applications" +category = "Utilities" visible = true [launch] diff --git a/programs/src/charmap/manifest.toml b/programs/src/charmap/manifest.toml index 3db18fd..c09c0a5 100644 --- a/programs/src/charmap/manifest.toml +++ b/programs/src/charmap/manifest.toml @@ -4,7 +4,7 @@ binary = "charmap.elf" icon = "accessories-character-map.svg" [menu] -category = "Applications" +category = "Utilities" visible = true [launch] diff --git a/programs/src/desktop/desktop_internal.hpp b/programs/src/desktop/desktop_internal.hpp index f97ce6b..6ffb2c0 100644 --- a/programs/src/desktop/desktop_internal.hpp +++ b/programs/src/desktop/desktop_internal.hpp @@ -48,6 +48,7 @@ static constexpr int MENU_DIV_H = 10; struct MenuRow { bool is_category; + bool expanded; // category rows only: children shown (dividers always true) char label[48]; DesktopBuiltinId app_id; // desktop builtin ID, or DESKTOP_BUILTIN_NONE for categories/dividers bool external; // true = spawn binary_path @@ -74,23 +75,15 @@ inline MenuRow* menu_alloc_row() { return &menu_rows[menu_row_count++]; } -// Collapsible category state (categories 0-3 are toggleable; divider category always expanded) -static constexpr int MENU_NUM_CATS = 5; -inline bool menu_cat_expanded[MENU_NUM_CATS] = { false, false, false, false, true }; - -inline int menu_get_cat(int row_idx) { - int cat = -1; - for (int i = 0; i <= row_idx; i++) - if (menu_rows[i].is_category) cat++; - return cat; -} - +// Collapsible categories: each category row carries its own expanded flag, +// so the menu can hold any number of manifest-defined categories. Dividers +// (empty label) are always expanded. inline bool menu_row_visible(int row_idx) { const MenuRow& row = menu_rows[row_idx]; if (row.is_category) return true; - int cat = menu_get_cat(row_idx); - if (cat < 0 || cat >= MENU_NUM_CATS) return true; - return menu_cat_expanded[cat]; + for (int i = row_idx - 1; i >= 0; i--) + if (menu_rows[i].is_category) return menu_rows[i].expanded; + return true; } inline int menu_row_height(const MenuRow& row) { @@ -102,17 +95,11 @@ inline int menu_total_height() { // Single-pass walk to avoid O(N^2) from menu_row_visible repeatedly // counting categories from the start. int h = 10; // top + bottom padding - int cur_cat = -1; + bool cat_expanded = true; for (int i = 0; i < menu_row_count; i++) { const MenuRow& row = menu_rows[i]; - if (row.is_category) cur_cat++; - bool visible = true; - if (!row.is_category) { - if (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) { - visible = menu_cat_expanded[cur_cat]; - } - } - if (visible) h += menu_row_height(row); + if (row.is_category) cat_expanded = row.expanded; + if (row.is_category || cat_expanded) h += menu_row_height(row); } return h; } @@ -126,6 +113,7 @@ inline int menu_add_category(const char* label) { if (!rp) return -1; MenuRow& r = *rp; r.is_category = true; + r.expanded = !label[0]; // named categories start collapsed; dividers stay open montauk::strncpy(r.label, label, sizeof(r.label)); r.app_id = DESKTOP_BUILTIN_NONE; r.external = false; @@ -140,6 +128,7 @@ inline int menu_add_embedded(const char* label, DesktopBuiltinId app_id, SvgIcon if (!rp) return -1; MenuRow& r = *rp; r.is_category = false; + r.expanded = false; montauk::strncpy(r.label, label, sizeof(r.label)); r.app_id = app_id; r.external = false; @@ -154,6 +143,7 @@ inline int menu_add_external(const char* label, const char* binary, SvgIcon* ico if (!rp) return -1; MenuRow& r = *rp; r.is_category = false; + r.expanded = false; montauk::strncpy(r.label, label, sizeof(r.label)); r.app_id = DESKTOP_BUILTIN_NONE; r.external = true; diff --git a/programs/src/desktop/input.cpp b/programs/src/desktop/input.cpp index e5923ce..c1e8f1c 100644 --- a/programs/src/desktop/input.cpp +++ b/programs/src/desktop/input.cpp @@ -360,16 +360,16 @@ void gui::desktop_handle_mouse(DesktopState* ds) { if (menu_rect.contains(mx, my)) { // Walk visible rows to find which one was clicked int iy = menu_y + 5; - int cur_cat = -1; + bool cat_expanded = true; for (int i = 0; i < menu_row_count; i++) { - const MenuRow& row = menu_rows[i]; - if (row.is_category) cur_cat++; - if (!menu_row_visible(i)) continue; + MenuRow& row = menu_rows[i]; + if (row.is_category) cat_expanded = row.expanded; + else if (!cat_expanded) continue; int row_h = menu_row_height(row); if (my >= iy && my < iy + row_h) { - if (row.is_category && row.label[0] && cur_cat >= 0 && cur_cat < MENU_NUM_CATS - 1) { + if (row.is_category && row.label[0]) { // Toggle category expand/collapse - menu_cat_expanded[cur_cat] = !menu_cat_expanded[cur_cat]; + row.expanded = !row.expanded; } else if (!row.is_category) { if (row.external) { if (row.launch_with_home) { diff --git a/programs/src/desktop/main.cpp b/programs/src/desktop/main.cpp index 1652431..f057cd0 100644 --- a/programs/src/desktop/main.cpp +++ b/programs/src/desktop/main.cpp @@ -121,7 +121,11 @@ void desktop_scan_apps(DesktopState* ds) { const char* name = doc.get_string("app.name", "Unknown"); const char* binary = doc.get_string("app.binary", ""); const char* icon_file = doc.get_string("app.icon", ""); - const char* category = doc.get_string("menu.category", "Applications"); + const char* category = doc.get_string("menu.category", "Productivity"); + // "Applications" was the old catch-all; keep older manifests out of + // a stray custom category. + if (!category[0] || montauk::streq(category, "Applications")) + category = "Productivity"; bool visible = doc.get_bool("menu.visible", true); bool launch_with_home = doc.get_bool("launch.pass_home_dir", true); const char* section_name = doc.get_string("desktop.section", "apps"); @@ -197,18 +201,55 @@ void desktop_scan_apps(DesktopState* ds) { // Menu Builder // ============================================================================ -// Category definitions and their ordering -static const char* CATEGORY_NAMES[] = { "Applications", "Internet", "System", "Games" }; -static constexpr int NUM_CATEGORIES = 4; +// Predefined categories and their ordering. Manifests may name any other +// category; those are appended after these, sorted by name. +// Builtins reference these by index (DesktopBuiltinEntry::launcher_category). +static const char* CATEGORY_NAMES[] = { + "Productivity", "Media", "Internet", + "Utilities", "Games", "Hardware", "System", +}; +static constexpr int NUM_CATEGORIES = sizeof(CATEGORY_NAMES) / sizeof(CATEGORY_NAMES[0]); + +static bool category_eq(const char* a, const char* b) { + for (;; a++, b++) { + char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b; + if (ca != cb) return false; + if (!ca) return true; + } +} + +static int category_cmp(const char* a, const char* b) { + for (;; a++, b++) { + char ca = (*a >= 'A' && *a <= 'Z') ? *a + 32 : *a; + char cb = (*b >= 'A' && *b <= 'Z') ? *b + 32 : *b; + if (ca != cb || !ca) return (int)(unsigned char)ca - (int)(unsigned char)cb; + } +} + +static bool menu_item_shown(DesktopState* ds, const DesktopItem* item) { + return item->menu_visible && !(item->admin_only && !ds->is_admin); +} + +static void menu_add_manifest_items(DesktopState* ds, const char* category) { + for (int x = 0; x < ds->desktop_item_count; x++) { + DesktopItem* item = &ds->desktop_items[x]; + if (!menu_item_shown(ds, item)) continue; + if (category_eq(item->category, category)) { + menu_add_external(item->name, item->binary_path, + &item->icon, item->launch_with_home); + } + } +} void desktop_build_menu(DesktopState* ds) { menu_row_count = 0; int builtin_count = 0; const DesktopBuiltinEntry* builtins = desktop_builtin_registry(&builtin_count); - // Build each category + // Build each predefined category for (int cat = 0; cat < NUM_CATEGORIES; cat++) { - menu_add_category(CATEGORY_NAMES[cat]); + int header = menu_add_category(CATEGORY_NAMES[cat]); // Add desktop builtins in this category for (int b = 0; b < builtin_count; b++) { @@ -218,19 +259,48 @@ void desktop_build_menu(DesktopState* ds) { } } - // Add manifest-backed items in this category. + menu_add_manifest_items(ds, CATEGORY_NAMES[cat]); + + // Drop the header again if nothing landed under it. + if (header >= 0 && menu_row_count == header + 1) menu_row_count = header; + } + + // Custom categories: the first item of each distinct, non-predefined + // category (case-insensitive) represents it; emit them in name order. + const DesktopItem** custom = ds->desktop_item_count > 0 + ? (const DesktopItem**)montauk::malloc( + (uint64_t)ds->desktop_item_count * sizeof(DesktopItem*)) + : nullptr; + int custom_count = 0; + if (custom) { for (int x = 0; x < ds->desktop_item_count; x++) { - DesktopItem* item = &ds->desktop_items[x]; - if (!item->menu_visible || (item->admin_only && !ds->is_admin)) continue; - if (montauk::streq(item->category, CATEGORY_NAMES[cat])) { - menu_add_external(item->name, item->binary_path, - &item->icon, item->launch_with_home); + const DesktopItem* item = &ds->desktop_items[x]; + if (!menu_item_shown(ds, item)) continue; + + bool known = false; + for (int c = 0; c < NUM_CATEGORIES && !known; c++) + known = category_eq(item->category, CATEGORY_NAMES[c]); + for (int c = 0; c < custom_count && !known; c++) + known = category_eq(item->category, custom[c]->category); + if (known) continue; + + int pos = custom_count; + while (pos > 0 && category_cmp(custom[pos - 1]->category, item->category) > 0) { + custom[pos] = custom[pos - 1]; + pos--; } + custom[pos] = item; + custom_count++; } + for (int c = 0; c < custom_count; c++) { + menu_add_category(custom[c]->category); + menu_add_manifest_items(ds, custom[c]->category); + } + montauk::mfree(custom); } // Divider + always-visible entries - menu_add_category(""); // divider (cat 4, always expanded) + menu_add_category(""); // divider (always expanded) for (int b = 0; b < builtin_count; b++) { if (builtins[b].launcher_footer) { menu_add_embedded(builtins[b].label, builtins[b].id, diff --git a/programs/src/desktop/panel.cpp b/programs/src/desktop/panel.cpp index 911548c..3bc1223 100644 --- a/programs/src/desktop/panel.cpp +++ b/programs/src/desktop/panel.cpp @@ -271,13 +271,13 @@ void desktop_draw_app_menu(DesktopState* ds) { int mx = ds->mouse.x; int my = ds->mouse.y; int iy = menu_y + 5; - int cur_cat = -1; + bool cat_expanded = true; for (int i = 0; i < menu_row_count; i++) { const MenuRow& row = menu_rows[i]; - if (row.is_category) cur_cat++; - if (!menu_row_visible(i)) continue; + if (row.is_category) cat_expanded = row.expanded; + else if (!cat_expanded) continue; int row_h = menu_row_height(row); @@ -297,7 +297,7 @@ void desktop_draw_app_menu(DesktopState* ds) { Color::from_rgb(0xE8, 0xE8, 0xE8)); // Arrow indicator: > for collapsed, v for expanded - bool expanded = (cur_cat >= 0 && cur_cat < MENU_NUM_CATS) && menu_cat_expanded[cur_cat]; + bool expanded = row.expanded; int ax = menu_x + 10; int ay = iy + row_h / 2; Color arrow_color = Color::from_rgb(0x88, 0x88, 0x88); diff --git a/programs/src/devexplorer/manifest.toml b/programs/src/devexplorer/manifest.toml index 5f24534..7639e84 100644 --- a/programs/src/devexplorer/manifest.toml +++ b/programs/src/devexplorer/manifest.toml @@ -4,5 +4,5 @@ binary = "devexplorer.elf" icon = "hardware.svg" [menu] -category = "System" +category = "Hardware" visible = true diff --git a/programs/src/disks/manifest.toml b/programs/src/disks/manifest.toml index 0a4bc6f..354f4c4 100644 --- a/programs/src/disks/manifest.toml +++ b/programs/src/disks/manifest.toml @@ -4,5 +4,5 @@ binary = "disks.elf" icon = "gparted.svg" [menu] -category = "System" +category = "Hardware" visible = true diff --git a/programs/src/fontpreview/manifest.toml b/programs/src/fontpreview/manifest.toml index 527178e..8c54550 100644 --- a/programs/src/fontpreview/manifest.toml +++ b/programs/src/fontpreview/manifest.toml @@ -4,7 +4,7 @@ binary = "fontpreview.elf" icon = "font-viewer.svg" [menu] -category = "Applications" +category = "Utilities" visible = false [launch] diff --git a/programs/src/futextest/main.cpp b/programs/src/futextest/main.cpp new file mode 100644 index 0000000..dc335f3 --- /dev/null +++ b/programs/src/futextest/main.cpp @@ -0,0 +1,171 @@ +/* + * futextest - SYS_FUTEX_WAIT / SYS_FUTEX_WAKE regression tests + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include + +namespace { + int failures = 0; + + void check(bool condition, const char* name) { + montauk::print(condition ? "PASS " : "FAIL "); + montauk::print(name); + montauk::putchar('\n'); + if (!condition) failures++; + } + + // ==================================================================== + // Futex mutex (Drepper, "Futexes Are Tricky", mutex #3): + // 0 = unlocked, 1 = locked, 2 = locked with possible waiters. + // ==================================================================== + + struct FutexMutex { + uint32_t state = 0; + + void lock() { + uint32_t c = 0; + if (__atomic_compare_exchange_n(&state, &c, 1, false, + __ATOMIC_ACQUIRE, __ATOMIC_RELAXED)) + return; + if (c != 2) c = __atomic_exchange_n(&state, 2, __ATOMIC_ACQUIRE); + while (c != 0) { + montauk::futex_wait(&state, 2); + c = __atomic_exchange_n(&state, 2, __ATOMIC_ACQUIRE); + } + } + + void unlock() { + if (__atomic_fetch_sub(&state, 1, __ATOMIC_RELEASE) != 1) { + __atomic_store_n(&state, 0, __ATOMIC_RELEASE); + montauk::futex_wake(&state, 1); + } + } + }; + + constexpr int MutexThreads = 4; + constexpr int MutexIters = 20000; + + FutexMutex g_mutex; + volatile uint64_t g_counter = 0; + + int mutex_worker(void*) { + for (int i = 0; i < MutexIters; i++) { + g_mutex.lock(); + // Non-atomic read-modify-write: only correct under the mutex. + uint64_t v = g_counter; + if ((i & 255) == 0) montauk::yield(); + g_counter = v + 1; + g_mutex.unlock(); + } + return 0; + } + + // ==================================================================== + // Gate: threads park until main flips the word and wakes them. + // ==================================================================== + + uint32_t g_gate = 0; + uint32_t g_arrived = 0; + + int gate_worker(void*) { + __atomic_fetch_add(&g_arrived, 1, __ATOMIC_RELAXED); + while (__atomic_load_n(&g_gate, __ATOMIC_ACQUIRE) == 0) { + int r = montauk::futex_wait(&g_gate, 0); + if (r != 0 && r != montauk::WouldBlock) return 1; + } + return 0; + } + + bool join_all(const int* tids, int n) { + bool ok = true; + for (int i = 0; i < n; i++) { + int code = -1; + if (tids[i] < 0 || montauk::thread_join(tids[i], &code) != 0 || code != 0) + ok = false; + } + return ok; + } +} + +extern "C" void _start() { + montauk::print("futextest: SYS_FUTEX_WAIT / SYS_FUTEX_WAKE\n"); + + // --- Argument and fast-path checks --- + uint32_t word = 5; + check(montauk::futex_wait(&word, 4) == montauk::WouldBlock, + "wait returns WouldBlock on value mismatch"); + check(montauk::futex_wait(&word, 5, 0) == montauk::TimedOut, + "wait with zero timeout returns TimedOut on match"); + check(montauk::futex_wake(&word, 1) == 0, + "wake with no waiters returns 0"); + + uint32_t pair[2] = {0, 0}; + uint32_t* misaligned = (uint32_t*)((uint8_t*)pair + 1); + check(montauk::futex_wait(misaligned, 0) == montauk::InvalidArgument, + "wait rejects a misaligned address"); + check(montauk::futex_wake(misaligned, 1) == -(int)montauk::InvalidArgument, + "wake rejects a misaligned address"); + check(montauk::futex_wait((uint32_t*)0, 0) == montauk::BadPointer, + "wait rejects a null address"); + check(montauk::futex_wait((uint32_t*)0xFFFF800000001000ULL, 0) == montauk::BadPointer, + "wait rejects a kernel address"); + + // --- Timed wait --- + uint64_t t0 = montauk::get_milliseconds(); + int r = montauk::futex_wait(&word, 5, 30ULL * 1000 * 1000); + uint64_t elapsed = montauk::get_milliseconds() - t0; + check(r == montauk::TimedOut, "30 ms wait returns TimedOut"); + check(elapsed >= 29 && elapsed < 1000, "30 ms wait lasts about 30 ms"); + + // --- Wake one --- + { + g_gate = 0; g_arrived = 0; + int tid = montauk::thread_spawn(gate_worker, nullptr); + while (__atomic_load_n(&g_arrived, __ATOMIC_RELAXED) < 1) montauk::yield(); + montauk::sleep_ms(20); // give it time to park + __atomic_store_n(&g_gate, 1, __ATOMIC_RELEASE); + int woken = montauk::futex_wake(&g_gate, 1); + check(woken == 1, "wake(1) releases the parked waiter"); + check(join_all(&tid, 1), "woken waiter exits cleanly"); + } + + // --- Wake all --- + { + constexpr int N = 6; + int tids[N]; + g_gate = 0; g_arrived = 0; + for (int i = 0; i < N; i++) tids[i] = montauk::thread_spawn(gate_worker, nullptr); + while (__atomic_load_n(&g_arrived, __ATOMIC_RELAXED) < (uint32_t)N) montauk::yield(); + montauk::sleep_ms(30); + + // Partial wake must not release more than asked. The two released + // threads see the gate still closed and may park again, so wake_all + // finds between four and six waiters. + int first = montauk::futex_wake(&g_gate, 2); + check(first == 2, "wake(2) releases exactly two of six"); + + __atomic_store_n(&g_gate, 1, __ATOMIC_RELEASE); + int rest = montauk::futex_wake_all(&g_gate); + check(rest >= N - 2 && rest <= N, "wake_all releases every remaining waiter"); + check(join_all(tids, N), "all gate waiters exit cleanly"); + } + + // --- Contended futex mutex --- + { + int tids[MutexThreads]; + g_counter = 0; + for (int i = 0; i < MutexThreads; i++) + tids[i] = montauk::thread_spawn(mutex_worker, nullptr); + bool joined = join_all(tids, MutexThreads); + check(joined, "mutex workers exit cleanly"); + check(g_counter == (uint64_t)MutexThreads * MutexIters, + "futex mutex keeps the counter exact under contention"); + check(g_mutex.state == 0, "futex mutex ends unlocked"); + } + + montauk::print(failures == 0 ? "futextest: all passed\n" : "futextest: FAILURES\n"); + montauk::exit(failures == 0 ? 0 : 1); +} diff --git a/programs/src/imageviewer/manifest.toml b/programs/src/imageviewer/manifest.toml index 0566453..c23747a 100644 --- a/programs/src/imageviewer/manifest.toml +++ b/programs/src/imageviewer/manifest.toml @@ -4,5 +4,5 @@ binary = "imageviewer.elf" icon = "image-viewer.svg" [menu] -category = "Applications" +category = "Media" visible = false diff --git a/programs/src/music/manifest.toml b/programs/src/music/manifest.toml index 804775c..ec3e6a3 100644 --- a/programs/src/music/manifest.toml +++ b/programs/src/music/manifest.toml @@ -4,7 +4,7 @@ binary = "music.elf" icon = "audio-player.svg" [menu] -category = "Applications" +category = "Media" visible = true [launch] diff --git a/programs/src/paint/manifest.toml b/programs/src/paint/manifest.toml index eabab12..8a9088d 100644 --- a/programs/src/paint/manifest.toml +++ b/programs/src/paint/manifest.toml @@ -4,5 +4,5 @@ binary = "paint.elf" icon = "paint.svg" [menu] -category = "Applications" +category = "Media" visible = true diff --git a/programs/src/pdfviewer/manifest.toml b/programs/src/pdfviewer/manifest.toml index 9751d22..236cc01 100644 --- a/programs/src/pdfviewer/manifest.toml +++ b/programs/src/pdfviewer/manifest.toml @@ -4,5 +4,5 @@ binary = "pdfviewer.elf" icon = "pdf-viewer.svg" [menu] -category = "Applications" +category = "Productivity" visible = false diff --git a/programs/src/powermgr/manifest.toml b/programs/src/powermgr/manifest.toml index 93be0be..822fbc9 100644 --- a/programs/src/powermgr/manifest.toml +++ b/programs/src/powermgr/manifest.toml @@ -4,7 +4,7 @@ binary = "powermgr.elf" icon = "gnome-power-statistics.svg" [menu] -category = "System" +category = "Hardware" visible = true [launch] diff --git a/programs/src/printers/manifest.toml b/programs/src/printers/manifest.toml index ec325be..4057966 100644 --- a/programs/src/printers/manifest.toml +++ b/programs/src/printers/manifest.toml @@ -5,7 +5,7 @@ binary = "printers.elf" icon = "preferences-devices-printer.svg" [menu] -category = "System" +category = "Hardware" visible = true [desktop] diff --git a/programs/src/screenshot/manifest.toml b/programs/src/screenshot/manifest.toml index 3a533af..61f9c38 100644 --- a/programs/src/screenshot/manifest.toml +++ b/programs/src/screenshot/manifest.toml @@ -4,5 +4,5 @@ binary = "screenshot.elf" icon = "screenshot.svg" [menu] -category = "System" +category = "Utilities" visible = true diff --git a/programs/src/spreadsheet/manifest.toml b/programs/src/spreadsheet/manifest.toml index 97a2e3d..1690066 100644 --- a/programs/src/spreadsheet/manifest.toml +++ b/programs/src/spreadsheet/manifest.toml @@ -4,5 +4,5 @@ binary = "spreadsheet.elf" icon = "spreadsheet.svg" [menu] -category = "Applications" +category = "Productivity" visible = true diff --git a/programs/src/terminal/manifest.toml b/programs/src/terminal/manifest.toml index 7a27679..30ddd12 100644 --- a/programs/src/terminal/manifest.toml +++ b/programs/src/terminal/manifest.toml @@ -4,7 +4,7 @@ binary = "terminal.elf" icon = "utilities-terminal.svg" [menu] -category = "Applications" +category = "System" visible = true [launch] diff --git a/programs/src/texteditor/manifest.toml b/programs/src/texteditor/manifest.toml index 36d3198..a918c91 100644 --- a/programs/src/texteditor/manifest.toml +++ b/programs/src/texteditor/manifest.toml @@ -4,7 +4,7 @@ binary = "texteditor.elf" icon = "texteditor.svg" [menu] -category = "Applications" +category = "Productivity" visible = true [launch] diff --git a/programs/src/video/manifest.toml b/programs/src/video/manifest.toml index 3a5cabb..2859ca0 100644 --- a/programs/src/video/manifest.toml +++ b/programs/src/video/manifest.toml @@ -4,5 +4,5 @@ binary = "video.elf" icon = "video.svg" [menu] -category = "Applications" +category = "Media" visible = true diff --git a/programs/src/wordprocessor/manifest.toml b/programs/src/wordprocessor/manifest.toml index f839ec9..6507d52 100644 --- a/programs/src/wordprocessor/manifest.toml +++ b/programs/src/wordprocessor/manifest.toml @@ -4,5 +4,5 @@ binary = "wordprocessor.elf" icon = "wordprocessor.svg" [menu] -category = "Applications" +category = "Productivity" visible = true