From fdbb233cd3d5ede8eecb6f302c1557bb92d67041 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 30 Aug 2026 08:05:31 +0200 Subject: [PATCH] fix: fix login wallpaper startup delay --- kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Heap.hpp | 40 ++++- kernel/src/Api/Syscall.cpp | 2 + kernel/src/Api/Syscall.hpp | 5 + kernel/src/Fs/ProtectedPaths.cpp | 4 + kernel/src/Fs/Ramdisk.cpp | 26 +++- programs/include/Api/Syscall.hpp | 3 + programs/include/libc/montauk.h | 1 + programs/include/montauk/syscall.h | 4 + programs/lib/libc/libc.c | 13 +- programs/src/login/login_wallpaper.cpp | 194 ++++++++++++++++++++++--- programs/src/login/main.cpp | 9 +- 12 files changed, 271 insertions(+), 32 deletions(-) diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index d96709d..873d0e9 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 179 +#define MONTAUK_BUILD_NUMBER 182 diff --git a/kernel/src/Api/Heap.hpp b/kernel/src/Api/Heap.hpp index 7b61e25..a4e149f 100644 --- a/kernel/src/Api/Heap.hpp +++ b/kernel/src/Api/Heap.hpp @@ -45,7 +45,12 @@ namespace montauk::abi { static constexpr uint64_t VmProtWrite = 2; static constexpr uint64_t VmProtExec = 4; - inline uint64_t Sys_MapAnonymous(uint64_t size, uint64_t prot) { + // Sys_MapAnonymous flags. Populate commits the whole range at mapping + // time; without it every page is materialized on first touch. + static constexpr uint64_t VmFlagPopulate = 1; + + inline uint64_t Sys_MapAnonymous(uint64_t size, uint64_t prot, + uint64_t flags = 0) { auto* proc = Sched::GetCurrentProcessPtr(); if (proc == nullptr) return 0; int slot = GetCurrentSlot(); @@ -83,6 +88,33 @@ namespace montauk::abi { g_heapAllocs[slot] = new HeapAlloc { userVa, numPages, prot, allocationId, g_heapAllocs[slot] }; + // Populate is best effort: commit as much of the range as the frame + // allocator will give up front, and leave the remainder to the fault + // path. A caller that is about to touch every page (a decode buffer, + // a heap slab) then pays one loop instead of one trap, one mutex + // acquire and one VMA walk per 4 KiB. + if ((flags & VmFlagPopulate) != 0) { + bool writable = (prot & VmProtWrite) != 0; + bool executable = (prot & VmProtExec) != 0; + // Bounded so one syscall cannot pin an unbounded amount of memory + // with the slot's heap lock held. Anything past the cap faults in. + static constexpr uint64_t MaxPopulatePages = 64 * 1024 * 1024 / 0x1000; + uint64_t populate = numPages < MaxPopulatePages ? numPages + : MaxPopulatePages; + for (uint64_t i = 0; i < populate; i++) { + uint64_t pageVa = userVa + i * 0x1000ULL; + void* page = Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) break; + uint64_t phys = Memory::SubHHDM((uint64_t)page); + if (!Memory::VMM::Paging::MapUserInPermissions( + proc->pml4Phys, phys, pageVa, writable, executable)) { + Memory::g_pfa->Free(page); + break; + } + Sched::g_allocatedPages[slot]++; + } + } + g_heapLocks[slot].Release(); return userVa; } @@ -91,6 +123,12 @@ namespace montauk::abi { return Sys_MapAnonymous(size, VmProtRead | VmProtWrite); } + // As Sys_Alloc, but commits the pages immediately instead of faulting them + // in one at a time. + inline uint64_t Sys_AllocEager(uint64_t size) { + return Sys_MapAnonymous(size, VmProtRead | VmProtWrite, VmFlagPopulate); + } + // Reset heap allocation tracking for a process slot. // The actual physical pages are freed by Paging::FreeUserHalf() during process cleanup. inline void CleanupHeapForSlot(int slot, uint64_t /*pml4Phys*/) { diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index 0d881ed..d935770 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -116,6 +116,8 @@ namespace montauk::abi { (int)frame->arg4); case SYS_ALLOC: return (int64_t)Sys_Alloc(frame->arg1); + case SYS_ALLOC_EAGER: + return (int64_t)Sys_AllocEager(frame->arg1); case SYS_FREE: Sys_Free(frame->arg1); return 0; diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 71c7a9c..f05602c 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -338,6 +338,11 @@ namespace montauk::abi { static constexpr uint64_t SYS_SPAWN_CAPS = 185; static constexpr uint64_t SYS_SPAWN_REDIR_CAPS = 186; + /* Heap.hpp -- as SYS_ALLOC, but commits every page up front instead of + faulting them in one at a time. For buffers the caller is about to + touch in full (image decode, heap slabs). */ + static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure + /* 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/Fs/ProtectedPaths.cpp b/kernel/src/Fs/ProtectedPaths.cpp index 6d8963e..0e9a99a 100644 --- a/kernel/src/Fs/ProtectedPaths.cpp +++ b/kernel/src/Fs/ProtectedPaths.cpp @@ -48,6 +48,10 @@ namespace Fs { {"/config/init.toml", false, montauk::abi::CAP_USER_ADMIN}, {"/config/ssh.toml", false, montauk::abi::CAP_USER_ADMIN}, {"/config/capabilities.toml",false, montauk::abi::CAP_USER_ADMIN}, + // Pre-scaled wallpaper the login screen blits before it has decoded + // anything. It is drawn on a screen that is about to take a password, + // so it must not be plantable by an unprivileged process. + {"/config/wallpaper.cache", false, montauk::abi::CAP_USER_ADMIN}, // Read by the Bluetooth driver at controller bring-up. {"/config/bluetooth.toml", false, montauk::abi::CAP_DEVICE_ADMIN}, // Program images. Capability grants are keyed on binary path, so a diff --git a/kernel/src/Fs/Ramdisk.cpp b/kernel/src/Fs/Ramdisk.cpp index aea7111..328220e 100644 --- a/kernel/src/Fs/Ramdisk.cpp +++ b/kernel/src/Fs/Ramdisk.cpp @@ -355,10 +355,18 @@ namespace Fs::Ramdisk { uint64_t newCap = entry.size; if (endOffset > newCap) newCap = endOffset; if (newCap < 256) newCap = 256; - // Round up to next power of 2 for growth - uint64_t rounded = 256; - while (rounded < newCap) rounded *= 2; - newCap = rounded; + // Small files round to the next power of 2, so an appender grows + // in a few steps. Large ones round to a page instead: the kernel + // heap grows in physically contiguous runs, and doubling an 8 MiB + // write into a 16 MiB block asks the frame allocator for twice the + // contiguous span the file actually needs. + if (newCap < 64 * 1024) { + uint64_t rounded = 256; + while (rounded < newCap) rounded *= 2; + newCap = rounded; + } else { + newCap = (newCap + 0xFFFULL) & ~0xFFFULL; + } uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap); if (newBuf == nullptr) return -1; @@ -374,8 +382,14 @@ namespace Fs::Ramdisk { // Grow buffer if needed if (endOffset > entry.capacity) { - uint64_t newCap = entry.capacity; - while (newCap < endOffset) newCap *= 2; + // Double while small, then grow in fixed 1 MiB steps. Doubling all + // the way keeps growth amortized but overshoots badly on multi-MiB + // files, and every byte of overshoot is a physically contiguous + // kernel-heap run this file holds for the rest of the boot. + static constexpr uint64_t MaxGrowStep = 1024 * 1024; + uint64_t newCap = entry.capacity < 256 ? 256 : entry.capacity; + while (newCap < endOffset) + newCap += (newCap < MaxGrowStep) ? newCap : MaxGrowStep; uint8_t* newBuf = (uint8_t*)Memory::g_heap->Request(newCap); if (newBuf == nullptr) return -1; diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 81fd7b0..94965c4 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -252,6 +252,9 @@ namespace montauk::abi { static constexpr uint64_t SYS_TERMINAL_ATTACHED = 177; // () -> 1 when connected to a userspace terminal static constexpr uint64_t SYS_SPAWN_CAPS = 185; static constexpr uint64_t SYS_SPAWN_REDIR_CAPS = 186; + // As SYS_ALLOC, but commits every page up front instead of faulting them + // in one at a time. For buffers the caller is about to touch in full. + static constexpr uint64_t SYS_ALLOC_EAGER = 187; // (bytes) -> va, 0 on failure /* Kernel-owned process capabilities. User identities may namespace per-user resources, but never participate in authorization decisions. */ diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index f813811..2e33d42 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -210,6 +210,7 @@ extern "C" { #define MTK_SYS_USB_BULK_IN_READ 184 #define MTK_SYS_SPAWN_CAPS 185 #define MTK_SYS_SPAWN_REDIR_CAPS 186 +#define MTK_SYS_ALLOC_EAGER 187 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/include/montauk/syscall.h b/programs/include/montauk/syscall.h index 326670c..4e32286 100644 --- a/programs/include/montauk/syscall.h +++ b/programs/include/montauk/syscall.h @@ -191,6 +191,10 @@ namespace montauk { // Memory inline void* alloc(uint64_t size) { return (void*)syscall1(montauk::abi::SYS_ALLOC, size); } + // As alloc(), but the kernel commits the whole range immediately. Use it + // for a buffer that is about to be written end to end: the lazy path costs + // one page fault, one mutex acquire and one VMA walk per 4 KiB. + inline void* alloc_eager(uint64_t size) { return (void*)syscall1(montauk::abi::SYS_ALLOC_EAGER, size); } inline void free(void* ptr) { syscall1(montauk::abi::SYS_FREE, (uint64_t)ptr); } // Timekeeping diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index ff110da..21440be 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -99,6 +99,7 @@ static inline long _mtk_syscall4(long nr, long a1, long a2, long a3, long a4) { #define SYS_CLOSE 9 #define SYS_READDIR 10 #define SYS_ALLOC 11 +#define SYS_ALLOC_EAGER 187 #define SYS_FREE 12 #define SYS_GETMILLISECONDS 14 #define SYS_GETCHAR 18 @@ -679,6 +680,14 @@ int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } #define HEAP_ALIGN 16ULL #define DIRECT_THRESHOLD (256ULL * 1024ULL) +/* A direct mapping is one object the caller sized itself, so it is nearly + always written end to end (an image buffer, a loaded file). Committing it + up front replaces one page fault, one kernel mutex acquire and one VMA walk + per 4 KiB with a single loop -- thousands of traps for a decoded image. + Beyond the cap, stay lazy: a very large mapping is more likely to be a + sparsely touched reservation, and eager commit would pin the lot. */ +#define EAGER_DIRECT_LIMIT (32ULL * 1024ULL * 1024ULL) + struct HeapHeader { uint64_t magic; uint64_t requested_size; @@ -855,8 +864,10 @@ static void *heap_malloc_locked(size_t size) { if (needed >= DIRECT_THRESHOLD) { if (needed > UINT64_MAX - 0xFFFULL) return NULL; uint64_t mapping_size = (needed + 0xFFFULL) & ~0xFFFULL; + long alloc_nr = (mapping_size <= EAGER_DIRECT_LIMIT) + ? SYS_ALLOC_EAGER : SYS_ALLOC; struct HeapHeader *hdr = (struct HeapHeader *) - _mtk_syscall1(SYS_ALLOC, (long)mapping_size); + _mtk_syscall1(alloc_nr, (long)mapping_size); if (hdr == NULL) return NULL; hdr->magic = DIRECT_MAGIC; hdr->requested_size = size; diff --git a/programs/src/login/login_wallpaper.cpp b/programs/src/login/login_wallpaper.cpp index ddac59a..d1edc2a 100644 --- a/programs/src/login/login_wallpaper.cpp +++ b/programs/src/login/login_wallpaper.cpp @@ -11,25 +11,144 @@ namespace { // Shipped fallback shown when no wallpaper is configured (see NOTICES.txt). constexpr const char* kDefaultWallpaperPath = "0:/os/wallpapers/default.jpg"; +// Screen-sized, already tinted pixels, so a re-login skips the JPEG decode and +// the rescale entirely. Kernel-protected (CAP_USER_ADMIN) because it is drawn +// on a screen that is about to take a password. +constexpr const char* kCachePath = "0:/config/wallpaper.cache"; + +constexpr uint32_t kCacheMagic = 0x4350574DU; // "MWPC" +constexpr uint32_t kCacheVersion = 1; + +// The cache lives on the ramdisk, which is kernel heap: past this size it +// costs more memory for the rest of the boot than the decode it saves. +constexpr uint64_t kMaxCacheBytes = 32ULL * 1024 * 1024; + constexpr uint32_t kLoginOverlayAlpha = 0x38; constexpr uint32_t kLoginOverlayInvAlpha = 255 - kLoginOverlayAlpha; +// Fixed-size record. The pixels follow it in the same allocation, so the size +// must stay a multiple of 16 to keep them aligned. +struct CacheHeader { + uint32_t magic; + uint32_t version; + int32_t width; // screen the pixels were scaled for + int32_t height; + uint64_t sourceSize; // source image, as it was when baked + int64_t sourceMtime; + uint32_t overlayAlpha; // tint baked into the pixels + uint32_t reserved; + char sourcePath[256]; + uint64_t padding; +}; +static_assert(sizeof(CacheHeader) % 16 == 0, + "cache header must stay 16-byte aligned"); + static uint8_t dim_component(uint8_t value) { uint32_t scaled = kLoginOverlayInvAlpha * value; return (uint8_t)((scaled + 1 + (scaled >> 8)) >> 8); } -} // namespace - -bool load_login_wallpaper(LoginState* ls) { +// Pick the configured wallpaper, falling back to the shipped one, and return +// the path that actually opens along with its stat record. +bool resolve_source(char* outPath, int cap, montauk::abi::FileStat& outStat) { auto doc = montauk::config::load("desktop"); char wp[256]; montauk::strncpy(wp, doc.get_string("wallpaper.path", ""), sizeof(wp)); doc.destroy(); - int fd = -1; - if (wp[0] != '\0') fd = montauk::open(wp); - if (fd < 0) fd = montauk::open(kDefaultWallpaperPath); + const char* candidates[2] = { wp, kDefaultWallpaperPath }; + for (int i = 0; i < 2; i++) { + if (candidates[i][0] == '\0') continue; + int fd = montauk::open(candidates[i]); + if (fd < 0) continue; + montauk::close(fd); + montauk::strncpy(outPath, candidates[i], cap); + montauk::memset(&outStat, 0, sizeof(outStat)); + montauk::stat(outPath, &outStat); // best effort: 0/0 still validates + return true; + } + return false; +} + +// Allocate one block holding the cache header followed by the screen-sized +// pixel buffer, so saving the cache is a single write with no extra copy. +uint8_t* allocate_blob(int w, int h) { + uint64_t bytes = sizeof(CacheHeader) + (uint64_t)w * h * 4; + return (uint8_t*)montauk::malloc(bytes); +} + +bool header_matches(const CacheHeader& h, const LoginState* ls, + const char* srcPath, const montauk::abi::FileStat& st) { + return h.magic == kCacheMagic + && h.version == kCacheVersion + && h.width == ls->screen_w + && h.height == ls->screen_h + && h.sourceSize == st.size + && h.sourceMtime == st.mtime + && h.overlayAlpha == kLoginOverlayAlpha + && montauk::streq(h.sourcePath, srcPath); +} + +bool load_from_cache(LoginState* ls, const char* srcPath, + const montauk::abi::FileStat& st, uint8_t*& blob) { + int fd = montauk::open(kCachePath); + if (fd < 0) return false; + + uint64_t pixelBytes = (uint64_t)ls->screen_w * ls->screen_h * 4; + bool ok = montauk::getsize(fd) == sizeof(CacheHeader) + pixelBytes; + + CacheHeader header; + if (ok) ok = montauk::read(fd, (uint8_t*)&header, 0, sizeof(header)) + == (int)sizeof(header); + if (ok) ok = header_matches(header, ls, srcPath, st); + if (!ok) { + montauk::close(fd); + return false; + } + + if (blob == nullptr) blob = allocate_blob(ls->screen_w, ls->screen_h); + if (blob == nullptr) { + montauk::close(fd); + return false; + } + + // Read in one call: the buffer is already committed, so this is a single + // kernel-side memcpy out of the ramdisk. + int got = montauk::read(fd, blob + sizeof(CacheHeader), sizeof(CacheHeader), + pixelBytes); + montauk::close(fd); + return got == (int)pixelBytes; +} + +void save_to_cache(const LoginState* ls, const char* srcPath, + const montauk::abi::FileStat& st, uint8_t* blob) { + uint64_t pixelBytes = (uint64_t)ls->screen_w * ls->screen_h * 4; + uint64_t total = sizeof(CacheHeader) + pixelBytes; + if (total > kMaxCacheBytes) return; + + CacheHeader* header = (CacheHeader*)blob; + montauk::memset(header, 0, sizeof(*header)); + header->magic = kCacheMagic; + header->version = kCacheVersion; + header->width = ls->screen_w; + header->height = ls->screen_h; + header->sourceSize = st.size; + header->sourceMtime = st.mtime; + header->overlayAlpha = kLoginOverlayAlpha; + montauk::strncpy(header->sourcePath, srcPath, sizeof(header->sourcePath)); + + int fd = montauk::fcreate(kCachePath); + if (fd < 0) return; // read-only volume or no authority: not fatal + // One write, so the file is never briefly visible half-written and the + // ramdisk sizes its backing buffer once. + montauk::fwrite(fd, blob, 0, total); + montauk::close(fd); +} + +// Decode the source image and scale it to the screen, writing tinted pixels +// into the blob's pixel area. +bool decode_and_scale(LoginState* ls, const char* srcPath, uint8_t*& blob) { + int fd = montauk::open(srcPath); if (fd < 0) return false; uint64_t size = montauk::getsize(fd); @@ -59,11 +178,12 @@ bool load_login_wallpaper(LoginState* ls) { int dst_w = ls->screen_w; int dst_h = ls->screen_h; - uint32_t* scaled = (uint32_t*)montauk::malloc((uint64_t)dst_w * dst_h * 4); - if (!scaled) { + if (blob == nullptr) blob = allocate_blob(dst_w, dst_h); + if (!blob) { stbi_image_free(rgb); return false; } + uint32_t* scaled = (uint32_t*)(blob + sizeof(CacheHeader)); int src_crop_w, src_crop_h, src_x0, src_y0; if ((int64_t)img_w * dst_h > (int64_t)img_h * dst_w) { @@ -78,29 +198,59 @@ bool load_login_wallpaper(LoginState* ls) { src_y0 = (img_h - src_crop_h) / 2; } + // Source column per destination column, computed once. Inline, the same + // expression costs one 64-bit divide per pixel -- millions of them, and + // idiv neither pipelines nor vectorizes. + int* col = (int*)montauk::malloc((uint64_t)dst_w * sizeof(int)); + if (!col) { + stbi_image_free(rgb); + return false; + } + for (int x = 0; x < dst_w; x++) { + int sx = src_x0 + (int)((int64_t)x * src_crop_w / dst_w); + if (sx < 0) sx = 0; + if (sx >= img_w) sx = img_w - 1; + col[x] = sx * 3; + } + 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; if (sy >= img_h) sy = img_h - 1; + const unsigned char* row = rgb + (int64_t)sy * img_w * 3; + uint32_t* dst = scaled + (int64_t)y * dst_w; for (int x = 0; x < dst_w; x++) { - int sx = src_x0 + (int)((int64_t)x * src_crop_w / dst_w); - if (sx < 0) sx = 0; - if (sx >= img_w) sx = img_w - 1; - int si = (sy * img_w + sx) * 3; - uint8_t r = dim_component(rgb[si]); - uint8_t g = dim_component(rgb[si + 1]); - uint8_t b = dim_component(rgb[si + 2]); - scaled[y * dst_w + x] = 0xFF000000u - | ((uint32_t)r << 16) - | ((uint32_t)g << 8) - | (uint32_t)b; + const unsigned char* src = row + col[x]; + dst[x] = 0xFF000000u + | ((uint32_t)dim_component(src[0]) << 16) + | ((uint32_t)dim_component(src[1]) << 8) + | (uint32_t)dim_component(src[2]); } } + montauk::mfree(col); stbi_image_free(rgb); - ls->bg_wallpaper = scaled; - ls->bg_wallpaper_w = dst_w; - ls->bg_wallpaper_h = dst_h; + return true; +} + +} // namespace + +bool load_login_wallpaper(LoginState* ls) { + char srcPath[256]; + montauk::abi::FileStat st; + if (!resolve_source(srcPath, sizeof(srcPath), st)) return false; + + uint8_t* blob = nullptr; + bool cached = load_from_cache(ls, srcPath, st, blob); + if (!cached && !decode_and_scale(ls, srcPath, blob)) { + if (blob) montauk::mfree(blob); + return false; + } + if (!cached) save_to_cache(ls, srcPath, st, blob); + + ls->bg_wallpaper = (uint32_t*)(blob + sizeof(CacheHeader)); + ls->bg_wallpaper_w = ls->screen_w; + ls->bg_wallpaper_h = ls->screen_h; ls->has_wallpaper = true; return true; } diff --git a/programs/src/login/main.cpp b/programs/src/login/main.cpp index bb9ed9f..cea7301 100644 --- a/programs/src/login/main.cpp +++ b/programs/src/login/main.cpp @@ -69,7 +69,6 @@ extern "C" void _start() { gui::fonts::init(); montauk::set_mouse_bounds(ls->screen_w - 1, ls->screen_h - 1); - load_login_wallpaper(ls); // MTK theme (picks up the system accent). The compose buffer is only // needed when the framebuffer pitch is not tightly packed; otherwise the @@ -83,6 +82,14 @@ extern "C" void _start() { maybe_run_setup_session(); initialize_login_mode(ls); + // Put the login card on screen before touching the wallpaper. Decoding a + // multi-megapixel JPEG takes long enough to read as a hang if nothing has + // been painted yet; drawn first, it lands as a background appearing behind + // a screen the user can already type into. The loop below redraws with the + // wallpaper because first_frame is still set. + draw_login_screen(ls); + load_login_wallpaper(ls); + bool first_frame = true; uint64_t input_serial = montauk::input_wait(0, 0); for (;;) {