feat: add SYS_FUTEX_WAIT and SYS_FUTEX_WAKE syscalls; revamp desktop application categories
This commit is contained in:
1 parent
60ca9c78c7
commit
abfd9de8f6
35 files changed
+545
-70
No files matched your search
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 296
|
||||
#define MONTAUK_BUILD_NUMBER 302
|
||||
@@ -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;
|
||||
|
||||
@@ -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<uint32_t>(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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,5 +23,7 @@ enum StatusCodes {
|
||||
NoSuchPath = -11,
|
||||
Busy = -12,
|
||||
BufferTooSmall = -13,
|
||||
OutOfMemory = -14
|
||||
OutOfMemory = -14,
|
||||
TimedOut = -15,
|
||||
WouldBlock = -16
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,5 +4,5 @@ binary = "audio.elf"
|
||||
icon = "pavucontrol.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
@@ -4,5 +4,5 @@ binary = "bluetooth.elf"
|
||||
icon = "bluetooth.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
@@ -4,7 +4,7 @@ binary = "calculator.elf"
|
||||
icon = "accessories-calculator.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Utilities"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -4,7 +4,7 @@ binary = "charmap.elf"
|
||||
icon = "accessories-character-map.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Utilities"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,5 +4,5 @@ binary = "devexplorer.elf"
|
||||
icon = "hardware.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
@@ -4,5 +4,5 @@ binary = "disks.elf"
|
||||
icon = "gparted.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
@@ -4,7 +4,7 @@ binary = "fontpreview.elf"
|
||||
icon = "font-viewer.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Utilities"
|
||||
visible = false
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* futextest - SYS_FUTEX_WAIT / SYS_FUTEX_WAKE regression tests
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/thread.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -4,5 +4,5 @@ binary = "imageviewer.elf"
|
||||
icon = "image-viewer.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Media"
|
||||
visible = false
|
||||
@@ -4,7 +4,7 @@ binary = "music.elf"
|
||||
icon = "audio-player.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Media"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -4,5 +4,5 @@ binary = "paint.elf"
|
||||
icon = "paint.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Media"
|
||||
visible = true
|
||||
@@ -4,5 +4,5 @@ binary = "pdfviewer.elf"
|
||||
icon = "pdf-viewer.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Productivity"
|
||||
visible = false
|
||||
@@ -4,7 +4,7 @@ binary = "powermgr.elf"
|
||||
icon = "gnome-power-statistics.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -5,7 +5,7 @@ binary = "printers.elf"
|
||||
icon = "preferences-devices-printer.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Hardware"
|
||||
visible = true
|
||||
|
||||
[desktop]
|
||||
|
||||
@@ -4,5 +4,5 @@ binary = "screenshot.elf"
|
||||
icon = "screenshot.svg"
|
||||
|
||||
[menu]
|
||||
category = "System"
|
||||
category = "Utilities"
|
||||
visible = true
|
||||
@@ -4,5 +4,5 @@ binary = "spreadsheet.elf"
|
||||
icon = "spreadsheet.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Productivity"
|
||||
visible = true
|
||||
@@ -4,7 +4,7 @@ binary = "terminal.elf"
|
||||
icon = "utilities-terminal.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "System"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -4,7 +4,7 @@ binary = "texteditor.elf"
|
||||
icon = "texteditor.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Productivity"
|
||||
visible = true
|
||||
|
||||
[launch]
|
||||
|
||||
@@ -4,5 +4,5 @@ binary = "video.elf"
|
||||
icon = "video.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Media"
|
||||
visible = true
|
||||
@@ -4,5 +4,5 @@ binary = "wordprocessor.elf"
|
||||
icon = "wordprocessor.svg"
|
||||
|
||||
[menu]
|
||||
category = "Applications"
|
||||
category = "Productivity"
|
||||
visible = true
|
||||
Reference in new issue
Block a user