feat: add threading to Scheduler, fix desktop background selection freeze issue
This commit is contained in:
@@ -167,6 +167,12 @@ namespace Montauk {
|
||||
// Block until the mixer state serial differs from the caller's snapshot.
|
||||
static constexpr uint64_t SYS_AUDIOWAIT = 129;
|
||||
|
||||
// Threading
|
||||
static constexpr uint64_t SYS_THREAD_SPAWN = 130;
|
||||
static constexpr uint64_t SYS_THREAD_EXIT = 131;
|
||||
static constexpr uint64_t SYS_THREAD_JOIN = 132;
|
||||
static constexpr uint64_t SYS_THREAD_SELF = 133;
|
||||
|
||||
static constexpr uint32_t CLIPBOARD_MAX_TEXT_BYTES = 256 * 1024;
|
||||
|
||||
static constexpr uint32_t IPC_SIGNAL_READABLE = 1u << 0;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* thread.h
|
||||
* Userspace threading primitives for MontaukOS
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*
|
||||
* Threads share their parent's address space, IPC handles, cwd, and
|
||||
* heap allocator. The caller owns the thread stack: thread_spawn
|
||||
* allocates one out of the user heap and the kernel uses it verbatim.
|
||||
*
|
||||
* Lifetimes:
|
||||
* - thread_spawn returns a positive TID on success.
|
||||
* - thread_join blocks until the target TID exits, frees its kernel
|
||||
* stack, then returns its exit code via *out_code.
|
||||
* - thread_exit terminates only the current thread. If the main thread
|
||||
* calls exit() (SYS_EXIT) the whole process tears down, killing any
|
||||
* surviving sibling threads.
|
||||
* - thread_self returns the calling thread's TID.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/heap.h>
|
||||
|
||||
namespace montauk {
|
||||
|
||||
// Default per-thread stack size (64 KiB). Enough for typical app work;
|
||||
// matches the main thread's 32 KiB lower bound with headroom for
|
||||
// TrueType rendering call chains.
|
||||
static constexpr uint64_t DEFAULT_THREAD_STACK_BYTES = 64 * 1024;
|
||||
|
||||
using ThreadEntry = int (*)(void* arg);
|
||||
|
||||
// Terminate the calling thread. Never returns. If called by the main
|
||||
// thread this is equivalent to exit() (the whole process exits).
|
||||
[[noreturn]] inline void thread_exit(int code = 0) {
|
||||
syscall1(Montauk::SYS_THREAD_EXIT, (uint64_t)code);
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
struct ThreadCtx {
|
||||
ThreadEntry user_entry;
|
||||
void* user_arg;
|
||||
void* stack_base;
|
||||
};
|
||||
|
||||
// Userspace trampoline: bridges from the raw entry the kernel jumps
|
||||
// to into the typed entry, then funnels into SYS_THREAD_EXIT. We
|
||||
// route the exit through libc rather than relying on a kernel-side
|
||||
// exit stub on the user stack -- the kernel never writes to user
|
||||
// memory on this path.
|
||||
//
|
||||
// The thread's stack itself is intentionally not freed here: we are
|
||||
// still running on it. It is reclaimed when the process exits, or
|
||||
// the joiner may free it explicitly after thread_join.
|
||||
[[noreturn]] inline void thread_trampoline(detail::ThreadCtx* ctx) {
|
||||
int code = ctx->user_entry(ctx->user_arg);
|
||||
montauk::mfree(ctx);
|
||||
thread_exit(code);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn a new thread that begins executing `entry(arg)`. Returns the
|
||||
// new TID on success, or -1 on failure. The thread's stack is
|
||||
// allocated from the user heap; it is leaked on thread exit (the
|
||||
// thread itself cannot free the stack it is running on). The kernel
|
||||
// reclaims it on process exit. Callers that need to spawn many short-
|
||||
// lived threads should pool stacks themselves.
|
||||
inline int thread_spawn(ThreadEntry entry, void* arg,
|
||||
uint64_t stack_bytes = 0) {
|
||||
if (entry == nullptr) return -1;
|
||||
if (stack_bytes == 0) stack_bytes = DEFAULT_THREAD_STACK_BYTES;
|
||||
stack_bytes = (stack_bytes + 15) & ~15ULL;
|
||||
|
||||
void* stack = montauk::malloc(stack_bytes);
|
||||
if (stack == nullptr) return -1;
|
||||
auto* ctx = (detail::ThreadCtx*)montauk::malloc(sizeof(detail::ThreadCtx));
|
||||
if (ctx == nullptr) {
|
||||
montauk::mfree(stack);
|
||||
return -1;
|
||||
}
|
||||
ctx->user_entry = entry;
|
||||
ctx->user_arg = arg;
|
||||
ctx->stack_base = stack;
|
||||
|
||||
uint64_t stack_top = ((uint64_t)stack + stack_bytes) & ~0xFULL;
|
||||
int tid = (int)syscall3(Montauk::SYS_THREAD_SPAWN,
|
||||
(uint64_t)&detail::thread_trampoline,
|
||||
(uint64_t)ctx, stack_top);
|
||||
if (tid < 0) {
|
||||
montauk::mfree(ctx);
|
||||
montauk::mfree(stack);
|
||||
return -1;
|
||||
}
|
||||
return tid;
|
||||
}
|
||||
|
||||
// Block until the thread identified by `tid` terminates. Returns 0 on
|
||||
// success (with the thread's exit code in *out_code if non-null) or
|
||||
// -1 if `tid` is not a joinable sibling.
|
||||
inline int thread_join(int tid, int* out_code = nullptr) {
|
||||
return (int)syscall2(Montauk::SYS_THREAD_JOIN,
|
||||
(uint64_t)tid, (uint64_t)out_code);
|
||||
}
|
||||
|
||||
// Return the calling thread's TID (== getpid() for the main thread).
|
||||
inline int thread_self() {
|
||||
return (int)syscall0(Montauk::SYS_THREAD_SELF);
|
||||
}
|
||||
|
||||
// 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.
|
||||
struct Mutex {
|
||||
volatile uint32_t locked = 0;
|
||||
|
||||
void lock() {
|
||||
while (__atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) != 0) {
|
||||
montauk::yield();
|
||||
}
|
||||
}
|
||||
|
||||
bool try_lock() {
|
||||
return __atomic_exchange_n(&locked, 1, __ATOMIC_ACQUIRE) == 0;
|
||||
}
|
||||
|
||||
void unlock() {
|
||||
__atomic_store_n(&locked, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -163,8 +163,16 @@ bool settings_handle_appearance_click(Window* win, SettingsState* st, int mx, in
|
||||
montauk::strcpy(fullpath, st->desktop->home_dir);
|
||||
str_append(fullpath, "/", 256);
|
||||
str_append(fullpath, st->wp_files.names[i], 256);
|
||||
wallpaper_load(&s, fullpath,
|
||||
st->desktop->screen_w, st->desktop->screen_h);
|
||||
// Decode and scale off the UI thread so rapid cycling
|
||||
// between high-res images stays responsive. The radio state
|
||||
// and persisted path update immediately; the pixel buffer
|
||||
// arrives asynchronously and is swapped in by
|
||||
// wallpaper_poll() on the next frame.
|
||||
s.bg_image = true;
|
||||
s.bg_gradient = false;
|
||||
montauk::strncpy(s.bg_image_path, fullpath, 127);
|
||||
wallpaper_load_async(fullpath,
|
||||
st->desktop->screen_w, st->desktop->screen_h);
|
||||
desktop_mark_background_dirty(st->desktop);
|
||||
settings_persist(st);
|
||||
return true;
|
||||
|
||||
@@ -684,6 +684,12 @@ void gui::desktop_run(DesktopState* ds) {
|
||||
uint64_t now = montauk::get_milliseconds();
|
||||
sceneChanged |= desktop_refresh_panel_state(ds, now);
|
||||
|
||||
// Pick up any wallpaper buffer produced by a background loader.
|
||||
if (wallpaper_poll(&ds->settings)) {
|
||||
desktop_mark_background_dirty(ds);
|
||||
sceneChanged = true;
|
||||
}
|
||||
|
||||
uint64_t launcherBlinkToken = desktop_launcher_blink_token(ds, now);
|
||||
if (launcherBlinkToken != lastLauncherBlinkToken) {
|
||||
lastLauncherBlinkToken = launcherBlinkToken;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/string.h>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/thread.h>
|
||||
#include <gui/gui.hpp>
|
||||
#include <gui/desktop.hpp>
|
||||
|
||||
@@ -29,66 +30,52 @@ namespace gui {
|
||||
// Wallpaper loading
|
||||
// ============================================================================
|
||||
|
||||
// Load a JPEG file and scale it to cover the given screen dimensions.
|
||||
// Stores the scaled ARGB pixel buffer in settings. Returns true on success.
|
||||
inline bool wallpaper_load(DesktopSettings* s, const char* path,
|
||||
int screen_w, int screen_h) {
|
||||
// Free existing wallpaper
|
||||
if (s->bg_wallpaper) {
|
||||
montauk::mfree(s->bg_wallpaper);
|
||||
s->bg_wallpaper = nullptr;
|
||||
s->bg_wallpaper_w = 0;
|
||||
s->bg_wallpaper_h = 0;
|
||||
}
|
||||
|
||||
// Read file
|
||||
// Decode `path` and scale it to cover (screen_w x screen_h). Returns a newly
|
||||
// montauk::malloc'd ARGB buffer the caller owns, or nullptr on failure. This
|
||||
// is the heavy lifting; it touches no shared state and is safe to run from a
|
||||
// worker thread.
|
||||
inline uint32_t* wallpaper_decode_scaled(const char* path,
|
||||
int screen_w, int screen_h) {
|
||||
int fd = montauk::open(path);
|
||||
if (fd < 0) return false;
|
||||
if (fd < 0) return nullptr;
|
||||
|
||||
uint64_t size = montauk::getsize(fd);
|
||||
if (size == 0 || size > 16 * 1024 * 1024) {
|
||||
montauk::close(fd);
|
||||
return false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint8_t* filedata = (uint8_t*)montauk::malloc(size);
|
||||
if (!filedata) { montauk::close(fd); return false; }
|
||||
if (!filedata) { montauk::close(fd); return nullptr; }
|
||||
|
||||
int bytes_read = montauk::read(fd, filedata, 0, size);
|
||||
montauk::close(fd);
|
||||
if (bytes_read <= 0) { montauk::mfree(filedata); return false; }
|
||||
if (bytes_read <= 0) { montauk::mfree(filedata); return nullptr; }
|
||||
|
||||
// Decode JPEG
|
||||
int img_w, img_h, channels;
|
||||
unsigned char* rgb = stbi_load_from_memory(filedata, bytes_read,
|
||||
&img_w, &img_h, &channels, 3);
|
||||
montauk::mfree(filedata);
|
||||
if (!rgb) return false;
|
||||
if (!rgb) return nullptr;
|
||||
|
||||
// Scale to cover screen (crop to fill, maintain aspect ratio)
|
||||
int dst_w = screen_w;
|
||||
int dst_h = screen_h;
|
||||
|
||||
uint32_t* scaled = (uint32_t*)montauk::malloc((uint64_t)dst_w * dst_h * 4);
|
||||
if (!scaled) { stbi_image_free(rgb); return false; }
|
||||
if (!scaled) { stbi_image_free(rgb); return nullptr; }
|
||||
|
||||
// Compute source crop region for "cover" scaling
|
||||
int src_crop_w, src_crop_h, src_x0, src_y0;
|
||||
if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) {
|
||||
// Image is wider — crop sides
|
||||
src_crop_h = img_h;
|
||||
src_crop_w = (int)((int64_t)img_h * dst_w / dst_h);
|
||||
src_x0 = (img_w - src_crop_w) / 2;
|
||||
src_y0 = 0;
|
||||
} else {
|
||||
// Image is taller — crop top/bottom
|
||||
src_crop_w = img_w;
|
||||
src_crop_h = (int)((int64_t)img_w * dst_h / dst_w);
|
||||
src_x0 = 0;
|
||||
src_y0 = (img_h - src_crop_h) / 2;
|
||||
}
|
||||
|
||||
// Nearest-neighbor scale from cropped region to destination
|
||||
for (int y = 0; y < dst_h; y++) {
|
||||
int sy = src_y0 + (int)((int64_t)y * src_crop_h / dst_h);
|
||||
if (sy < 0) sy = 0;
|
||||
@@ -106,14 +93,137 @@ inline bool wallpaper_load(DesktopSettings* s, const char* path,
|
||||
}
|
||||
|
||||
stbi_image_free(rgb);
|
||||
return scaled;
|
||||
}
|
||||
|
||||
// Synchronous loader retained for code paths that genuinely want to block
|
||||
// (e.g. desktop boot, where there is no UI to keep responsive yet).
|
||||
inline bool wallpaper_load(DesktopSettings* s, const char* path,
|
||||
int screen_w, int screen_h) {
|
||||
uint32_t* scaled = wallpaper_decode_scaled(path, screen_w, screen_h);
|
||||
if (!scaled) return false;
|
||||
|
||||
if (s->bg_wallpaper) montauk::mfree(s->bg_wallpaper);
|
||||
s->bg_wallpaper = scaled;
|
||||
s->bg_wallpaper_w = dst_w;
|
||||
s->bg_wallpaper_h = dst_h;
|
||||
s->bg_wallpaper_w = screen_w;
|
||||
s->bg_wallpaper_h = screen_h;
|
||||
montauk::strncpy(s->bg_image_path, path, 127);
|
||||
s->bg_image = true;
|
||||
s->bg_gradient = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Async wallpaper loader
|
||||
//
|
||||
// Decoding a screen-sized JPEG blocks the desktop event loop. The async
|
||||
// loader spawns a worker thread per click; rapid cycling is tolerated via a
|
||||
// generation counter -- only the highest-gen worker's output is published.
|
||||
// The main thread calls wallpaper_poll() each frame to swap in any new
|
||||
// buffer (it owns the actual handoff and the free of the previous one).
|
||||
// ============================================================================
|
||||
|
||||
struct WallpaperPublishSlot {
|
||||
montauk::Mutex lock;
|
||||
uint64_t latest_gen; // bumped on every load request
|
||||
uint64_t pending_gen; // gen of pending_pixels (0 = empty)
|
||||
uint64_t applied_gen; // highest gen consumed by the main thread
|
||||
uint32_t* pending_pixels; // worker-produced ARGB buffer awaiting handoff
|
||||
int pending_w, pending_h;
|
||||
char pending_path[128];
|
||||
};
|
||||
|
||||
inline WallpaperPublishSlot& wallpaper_slot() {
|
||||
static WallpaperPublishSlot s{};
|
||||
return s;
|
||||
}
|
||||
|
||||
struct WallpaperJob {
|
||||
uint64_t gen;
|
||||
int screen_w, screen_h;
|
||||
char path[256];
|
||||
};
|
||||
|
||||
inline int wallpaper_worker(void* raw) {
|
||||
auto* job = (WallpaperJob*)raw;
|
||||
uint32_t* scaled = wallpaper_decode_scaled(job->path, job->screen_w, job->screen_h);
|
||||
|
||||
auto& slot = wallpaper_slot();
|
||||
slot.lock.lock();
|
||||
bool publish = scaled != nullptr
|
||||
&& job->gen > slot.applied_gen
|
||||
&& job->gen > slot.pending_gen;
|
||||
if (publish) {
|
||||
if (slot.pending_pixels) {
|
||||
// A stale (lower-gen) pending buffer was never consumed -- drop it.
|
||||
montauk::mfree(slot.pending_pixels);
|
||||
}
|
||||
slot.pending_pixels = scaled;
|
||||
slot.pending_w = job->screen_w;
|
||||
slot.pending_h = job->screen_h;
|
||||
montauk::strncpy(slot.pending_path, job->path, 127);
|
||||
slot.pending_gen = job->gen;
|
||||
}
|
||||
slot.lock.unlock();
|
||||
|
||||
if (!publish && scaled) montauk::mfree(scaled);
|
||||
montauk::mfree(job);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Kick off a background decode. Returns true if the worker thread was
|
||||
// spawned; the actual wallpaper swap happens later when wallpaper_poll
|
||||
// observes the published buffer.
|
||||
inline bool wallpaper_load_async(const char* path,
|
||||
int screen_w, int screen_h) {
|
||||
auto* job = (WallpaperJob*)montauk::malloc(sizeof(WallpaperJob));
|
||||
if (!job) return false;
|
||||
montauk::strncpy(job->path, path, 255);
|
||||
job->screen_w = screen_w;
|
||||
job->screen_h = screen_h;
|
||||
|
||||
auto& slot = wallpaper_slot();
|
||||
slot.lock.lock();
|
||||
job->gen = ++slot.latest_gen;
|
||||
slot.lock.unlock();
|
||||
|
||||
int tid = montauk::thread_spawn(&wallpaper_worker, job);
|
||||
if (tid < 0) {
|
||||
montauk::mfree(job);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If a worker has produced a newer buffer than the main thread has applied,
|
||||
// swap it into `s` and free the previous wallpaper. Returns true if a new
|
||||
// wallpaper was installed (caller should mark the background dirty).
|
||||
inline bool wallpaper_poll(DesktopSettings* s) {
|
||||
auto& slot = wallpaper_slot();
|
||||
slot.lock.lock();
|
||||
if (slot.pending_gen == 0 || slot.pending_gen <= slot.applied_gen) {
|
||||
slot.lock.unlock();
|
||||
return false;
|
||||
}
|
||||
uint32_t* new_pixels = slot.pending_pixels;
|
||||
int new_w = slot.pending_w;
|
||||
int new_h = slot.pending_h;
|
||||
char new_path[128];
|
||||
montauk::strncpy(new_path, slot.pending_path, 127);
|
||||
new_path[127] = '\0';
|
||||
slot.applied_gen = slot.pending_gen;
|
||||
slot.pending_pixels = nullptr;
|
||||
slot.pending_gen = 0;
|
||||
slot.lock.unlock();
|
||||
|
||||
uint32_t* old = s->bg_wallpaper;
|
||||
s->bg_wallpaper = new_pixels;
|
||||
s->bg_wallpaper_w = new_w;
|
||||
s->bg_wallpaper_h = new_h;
|
||||
montauk::strncpy(s->bg_image_path, new_path, 127);
|
||||
s->bg_image = true;
|
||||
s->bg_gradient = false;
|
||||
if (old) montauk::mfree(old);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* main.cpp
|
||||
* Threading smoke test for MontaukOS.
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*
|
||||
* Spawns two worker threads that increment a shared counter under a
|
||||
* userspace mutex, joins them, and reports the result.
|
||||
*/
|
||||
|
||||
#include <montauk/syscall.h>
|
||||
#include <montauk/thread.h>
|
||||
|
||||
using montauk::Mutex;
|
||||
|
||||
static constexpr int ITERS_PER_THREAD = 100000;
|
||||
|
||||
struct WorkerState {
|
||||
Mutex* lock;
|
||||
int* counter;
|
||||
int delta;
|
||||
};
|
||||
|
||||
static int worker(void* raw) {
|
||||
auto* s = (WorkerState*)raw;
|
||||
for (int i = 0; i < ITERS_PER_THREAD; i++) {
|
||||
s->lock->lock();
|
||||
*(s->counter) += s->delta;
|
||||
s->lock->unlock();
|
||||
}
|
||||
return s->delta;
|
||||
}
|
||||
|
||||
static void print_int(int v) {
|
||||
char buf[16];
|
||||
int n = 0;
|
||||
if (v == 0) {
|
||||
buf[n++] = '0';
|
||||
} else {
|
||||
bool neg = v < 0;
|
||||
unsigned int u = neg ? (unsigned int)(-(long long)v) : (unsigned int)v;
|
||||
char tmp[16]; int t = 0;
|
||||
while (u) { tmp[t++] = (char)('0' + (u % 10)); u /= 10; }
|
||||
if (neg) buf[n++] = '-';
|
||||
while (t) buf[n++] = tmp[--t];
|
||||
}
|
||||
buf[n] = '\0';
|
||||
montauk::print(buf);
|
||||
}
|
||||
|
||||
extern "C" void _start() {
|
||||
Mutex lock;
|
||||
int counter = 0;
|
||||
|
||||
WorkerState a{&lock, &counter, +1};
|
||||
WorkerState b{&lock, &counter, +2};
|
||||
|
||||
int tidA = montauk::thread_spawn(worker, &a);
|
||||
int tidB = montauk::thread_spawn(worker, &b);
|
||||
if (tidA < 0 || tidB < 0) {
|
||||
montauk::print("threadtest: spawn failed\n");
|
||||
montauk::exit(1);
|
||||
}
|
||||
|
||||
montauk::print("threadtest: spawned tids ");
|
||||
print_int(tidA);
|
||||
montauk::print(" and ");
|
||||
print_int(tidB);
|
||||
montauk::print("\n");
|
||||
|
||||
int codeA = -1, codeB = -1;
|
||||
if (montauk::thread_join(tidA, &codeA) != 0) {
|
||||
montauk::print("threadtest: join A failed\n");
|
||||
montauk::exit(2);
|
||||
}
|
||||
if (montauk::thread_join(tidB, &codeB) != 0) {
|
||||
montauk::print("threadtest: join B failed\n");
|
||||
montauk::exit(3);
|
||||
}
|
||||
|
||||
int expected = ITERS_PER_THREAD * (1 + 2);
|
||||
montauk::print("threadtest: counter=");
|
||||
print_int(counter);
|
||||
montauk::print(" expected=");
|
||||
print_int(expected);
|
||||
montauk::print(" codes=");
|
||||
print_int(codeA);
|
||||
montauk::print("/");
|
||||
print_int(codeB);
|
||||
montauk::print("\n");
|
||||
|
||||
if (counter != expected) {
|
||||
montauk::print("threadtest: FAIL (counter mismatch)\n");
|
||||
montauk::exit(4);
|
||||
}
|
||||
if (codeA != 1 || codeB != 2) {
|
||||
montauk::print("threadtest: FAIL (exit codes)\n");
|
||||
montauk::exit(5);
|
||||
}
|
||||
montauk::print("threadtest: PASS\n");
|
||||
montauk::exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user