From 63d9270613b8143e78f49f7644104397ba3c3998 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Mon, 23 Mar 2026 20:09:11 +0100 Subject: [PATCH] feat: Symmetric Multiprocessing, text editor improvements, merge doom libc, implement math functions --- GNUmakefile | 2 +- kernel/src/ACPI/AcpiSleep.cpp | 9 + kernel/src/Api/Process.hpp | 6 +- kernel/src/Api/Syscall.hpp | 2 +- kernel/src/Api/WinServer.cpp | 121 +- kernel/src/Api/WinServer.hpp | 5 +- kernel/src/CppLib/Spinlock.hpp | 16 + kernel/src/Drivers/PS2/Mouse.cpp | 13 + kernel/src/Drivers/PS2/Mouse.hpp | 5 + kernel/src/Fs/Vfs.cpp | 96 +- kernel/src/Hal/Apic/Apic.cpp | 21 + kernel/src/Hal/Apic/Apic.hpp | 5 + kernel/src/Hal/Apic/ApicInit.cpp | 6 +- kernel/src/Hal/Apic/Interrupts.cpp | 12 +- kernel/src/Hal/Apic/IsrStubs.asm | 30 +- kernel/src/Hal/IDT.cpp | 11 +- kernel/src/Hal/SmpBoot.cpp | 279 ++++ kernel/src/Hal/SmpBoot.hpp | 64 + kernel/src/Hal/SyscallEntry.asm | 39 +- kernel/src/Main.cpp | 15 + kernel/src/Memory/Paging.cpp | 39 +- kernel/src/Memory/Paging.hpp | 7 + kernel/src/Platform/Limine.hpp | 10 + kernel/src/Sched/Scheduler.cpp | 447 +++++-- kernel/src/Sched/Scheduler.hpp | 19 +- kernel/src/Terminal/Terminal.cpp | 13 + kernel/src/Terminal/Terminal.hpp | 10 +- kernel/src/Timekeeping/ApicTimer.cpp | 33 +- kernel/src/Timekeeping/ApicTimer.hpp | 3 + programs/GNUmakefile | 16 +- programs/include/libc/math.h | 11 + programs/include/libc/unistd.h | 2 + programs/lib/libc/Makefile | 6 +- programs/lib/libc/libc.c | 292 +++- programs/lib/libc/obj/libc.o | Bin 186120 -> 228768 bytes programs/lib/libjpeg/libjpeg.a | Bin 330158 -> 330158 bytes programs/lib/libjpegwrite/libjpegwrite.a | Bin 134080 -> 134080 bytes programs/lib/tls/libtls.a | Bin 107778 -> 107778 bytes programs/src/desktop/apps/app_filemanager.cpp | 4 +- programs/src/desktop/apps/app_texteditor.cpp | 1031 +-------------- programs/src/doom/Makefile | 5 +- programs/src/doom/libc.c | 1124 ---------------- programs/src/texteditor/Makefile | 86 ++ programs/src/texteditor/main.cpp | 1171 +++++++++++++++++ programs/src/texteditor/stb_truetype_impl.cpp | 35 + programs/src/texteditor/syntax_highlight.hpp | 287 ++++ 46 files changed, 3004 insertions(+), 2404 deletions(-) create mode 100644 kernel/src/Hal/SmpBoot.cpp create mode 100644 kernel/src/Hal/SmpBoot.hpp delete mode 100644 programs/src/doom/libc.c create mode 100644 programs/src/texteditor/Makefile create mode 100644 programs/src/texteditor/main.cpp create mode 100644 programs/src/texteditor/stb_truetype_impl.cpp create mode 100644 programs/src/texteditor/syntax_highlight.hpp diff --git a/GNUmakefile b/GNUmakefile index c815479..bb82296 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -6,7 +6,7 @@ MAKEFLAGS += -rR ARCH := x86_64 # Default user QEMU flags. These are appended to the QEMU command calls. -QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log +QEMUFLAGS := -smp 4 -m 2G -d int -D qemu.log -no-reboot override IMAGE_NAME := montauk-$(ARCH) diff --git a/kernel/src/ACPI/AcpiSleep.cpp b/kernel/src/ACPI/AcpiSleep.cpp index bc1ec53..4aa666e 100644 --- a/kernel/src/ACPI/AcpiSleep.cpp +++ b/kernel/src/ACPI/AcpiSleep.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include using namespace Kt; @@ -424,6 +425,14 @@ namespace Hal { Hal::LoadTSS(); reinitProgress(0x03); // Syscalls Montauk::InitializeSyscalls(); + // Re-set GS base for BSP (lost during S3) + { + auto* bsp = Smp::GetCpuData(0); + if (bsp) { + Hal::WriteMSR(0xC0000101, (uint64_t)bsp); // IA32_GS_BASE + Hal::WriteMSR(0xC0000102, 0); // IA32_KERNEL_GS_BASE + } + } reinitProgress(0x04); // PAT Hal::InitializePAT(); reinitProgress(0x05); // PIC diff --git a/kernel/src/Api/Process.hpp b/kernel/src/Api/Process.hpp index 023c0ae..c27a898 100644 --- a/kernel/src/Api/Process.hpp +++ b/kernel/src/Api/Process.hpp @@ -26,7 +26,7 @@ namespace Montauk { } static void Sys_SleepMs(uint64_t ms) { - Timekeeping::Sleep(ms); + Sched::BlockForSleep(ms); } static int Sys_GetPid() { @@ -34,9 +34,7 @@ namespace Montauk { } static void Sys_WaitPid(int pid) { - while (Sched::IsAlive(pid)) { - Sched::Schedule(); // yield until the process exits - } + Sched::BlockOnPid(pid); } static int Sys_Spawn(const char* path, const char* args) { diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 4092d19..f5cfad2 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -333,7 +333,7 @@ namespace Montauk { struct ProcInfo { int32_t pid; int32_t parentPid; - uint8_t state; // 0=Free, 1=Ready, 2=Running, 3=Terminated + uint8_t state; // 0=Free, 1=Ready, 2=Running, 3=Blocked, 4=Terminated uint8_t _pad[3]; char name[64]; uint64_t heapUsed; // heapNext - UserHeapBase (bytes) diff --git a/kernel/src/Api/WinServer.cpp b/kernel/src/Api/WinServer.cpp index c37f5fa..5aa24b6 100644 --- a/kernel/src/Api/WinServer.cpp +++ b/kernel/src/Api/WinServer.cpp @@ -10,15 +10,24 @@ #include #include #include +#include #include namespace WinServer { static WindowSlot g_slots[MaxWindows]; static int g_uiScale = 1; + static kcp::Mutex wsLock; + + // RAII lock guard for WinServer operations + struct WsGuard { + WsGuard() { wsLock.Acquire(); } + ~WsGuard() { wsLock.Release(); } + }; int Create(int ownerPid, uint64_t ownerPml4, const char* title, int w, int h, uint64_t& heapNext, uint64_t& outVa) { + WsGuard guard; // Find a free slot int slotIdx = -1; for (int i = 0; i < MaxWindows; i++) { @@ -56,12 +65,12 @@ namespace WinServer { } slot.title[tlen] = '\0'; - // Allocate physical pages and map into owner's address space + // Allocate live pixel pages (app renders here) and snapshot pages + // (compositor reads here). Present copies live -> snapshot. uint64_t userVa = heapNext; for (int i = 0; i < numPages; i++) { void* page = Memory::g_pfa->AllocateZeroed(); if (page == nullptr) { - // Cleanup on failure - mark slot unused slot.used = false; return -1; } @@ -71,6 +80,14 @@ namespace WinServer { slot.used = false; return -1; } + + // Allocate corresponding snapshot page + void* snapPage = Memory::g_pfa->AllocateZeroed(); + if (snapPage == nullptr) { + slot.used = false; + return -1; + } + slot.snapshotPhysPages[i] = Memory::SubHHDM((uint64_t)snapPage); } slot.ownerVa = userVa; @@ -84,6 +101,7 @@ namespace WinServer { } int Destroy(int windowId, int callerPid) { + WsGuard guard; if (windowId < 0 || windowId >= MaxWindows) return -1; WindowSlot& slot = g_slots[windowId]; if (!slot.used || slot.ownerPid != callerPid) return -1; @@ -113,11 +131,14 @@ namespace WinServer { } } - // Free physical pixel pages + // Free physical pixel pages and snapshot pages for (int i = 0; i < slot.pixelNumPages; i++) { if (slot.pixelPhysPages[i] != 0) { Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i])); } + if (slot.snapshotPhysPages[i] != 0) { + Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i])); + } } slot.used = false; @@ -125,15 +146,34 @@ namespace WinServer { } int Present(int windowId, int callerPid) { - if (windowId < 0 || windowId >= MaxWindows) return -1; + // Validate ownership under the lock (brief) + wsLock.Acquire(); + if (windowId < 0 || windowId >= MaxWindows) { wsLock.Release(); return -1; } WindowSlot& slot = g_slots[windowId]; - if (!slot.used || slot.ownerPid != callerPid) return -1; + if (!slot.used || slot.ownerPid != callerPid) { wsLock.Release(); return -1; } + int numPages = slot.pixelNumPages; + wsLock.Release(); + // Snapshot memcpy OUTSIDE the lock. This is safe because only the + // owning process calls Present/Resize, and a process runs on one + // CPU at a time. Holding wsLock during an 8MB copy would block + // ALL other WinServer operations (Poll, Enumerate, SendEvent) + // across all CPUs, causing convoy stalls and lockups. + for (int i = 0; i < numPages; i++) { + void* src = (void*)Memory::HHDM(slot.pixelPhysPages[i]); + void* dst = (void*)Memory::HHDM(slot.snapshotPhysPages[i]); + memcpy(dst, src, 0x1000); + } + + // Set dirty under lock + wsLock.Acquire(); slot.dirty = true; + wsLock.Release(); return 0; } int Poll(int windowId, int callerPid, Montauk::WinEvent* outEvent) { + WsGuard guard; if (windowId < 0 || windowId >= MaxWindows) return -1; WindowSlot& slot = g_slots[windowId]; if (!slot.used || slot.ownerPid != callerPid) return -1; @@ -146,6 +186,7 @@ namespace WinServer { } int Enumerate(Montauk::WinInfo* outArray, int maxCount) { + WsGuard guard; int count = 0; for (int i = 0; i < MaxWindows && count < maxCount; i++) { if (!g_slots[i].used) continue; @@ -164,6 +205,7 @@ namespace WinServer { } uint64_t Map(int windowId, int callerPid, uint64_t callerPml4, uint64_t& heapNext) { + WsGuard guard; if (windowId < 0 || windowId >= MaxWindows) return 0; WindowSlot& slot = g_slots[windowId]; if (!slot.used) return 0; @@ -175,8 +217,10 @@ namespace WinServer { uint64_t userVa = heapNext; + // Map the SNAPSHOT pages (not live pages) so the compositor + // always reads a complete frame captured at Present time. for (int i = 0; i < slot.pixelNumPages; i++) { - if (!Memory::VMM::Paging::MapUserIn(callerPml4, slot.pixelPhysPages[i], + if (!Memory::VMM::Paging::MapUserIn(callerPml4, slot.snapshotPhysPages[i], userVa + (uint64_t)i * 0x1000)) { return 0; } @@ -189,7 +233,8 @@ namespace WinServer { return userVa; } - int SendEvent(int windowId, const Montauk::WinEvent* event) { + // Internal: send event without acquiring lock (caller must hold wsLock) + static int SendEventLocked(int windowId, const Montauk::WinEvent* event) { if (windowId < 0 || windowId >= MaxWindows) return -1; WindowSlot& slot = g_slots[windowId]; if (!slot.used) return -1; @@ -202,8 +247,14 @@ namespace WinServer { return 0; } + int SendEvent(int windowId, const Montauk::WinEvent* event) { + WsGuard guard; + return SendEventLocked(windowId, event); + } + int Resize(int windowId, int callerPid, uint64_t ownerPml4, int newW, int newH, uint64_t& heapNext, uint64_t& outVa) { + WsGuard guard; if (windowId < 0 || windowId >= MaxWindows) return -1; WindowSlot& slot = g_slots[windowId]; if (!slot.used || slot.ownerPid != callerPid) return -1; @@ -217,19 +268,21 @@ namespace WinServer { int numPages = (int)((bufSize + 0xFFF) / 0x1000); if (numPages > MaxPixelPages) return -1; - // Unmap old pixel pages from desktop's address space (if mapped) - if (slot.desktopVa != 0 && slot.desktopPid != 0) { - auto* desktopProc = Sched::GetProcessByPid(slot.desktopPid); - if (desktopProc) { - for (int i = 0; i < slot.pixelNumPages; i++) { - Memory::VMM::Paging::UnmapUserIn( - desktopProc->pml4Phys, - slot.desktopVa + (uint64_t)i * 0x1000); - } - } - } + // Do NOT unmap/free snapshot pages from the desktop here. + // The desktop may be running on another CPU with stale TLB entries + // pointing to the old snapshot pages. Unmapping + freeing them + // causes a page fault on the desktop when the TLB entry is evicted. + // Instead, leave them mapped -- the desktop will get new snapshot + // pages on its next Map call (desktopVa is invalidated below). + // The old snapshot pages remain in the desktop's page tables and + // are reclaimed by FreeUserHalf when the desktop exits. - // Unmap old pixel pages from owner's address space, then free them + // Unmap old LIVE pixel pages from owner's address space and free them. + // Free old snapshot pages too (don't leak). The desktop's PTEs still + // point to the old physical addresses, but we don't clear them (no + // UnmapUserIn for desktop) to avoid TLB-eviction page faults. The + // desktop may read one frame of garbage from the freed pages before + // re-mapping -- acceptable during a resize. int oldNumPages = slot.pixelNumPages; for (int i = 0; i < oldNumPages; i++) { Memory::VMM::Paging::UnmapUserIn( @@ -238,9 +291,13 @@ namespace WinServer { Memory::g_pfa->Free((void*)Memory::HHDM(slot.pixelPhysPages[i])); slot.pixelPhysPages[i] = 0; } + if (slot.snapshotPhysPages[i] != 0) { + Memory::g_pfa->Free((void*)Memory::HHDM(slot.snapshotPhysPages[i])); + slot.snapshotPhysPages[i] = 0; + } } - // Allocate new pages and map into owner's address space + // Allocate new live + snapshot pages and map live into owner's address space uint64_t userVa = heapNext; for (int i = 0; i < numPages; i++) { void* page = Memory::g_pfa->AllocateZeroed(); @@ -250,6 +307,10 @@ namespace WinServer { if (!Memory::VMM::Paging::MapUserIn(ownerPml4, physAddr, userVa + (uint64_t)i * 0x1000)) { return -1; } + + void* snapPage = Memory::g_pfa->AllocateZeroed(); + if (snapPage == nullptr) return -1; + slot.snapshotPhysPages[i] = Memory::SubHHDM((uint64_t)snapPage); } slot.width = newW; @@ -267,6 +328,7 @@ namespace WinServer { } int SetCursor(int windowId, int callerPid, int cursor) { + WsGuard guard; if (windowId < 0 || windowId >= MaxWindows) return -1; WindowSlot& slot = g_slots[windowId]; if (!slot.used || slot.ownerPid != callerPid) return -1; @@ -275,6 +337,7 @@ namespace WinServer { } int SetScale(int scale) { + WsGuard guard; if (scale < 0) scale = 0; if (scale > 2) scale = 2; g_uiScale = scale; @@ -286,7 +349,7 @@ namespace WinServer { ev.scale.scale = scale; for (int i = 0; i < MaxWindows; i++) { if (g_slots[i].used) { - SendEvent(i, &ev); + SendEventLocked(i, &ev); } } return 0; @@ -297,6 +360,7 @@ namespace WinServer { } void CleanupProcess(int pid) { + WsGuard guard; for (int i = 0; i < MaxWindows; i++) { if (g_slots[i].used && g_slots[i].ownerPid == pid) { Kt::KernelLogStream(Kt::INFO, "WinServer") << "Cleaning up window " @@ -314,10 +378,19 @@ namespace WinServer { } } - // Do NOT free physical pixel pages here — they are still mapped + // Do NOT free live pixel pages here -- they are still mapped // in the owner's page tables and FreeUserHalf() (called right - // after CleanupProcess) will free them. Freeing here would - // cause a double-free, creating a cycle in the PFA free list. + // after CleanupProcess) will free them. + + // DO free snapshot pages -- they are NOT in the owner's page + // tables (they were mapped into the desktop, and we just + // unmapped them above). If we don't free them here, they leak. + for (int p = 0; p < g_slots[i].pixelNumPages; p++) { + if (g_slots[i].snapshotPhysPages[p] != 0) { + Memory::g_pfa->Free((void*)Memory::HHDM(g_slots[i].snapshotPhysPages[p])); + g_slots[i].snapshotPhysPages[p] = 0; + } + } g_slots[i].used = false; } diff --git a/kernel/src/Api/WinServer.hpp b/kernel/src/Api/WinServer.hpp index 3563b6c..0679eeb 100644 --- a/kernel/src/Api/WinServer.hpp +++ b/kernel/src/Api/WinServer.hpp @@ -12,14 +12,15 @@ namespace WinServer { static constexpr int MaxWindows = 8; static constexpr int MaxEvents = 64; - static constexpr int MaxPixelPages = 2048; // up to 2048x1024 @ 32bpp = 8MB + static constexpr int MaxPixelPages = 8192; // up to 3840x2160 @ 32bpp = 32MB struct WindowSlot { bool used; int ownerPid; char title[64]; int width, height; - uint64_t pixelPhysPages[MaxPixelPages]; + uint64_t pixelPhysPages[MaxPixelPages]; // live buffer (app writes here) + uint64_t snapshotPhysPages[MaxPixelPages]; // snapshot (compositor reads here) int pixelNumPages; uint64_t ownerVa; // VA in owner's address space uint64_t desktopVa; // VA in desktop's address space (0 = not yet mapped) diff --git a/kernel/src/CppLib/Spinlock.hpp b/kernel/src/CppLib/Spinlock.hpp index 5891f3c..d1e8871 100644 --- a/kernel/src/CppLib/Spinlock.hpp +++ b/kernel/src/CppLib/Spinlock.hpp @@ -15,4 +15,20 @@ namespace kcp { void Acquire(); void Release(); }; + + // Non-interrupt-disabling mutex for subsystems that are only called + // from process/syscall context (never from interrupt handlers). + // Keeps interrupts enabled while spinning and while held. + class Mutex { + std::atomic_flag flag{ATOMIC_FLAG_INIT}; + public: + void Acquire() { + while (flag.test_and_set(std::memory_order_acquire)) { + asm volatile("pause"); + } + } + void Release() { + flag.clear(std::memory_order_release); + } + }; }; \ No newline at end of file diff --git a/kernel/src/Drivers/PS2/Mouse.cpp b/kernel/src/Drivers/PS2/Mouse.cpp index 5f47f9d..de39635 100644 --- a/kernel/src/Drivers/PS2/Mouse.cpp +++ b/kernel/src/Drivers/PS2/Mouse.cpp @@ -197,6 +197,19 @@ namespace Drivers::PS2::Mouse { g_MaxY = maxY; } + void FlushState() { + g_PacketIndex = 0; + + // Drain any stale bytes from the PS/2 controller data port + constexpr uint16_t StatusPort = 0x64; + constexpr uint16_t DataPort_ = 0x60; + for (int i = 0; i < 32; i++) { + uint8_t status = Io::In8(StatusPort); + if (!(status & 0x01)) break; // output buffer empty + Io::In8(DataPort_); // discard byte + } + } + void InjectMouseReport(uint8_t buttons, int8_t deltaX, int8_t deltaY, int8_t scroll) { g_StateLock.Acquire(); diff --git a/kernel/src/Drivers/PS2/Mouse.hpp b/kernel/src/Drivers/PS2/Mouse.hpp index ea5514b..00565a1 100644 --- a/kernel/src/Drivers/PS2/Mouse.hpp +++ b/kernel/src/Drivers/PS2/Mouse.hpp @@ -34,6 +34,11 @@ namespace Drivers::PS2::Mouse { void SetBounds(int32_t maxX, int32_t maxY); + // Reset packet assembly state and drain pending PS/2 data. + // Call after boot to ensure clean mouse state regardless of + // any bytes lost during interrupt-heavy boot sequence. + void FlushState(); + // Inject a mouse report from an external source (e.g., USB HID mouse) void InjectMouseReport(uint8_t buttons, int8_t deltaX, int8_t deltaY, int8_t scroll); diff --git a/kernel/src/Fs/Vfs.cpp b/kernel/src/Fs/Vfs.cpp index 8948689..c5794ec 100644 --- a/kernel/src/Fs/Vfs.cpp +++ b/kernel/src/Fs/Vfs.cpp @@ -6,6 +6,7 @@ #include "Vfs.hpp" #include +#include namespace Fs::Vfs { @@ -18,6 +19,11 @@ namespace Fs::Vfs { static FsDriver* driveTable[MaxDrives]; static HandleEntry handleTable[MaxHandles]; + // Protects handle table and driver dispatch from concurrent CPU access. + // Uses Mutex (not Spinlock) so interrupts stay enabled while held -- + // VFS is never called from interrupt context. + static kcp::Mutex vfsLock; + // Parse "N:/path" into drive number and local path. // Returns true on success, sets outDrive and outPath. static bool ParsePath(const char* path, int& outDrive, const char*& outPath) { @@ -74,23 +80,18 @@ namespace Fs::Vfs { int drive; const char* localPath; - if (!ParsePath(path, drive, localPath)) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format"; - return -1; - } + if (!ParsePath(path, drive, localPath)) return -1; + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; - if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; - return -1; - } + vfsLock.Acquire(); int localHandle = driveTable[drive]->Open(localPath); - if (localHandle < 0) return -1; + if (localHandle < 0) { vfsLock.Release(); return -1; } int globalHandle = AllocHandle(); if (globalHandle < 0) { driveTable[drive]->Close(localHandle); - Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles"; + vfsLock.Release(); return -1; } @@ -98,61 +99,67 @@ namespace Fs::Vfs { handleTable[globalHandle].driveNumber = drive; handleTable[globalHandle].localHandle = localHandle; + vfsLock.Release(); return globalHandle; } int VfsRead(int handle, uint8_t* buffer, uint64_t offset, uint64_t size) { - if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1; + vfsLock.Acquire(); + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return -1; } HandleEntry& entry = handleTable[handle]; - return driveTable[entry.driveNumber]->Read(entry.localHandle, buffer, offset, size); + int result = driveTable[entry.driveNumber]->Read(entry.localHandle, buffer, offset, size); + vfsLock.Release(); + return result; } uint64_t VfsGetSize(int handle) { - if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return 0; + vfsLock.Acquire(); + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return 0; } HandleEntry& entry = handleTable[handle]; - return driveTable[entry.driveNumber]->GetSize(entry.localHandle); + uint64_t result = driveTable[entry.driveNumber]->GetSize(entry.localHandle); + vfsLock.Release(); + return result; } void VfsClose(int handle) { - if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return; + vfsLock.Acquire(); + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return; } HandleEntry& entry = handleTable[handle]; driveTable[entry.driveNumber]->Close(entry.localHandle); entry.inUse = false; + vfsLock.Release(); } int VfsWrite(int handle, const uint8_t* buffer, uint64_t offset, uint64_t size) { - if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) return -1; + vfsLock.Acquire(); + if (handle < 0 || handle >= MaxHandles || !handleTable[handle].inUse) { vfsLock.Release(); return -1; } HandleEntry& entry = handleTable[handle]; - if (driveTable[entry.driveNumber]->Write == nullptr) return -1; - return driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size); + if (driveTable[entry.driveNumber]->Write == nullptr) { vfsLock.Release(); return -1; } + int result = driveTable[entry.driveNumber]->Write(entry.localHandle, buffer, offset, size); + vfsLock.Release(); + return result; } int VfsCreate(const char* path) { int drive; const char* localPath; - if (!ParsePath(path, drive, localPath)) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for Create"; - return -1; - } - - if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; - return -1; - } - + if (!ParsePath(path, drive, localPath)) return -1; + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; if (driveTable[drive]->Create == nullptr) return -1; + vfsLock.Acquire(); + int localHandle = driveTable[drive]->Create(localPath); - if (localHandle < 0) return -1; + if (localHandle < 0) { vfsLock.Release(); return -1; } int globalHandle = AllocHandle(); if (globalHandle < 0) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "No free handles"; + vfsLock.Release(); return -1; } @@ -160,6 +167,7 @@ namespace Fs::Vfs { handleTable[globalHandle].driveNumber = drive; handleTable[globalHandle].localHandle = localHandle; + vfsLock.Release(); return globalHandle; } @@ -168,12 +176,13 @@ namespace Fs::Vfs { const char* localPath; if (!ParsePath(path, drive, localPath)) return -1; - if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; - if (driveTable[drive]->Delete == nullptr) return -1; - return driveTable[drive]->Delete(localPath); + vfsLock.Acquire(); + int result = driveTable[drive]->Delete(localPath); + vfsLock.Release(); + return result; } int VfsMkdir(const char* path) { @@ -184,7 +193,10 @@ namespace Fs::Vfs { if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; if (driveTable[drive]->Mkdir == nullptr) return -1; - return driveTable[drive]->Mkdir(localPath); + vfsLock.Acquire(); + int result = driveTable[drive]->Mkdir(localPath); + vfsLock.Release(); + return result; } int VfsDriveList(int* outDrives, int maxEntries) { @@ -201,17 +213,13 @@ namespace Fs::Vfs { int drive; const char* localPath; - if (!ParsePath(path, drive, localPath)) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Invalid path format for ReadDir"; - return -1; - } + if (!ParsePath(path, drive, localPath)) return -1; + if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) return -1; - if (drive < 0 || drive >= MaxDrives || driveTable[drive] == nullptr) { - Kt::KernelLogStream(Kt::ERROR, "VFS") << "Drive " << drive << " not registered"; - return -1; - } - - return driveTable[drive]->ReadDir(localPath, outNames, maxEntries); + vfsLock.Acquire(); + int result = driveTable[drive]->ReadDir(localPath, outNames, maxEntries); + vfsLock.Release(); + return result; } } diff --git a/kernel/src/Hal/Apic/Apic.cpp b/kernel/src/Hal/Apic/Apic.cpp index d731e47..2a4df27 100644 --- a/kernel/src/Hal/Apic/Apic.cpp +++ b/kernel/src/Hal/Apic/Apic.cpp @@ -64,6 +64,27 @@ namespace Hal { << " max LVT=" << base::dec << (uint64_t)((version >> 16) & 0xFF); } + void InitializeAP() { + if (!g_apicBase) return; + + // Re-assert APIC Global Enable in MSR + uint64_t msrValue = ReadMSR(MSR_APIC_BASE); + if (!(msrValue & (1ULL << 11))) { + msrValue |= (1ULL << 11); + WriteMSR(MSR_APIC_BASE, msrValue); + } + + // Enable APIC software enable + set spurious vector + uint32_t svr = ReadRegister(REG_SPURIOUS); + svr |= (1 << 8); + svr = (svr & 0xFFFFFF00) | SPURIOUS_VECTOR; + WriteRegister(REG_SPURIOUS, svr); + + // Accept all interrupts + WriteRegister(REG_TPR, 0); + // No log here -- APs boot in parallel and the BSP logs a summary. + } + void Reinitialize() { if (!g_apicBase) return; diff --git a/kernel/src/Hal/Apic/Apic.hpp b/kernel/src/Hal/Apic/Apic.hpp index e05bd13..cef1177 100644 --- a/kernel/src/Hal/Apic/Apic.hpp +++ b/kernel/src/Hal/Apic/Apic.hpp @@ -33,6 +33,11 @@ namespace Hal { void Initialize(uint64_t apicBasePhys); + // Initialize the Local APIC on an AP. + // Uses the same MMIO base as the BSP (each CPU's APIC is at the same + // physical address but routes to its own hardware). + void InitializeAP(); + // Re-enable the Local APIC after S3 resume. // The MMIO mapping and base address survive (they're in page tables / RAM). // Only the SVR and TPR hardware registers need reprogramming. diff --git a/kernel/src/Hal/Apic/ApicInit.cpp b/kernel/src/Hal/Apic/ApicInit.cpp index 1929249..b1fd688 100644 --- a/kernel/src/Hal/Apic/ApicInit.cpp +++ b/kernel/src/Hal/Apic/ApicInit.cpp @@ -70,9 +70,9 @@ namespace Hal { IoApic::RouteIrq(IRQ_KEYBOARD, IRQ_VECTOR_BASE + IRQ_KEYBOARD, bspApicId); IoApic::RouteIrq(IRQ_MOUSE, IRQ_VECTOR_BASE + IRQ_MOUSE, bspApicId); - // Step 8: Enable interrupts - asm volatile("sti"); + // Note: sti is NOT called here. The caller (Main.cpp) enables + // interrupts after setting up per-CPU GS base for SMP/SWAPGS. - KernelLogStream(OK, "APIC") << "APIC subsystem initialized, interrupts enabled"; + KernelLogStream(OK, "APIC") << "APIC subsystem initialized"; } }; diff --git a/kernel/src/Hal/Apic/Interrupts.cpp b/kernel/src/Hal/Apic/Interrupts.cpp index a1f4f90..e67f2f9 100644 --- a/kernel/src/Hal/Apic/Interrupts.cpp +++ b/kernel/src/Hal/Apic/Interrupts.cpp @@ -42,10 +42,16 @@ namespace Hal { // C linkage dispatch function called from assembly stubs extern "C" void HalIrqDispatch(uint64_t irqNumber) { + // Send EOI BEFORE calling the handler. The handler may context-switch + // (via Tick -> Schedule -> SchedContextSwitch) and never return here. + // If EOI is deferred until after the handler, the LAPIC timer vector + // stays masked on this CPU -- no more timer interrupts, no more + // scheduling, and the system eventually locks up. + // This is safe because the ISR runs with IF=0; the LAPIC won't + // deliver a new interrupt until iretq re-enables them. + Hal::LocalApic::SendEOI(); + if (irqNumber < Hal::IRQ_COUNT && Hal::g_irqHandlers[irqNumber] != nullptr) { Hal::g_irqHandlers[irqNumber]((uint8_t)irqNumber); } - - // Send End of Interrupt to the Local APIC - Hal::LocalApic::SendEOI(); } diff --git a/kernel/src/Hal/Apic/IsrStubs.asm b/kernel/src/Hal/Apic/IsrStubs.asm index ca5b900..9978f40 100644 --- a/kernel/src/Hal/Apic/IsrStubs.asm +++ b/kernel/src/Hal/Apic/IsrStubs.asm @@ -1,7 +1,7 @@ ; ; IsrStubs.asm -; Hardware interrupt (IRQ) entry stubs for APIC -; Copyright (c) 2025 Daniel Hammer +; Hardware interrupt (IRQ) entry stubs for APIC (SMP-aware with SWAPGS) +; Copyright (c) 2025-2026 Daniel Hammer ; [bits 64] @@ -10,18 +10,27 @@ section .text ; External C++ handler function extern HalIrqDispatch -; Macro to define an IRQ stub -; Each stub pushes the IRQ number and jumps to the common handler +; ==================================================================== +; IRQ stub macro with SWAPGS support +; Each stub checks the saved CS to determine if we came from user mode. +; If so, SWAPGS is executed to switch GS to per-CPU data. +; ==================================================================== %macro IRQ_STUB 1 global IrqStub%1 IrqStub%1: - push rax ; Save rax (we use it for the IRQ number) - mov rax, %1 ; IRQ number + cmp qword [rsp + 8], 0x08 ; check saved CS: kernel? + je %%from_kernel + swapgs ; from user mode: swap to kernel GS +%%from_kernel: + push rax ; save rax (used for IRQ number) + mov rax, %1 ; IRQ number jmp IrqCommon %endmacro +; ==================================================================== ; Common IRQ handler: saves all general-purpose registers, ; calls the C++ dispatch function, restores registers, and returns. +; ==================================================================== IrqCommon: ; rax is already on the stack and holds the IRQ number push rcx @@ -63,10 +72,17 @@ IrqCommon: pop rcx pop rax + ; SWAPGS back if returning to user mode + cmp qword [rsp + 8], 0x08 ; check saved CS: kernel? + je .from_kernel_exit + swapgs ; returning to user mode: restore user GS +.from_kernel_exit: iretq +; ==================================================================== ; Define stubs for IRQs 0..47 (vectors 32..79) ; 0-23: legacy ISA IRQs via IOAPIC, 24-47: MSI vectors +; ==================================================================== IRQ_STUB 0 IRQ_STUB 1 IRQ_STUB 2 @@ -121,7 +137,9 @@ global IrqStubSpurious IrqStubSpurious: iretq +; ==================================================================== ; Export the stub table for C++ to reference +; ==================================================================== section .data global IrqStubTable IrqStubTable: diff --git a/kernel/src/Hal/IDT.cpp b/kernel/src/Hal/IDT.cpp index d45c333..4685135 100644 --- a/kernel/src/Hal/IDT.cpp +++ b/kernel/src/Hal/IDT.cpp @@ -73,10 +73,15 @@ namespace Hal { __attribute__((interrupt)) void ExceptionHandler(System::PanicFrame* frame) { uint64_t cs = GetExceptionCS(i, frame); + bool fromUser = (cs & 3) == 3; + + // SWAPGS: if from user mode, GS base is user-defined. + // Swap to kernel per-CPU GS base so scheduler calls work. + if (fromUser) asm volatile("swapgs"); // If the fault originated in user-mode (ring 3), kill the process // instead of panicking the entire system. - if ((cs & 3) == 3 && Sched::GetCurrentPid() >= 0) { + if (fromUser && Sched::GetCurrentPid() >= 0) { auto* proc = Sched::GetCurrentProcessPtr(); Kt::KernelLogStream(Kt::ERROR, "Exception") << ExceptionStrings[i] << " in process \"" @@ -88,6 +93,10 @@ namespace Hal { frame->InterruptVector = i; Panic(ExceptionStrings[i], frame); } + + // Unreachable in practice (user faults exit, kernel faults panic), + // but balance the SWAPGS for correctness. + if (fromUser) asm volatile("swapgs"); } void LoadIDT(IDTRStruct& idtr) { diff --git a/kernel/src/Hal/SmpBoot.cpp b/kernel/src/Hal/SmpBoot.cpp new file mode 100644 index 0000000..a657930 --- /dev/null +++ b/kernel/src/Hal/SmpBoot.cpp @@ -0,0 +1,279 @@ +/* + * SmpBoot.cpp + * Symmetric Multiprocessing bootstrap and AP entry + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "SmpBoot.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Defined in Platform/Limine.hpp (included only by Main.cpp to avoid +// duplicating the LIMINE_BASE_REVISION tag). +extern volatile limine_mp_request mp_request; +#include + +// Verify assembly offsets match struct layout +static_assert(__builtin_offsetof(Smp::CpuData, selfPtr) == CPUDATA_SELF_PTR, "CpuData offset mismatch: selfPtr"); +static_assert(__builtin_offsetof(Smp::CpuData, kernelRsp) == CPUDATA_KERNEL_RSP, "CpuData offset mismatch: kernelRsp"); +static_assert(__builtin_offsetof(Smp::CpuData, userRspScratch)== CPUDATA_USER_RSP, "CpuData offset mismatch: userRspScratch"); +static_assert(__builtin_offsetof(Smp::CpuData, currentSlot) == CPUDATA_CURRENT_SLOT, "CpuData offset mismatch: currentSlot"); + +extern "C" void SyscallEntry(); + +// Assembly helpers (GDT.asm) +extern "C" void LoadGDT(Hal::GDTPointer* ptr); +extern "C" void ReloadSegments(); +extern "C" void LoadTR(); + +using namespace Kt; + +namespace Smp { + + static CpuData g_cpus[MaxCPUs]; + static int g_cpuCount = 0; + + CpuData* GetCpuData(int index) { + if (index < 0 || index >= g_cpuCount) return nullptr; + return &g_cpus[index]; + } + + int GetCpuCount() { + return g_cpuCount; + } + + // ==================================================================== + // Per-CPU GDT/TSS setup + // ==================================================================== + + static void SetupPerCpuGdtTss(CpuData& cpu) { + // Zero the TSS + memset(&cpu.cpuTss, 0, sizeof(Hal::TSS64)); + cpu.cpuTss.iopbOffset = sizeof(Hal::TSS64); + + // Copy the standard GDT layout + cpu.cpuGdt = { + {0xFFFF, 0, 0, 0x00, 0x00, 0}, // 0x00 Null + {0xFFFF, 0, 0, 0x9A, 0xA0, 0}, // 0x08 KernelCode + {0xFFFF, 0, 0, 0x92, 0xA0, 0}, // 0x10 KernelData + {0xFFFF, 0, 0, 0xF2, 0xA0, 0}, // 0x18 UserData + {0xFFFF, 0, 0, 0xFA, 0xA0, 0}, // 0x20 UserCode + {0, 0, 0, 0, 0, 0}, // 0x28 TSS low + {0, 0, 0, 0, 0, 0}, // 0x30 TSS high + }; + + // Encode the 16-byte TSS descriptor + uint64_t base = (uint64_t)&cpu.cpuTss; + uint32_t limit = sizeof(Hal::TSS64) - 1; + + // Low 8 bytes + cpu.cpuGdt.TSS.LimitLow = limit & 0xFFFF; + cpu.cpuGdt.TSS.BaseLow = base & 0xFFFF; + cpu.cpuGdt.TSS.BaseMiddle = (base >> 16) & 0xFF; + cpu.cpuGdt.TSS.AccessByte = 0x89; // Present, 64-bit TSS Available + cpu.cpuGdt.TSS.GranularityByte = (limit >> 16) & 0x0F; + cpu.cpuGdt.TSS.BaseHigh = (base >> 24) & 0xFF; + + // High 8 bytes (base[63:32]) + uint32_t baseUpper = (uint32_t)(base >> 32); + cpu.cpuGdt.TSSHigh.LimitLow = baseUpper & 0xFFFF; + cpu.cpuGdt.TSSHigh.BaseLow = (baseUpper >> 16) & 0xFFFF; + cpu.cpuGdt.TSSHigh.BaseMiddle = 0; + cpu.cpuGdt.TSSHigh.AccessByte = 0; + cpu.cpuGdt.TSSHigh.GranularityByte = 0; + cpu.cpuGdt.TSSHigh.BaseHigh = 0; + + cpu.tss = &cpu.cpuTss; + } + + // ==================================================================== + // Set GS base MSRs for per-CPU data + // ==================================================================== + + static constexpr uint32_t IA32_GS_BASE = 0xC0000101; + static constexpr uint32_t IA32_KERNEL_GS_BASE = 0xC0000102; + + static void SetGSBase(CpuData* cpu) { + // In kernel mode, GSBASE = per-CPU data pointer. + // KernelGSBASE = 0 (user GS base, swapped in on swapgs). + Hal::WriteMSR(IA32_GS_BASE, (uint64_t)cpu); + Hal::WriteMSR(IA32_KERNEL_GS_BASE, 0); + } + + // ==================================================================== + // BSP initialization + // ==================================================================== + + void InitBsp() { + memset(g_cpus, 0, sizeof(g_cpus)); + + CpuData& bsp = g_cpus[0]; + bsp.selfPtr = (uint64_t)&bsp; + bsp.cpuIndex = 0; + bsp.lapicId = Hal::LocalApic::GetId(); + bsp.currentSlot = -1; + bsp.started = true; + + // BSP uses the global TSS (already set up in PrepareGDT) + bsp.tss = &Hal::g_tss; + + // Set GS base for BSP + SetGSBase(&bsp); + + g_cpuCount = 1; + + KernelLogStream(OK, "SMP") << "BSP initialized (LAPIC ID " << (uint64_t)bsp.lapicId << ")"; + } + + // ==================================================================== + // AP entry point + // Called by Limine when goto_address is written. + // RDI = pointer to limine_mp_info for this CPU. + // Runs on a 64KiB Limine-provided stack. + // ==================================================================== + + static void ApEntry(limine_mp_info* info) { + // Find our CpuData (stored in extra_argument by BootAPs) + CpuData* cpu = (CpuData*)info->extra_argument; + + // --- Load per-CPU GDT --- + Hal::GDTPointer gdtPtr { + .Size = sizeof(Hal::BasicGDT) - 1, + .GDTAddress = (uint64_t)&cpu->cpuGdt, + }; + LoadGDT(&gdtPtr); + ReloadSegments(); + + // --- Load TSS --- + LoadTR(); + + // --- Load shared IDT --- + Hal::IDTReload(); + + // --- Switch to kernel page tables --- + Memory::VMM::LoadCR3(Memory::VMM::g_paging->PML4); + + // --- Enable SSE --- + Hal::EnableSSE(); + + // --- Set GS base --- + SetGSBase(cpu); + + // --- Initialize local APIC --- + Hal::LocalApic::InitializeAP(); + + // --- Program SYSCALL MSRs --- + uint64_t efer = Hal::ReadMSR(Hal::IA32_EFER); + efer |= 1; // SCE + Hal::WriteMSR(Hal::IA32_EFER, efer); + + uint64_t star = (0x0010ULL << 48) | (0x0008ULL << 32); + Hal::WriteMSR(Hal::IA32_STAR, star); + Hal::WriteMSR(Hal::IA32_LSTAR, (uint64_t)SyscallEntry); + Hal::WriteMSR(Hal::IA32_FMASK, 0x200); + + // --- Program PAT (entry 1 = WC) --- + Hal::InitializePAT(); + + // --- Calibrate and start APIC timer --- + Timekeeping::ApicTimerInitializeAP(); + + // --- Signal that we are online --- + cpu->started = true; + + // --- Enable interrupts and enter idle loop --- + asm volatile("sti"); + + for (;;) { + asm volatile("hlt"); + } + } + + // ==================================================================== + // Boot all APs + // ==================================================================== + + void BootAPs() { + if (mp_request.response == nullptr) { + KernelLogStream(WARNING, "SMP") << "No MP response from bootloader - single CPU mode"; + return; + } + + auto* resp = mp_request.response; + uint64_t cpuCount = resp->cpu_count; + + KernelLogStream(INFO, "SMP") << "Bootloader reports " << cpuCount << " CPU(s), BSP LAPIC ID " + << (uint64_t)resp->bsp_lapic_id; + + if (cpuCount <= 1) { + KernelLogStream(INFO, "SMP") << "Single CPU system - no APs to boot"; + return; + } + + if (cpuCount > (uint64_t)MaxCPUs) { + KernelLogStream(WARNING, "SMP") << "Clamping CPU count from " << cpuCount << " to " << (uint64_t)MaxCPUs; + cpuCount = MaxCPUs; + } + + // Prepare all APs, then wake them all at once. This is safe because: + // - APs use BSP's timer calibration (no PIT contention) + // - APs don't log (no terminal contention) + // - Each AP's init is purely local (GDT, TSS, APIC, MSRs) + int apIndex = 1; // BSP is index 0 + for (uint64_t i = 0; i < cpuCount; i++) { + limine_mp_info* info = resp->cpus[i]; + + if (info->lapic_id == resp->bsp_lapic_id) continue; + if (apIndex >= MaxCPUs) break; + + CpuData& ap = g_cpus[apIndex]; + ap.selfPtr = (uint64_t)≈ + ap.cpuIndex = apIndex; + ap.lapicId = info->lapic_id; + ap.currentSlot = -1; + ap.started = false; + + SetupPerCpuGdtTss(ap); + info->extra_argument = (uint64_t)≈ + + // Wake this AP (it runs ApEntry in parallel with other APs) + __atomic_store_n(&info->goto_address, (limine_goto_address)ApEntry, __ATOMIC_SEQ_CST); + + apIndex++; + } + + g_cpuCount = apIndex; + + // Wait for all APs to come online + for (int i = 1; i < g_cpuCount; i++) { + volatile bool* flag = &g_cpus[i].started; + uint64_t timeout = 100000000; + while (!*flag && timeout > 0) { + asm volatile("pause"); + timeout--; + } + + if (!*flag) { + KernelLogStream(ERROR, "SMP") << "AP " << i << " (LAPIC " + << (uint64_t)g_cpus[i].lapicId << ") failed to start"; + } + } + + // Count how many actually started + int onlineCount = 1; // BSP + for (int i = 1; i < g_cpuCount; i++) { + if (g_cpus[i].started) onlineCount++; + } + + KernelLogStream(OK, "SMP") << onlineCount << " CPU(s) online"; + } +} diff --git a/kernel/src/Hal/SmpBoot.hpp b/kernel/src/Hal/SmpBoot.hpp new file mode 100644 index 0000000..661c0c6 --- /dev/null +++ b/kernel/src/Hal/SmpBoot.hpp @@ -0,0 +1,64 @@ +/* + * SmpBoot.hpp + * Symmetric Multiprocessing bootstrap and per-CPU data + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +// ==================================================================== +// Assembly-visible offsets into CpuData +// These MUST match the struct layout below. Verified by static_assert +// in SmpBoot.cpp. +// ==================================================================== +#define CPUDATA_SELF_PTR 0 +#define CPUDATA_KERNEL_RSP 8 +#define CPUDATA_USER_RSP 16 +#define CPUDATA_CURRENT_SLOT 24 + +namespace Smp { + + static constexpr int MaxCPUs = 64; + + struct CpuData { + // === Fields accessed by assembly (offsets defined above) === + uint64_t selfPtr; // offset 0: pointer to self + uint64_t kernelRsp; // offset 8: kernel stack top for SYSCALL + uint64_t userRspScratch; // offset 16: scratch for saving user RSP + int32_t currentSlot; // offset 24: process table slot, -1 = idle + int32_t _pad0; // offset 28: padding + + // === C++-only fields === + int cpuIndex; // 0-based CPU index + uint32_t lapicId; // local APIC ID + uint64_t idleSavedRsp; // RSP saved when switching from idle to process + volatile bool started; // set by AP after init is complete + + Hal::TSS64* tss; // pointer to this CPU's TSS + + // Per-CPU GDT and TSS (APs use these; BSP uses globals) + Hal::BasicGDT cpuGdt __attribute__((aligned(16))); + Hal::TSS64 cpuTss __attribute__((aligned(16))); + }; + + // Get the current CPU's data (reads gs:0 which holds the self-pointer) + inline CpuData* GetCurrentCpuData() { + CpuData* ptr; + asm volatile("mov %%gs:0, %0" : "=r"(ptr)); + return ptr; + } + + // Get CPU data by index + CpuData* GetCpuData(int index); + + // Number of online CPUs + int GetCpuCount(); + + // Initialize BSP per-CPU data (call before interrupts are enabled) + void InitBsp(); + + // Boot all Application Processors (call after all subsystems ready) + void BootAPs(); +} diff --git a/kernel/src/Hal/SyscallEntry.asm b/kernel/src/Hal/SyscallEntry.asm index d18eaa1..c4caea3 100644 --- a/kernel/src/Hal/SyscallEntry.asm +++ b/kernel/src/Hal/SyscallEntry.asm @@ -1,28 +1,34 @@ ; ; SyscallEntry.asm -; SYSCALL/SYSRET entry point and user-mode transition -; Copyright (c) 2025 Daniel Hammer +; SYSCALL/SYSRET entry point and user-mode transition (SMP-aware) +; Copyright (c) 2025-2026 Daniel Hammer ; [bits 64] section .text extern SyscallDispatch -extern g_kernelRsp -; ============================================================ -; SyscallEntry — called by the SYSCALL instruction +; ==================================================================== +; Per-CPU data offsets (must match CpuData in SmpBoot.hpp) +; ==================================================================== +%define CPUDATA_KERNEL_RSP 8 +%define CPUDATA_USER_RSP 16 + +; ==================================================================== +; SyscallEntry -- called by the SYSCALL instruction ; RCX = user RIP, R11 = user RFLAGS, RAX = syscall number ; Args: RDI, RSI, RDX, R10, R8, R9 ; Interrupts are masked (FMASK clears IF) -; ============================================================ +; ==================================================================== global SyscallEntry SyscallEntry: - mov [rel g_userRsp], rsp ; stash user RSP - mov rsp, [rel g_kernelRsp] ; switch to kernel stack + swapgs ; GS now points to per-CPU CpuData + mov [gs:CPUDATA_USER_RSP], rsp ; save user RSP to per-CPU scratch + mov rsp, [gs:CPUDATA_KERNEL_RSP] ; switch to per-CPU kernel stack ; Build SyscallFrame on kernel stack (push order matches struct) - push qword [rel g_userRsp] ; user_rsp + push qword [gs:CPUDATA_USER_RSP] ; user_rsp push rcx ; user_rip push r11 ; user_rflags push rax ; syscall_nr @@ -61,13 +67,14 @@ SyscallEntry: pop rcx ; user RIP pop rsp ; user RSP + swapgs ; restore user GS o64 sysret -; ============================================================ -; JumpToUserMode — initial transition to ring 3 via IRETQ +; ==================================================================== +; JumpToUserMode -- initial transition to ring 3 via IRETQ ; RDI = user RIP (entry point) ; RSI = user RSP (top of user stack) -; ============================================================ +; ==================================================================== global JumpToUserMode JumpToUserMode: mov ax, 0x1B ; UserData | RPL3 @@ -79,11 +86,5 @@ JumpToUserMode: push 0x202 ; RFLAGS (IF=1) push 0x23 ; CS = UserCode | RPL3 push rdi ; RIP = entry point + swapgs ; switch from kernel GS to user GS iretq - -; ============================================================ -; BSS: scratch space for user RSP save -; ============================================================ -section .bss -global g_userRsp -g_userRsp: resq 1 diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index 236895b..81f7431 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include using namespace Kt; namespace Memory { @@ -145,6 +146,13 @@ extern "C" void kmain() { Hal::ApicInitialize(g_acpi.GetXSDT()); + // Set up BSP per-CPU data (GS base) before enabling interrupts. + // ISR stubs use SWAPGS which requires GS base to point to CpuData. + Smp::InitBsp(); + + // Now safe to enable interrupts (SWAPGS-aware ISR stubs are installed) + asm volatile("sti"); + // Initialize ACPI events (SCI, power button) after APIC is ready Hal::AcpiEvents::Initialize(g_acpi.GetXSDT()); @@ -220,6 +228,13 @@ extern "C" void kmain() { Sched::Initialize(); + // Boot Application Processors (all subsystems ready, APs can schedule) + Smp::BootAPs(); + + // Flush any stale PS/2 mouse bytes that accumulated during boot + // (edge-triggered IRQs can be lost while spinlocks disable interrupts) + Drivers::PS2::Mouse::FlushState(); + Kt::SuppressKernelLog(); Sched::Spawn("0:/os/init.elf"); diff --git a/kernel/src/Memory/Paging.cpp b/kernel/src/Memory/Paging.cpp index 79f0eb1..39ae8d9 100644 --- a/kernel/src/Memory/Paging.cpp +++ b/kernel/src/Memory/Paging.cpp @@ -2,10 +2,17 @@ #include #include #include +#include namespace Memory::VMM { Paging* g_paging = nullptr; + // Protects user page table modifications from concurrent SMP access + static kcp::Mutex pagingLock; + + void LockUserPaging() { pagingLock.Acquire(); } + void UnlockUserPaging() { pagingLock.Release(); } + extern "C" uint64_t KernelStartSymbol; std::uint64_t GetPhysKernelAddress(std::uint64_t virtualAddress) { @@ -184,7 +191,9 @@ namespace Memory::VMM { } bool Paging::MapUserIn(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + pagingLock.Acquire(); if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { + pagingLock.Release(); Panic("Non-aligned address in Paging::MapUserIn!", nullptr); } @@ -211,22 +220,25 @@ namespace Memory::VMM { PageTable* pml4 = (PageTable*)pml4Phys; auto pml3 = walkLevel(pml4, va.GetL4Index()); - if (!pml3) return false; + if (!pml3) { pagingLock.Release(); return false; } auto pml2 = walkLevel(pml3, va.GetL3Index()); - if (!pml2) return false; + if (!pml2) { pagingLock.Release(); return false; } auto pml1 = walkLevel(pml2, va.GetL2Index()); - if (!pml1) return false; + if (!pml1) { pagingLock.Release(); return false; } PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); pageEntry->Present = true; pageEntry->Writable = true; pageEntry->Supervisor = 1; pageEntry->Address = physicalAddress >> 12; + pagingLock.Release(); return true; } bool Paging::MapUserInWC(std::uint64_t pml4Phys, std::uint64_t physicalAddress, std::uint64_t virtualAddress) { + pagingLock.Acquire(); if (virtualAddress % 0x1000 != 0 || physicalAddress % 0x1000 != 0) { + pagingLock.Release(); Panic("Non-aligned address in Paging::MapUserInWC!", nullptr); } @@ -251,25 +263,27 @@ namespace Memory::VMM { PageTable* pml4 = (PageTable*)pml4Phys; auto pml3 = walkLevel(pml4, va.GetL4Index()); - if (!pml3) return false; + if (!pml3) { pagingLock.Release(); return false; } auto pml2 = walkLevel(pml3, va.GetL3Index()); - if (!pml2) return false; + if (!pml2) { pagingLock.Release(); return false; } auto pml1 = walkLevel(pml2, va.GetL2Index()); - if (!pml1) return false; + if (!pml1) { pagingLock.Release(); return false; } PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); pageEntry->Present = true; pageEntry->Writable = true; pageEntry->Supervisor = 1; - pageEntry->WriteThrough = true; // PWT=1, PCD=0 → PAT entry 1 = WC + pageEntry->WriteThrough = true; // PWT=1, PCD=0 -> PAT entry 1 = WC pageEntry->Address = physicalAddress >> 12; + pagingLock.Release(); return true; } void Paging::UnmapUserIn(std::uint64_t pml4Phys, std::uint64_t virtualAddress) { + pagingLock.Acquire(); VirtualAddress va(virtualAddress); - // Walk without allocating — if any level is absent, nothing is mapped. + // Walk without allocating -- if any level is absent, nothing is mapped. auto walkRead = [](PageTable* table, uint64_t index) -> PageTable* { PageTableEntry* entry = (PageTableEntry*)Memory::HHDM(&table->entries[index]); if (!entry->Present) return nullptr; @@ -278,20 +292,21 @@ namespace Memory::VMM { PageTable* pml4 = (PageTable*)pml4Phys; auto pml3 = walkRead(pml4, va.GetL4Index()); - if (!pml3) return; + if (!pml3) { pagingLock.Release(); return; } auto pml2 = walkRead(pml3, va.GetL3Index()); - if (!pml2) return; + if (!pml2) { pagingLock.Release(); return; } auto pml1 = walkRead(pml2, va.GetL2Index()); - if (!pml1) return; + if (!pml1) { pagingLock.Release(); return; } PageTableEntry* pageEntry = (PageTableEntry*)Memory::HHDM(&pml1->entries[va.GetPageIndex()]); - if (!pageEntry->Present) return; + if (!pageEntry->Present) { pagingLock.Release(); return; } // Clear the entire 8-byte PTE *(uint64_t*)pageEntry = 0; // Invalidate TLB for this virtual address asm volatile("invlpg (%0)" :: "r"(virtualAddress) : "memory"); + pagingLock.Release(); } void Paging::FreeUserHalf(std::uint64_t pml4Phys) { diff --git a/kernel/src/Memory/Paging.hpp b/kernel/src/Memory/Paging.hpp index c2e031f..1d05aba 100644 --- a/kernel/src/Memory/Paging.hpp +++ b/kernel/src/Memory/Paging.hpp @@ -124,6 +124,13 @@ public: extern Paging* g_paging; + // Protects user page table modifications from concurrent access. + // Required for SMP: e.g. WinServer::Resize unmaps pages from the + // desktop's page tables while the desktop may be allocating pages + // on another CPU. + void LockUserPaging(); + void UnlockUserPaging(); + extern "C" uint64_t GetCR3(); extern "C" void LoadCR3(PageTable* PML4); diff --git a/kernel/src/Platform/Limine.hpp b/kernel/src/Platform/Limine.hpp index ea2e7de..09167ca 100644 --- a/kernel/src/Platform/Limine.hpp +++ b/kernel/src/Platform/Limine.hpp @@ -76,6 +76,16 @@ namespace { }; } + + // MP request is outside the anonymous namespace so SmpBoot.cpp can + // reference it via extern. + __attribute__((used, section(".limine_requests"))) + volatile limine_mp_request mp_request = { + .id = LIMINE_MP_REQUEST, + .revision = 0, + .response = nullptr, + .flags = 0 + }; // Finally, define the start and end markers for the Limine requests. // These can also be moved anywhere, to any .cpp file, as seen fit. diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index d0f9f59..497ef48 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -1,7 +1,7 @@ /* * Scheduler.cpp - * Preemptive process scheduler with user-mode support - * Copyright (c) 2025 Daniel Hammer + * Preemptive process scheduler with SMP support + * Copyright (c) 2025-2026 Daniel Hammer */ #include "Scheduler.hpp" @@ -12,8 +12,11 @@ #include #include #include +#include #include #include +#include +#include #include // Assembly: context switch with CR3 and FPU state parameters @@ -23,16 +26,16 @@ extern "C" void SchedContextSwitch(uint64_t* oldRsp, uint64_t newRsp, uint64_t n // Assembly: jump to user mode via IRETQ extern "C" void JumpToUserMode(uint64_t rip, uint64_t rsp); -// Global kernel RSP for SYSCALL entry (written by scheduler, read by SyscallEntry.asm) -extern "C" uint64_t g_kernelRsp; -uint64_t g_kernelRsp = 0; - namespace Sched { static Process processTable[MaxProcesses]; - static int currentPid = -1; // -1 = idle (kernel main loop) static int nextPid = 0; - static uint64_t idleSavedRsp = 0; + + // The scheduler lock MUST be a Spinlock (interrupt-disabling). + // It is held ACROSS context switches to prevent the race where + // another CPU picks up a process whose RSP hasn't been saved yet. + // The resumed process releases it. + static kcp::Spinlock schedLock; // The idle loop runs in the kernel PML4 static uint64_t GetKernelCR3() { @@ -41,18 +44,22 @@ namespace Sched { // Startup function for newly spawned processes. // SchedContextSwitch "returns" here on first schedule. + // The schedLock is held (acquired by the switching-from CPU's Schedule). static void ProcessStartup() { - // Send EOI for the timer IRQ that triggered the context switch - Hal::LocalApic::SendEOI(); + // Release the schedLock that the switching-from CPU held + schedLock.Release(); - if (currentPid >= 0) { - Process& proc = processTable[currentPid]; + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; - // Set up kernel RSP for SYSCALL entry - g_kernelRsp = proc.kernelStackTop; + if (slot >= 0) { + Process& proc = processTable[slot]; - // Set up TSS RSP0 for hardware interrupts from ring 3 - Hal::g_tss.rsp0 = proc.kernelStackTop; + // Set up per-CPU kernel RSP for SYSCALL entry + cpu->kernelRsp = proc.kernelStackTop; + + // Set up per-CPU TSS RSP0 for hardware interrupts from ring 3 + cpu->tss->rsp0 = proc.kernelStackTop; // Jump to user mode (never returns) JumpToUserMode(proc.entryPoint, proc.userStackTop); @@ -78,6 +85,9 @@ namespace Sched { processTable[i].userStackTop = 0; processTable[i].heapNext = 0; processTable[i].args[0] = '\0'; + processTable[i].runningOnCpu = -1; + processTable[i].waitingForPid = -1; + processTable[i].sleepUntilTick = 0; processTable[i].redirected = false; processTable[i].parentPid = -1; processTable[i].outBuf = nullptr; @@ -92,15 +102,15 @@ namespace Sched { processTable[i].termRows = 0; } - currentPid = -1; nextPid = 0; - idleSavedRsp = 0; Kt::KernelLogStream(Kt::OK, "Sched") << "Initialized (" << MaxProcesses << " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)"; } int Spawn(const char* vfsPath, const char* args) { + schedLock.Acquire(); + int slot = -1; for (int i = 0; i < MaxProcesses; i++) { if (processTable[i].state == ProcessState::Free) { @@ -110,19 +120,29 @@ namespace Sched { } if (slot < 0) { + schedLock.Release(); Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots"; return -1; } + // Reserve the slot so another Spawn doesn't claim it. + // Use Running (not Ready!) so the scheduler doesn't try to + // dispatch this half-initialized process. + processTable[slot].state = ProcessState::Running; + processTable[slot].runningOnCpu = -1; + schedLock.Release(); + // Create per-process PML4 with kernel-half copied uint64_t pml4Phys = Memory::VMM::Paging::CreateUserPML4(); // Load ELF into the process's address space uint64_t entry = ElfLoad(vfsPath, pml4Phys); if (entry == 0) { - // Free the PML4 and any pages allocated during ELF load Memory::VMM::Paging::FreeUserHalf(pml4Phys); Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys)); + schedLock.Acquire(); + processTable[slot].state = ProcessState::Free; + schedLock.Release(); return -1; } @@ -132,6 +152,9 @@ namespace Sched { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack"; Memory::VMM::Paging::FreeUserHalf(pml4Phys); Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys)); + schedLock.Acquire(); + processTable[slot].state = ProcessState::Free; + schedLock.Release(); return -1; } void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages); @@ -140,6 +163,9 @@ namespace Sched { Memory::g_pfa->Free(firstPage); Memory::VMM::Paging::FreeUserHalf(pml4Phys); Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys)); + schedLock.Acquire(); + processTable[slot].state = ProcessState::Free; + schedLock.Release(); return -1; } @@ -151,6 +177,9 @@ namespace Sched { Memory::VMM::Paging::FreeUserHalf(pml4Phys); Memory::g_pfa->Free((void*)Memory::HHDM(pml4Phys)); Memory::g_pfa->Free(stackMem, StackPages); + schedLock.Acquire(); + processTable[slot].state = ProcessState::Free; + schedLock.Release(); }; // Allocate user stack pages and map them in the process PML4 @@ -174,7 +203,6 @@ namespace Sched { } // Allocate and map a user-space exit stub page. - // When _start() returns, it jumps here and calls SYS_EXIT(0). { void* stubPage = Memory::g_pfa->AllocateZeroed(); if (stubPage == nullptr) { @@ -198,7 +226,6 @@ namespace Sched { } // Push exit stub address as the return address on the user stack. - // UserStackTop - 8 falls at offset 0xFF8 within the top stack page. { uint8_t* topPage = (uint8_t*)Memory::HHDM(topStackPagePhys); *(uint64_t*)(topPage + 0xFF8) = ExitStubAddr; @@ -216,6 +243,8 @@ namespace Sched { *(--sp) = 0; // r14 *(--sp) = 0; // r15 + schedLock.Acquire(); + Process& proc = processTable[slot]; proc.pid = nextPid++; proc.state = ProcessState::Ready; @@ -230,8 +259,11 @@ namespace Sched { proc.sliceRemaining = TimeSliceMs; proc.pml4Phys = pml4Phys; proc.kernelStackTop = kernelStackTop; - proc.userStackTop = UserStackTop - 8; // account for pushed exit stub return address + proc.userStackTop = UserStackTop - 8; proc.heapNext = UserHeapBase; + proc.runningOnCpu = -1; + proc.waitingForPid = -1; + proc.sleepUntilTick = 0; // Copy arguments string into process proc.args[0] = '\0'; @@ -261,29 +293,60 @@ namespace Sched { *(uint16_t*)&proc.fpuState[0] = 0x037F; // FCW: default x87 control word *(uint32_t*)&proc.fpuState[24] = 0x1F80; // MXCSR: default SSE control/status - return proc.pid; + int resultPid = proc.pid; + schedLock.Release(); + + return resultPid; + } + + // ==================================================================== + // Schedule -- core context switch logic + // + // The schedLock is held ACROSS the context switch. This is critical: + // setting the old process to Ready and saving its RSP must be atomic + // with respect to other CPUs. If we released the lock before saving + // RSP, another CPU could pick up the process with a stale savedRsp. + // + // The RESUMED process releases the lock (it was acquired by whatever + // CPU called Schedule() and switched away from that process). + // New processes release it in ProcessStartup(). + // ==================================================================== + + // Reclaim terminated process slots. Called from BSP's Tick only, + // NOT from every Schedule() call on every CPU. This avoids holding + // schedLock (with interrupts disabled) during PFA::Free on the + // hot scheduling path. + static void ReclaimTerminated() { + schedLock.Acquire(); + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Terminated) { + // Grab the pointers, mark Free, then free memory after releasing lock + void* stackBase = (processTable[i].stackBase != 0) + ? (void*)processTable[i].stackBase : nullptr; + void* pml4 = (processTable[i].pml4Phys != 0) + ? (void*)Memory::HHDM(processTable[i].pml4Phys) : nullptr; + processTable[i].stackBase = 0; + processTable[i].pml4Phys = 0; + processTable[i].state = ProcessState::Free; + + // Release lock during PFA::Free to minimize hold time + schedLock.Release(); + if (stackBase) Memory::g_pfa->Free(stackBase, StackPages); + if (pml4) Memory::g_pfa->Free(pml4); + schedLock.Acquire(); + } + } + schedLock.Release(); } void Schedule() { - // Reclaim terminated process slots — free deferred resources first - for (int i = 0; i < MaxProcesses; i++) { - if (processTable[i].state == ProcessState::Terminated) { - // Free kernel stack (deferred from ExitProcess — can't free while running on it) - if (processTable[i].stackBase != 0) { - Memory::g_pfa->Free((void*)processTable[i].stackBase, StackPages); - processTable[i].stackBase = 0; - } - // Free the PML4 page (deferred from ExitProcess — can't free while it's CR3) - if (processTable[i].pml4Phys != 0) { - Memory::g_pfa->Free((void*)Memory::HHDM(processTable[i].pml4Phys)); - processTable[i].pml4Phys = 0; - } - processTable[i].state = ProcessState::Free; - } - } + auto* cpu = Smp::GetCurrentCpuData(); + schedLock.Acquire(); + + // Find the next Ready process (round-robin from after current slot) int next = -1; - int start = (currentPid >= 0) ? currentPid + 1 : 0; + int start = (cpu->currentSlot >= 0) ? cpu->currentSlot + 1 : 0; for (int i = 0; i < MaxProcesses; i++) { int idx = (start + i) % MaxProcesses; @@ -294,83 +357,146 @@ namespace Sched { } if (next < 0) { + // No ready processes. If we were running one, return to idle. + if (cpu->currentSlot >= 0) { + int oldSlot = cpu->currentSlot; + processTable[oldSlot].state = ProcessState::Ready; + processTable[oldSlot].runningOnCpu = -1; + cpu->currentSlot = -1; + + // Lock held across context switch -- the idle loop + // doesn't release it; the next Schedule() that resumes + // a process from idle will release it via the resumed + // process path. But idle is special: we release here + // because idle doesn't go through ProcessStartup. + // The RSP save is safe because interrupts are disabled + // (Spinlock), so no timer can fire between Ready and save. + SchedContextSwitch(&processTable[oldSlot].savedRsp, cpu->idleSavedRsp, + GetKernelCR3(), processTable[oldSlot].fpuState, nullptr); + // Resumed from idle -- lock was held by whoever switched to us + schedLock.Release(); + } else { + schedLock.Release(); + } return; } - if (next == currentPid) { + if (next == cpu->currentSlot) { + // Same process, just reset time slice + processTable[next].sliceRemaining = TimeSliceMs; + schedLock.Release(); return; } + // Prepare the context switch uint64_t* oldRspPtr; - uint64_t oldCR3; + int oldSlot = cpu->currentSlot; - if (currentPid >= 0) { - processTable[currentPid].state = ProcessState::Ready; - oldRspPtr = &processTable[currentPid].savedRsp; + if (oldSlot >= 0) { + processTable[oldSlot].state = ProcessState::Ready; + processTable[oldSlot].runningOnCpu = -1; + oldRspPtr = &processTable[oldSlot].savedRsp; } else { - oldRspPtr = &idleSavedRsp; + oldRspPtr = &cpu->idleSavedRsp; } - int oldPid = currentPid; - currentPid = next; + cpu->currentSlot = next; processTable[next].state = ProcessState::Running; + processTable[next].runningOnCpu = cpu->cpuIndex; processTable[next].sliceRemaining = TimeSliceMs; uint64_t newCR3 = processTable[next].pml4Phys; - // Disable interrupts while updating global kernel RSP and TSS, - // preventing an interrupt from using stale values mid-update. - asm volatile("cli"); + // Update per-CPU kernel RSP and TSS RSP0 + cpu->kernelRsp = processTable[next].kernelStackTop; + cpu->tss->rsp0 = processTable[next].kernelStackTop; - // Update kernel RSP for SYSCALL entry - g_kernelRsp = processTable[next].kernelStackTop; - - // Update TSS RSP0 for hardware interrupts from ring 3 - Hal::g_tss.rsp0 = processTable[next].kernelStackTop; - - asm volatile("sti"); - - uint8_t* oldFpu = (oldPid >= 0) ? processTable[oldPid].fpuState : nullptr; + uint8_t* oldFpu = (oldSlot >= 0) ? processTable[oldSlot].fpuState : nullptr; uint8_t* newFpu = processTable[next].fpuState; + + // DO NOT release schedLock here! It is held across the context + // switch so that setting Ready + saving RSP is atomic. The + // resumed process releases it. SchedContextSwitch(oldRspPtr, processTable[next].savedRsp, newCR3, oldFpu, newFpu); + + // We reach here when this process is resumed by another CPU's + // Schedule(). That CPU held the lock across its context switch. + // Release it now. + schedLock.Release(); } void Tick() { - if (currentPid < 0) { - // Idle — check if any process became ready - Schedule(); + auto* cpu = Smp::GetCurrentCpuData(); + + // BSP: wake sleeping processes and reclaim terminated slots + if (cpu->cpuIndex == 0) { + uint64_t now = Timekeeping::GetTicks(); + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Blocked && + processTable[i].sleepUntilTick != 0 && + now >= processTable[i].sleepUntilTick) { + processTable[i].sleepUntilTick = 0; + processTable[i].state = ProcessState::Ready; + } + } + + // Reclaim terminated process memory (BSP only, once per tick) + ReclaimTerminated(); + } + + int slot = cpu->currentSlot; + + if (slot < 0) { + // Idle CPU. Do a quick lockless scan before taking the + // expensive schedLock path. On a 32-core system with 5 + // active processes, 27 CPUs are idle -- without this check, + // they'd each acquire schedLock 1000x/sec to find nothing. + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Ready) { + Schedule(); + return; + } + } + // Nothing ready -- stay halted, don't touch schedLock return; } - if (processTable[currentPid].sliceRemaining > 0) { - processTable[currentPid].sliceRemaining--; + if (processTable[slot].sliceRemaining > 0) { + processTable[slot].sliceRemaining--; } - if (processTable[currentPid].sliceRemaining == 0) { + if (processTable[slot].sliceRemaining == 0) { Schedule(); } } int GetCurrentPid() { - return (currentPid >= 0) ? processTable[currentPid].pid : -1; + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; + return (slot >= 0) ? processTable[slot].pid : -1; } Process* GetCurrentProcessPtr() { - if (currentPid < 0) return nullptr; - return &processTable[currentPid]; + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; + if (slot < 0) return nullptr; + return &processTable[slot]; } void ExitProcess() { - if (currentPid < 0) { + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; + + if (slot < 0) { return; } - Process& proc = processTable[currentPid]; + Process& proc = processTable[slot]; - // Clean up any windows owned by this process (unmaps pixel pages from desktop) + // Clean up any windows owned by this process WinServer::CleanupProcess(proc.pid); - // Free I/O redirect buffers (kernel-allocated pages) + // Free I/O redirect buffers if (proc.outBuf) { Memory::g_pfa->Free(proc.outBuf); proc.outBuf = nullptr; @@ -380,12 +506,139 @@ namespace Sched { proc.inBuf = nullptr; } - // Free all user-space physical pages and page table structures (entries 0-255). - // This covers ELF code/data, user stack, exit stub, heap, and window pixel buffers. - // The PML4 page itself is freed later in Schedule() (can't free while it's CR3). + // Free all user-space physical pages and page table structures Memory::VMM::Paging::FreeUserHalf(proc.pml4Phys); + schedLock.Acquire(); + + int exitingPid = proc.pid; proc.state = ProcessState::Terminated; + proc.runningOnCpu = -1; + + // Wake any processes blocked on this PID + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Blocked && + processTable[i].waitingForPid == exitingPid) { + processTable[i].state = ProcessState::Ready; + processTable[i].waitingForPid = -1; + } + } + + // Find next ready process + int next = -1; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Ready) { + next = i; + break; + } + } + + if (next >= 0) { + cpu->currentSlot = next; + processTable[next].state = ProcessState::Running; + processTable[next].runningOnCpu = cpu->cpuIndex; + processTable[next].sliceRemaining = TimeSliceMs; + + uint64_t newCR3 = processTable[next].pml4Phys; + cpu->kernelRsp = processTable[next].kernelStackTop; + cpu->tss->rsp0 = processTable[next].kernelStackTop; + + // Lock held across context switch -- resumed process releases it + SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, newCR3, + processTable[slot].fpuState, processTable[next].fpuState); + schedLock.Release(); + } else { + cpu->currentSlot = -1; + + // Switch to idle -- release after resuming from idle + SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp, GetKernelCR3(), + processTable[slot].fpuState, nullptr); + schedLock.Release(); + } + + for (;;) { + asm volatile("hlt"); + } + } + + void BlockOnPid(int pid) { + // If the target is already dead, return immediately + if (!IsAlive(pid)) return; + + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; + if (slot < 0) return; + + schedLock.Acquire(); + + // Double-check under lock (target might have exited between + // the lockless IsAlive check and acquiring the lock) + bool stillAlive = false; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].pid == pid) { + auto s = processTable[i].state; + stillAlive = (s == ProcessState::Ready || + s == ProcessState::Running || + s == ProcessState::Blocked); + break; + } + } + + if (!stillAlive) { + schedLock.Release(); + return; + } + + // Mark current process as Blocked -- scheduler will skip it. + // ExitProcess will wake us when the target terminates. + processTable[slot].state = ProcessState::Blocked; + processTable[slot].waitingForPid = pid; + processTable[slot].runningOnCpu = -1; + + // Find next ready process to switch to + int next = -1; + for (int i = 0; i < MaxProcesses; i++) { + if (processTable[i].state == ProcessState::Ready) { + next = i; + break; + } + } + + if (next >= 0) { + cpu->currentSlot = next; + processTable[next].state = ProcessState::Running; + processTable[next].runningOnCpu = cpu->cpuIndex; + processTable[next].sliceRemaining = TimeSliceMs; + + cpu->kernelRsp = processTable[next].kernelStackTop; + cpu->tss->rsp0 = processTable[next].kernelStackTop; + + SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, + processTable[next].pml4Phys, + processTable[slot].fpuState, processTable[next].fpuState); + schedLock.Release(); + } else { + // No ready process -- go idle + cpu->currentSlot = -1; + + SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp, + GetKernelCR3(), processTable[slot].fpuState, nullptr); + schedLock.Release(); + } + } + + void BlockForSleep(uint64_t ms) { + if (ms == 0) return; + + auto* cpu = Smp::GetCurrentCpuData(); + int slot = cpu->currentSlot; + if (slot < 0) return; + + schedLock.Acquire(); + + processTable[slot].state = ProcessState::Blocked; + processTable[slot].sleepUntilTick = Timekeeping::GetTicks() + ms; + processTable[slot].runningOnCpu = -1; int next = -1; for (int i = 0; i < MaxProcesses; i++) { @@ -396,34 +649,34 @@ namespace Sched { } if (next >= 0) { - int old = currentPid; - currentPid = next; + cpu->currentSlot = next; processTable[next].state = ProcessState::Running; + processTable[next].runningOnCpu = cpu->cpuIndex; processTable[next].sliceRemaining = TimeSliceMs; - uint64_t newCR3 = processTable[next].pml4Phys; - g_kernelRsp = processTable[next].kernelStackTop; - Hal::g_tss.rsp0 = processTable[next].kernelStackTop; + cpu->kernelRsp = processTable[next].kernelStackTop; + cpu->tss->rsp0 = processTable[next].kernelStackTop; - SchedContextSwitch(&processTable[old].savedRsp, processTable[next].savedRsp, newCR3, - processTable[old].fpuState, processTable[next].fpuState); + SchedContextSwitch(&processTable[slot].savedRsp, processTable[next].savedRsp, + processTable[next].pml4Phys, + processTable[slot].fpuState, processTable[next].fpuState); + schedLock.Release(); } else { - int old = currentPid; - currentPid = -1; - SchedContextSwitch(&processTable[old].savedRsp, idleSavedRsp, GetKernelCR3(), - processTable[old].fpuState, nullptr); - } + cpu->currentSlot = -1; - for (;;) { - asm volatile("hlt"); + SchedContextSwitch(&processTable[slot].savedRsp, cpu->idleSavedRsp, + GetKernelCR3(), processTable[slot].fpuState, nullptr); + schedLock.Release(); } } bool IsAlive(int pid) { for (int i = 0; i < MaxProcesses; i++) { if (processTable[i].pid == pid) { - return processTable[i].state == ProcessState::Ready - || processTable[i].state == ProcessState::Running; + auto s = processTable[i].state; + return s == ProcessState::Ready + || s == ProcessState::Running + || s == ProcessState::Blocked; } } return false; @@ -431,10 +684,12 @@ namespace Sched { Process* GetProcessByPid(int pid) { for (int i = 0; i < MaxProcesses; i++) { - if (processTable[i].pid == pid && - (processTable[i].state == ProcessState::Ready || - processTable[i].state == ProcessState::Running)) { - return &processTable[i]; + if (processTable[i].pid == pid) { + auto s = processTable[i].state; + if (s == ProcessState::Ready || s == ProcessState::Running || + s == ProcessState::Blocked) { + return &processTable[i]; + } } } return nullptr; diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 93f9af1..bae61d5 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -1,7 +1,7 @@ /* * Scheduler.hpp - * Preemptive process scheduler with user-mode support - * Copyright (c) 2025 Daniel Hammer + * Preemptive process scheduler with SMP support + * Copyright (c) 2025-2026 Daniel Hammer */ #pragma once @@ -24,12 +24,15 @@ namespace Sched { Free, Ready, Running, + Blocked, // Waiting (e.g. waitpid) -- not schedulable Terminated }; struct Process { int pid; ProcessState state; + int waitingForPid; // PID this process is blocked on (-1 if none) + uint64_t sleepUntilTick; // Tick deadline for SYS_SLEEP_MS (0 = not sleeping) char name[64]; uint64_t savedRsp; uint64_t stackBase; // Bottom of allocated kernel stack (lowest address) @@ -41,6 +44,8 @@ namespace Sched { uint64_t heapNext; // Simple bump allocator for user heap char args[256]; // Command-line arguments (set by parent via Spawn) + int runningOnCpu; // CPU index running this process (-1 if not running) + // I/O redirection for GUI terminal bool redirected = false; int parentPid = -1; @@ -67,7 +72,7 @@ namespace Sched { int Spawn(const char* vfsPath, const char* args = nullptr); void Schedule(); - // Called from the APIC timer handler on every tick. + // Called from the APIC timer handler on every tick (per-CPU). void Tick(); // Get the PID of the currently running process (-1 if idle) @@ -79,9 +84,15 @@ namespace Sched { // Called by terminated processes to mark themselves done void ExitProcess(); - // Check if a process is still alive (Ready or Running) + // Check if a process is still alive (Ready, Running, or Blocked) bool IsAlive(int pid); + // Block the current process until the given PID exits. + void BlockOnPid(int pid); + + // Block the current process for the given number of milliseconds. + void BlockForSleep(uint64_t ms); + // Find a process by PID (returns nullptr if not found or not alive) Process* GetProcessByPid(int pid); diff --git a/kernel/src/Terminal/Terminal.cpp b/kernel/src/Terminal/Terminal.cpp index e784c86..6b3c5a7 100644 --- a/kernel/src/Terminal/Terminal.cpp +++ b/kernel/src/Terminal/Terminal.cpp @@ -13,6 +13,7 @@ #include "../Libraries/String.hpp" #include "../Libraries/Memory.hpp" #include +#include namespace Kt { flanterm_context *ctx; @@ -22,6 +23,11 @@ namespace Kt { uint32_t g_kernelLogDepth = 0; bool g_suppressKernelLog = false; + // Protects flanterm writes from concurrent CPU access. + // Mutex (not Spinlock) so interrupts stay enabled -- prevents dropped + // PS/2 mouse/keyboard bytes during log output. + kcp::Mutex g_termLock; + // 64KB ring buffer for kernel log messages static constexpr uint64_t KLOG_BUF_SIZE = 65536; static char g_klogBuf[KLOG_BUF_SIZE]; @@ -228,6 +234,13 @@ namespace Kt { } } + // Once a graphical app takes over, suppress ALL flanterm writes + // (SYS_PRINT from user processes, etc.) to avoid painting text + // over the GUI framebuffer. + if (g_suppressKernelLog && g_kernelLogDepth == 0) { + return; + } + if (c == '\n') { flanterm_write(ctx, "\r\n", 2); return; diff --git a/kernel/src/Terminal/Terminal.hpp b/kernel/src/Terminal/Terminal.hpp index 4c9447c..9402819 100644 --- a/kernel/src/Terminal/Terminal.hpp +++ b/kernel/src/Terminal/Terminal.hpp @@ -10,6 +10,7 @@ #include #include +#include using namespace kcp; @@ -42,6 +43,11 @@ namespace Kt void Putchar(char c); void Print(const char *text); + // Terminal mutex - protects flanterm from concurrent CPU access. + // Uses Mutex (not Spinlock) to keep interrupts enabled while held, + // preventing dropped PS/2 mouse/keyboard bytes during log output. + extern kcp::Mutex g_termLock; + void UpdatePanelBar(const char* panelText); void Rescale(std::size_t font_scale_x, std::size_t font_scale_y); @@ -138,7 +144,8 @@ public: } } - KernelLogStream(KernelLogLevel level, const char* desiredComponentName) { /* level for potential future rich event logging */ + KernelLogStream(KernelLogLevel level, const char* desiredComponentName) { + g_termLock.Acquire(); g_kernelLogDepth++; componentName = desiredComponentName; @@ -148,6 +155,7 @@ public: ~KernelLogStream() { localStream << newline; g_kernelLogDepth--; + g_termLock.Release(); } template diff --git a/kernel/src/Timekeeping/ApicTimer.cpp b/kernel/src/Timekeeping/ApicTimer.cpp index 108b6e3..2003c1d 100644 --- a/kernel/src/Timekeeping/ApicTimer.cpp +++ b/kernel/src/Timekeeping/ApicTimer.cpp @@ -7,6 +7,7 @@ #include "ApicTimer.hpp" #include #include +#include #include #include #include @@ -40,15 +41,18 @@ namespace Timekeeping { static bool g_schedEnabled = false; - // Timer IRQ handler: increment tick count, poll NIC, and drive scheduler + // Timer IRQ handler: BSP handles timekeeping+polling, all CPUs run scheduler static void TimerHandler(uint8_t) { - g_tickCount = g_tickCount + 1; + auto* cpu = Smp::GetCurrentCpuData(); - // In polling mode, drain the NIC's RX ring from timer context - // (equivalent to a real NIC IRQ handler, runs with interrupts disabled) - Drivers::Net::E1000E::Poll(); - Drivers::USB::Xhci::ProcessDeferredWork(); - Drivers::USB::HidKeyboard::Tick(); + if (cpu->cpuIndex == 0) { + // BSP: increment global tick count and poll devices + g_tickCount = g_tickCount + 1; + + Drivers::Net::E1000E::Poll(); + Drivers::USB::Xhci::ProcessDeferredWork(); + Drivers::USB::HidKeyboard::Tick(); + } if (g_schedEnabled) { Sched::Tick(); @@ -165,12 +169,25 @@ namespace Timekeeping { g_schedEnabled = true; } + void ApicTimerInitializeAP() { + // Use the BSP's calibrated ticks-per-ms value directly. + // All cores share the same bus clock, so the APIC timer rate is + // identical. This avoids PIT contention during AP boot. + if (g_ticksPerMs == 0) return; + + // Configure periodic timer at 1000 Hz (same vector as BSP) + uint32_t lvt = (Hal::IRQ_VECTOR_BASE + Hal::IRQ_TIMER) | LVT_PERIODIC; + Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_DIVIDE, DIVIDE_BY_16); + Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_LVT, lvt); + Hal::LocalApic::WriteRegister(Hal::LocalApic::REG_TIMER_INITIAL, g_ticksPerMs); + } + void Sleep(uint64_t ms) { uint64_t target = g_tickCount + ms; while (g_tickCount < target) { // Yield to other processes instead of hlt. Using hlt here causes // a deadlock: if the timer ISR preempts us during hlt and context- - // switches away (via Tick → Schedule), EOI is never sent, so no + // switches away (via Tick -> Schedule), EOI is never sent, so no // more timer interrupts fire and any process doing hlt freezes. Sched::Schedule(); } diff --git a/kernel/src/Timekeeping/ApicTimer.hpp b/kernel/src/Timekeeping/ApicTimer.hpp index c6a291f..a8b5b84 100644 --- a/kernel/src/Timekeeping/ApicTimer.hpp +++ b/kernel/src/Timekeeping/ApicTimer.hpp @@ -11,6 +11,9 @@ namespace Timekeeping { // Initialize the APIC timer: calibrate against PIT, start periodic interrupts void ApicTimerInitialize(); + // Initialize the APIC timer on an AP (calibrate + start, no IRQ handler registration) + void ApicTimerInitializeAP(); + // Reinitialize the APIC timer after S3 resume using the previously // calibrated tick rate. Skips PIT calibration and IRQ registration // (both survive in RAM). Only reprograms the timer hardware registers. diff --git a/programs/GNUmakefile b/programs/GNUmakefile index 0205300..3a5b503 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,7 +59,7 @@ BINDIR := bin PROGRAMS := $(notdir $(wildcard src/*)) # Programs with custom Makefiles (built separately). -CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo paint tcc screenshot +CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth desktop login shell rpgdemo paint tcc screenshot texteditor SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS)) # Build targets: system programs go to bin/os/, apps go to bin/apps//. @@ -81,9 +81,9 @@ CA_CERTS := $(BINDIR)/etc/ca-certificates.crt # Common shared assets (wallpapers, etc.) COMMONKEEP := $(BINDIR)/common/.keep -.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo paint tcc screenshot icons fonts bearssl libc tls libjpeg libjpegwrite install-apps +.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth login desktop shell rpgdemo paint tcc screenshot texteditor icons fonts bearssl libc tls libjpeg libjpegwrite install-apps -all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo paint tcc screenshot login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) +all: bearssl libc libjpeg libjpegwrite tls $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet pdfviewer disks devexplorer installer volume music bluetooth doom rpgdemo paint tcc screenshot texteditor login desktop shell icons fonts install-apps $(MANDST) $(WWWDST) $(CA_CERTS) $(COMMONKEEP) # Build BearSSL static library (cross-compiled for freestanding x86_64). BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc) @@ -192,12 +192,16 @@ fonts: paint: libc $(MAKE) -C src/paint +# Build text editor standalone GUI app (depends on libc). +texteditor: libc + $(MAKE) -C src/texteditor + # Build RPG demo game via its own Makefile (depends on libc). rpgdemo: libc $(MAKE) -C src/rpgdemo -# Build doom via its own Makefile. -doom: +# Build doom via its own Makefile (depends on libc). +doom: libc $(MAKE) -C src/doom # Build screenshot tool (depends on libc and libjpegwrite). @@ -214,7 +218,7 @@ tcc: libc cp include/libc/sys/*.h $(BINDIR)/lib/tcc/include/sys/ # Install app bundles (manifests, icons, data files) into bin/apps//. -install-apps: doom rpgdemo paint spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth screenshot +install-apps: doom rpgdemo paint spreadsheet weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music bluetooth screenshot texteditor ../scripts/install_apps.sh # Copy man pages into bin/man/ so mkramdisk.sh picks them up. diff --git a/programs/include/libc/math.h b/programs/include/libc/math.h index b4522b2..d1158d4 100644 --- a/programs/include/libc/math.h +++ b/programs/include/libc/math.h @@ -17,13 +17,24 @@ double ceil(double x); double sqrt(double x); double sin(double x); double cos(double x); +double tan(double x); +double atan(double x); double atan2(double y, double x); double pow(double base, double exp); double log(double x); +double log2(double x); +double log10(double x); double exp(double x); double fmod(double x, double y); double round(double x); +float floorf(float x); +float ceilf(float x); +float fabsf(float x); +float sqrtf(float x); +float sinf(float x); +float cosf(float x); + #ifdef __cplusplus } #endif diff --git a/programs/include/libc/unistd.h b/programs/include/libc/unistd.h index 5793856..4912e83 100644 --- a/programs/include/libc/unistd.h +++ b/programs/include/libc/unistd.h @@ -14,6 +14,8 @@ int write(int fd, const void *buf, size_t count); int close(int fd); long lseek(int fd, long offset, int whence); +unsigned int sleep(unsigned int seconds); + #ifdef __cplusplus } #endif diff --git a/programs/lib/libc/Makefile b/programs/lib/libc/Makefile index 5f7490f..d9f2a3a 100644 --- a/programs/lib/libc/Makefile +++ b/programs/lib/libc/Makefile @@ -36,10 +36,8 @@ CFLAGS := \ -fdata-sections \ -m64 \ -march=x86-64 \ - -mno-80387 \ - -mno-mmx \ - -mno-sse \ - -mno-sse2 \ + -msse \ + -msse2 \ -mno-red-zone \ -mcmodel=small \ -isystem $(LIBC_INC) \ diff --git a/programs/lib/libc/libc.c b/programs/lib/libc/libc.c index 52a9900..d3edb5a 100644 --- a/programs/lib/libc/libc.c +++ b/programs/lib/libc/libc.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -626,7 +627,18 @@ char *getenv(const char *name) { return NULL; } +static void (*_atexit_funcs[32])(void); +static int _atexit_count = 0; + +int atexit(void (*func)(void)) { + if (_atexit_count >= 32 || func == NULL) return -1; + _atexit_funcs[_atexit_count++] = func; + return 0; +} + void exit(int status) { + for (int i = _atexit_count - 1; i >= 0; i--) + _atexit_funcs[i](); _zos_syscall1(SYS_EXIT, (long)status); __builtin_unreachable(); } @@ -1439,19 +1451,6 @@ int fstat(int fd, struct stat *buf) { return 0; } -/* ======================================================================== - stdlib.h: system() and atexit() - ======================================================================== */ - -static void (*_atexit_funcs[32])(void); -static int _atexit_count = 0; - -int atexit(void (*func)(void)) { - if (_atexit_count >= 32 || func == NULL) return -1; - _atexit_funcs[_atexit_count++] = func; - return 0; -} - /* ======================================================================== stdlib.h: qsort ======================================================================== */ @@ -1520,3 +1519,270 @@ ldiv_t ldiv(long numer, long denom) { long atol(const char *s) { return strtol(s, NULL, 10); } + +/* ======================================================================== + math.h functions + ======================================================================== */ + +/* Constants */ +#define M_PI 3.14159265358979323846 +#define M_PI_2 1.57079632679489661923 +#define M_LN2 0.69314718055994530942 +#define M_LOG2E 1.44269504088896340736 + +double fabs(double x) { return x < 0 ? -x : x; } + +double floor(double x) { + double t = (double)(long long)x; + return (x < t) ? t - 1.0 : t; +} + +double ceil(double x) { + double f = floor(x); + return (x > f) ? f + 1.0 : f; +} + +double fmod(double x, double y) { + if (y == 0.0) return 0.0; + return x - (double)((long long)(x / y)) * y; +} + +double sqrt(double x) { + if (x <= 0.0) return 0.0; + double guess = x; + for (int i = 0; i < 20; i++) + guess = (guess + x / guess) * 0.5; + return guess; +} + +/* ---- sin / cos via range reduction + minimax polynomial ---- */ + +/* Reduce x to [-pi, pi] */ +static double _reduce_angle(double x) { + /* Bring into [-2pi, 2pi] via fmod, then into [-pi, pi] */ + x = fmod(x, 2.0 * M_PI); + if (x > M_PI) x -= 2.0 * M_PI; + else if (x < -M_PI) x += 2.0 * M_PI; + return x; +} + +/* Core sin approximation for x in [-pi/2, pi/2]. + Taylor series to degree 17 for < 1e-11 accuracy at the boundary. */ +static double _sin_core(double x) { + double x2 = x * x; + return x * (1.0 + x2 * (-1.0/6.0 + x2 * (1.0/120.0 + x2 * (-1.0/5040.0 + + x2 * (1.0/362880.0 + x2 * (-1.0/39916800.0 + + x2 * (1.0/6227020800.0 + x2 * (-1.0/1307674368000.0)))))))); +} + +double sin(double x) { + x = _reduce_angle(x); + /* Reduce to [-pi/2, pi/2] using sin(pi - x) = sin(x) */ + if (x > M_PI_2) x = M_PI - x; + else if (x < -M_PI_2) x = -M_PI - x; + return _sin_core(x); +} + +double cos(double x) { + return sin(x + M_PI_2); +} + +/* ---- log via exponent extraction + polynomial on [1, 2) ---- */ + +/* Union for double bit manipulation */ +typedef union { double d; uint64_t u; } _dbl_bits; + +double log(double x) { + if (x <= 0.0) return -HUGE_VAL; + if (x == 1.0) return 0.0; + + /* Extract exponent and mantissa: x = m * 2^e, where m in [1, 2) */ + _dbl_bits bits; + bits.d = x; + int e = (int)((bits.u >> 52) & 0x7FF) - 1023; + bits.u = (bits.u & 0x000FFFFFFFFFFFFFULL) | 0x3FF0000000000000ULL; + double m = bits.d; + + /* log(x) = e * ln(2) + log(m), where m in [1, 2) + Use log(m) = log((1+f)/(1-f)) = 2*(f + f^3/3 + f^5/5 + ...) where f = (m-1)/(m+1) */ + double f = (m - 1.0) / (m + 1.0); + double f2 = f * f; + double ln_m = 2.0 * f * (1.0 + f2 * (1.0/3.0 + f2 * (1.0/5.0 + f2 * (1.0/7.0 + + f2 * (1.0/9.0 + f2 * (1.0/11.0 + f2 * (1.0/13.0 + + f2 * (1.0/15.0 + f2 * (1.0/17.0))))))))); + + return (double)e * M_LN2 + ln_m; +} + +/* ---- exp via range reduction to [0, ln2) + polynomial ---- */ + +double exp(double x) { + if (x == 0.0) return 1.0; + if (x < -708.0) return 0.0; + if (x > 709.0) return HUGE_VAL; + + /* Range reduction: exp(x) = 2^k * exp(r), where x = k*ln(2) + r, |r| <= ln(2)/2 */ + double k_real = floor(x * M_LOG2E + 0.5); + int k = (int)k_real; + double r = x - k_real * M_LN2; + + /* Pade-like polynomial for exp(r), |r| <= ~0.347: + 1 + r + r^2/2 + r^3/6 + r^4/24 + r^5/120 + r^6/720 + r^7/5040 */ + double exp_r = 1.0 + r * (1.0 + r * (1.0/2.0 + r * (1.0/6.0 + + r * (1.0/24.0 + r * (1.0/120.0 + r * (1.0/720.0 + r * (1.0/5040.0))))))); + + /* Multiply by 2^k via bit manipulation */ + _dbl_bits bits; + bits.d = exp_r; + bits.u += (uint64_t)k << 52; + return bits.d; +} + +/* ---- pow via exp(exp * log(base)) ---- */ + +double pow(double base, double e) { + if (e == 0.0) return 1.0; + if (base == 0.0) return 0.0; + if (base == 1.0) return 1.0; + if (e == 1.0) return base; + + /* Integer exponent fast path */ + if (e == (double)(long long)e && fabs(e) < 64) { + long long ei = (long long)e; + int neg = 0; + if (ei < 0) { neg = 1; ei = -ei; } + double r = 1.0; + double b = base; + while (ei > 0) { + if (ei & 1) r *= b; + b *= b; + ei >>= 1; + } + return neg ? 1.0 / r : r; + } + + /* General case */ + if (base < 0.0) return 0.0; /* negative base with fractional exp is undefined (in reals) */ + return exp(e * log(base)); +} + +double tan(double x) { + double c = cos(x); + if (c == 0.0) return (sin(x) > 0.0) ? HUGE_VAL : -HUGE_VAL; + return sin(x) / c; +} + +double log2(double x) { return log(x) * M_LOG2E; } +double log10(double x) { return log(x) * 0.43429448190325182765; /* 1/ln(10) */ } + +/* ---- atan / atan2 via polynomial approximation ---- */ + +/* Core atan for |x| <= ~0.414 (= tan(pi/8)). + Taylor series converges well in this small range. */ +static double _atan_small(double x) { + double x2 = x * x; + return x * (1.0 + x2 * (-1.0/3.0 + x2 * (1.0/5.0 + x2 * (-1.0/7.0 + + x2 * (1.0/9.0 + x2 * (-1.0/11.0 + x2 * (1.0/13.0 + + x2 * (-1.0/15.0 + x2 * (1.0/17.0))))))))); +} + +#define M_PI_4 0.78539816339744830962 + +/* atan for x >= 0, using range reduction: + - |x| <= tan(pi/8) ~ 0.4142: polynomial directly + - 0.4142 < |x| <= 1: atan(x) = pi/4 + atan((x-1)/(x+1)) + - |x| > 1: atan(x) = pi/2 - atan(1/x) */ +static double _atan_positive(double x) { + if (x <= 0.41421356237309504) { + return _atan_small(x); + } else if (x <= 1.0) { + return M_PI_4 + _atan_small((x - 1.0) / (x + 1.0)); + } else { + return M_PI_2 - _atan_positive(1.0 / x); + } +} + +double atan2(double y, double x) { + if (x == 0.0 && y == 0.0) return 0.0; + if (x == 0.0) return (y > 0.0) ? M_PI_2 : -M_PI_2; + if (y == 0.0) return (x > 0.0) ? 0.0 : M_PI; + + double a = _atan_positive(fabs(y) / fabs(x)); + + /* Map to correct quadrant */ + if (x < 0.0) a = M_PI - a; + if (y < 0.0) a = -a; + return a; +} + +double atan(double x) { + if (x >= 0.0) return _atan_positive(x); + return -_atan_positive(-x); +} + +double round(double x) { return floor(x + 0.5); } + +/* ---- atof: basic floating-point string parser ---- */ + +double atof(const char *s) { + if (s == NULL) return 0.0; + + while (isspace((unsigned char)*s)) s++; + + int neg = 0; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') s++; + + /* Integer part */ + double val = 0.0; + while (isdigit((unsigned char)*s)) { + val = val * 10.0 + (*s - '0'); + s++; + } + + /* Fractional part */ + if (*s == '.') { + s++; + double place = 0.1; + while (isdigit((unsigned char)*s)) { + val += (*s - '0') * place; + place *= 0.1; + s++; + } + } + + /* Exponent part */ + if (*s == 'e' || *s == 'E') { + s++; + int eneg = 0; + if (*s == '-') { eneg = 1; s++; } + else if (*s == '+') s++; + int ev = 0; + while (isdigit((unsigned char)*s)) { + ev = ev * 10 + (*s - '0'); + s++; + } + double mul = 1.0; + while (ev-- > 0) mul *= 10.0; + if (eneg) val /= mul; + else val *= mul; + } + + return neg ? -val : val; +} + +float floorf(float x) { return (float)floor((double)x); } +float ceilf(float x) { return (float)ceil((double)x); } +float fabsf(float x) { return x < 0.0f ? -x : x; } +float sqrtf(float x) { return (float)sqrt((double)x); } +float sinf(float x) { return (float)sin((double)x); } +float cosf(float x) { return (float)cos((double)x); } + +/* ======================================================================== + unistd.h: sleep + ======================================================================== */ + +unsigned int sleep(unsigned int seconds) { + (void)seconds; + return 0; +} diff --git a/programs/lib/libc/obj/libc.o b/programs/lib/libc/obj/libc.o index e5031b446c2e04542f136edca452c86ab6745b2a..8d5d43a67fc0e04f0b1a92b3f0071c032e8f95c4 100644 GIT binary patch literal 228768 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≫wT6*P;Gw@pZz#DQ~z6Z3TH0rXNN9)#4ArZx#Ol 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∨Oj;fbRs|2y$-S$Jgff2RJOUHYMaW*(+S4i7U= z4^;Xgo*K!sn);uqk;A8r{?8mH|Br+?1dp3Gd}7wvux8S;88>azxH02=ruJAE{eK>t zmNk5&FZ#G?GqSQq|C+(y20Y)VsTa5?@<46`CTNr@caynh zwZx@B088A=xa8IsnuW@hE<6Vfp*OqcaGoZ3Zlnq`4_Z`nJ1Q5%=0>vsL%?g|%0?yf zZP6o(_c2gQTsx$ho-%6LEs~PRA+Q$3H;~MasSf8F(IPJ9N@b5}!|r|;P^{K) znRFdg%$KVTnPf0{xlwjR@#FPUGv3iC6t0jR%>u6W$i!l?Jl>G`Q!G@AQeLRnt1?;h zP>Sc!hH5WyW3NSxQqP3VS9>M~oKW0$3!8e5Mz!el7N7p$WaU-{ z?NXIr0_UZ!m3VgRseuJOagExocB*3E{+bvIhgH@sx+FT(L#*6C?(Z#nE_IQ9;YTr zFii71E&_!vDpj~7B1bE1WE`xFvUFS?&V#oAL^eo8ZTFaUnia!!myS6PJf^syCtaC&O;Mb^c%V z_-qQ>wCtDXxKJeu0ml-Z1_^wW6G{Qcdmbc9vz$KwG1lYT{P?#}E`lB%r?Cefl;|76^Z%D1Hs%6eXU8$k+eLhp^S|Zw6uikAO}0AN}LybHzc}?}ot5 zK>8KfeEJkW|5~W(>+!?i?e*zH)-OoVztPaQSf5e)K;_?N=%-k}Lesvn|>yzyNhoR5R zQvW)5VCUm!XQ@Bh(AQXhT0;L98v04ryYI1LQ`-Ma4Sk#S^Ah->q3_JH|Cbp08P+Gw z-^&fX{3vdc9{)dpZ~OulegsdBP6(VdCh_+HeCN+SeExhH8;5}B&(mSk=g%z$zjY+4 z;CCk|ieL5kU2X7p@*DYSRw5tMntz?apJDz{Y`++w`PUl!4)cc`{!$PBCWD{ig`af( z{HDQQ^e`Fj_8;HB*8SgY@UzUnDxv>74gOl@%k>}nAKwqx{QC`l=NPE$)Tdnk`hD2Y z&#<0PgCg%+|I>zE{(;m~tfWMs@-Gl1E)(38*kk;)jH|!r``{%PB*Uunq+JCDJew+ErSuc_1XIZcN zf6!Y_be;xB-8T5or zCsH=0c8FdoUu*CKys(3i=7oX$m9U9k#^rXS{}cFN2!i{bG3K@ZZ!!3p$B8c-u%=$WsYOr-VS=LgRrBvL_+uXa#n6i8KW6YJng64hObN~3Z}3~pA7VZB|5DgA z|2c!7eLD#`;#T_o|9e9(|6iV`v8jKF^?Ln$pTX{D3$YPdHxeHUlixRfQ>t`SPFUxxLAAZhA`){eiPoF}wiLjUpee!lTbEhdZqDTZ^b?Jrq+KJkVL`f)>_VZHQU zn$i3%hQ7vnnBF}>>#sHRE!Oj8blis4Z#VRlv()47Z2A1(X8qilObN~3ZRq9yuiufN zzt7OO-a*17{eDBAPLuwm1pZToKFj)36Xx$94E+Q@c)BEke*pCGHxR0NHP8AhLLI&S z|7Gwy%>ScmSC8M{4gOZo@n_7#pL>XZ{!H-!{SyiNIiUCTe~*X%F%SO)gP(aPf#)Ug zk2Cnc^YF_a{uu_pHcNim;Lm@OCY~IB4gkC2;h%5tCz!u5q5qc|`ZnuVCCq>P{xMe3 z=kLSLAaK(4?`DG^uzoPX{wC;s^C!*z<7FH@KWRW)di{LX;MbU+bo}0C=v%YY-)iV5 zSueknDQ*ZP!7=_`qp$y2&-}s9kLv#KVZOS|ADHGF$3xZli(bswW$-r%pB;Dr1e-nl z{mj?b-<`9GaJhgu*2k|04gSQ_{p$yhMPk3Kt7i;;_#Oh{oN(PxBjB3lsP!8Tu^i*Cpte82UEr`7$_eMfZQXp`T&> z*Jv0_{W+lb`7gu%+r-KP5O0M|&!5!>f8sn6zLoV7Y5OlR`1?HL_jwOLZ}4|ApWnt7 zeBbkfj~e`!J@faA9)8u}x0!!f9je3^*iC| z|8|2v!~ByH?B8ba7kT(!@$hdm_(4eE1qu9{4Swk1Z}af)HTW6k^KDFQg&zOi27fK{ z)2zhx_f^>R`g_3Ow?Y!~WgKPTRr4P=^ljF!h?SHGRDQ~^f2+rT+d-@CKVa}v?;}vs z{Y%H-PcmPgKcN4<;o<+);LDfylGg8E4E_whF%7{TzZ*ULgW&*$HKCcFkXlaJq|YzB zYVd>Sh(E-wV*kGdoA&=agP&pktE`tu&!59V?>m2tF<*X{82R7!@D~{T>>v@>Q8xrX z5X31;;P(%G{K;RDu+8<6e-muF|7nBYKA(h1*Po{v{A`E#?)mc;5C1%aAFLogN0fnA z?f%J16EgqH2gT?p5%y*WzGU#z%-_NF(f@bBrv3jNgWp+6!X*FSXz-_* zFZX%K-{s-s_lte~PhCj-GbxH+_58cj;LmvczuUuqz~EU(|7y}enaE$ z_WuVS{u2hj#r%Te2P*%V!C&U#-{axGXz*v4f288+{y%TV@ppRoKlSj}82rpE z`5A-1&%^(jho3X}S>`X2j&mHSeACcR&Qkw5L*JRDemm&luW$T-^pXeKffopJ_h+ACA|uU-NVx|6j*z=~egpuK)g=pbs6}F&U`*E1<{qqn7!A_&-$j z1C^f#4=8;0XLJJG_2l?Vx$gCo`1Kst%YKP> zvi?$a0#xP)39m& zXAFMjWG2Z#Q!~+A<75=!_cVOtH$8{XC_GTee+D+q|GLFLllYh6hen*B`+vQ~UqJjF z`~VO6&%vhow=!SP^B;eKNcW0>Z4kc1cNzN4_{9VW7fD4!f1ja$;Cd43>o;jm;zwDp zk3Z9_pZ^91bELX6@DS*I&Q_ zimUv!O;*h{D6o3eFqA*M-d|VbMR}zM5DUKYUwfD;Dku8ul7kU5Ias*4GMB28{E<{3 z*-|;vAD-a#hp+Z-uzF~I@wGpkO!Z&;Bg2#Zg$Jtk`VBARM0t1|K^21mi6IjhwpAuR zOC(zU<~Q;=C80;QJTPx1ik#dX`2|CV4L@nb$Wev9apQ|iPSs)sv(KM1_kwwZP6nR=ds(jRIP zp-qC+=qo(1^!<|Be^|A{fUu-?N9E9h?-gV#mG)4KWJzgXT=;g?`=MsxpR={4o6>Hc z!x)L+Cjjt~20pBY$_`tKRt(n}85kihYPVMH^oKU)&G60i&GMZyYeP&hU-b|9kh`bP zx@EoZ`;Ee?`2&q-wLuc#O2PoA7~x9l`MSz`=j|tcJ4A+V~ZjS^n@hfzW!ezA0GvZpHh}lYWlk zC5D~^TITQ}B)GTU1wi{mj!+uySVge*^dIfNnnfT%%Bu2$g%uYgS5mM;Y28LG%^e8_ zU0;)oM9Z51XHC8QUv_a{7sf8g5xUa>*Yaqzf0^VRMDhi+Yq`C3DywB4NMan{?+@)0m1p_GheEp(t1$Mr2f{o2p^p#wLZ2l1 zU)*O@`c##!4Az;Of?;zrlPX+a^;IcsJomInX1c^@HJBY&n3g~EB24tD6neC7GF{kAR3%6IQ<7lNnTmatE{NXDOp|0vcY3+NJ zCxd-&sdy(SwxF$l!1mC`qB5&!#b`enW#RhD+?Wx|QoU%!)##4(Bwy2p5(xM=Tfjj0 zM}Of#YuTH~`y*qr0);am&VcM+34ds>SmBjgSVFkM zFNQu&tlI4lzYS&hLwnFcA4|EH_z%A8fAJH8`!iTqY#j=Qi@k?1OL(aZ_LtUft^AGu z;M;N<41c2#RFd{MwisB?7LCfU`ewt*c=&>*>akj7PINpGz3MTETGjV*-Ew1hk>ZTikXX}HHK?UX zVOmM9v>GO$Z#D~m@VD2G2AcK`^Afp$Uorq&>m5lu}NS& zO4w|he_5n&^0i`(!1&&vG6xMp`(U9m-yfMKtR6*LAiRxutG>EbtNLfb4(Km6%NJw# z0HmxY#baJ**^Rf|{vveSVxjsfZqJepXc?}G7L$$Wm8>w69G){`@&&|JA6Vb8 zG9b2d!m&5Zhb<2h1NLJzbOHZ0J1V<}zi#O`5ByF-e>Vw~q>jRT;qY6qqxO5hRkA)# zeDpu;r?N2owX2t6JdS$NZx!!!tO%4oBmKhGE5;Hy5ITO1kpLWwo*oycinfXG?6v8IW4QbcgMKz%zn9j>&>OR*%vuM=AOOKUj=0REso>(>#CVG)(_HE4%u` zFT-xrMyF;~e$ErhD6Q&k9-&%%|A#*RsGtclgj#c~+c1}fzRE|wHHkA&MfebVNuk{z zh4%VFhjJFC5X26KSPvVBOgI=iWCA+Wng`*Mwqa6GX#h3hlgNQD^wsRRd>-Y`ap*;+ z5kH4#E$R?H9Qx#=&^}-2aL%G6*qWSgqx{`2$=LZ3i!%L5tSJ36R)-HortJ>>kZX;@Xo;Nh0_F3ZJes(ka&qmbv2$M*2>(Q$ zeYV{4&`#$|zy?A;?Y6E882mSL&mo*5$%CSOQWI&WP)`a@T>722dipT&*sv& zZNti7_+@O3teWAd2!vk@)D;Bkd{~D+T<{rfp8S#VdDv4~%S=!RY!pBzeK;Z`gYy1ar z2CbTVke3_6p02S~O@Kjom!zM?NC(2}19d|Kbx=zi{rm*7{_rl^E4&>z_+7ycdz}b| zu^y}!!SIkk7~bkc?`e_YL*R)m*h3yu!20V;rIA#VECZTQ8IJHMv30F zY7WEimmD3hs9s4%?QZ5I*cN%d8GsATSoxcpcB}sG5LJ!$Z(r4a^U;}J`6<0+4(LkY*bllckqYbE%@G1d@#Hl z6WuKAz5dbi&nPy#f6FN(6#ggH4g|tlt(rvehQ)+}LNysk2kS0B6o{0d19qbW4xt17 z1QbXWS<7Fc)NiWsYb~cZk^eWc3y28P+yizwB(j$Oi1KGi*VvW#t?}CcLzp_j1g~F`?L;P74-dceO7V-UGq>)~Ro>#E#8jLifVJct)Qk`=v3D_=q80 z@K_)`v(KT@@Tc*>H2?%7MfhoXJvu)Tls|k>qGZcJRk5Zq-I9X$S{h}(v6|Z5wz83= zZ@DqrzL7N0{5uS28%~23n*RfNz~c!YI=IX7Z{8qtV3qoGmp@wMf2Q@F)>e$gR)6?s z=)-DA7?Zc~EURJLn88*Jje{`*D}Ni>mtW8tI-F!(cRNZ7w#NMDNM_ht*W^dIEfSmr z=O`<@4Zr|h_*r<%!CmO>k}U{1MEQm9@}jkF+kymY(y}UY^>?9GvGALgJ8iqB{hB{A zQ8;K84e*CwFKBJ4Q|e!U3Sb?ZqCnR2iy=V4RuJ-se+cgkgb!HjL}b#z>cghBoT5}w z`eX8}nmb4;NS6mZaCXNGv}!y^g4bFabuT1SeZEzbjlvqNji=y^g>TN^Te__b?aePd zw4@LV+jjpQys;7MK?{3~zLELcimHE3TKIls{%Rz07R?FmomO~g@f*sNwz3o>#|VVc z=iAT^2uNbnvQ2I_szDb1yAS)D7aiuW3qr8?vQa2&d0X~(WImj4(E4*|@5tu!3AhL2 z3ALtKwPTToAhjc@ykW9{!&!ZJx>b83<=6Ri5Yn4-k%{%EL#z2q*aX=F_>qA`;))j- zwofV#ua^j`IuWuLFy_!Jz_^9xdO*K`F{d>@i(J8uW&>mh;Q#vPe|7)F$8X9Dny>?y z>Ysxs6a3xT=U={{vj2d*IkPXhGyQXRHFj~}*7+VX8^OY4UImsMSI$p9_?N`SO| z$^+qVc#9GU|6T4PYbsCc-?L39-Tih*alanaOh6f5wO)T4CVROb%Z=% z-S>UPD9Tsu@I8}{l9u;kt_|BmX29GPWJHe_wA0x+C*CllL zMC-a-K!)EAt@j3uMQOfpm*To1Rzrn&mNZ1f(PD^IvkY{Qy2Pp>e-o*TteQhIb+uKq z2aW*B`dT%qz!v&)Ix>0IbzP7tLJ+sL4{spIFNmDEt)yUoHBI%^@D5gnzRVvVDfZ$~FS%Aajvq1;!C06Z3 zYH9oHHU`Ks1+8rP`c_W2d0up39KV6OMcBlh=?#RZd7D4xIjb6CV;y5opoIwv9;;RZ zEptkBmLYOpCA;v9+hq3Z8;At^kY)G_U$mALOGrIx4`f&UdA{2qn%Bns z;?lx5t!011+B|MkaTmnq++D5ML*!Hh>7#&4VIiyL1XyFi z_bX}H_f?O{*Q&nrg}=r~TCp9X6n3usUoSwYUq|} zc|6vVy6lz*?G9glK5A7719S45$s2R(JI2-f>hhagfW0{Ua`TCJ^ZeS}`!HHGf)lOz z2dHcc)DZqU#*f^i)Rka>EonNNMp+Oiw-{w-O^l@W3l7-hEBp>ey4H;vsKwcTAd;F% zHtg|jGSgwi>jg}GX>)OSU%}P^-(Wc4%!!&?!R;6jX*3Zwd(rz-Y=iVsSkuAU=VEcDdI?JZyJDbUXoFXi3ERTraC0}vjmwxMv zn0)=A?&89IOD6ilZ(s^38Stsfa%}84^9RQNNT_a;D2b#FD=BPVyw4Y@!twOh0dGT@ z0c+ydaxU3n{r*CzuKCsY0O3=6mQ}2x^+Vgg}Z(EZp=&m&6{AC zh(8<=8@UzjVsqk$zO_VQDlQ zT=Jlot4kW0+wcb7(jex8pl)d6B#!sYazu#jcjPX{@^BFCIa-|U&5yrs)}g4#;X7d=BucL@h#IO-7( z0?{!GteWeQ8?(r&d6?d_t=gOL<^ZIm&F2!n@J4uTEB=bA>XR2#&MKYrMI$MfL0;&O zls~+IS>Qv5KT`qwon!z7J4$f&8lIkp^VfHa!+HMAMPx>3G9BR(f9&=Y&AT{9QHT@S zt8Sw5=y{71pDP6hCf|61lVUEfdIt zFVlW^Fzd2xYTbl97)fnYRp;QGel`L!Dm#uk1FIV%ro7d4%u9BIecncgZIHZbo1fg! zd%*y@f;;Rsq&a#T;RN;|HIxXy5eUCZq1w<@dAO~!0%gIvt8mQ@=Sqc}E2ahq9P$_L zvO<4D#bzIcx2Qh?bId+;ltFQ7*b}1m!SLZg_+8qaTPyyPfbJ;R5fH_Fy#9k=CcL4g zYjl6$$9FoFKA z0m|(h(7+r_lzV-ficdnH15z;|hI8zm#?N>^j>a*|oJ=-6wctIv5r;Xj8X7`d#2Ux+ zz_%9(HsPAb!Clc+f`xUfk-s#4)!It&R2aDb+Ak)-QSyh& z5q|l`c7IB@`6H3L0pt(YK1i=fNh2H|_zR`GH`h{y^sQ5adOgq-XMf#qDAPEp!@4gw;7>J-p(K<$rD)H^~o8xvewyOU}rH4^9$`PXhTto@gp}6 zIPmm`XJJ1vs|ouE2!(!~)ks0%&kRZ*!tq2w>pH4v!JT)+*H1fG{e!vilIp{Gl^2f2 z86DXQ&0IdKVTzzz_tba*3h4x5JC{};MmT&bMT#Qh1i}iav1(=@AKEt^c&zL80ln3* zqqWal#J1uv;Cm0Cwi?oGvR?zd>Q*Xi*=O5DoBGNxst@Dv^GD#1oVKm{hY{9we*o6N zv(EK19B{mBh3M8mV69UINSvesc+qcA{c6QXYu&rWkr`;7v@kuiZ#L2og<3{dANCYm z6W*gsoPifVF-zybTWk`g;$2B;;f4|lTXcu?92TavJB%Qd*Kd?J`s+FshdaSzER9Se zZFQpX=S~C+G5t;oAL6cUPu|Ke7}t2h^)=(dj7$W?*5<1L;0x_b!Cg+V@XyWv zfh-gIzaLr;D<_wS_7vHxXVs9{suwymc9G?uv(3NRhl3}Izb#i@tQ)*T=UKdE zQ*7ZSzv`mc-XIWuO9n{7KZaVzF7#n-49+@TA~0PwuE{vW5$E+tOqA?>16DDne`GhR|z zT{ts|MWh`6k%CC1p?>gAXkXZg_JvpDI*Q!i(KmazOShNQL7tvQFartZl)&bfFkD;l zNyCR|3(~=GAEpX$+dBh1l^e3WH2hi1d(rc$IUliy!<+@gMygyV;nnh&fQiV1xFe8! zlRd#7n~2;m^~Wlwu+SVgqwX3L=XzZ^kZvG&86)g05H|a$%99I=!@u-TTI}5(m;l0F z<#uZX`ADzMWeLKh-e(|D?9W9ZMd#3=9!t|O>4a&wPa9=gU*b(HX4-`NO>!egVReBT z{Nd*xq}l40CWMFl;in(x%zE!OtJX^pp`V4d{AZFXQo9Xxf$(eC1l&!jYk%T9bl1^w zq`$6~(D*mkHsOT==(V;HN!vfD<4mybd8$U3C@6N@00_*?TNw;L&)0pz9|rK1R51Kc zn7Oa@EjXz+-U(c#1NI<}QT0v`Ta>z|i8$TYlq7M7>V1FR(?ko6{K5_l*4;{!PK%7` zO+i%$=YQ+mjy#&dd^rg=2Mr9mn< zj(*yEx_c?Wsde{q7xkpH`F0ly3YVImi!ZBmYPzQ}sqTU<*J|DM7`SIhb_EqlYC|}! zOQa^h7^!Du4j~;vE*BRh^zTacLQ|IZ6#nl;eNF!#PjH%c2K{6wYKQPTkYoq&|Nv>wV}HiuL6u^_IgvjonazO z!P|i))3nLdQq1!Zy(EJ&^o*dNv?4JD@Qm9hI#MUe%s83x+F&K(DhKRTkr@M4n-)7<6xE1fjT;_^yYR;Z;dY<^*dM7QP;b(x45#sh?za$fb}|7G8OP- zz3pPESE{LL^gl!O&@$4Ip4t^nwFf-bPk?1%{_KfU9GC9&bf$3@6NbmR-p9+PK{)WF z%9keU?*eLXg?7Nxg;LHl)JfDp*SaV-?i1y@C=>a*m@GzPV4MslB;wItvTf&~NC`cn z{#&4hHJ(n-IlKYdI3J%cWHu=!-6}7guRY_* zcnM|3s-`8Z<7Yi`*0J#$mD+ReR@}u4sL93JI#0SvmT?<%supN3c(PSW#&5MIPnrr^ z=F>fcH@Fke04Ym`a{uVUw&YP>% z%OT+acxle8_vBHv^Q5Fsc9KBM$-6wgMXp-fN6CO^xDi8?667-uqorgi-Nn)jMo2Qe z3EN;xzuA+ePAJUN&7J`&PsT;st!^t$Zp$TMPPWnC>ghBd_>auBn_(9=8-;@Hc2DnB zzXDs@(RsVW<2f>49i$?q>e5`Q>uob8>gPaAR$atB7u#hgERTtxwbPv|J?Tegpxy55 zOb9DI2?Yw`C+)PTAB+z|$+#*#X{xz0He?f<`2Isoz(vYV|)Zc(9m^3)oO_ztlTu%?0 zHk^kwIM*{!oqBUfgY%@ej0G1|Y8Sc(OWKM_PIG5SQ$E8kywH;=Zc`!dRx=MPnXEO) zh;>Sdxkbrn8jCsONMcrCktQ)`csi;Ez+%quWFLw#Db0I^r%xL(XDMQy^Q*)>=YLL2 z9-c|=;W^XhUey1!qdh!Nch79xb*FpsRN4DuYAbb%;hKzM9*#4$P4#r@4gyEE`=d;4 zZLv-AB+Wf4HaQ`=MgSZ-`*C=h?CJDN`H`!FQRrIPMwt?!s87VQ=B=n)pnYv-yH-^swN0$G(>JCk+YK)r`Ikr%;<|&U#|g1kn$?!;6i@agfOd0g zsZ_B+!;|XCx`p6=4R;;e=k7#w0&~~J%^i2b&&2v%Eyt6ic7c}GmUH6D(RU^&xpb}Z zT*@A1Ur!g_ED}7PWAt1fPxfN)=f;-OmU=%=4>wF4=2-taJP!9{zTF1%G2}W4lbKsC zo|B9TUR5N=8btjFjH}!VZLgV0_0BfNIF&*a&J%;~M@2@7Q!(C7i2BJ`Gq4AI-^@{m z)DdPZ%q#qSXeMm|`6JH|a*lCfad?UK*Nwzqw~9yKJzk7~Zrz7e6Cit*tX%-VR&idN)`$vAfkwrXYl|YRkFC<@4rg zFT30ExcCVl7V^yZs>z`|DJL2&&|Wt?sAI)y>jrE$lUAbrFI@fwE&tBraszl!b&$S5 zib_WIb?G@X^E9|5N4A-xtd)+OF$-L0_PgU>#TK(?hqo6^ z&xc3FcGOYfU|DZw4$Ar$C6x0w$?1&Lw@uH2qhdQMPtF+IX?hy}bu5l#yG+l|zW|GC zytlQ0jb_r3dtXXpdn6-WrO%ha>FJoSxK*PzEw(x|o93Wb6bs z095jF{1JG0jCdis54-1nqWiFUe7kh14PqyfE>m=<^m9@9oz1AIeNflt>SSAw<0Fh?zMgxiyEoNDh z;hDL$i=|yYYw{F8Q2;P&iI%@a^XB!*KLMX(XJw#6I3gyyqPI&)kJwm5{dc*xl74Sy zv{BOU&3>wq`m;(_Ix2baT=JP$IX?4B($LMnN<+7sNwiX{K&hd{TK?iAYG}MmLldJKdI%A4xGC3}u240v(ASxKEMtRe#w8bArd@CHL4i}o zt7)CPYdr)qFg@$c^`N3D;5+EIGSS?B&|d+Il&S7R*e zdKNUx!M)SWbkc0j9PM0_!|gc~B|A?Jn6t=yP<4hqPzE7g?hKlKjE<2~%5juLeHe{c zf?~5Xce5RWH^#WS*@>}<`e&U6W6e&k1}7h(L5tI1w6qH+$ZrgHHE=vjQQs}kW@&gF zg<-A}J;y^3^`%aO6XO~Tbx|mS;Pi-knx9$d9#WyAoDH{!$tQITgdZE?1loIu$vm6( za&KJh5mDeB<+Q<@!Z`GZ`d}|ih4T>vGR>}SVxOkjPi@5vnC9?y%6#f*Vm8v;lRC#r z)7{f%DzUJ;r@g02GF@j}yq`-pJF6?~Xlg)@!vUt^6tlCFqndVH#Z&K(=#= z*6Myr&I9S@FOAs#x(V0rmwZ>hOtL3TQU3$HV(yoZjm$$NnEh}cd~D>YGTUWs4>3PB zMz`sguZ^|~@Hg%6x9a<#jkx$N$C%I;~W?A~_D z)*J44%RFtPk)rbI+7|Fy&#XIp(Wi_qH}yT5g4}Tgmx>vYw%!>0?olyFb8;V}y125n z33Luu>x?e_kN@Rdxv-g%Ed|>nM$%I~{K5cYOhV2~L zFn<-cO-9nBqhga1Mh%=!_i5YW?XPKPlfJ|}PqkzQU1khz zr)&%@jm~4kO;q7r79{8Vkm1P$BS+Q+TadkKxb*6H2$;NWnl*;!?4#m3YF|4}P_5y) z`(MZ6@UqKuZt(RO?pqqHz9atc9T9#RV;-PH|a%cK+k{= zw^`4kFY%m*ZM)n2*>klw^p13OQ=FAeb#Ll?*deAcKkvz$(}>IVv{Uw-yNo@`K|`>E zq(jo5biR-1AdyGFy30|N!W&&-vC)S)PMED-d54qbZ_zC6d$ei-)QG=*ZX6%{N!>M* z2Ad`CosY?T=l>4xx;RU}3IaqYaFS4p^WuqicXe}%!&3Bc&)%`F>FE}Q#p~@ZWAPp! z?2EKM#}u#6uN1Gy-6>P^HLV*N-9RJb5JVGYX$zIUjrm4{P)W}qql36Xg}7G9q_s62 zYU7+Z->Ih^&5Wd8pDuvp6Sx3FbFo;|>nBiBlzV9` z)$3!_iJ2Fodc8!oBfIPjwX752tk=(`GBG`u)|z^Ko?L5k=^jwMemT|Yt7NRJUcXXi zy?g=tk9zVv@L;TJTnf9rOLxUA*z#BFt}97g_Ha9JkF-5VqPO*X(B?Iyi|hdX=Kx;tDvjz*RUQ$*L)h22PU6rBy6SxVGj2ab5aSLycWA}-0L zwJvmUiZ)l@ZTuSFY}beI>E8Xo$->XqbJ-YVUk|sU^ugyJ?KNH0FNN2~eSL=BTXl%- zvV5v{9=mLeI?%C7Pjb&F?7mLc-BB-^xys#^!#X<~=y?h{-|Y-*?c8pgs;9=?JLCP5 zly8`Yl{Narto8j(i2W{6Nm8$nN>jcH|ok@+_b4 z*zyf;BcBw_eHOpXCMoK@2>)Zx-dlI=*_o9-ZdSNU`nV_8cCKCa)l)wOziuU`sMju~ z+4Kjat}B-^xS;oUo%N(d8662mbFb8T>S@$RqFkFW`}_*6pQ>gjA|vYOO5i-HF91s= z^z@Qv>O5CR?YAF3cj0pZp?6jxxX*=q<7ZqncY)^7v#E{q7?)o%S4+_4r-*hTcUY3U zEa&36S8B;ptz6&1UC(71dWy;emu1P1{~UVOd5YLlie%lo{$%K7zEYE>f;nw94Tt~y zl!;Uqc-(hE_X3^#d?@z45Ped#*Dn7ZYRfoYF}Y8LwnT#wRXdMZqGks1-H;d-3cPFh zUtT)VX4?E{6tB#Y<+OQ^?UlGe#z0JEvLEwY{a9kR$0s>x%zbuHxX!Qavfh{{q7Uo! zN8viR!NUKOaB07X15doIv3aggHmdJVbmV|G_@hBSPmr^9v~_0G)Cdoo+6N+C?PfNs z-NI#7%;FyV3s;*FgG{JdxLmU61|i!9`VSD{#`{GseX|;U+A*AFVf_u@E=x#hYdCF< z1-`L-wvCr6s6tluPj^@U%rYgMsQ(`B0r$m|?*3t3*R~_*3vNLfZkxM@SaCbuisMz` zmLl8KH%TSr(7@d)y3xQ*Ax_-S2GKt&0g^~pq7zciV|G_FC#abbN(TSnwbKIXH|-@kgab;(tgfF`rg4175T<=6aI3qESXdjx( z%%m>f5b5{CWB8vmGnb*rE?y&&CSwTw%Z9@91ZhEY99bC3NrX`~6qmF5OALnRUM;1q zVeb%0|1z&JqfTayH4VJJ6VK}}Hfr1=qz4^FE;N7FPW&E66Z;=UZ(6^3g}zJ9hn!xm zr0Q>o0=TiJNhuiI0 zmD#Em9cgW%$%Y$MixZ-au9bg1A&0)HUMAC$=5momK$?tk%#LbpEf=C^>uFWVrE4*> zMSsF?-@chGM#zQ|kSF=`C1$&+T+A+Tm+@EK^W0_J_7eA^AvKNli!{3gKP*z2=*L7m z6N!<+^*yCylA``m;&=|~Ic5$&lEFtD3H&_?KEe%tk{jIF=85_=crG}gJ4u2&vPv4m z-HmvI2MAF=7LCBt@F)%5k_{6i-C3wRZjLrg$S?{sJJgM6kWEI^pLQx!AL^!_OvTP4 zrxzzj@1dgmiy$J-QlPYyy=soZhl;`Cpi-LLZ9)u946Wm_4;J0C0WAh9P{;a0Y_J#< zw}H@D6$Pw{xj+Y16xh060qA#b==)|~2%)=1ofz$#X zhS(5XGni{=JA|6SYRw?7Aw8i6qEUxRi3W1b2w5}Gu3?GtMB<6D6-B$q*0HGn3BcIa zL&36-dWudPfa#dw;HaZZ?(KVMVY~LxW!?E( z1ai~Bf3pBv2m5>5PHrh?N2PeeZ~Ke*I#Z1+@M!2H@~KRe@m!WErJAM6z$yYVkts43 z0~KF+b_cT0$qPzqB1?40%10)vkbcJDXTa%pof)(fHe-x8$!!;NqD+qqK=re=BypU| za_;#k86+9by$tCQqlmM~iPX-xSW6b|{oRV$T8eOe0%goOUrTitp*=Gs+zdTQi1FZa zUdfOU-BgCW=W9Z^ZY~*fW^1N5s?y4vqaEx; z_AJWIWu6YX!pAnsuUse#2oMiGSd%Efa+v^7e$`W=w43j7HLsGb(R>%0W3k2`qv1nc zQJzPCiDM6cWgyD(F4Oi5lU>nguJ$<(h>o~r=h_#Zu3D8G8J{0>OxSGgcCm%w!4Crd zr`v4pw!di1B(`=Ruj;MiTZ<{d7XHP)Hf%1iV#VCXVi+sa|M zF6V1o`HPw^muXvvQYQ}TbFucaJ&-&^R%~>w>`us7)KiaU&ek@%V$+%DYa3k>+qBp5 zebN_+c7?W%O}h_Gr7uX~C8afqS{npMjm#F%w3B*QBQw9CakGPpQUwhP5fZ&>ZitIP|`AZx>l9k(BI{D597xmi^HDShX zbH&@(=5Fh!UYUlDZgbn*TEQ0O>!l(ss9XgN>Eic$F7=`E#% zSJ`{`;w`tkkJ??V-Rp`U$)Es=|Ku)?efSew-_RB0SZ3UbAE|_ET}pVz<^}h&5KO~V zd9CXza^@WE+OEu$4F?5W+m~$Hd0-^V+zMEzg6?_savQD z?J`%dQfF4O2+=m^%&N|9I&)!6xcOiR!Yy)3;Rv_bU992c4}Zjv)*U*I>T=lvt*9U6 zy38)nit=S@s8&=cQ^T~PaWXYrD>|K0edwDEpA6|UE^bZN?Fd)DD6W1{T>Tgqx9hL8 z>qgi{+wHnV+jZl141Jse-I<`zv7tLNG{a;`n7-=T)f1rfi257RU@)EXi*mO|)YtKd z2(E;F@QC`i37-5TQ9hVFBvCe)d?YdXNM2H3)#H!ts9J%??dJDY&jwN@JrL|a1%R#t zg1+iDBQFa05CN)t4NIjgS8MDqt|YMk-u|Mm`o|L3=VH~o3e1D)Zy8{J*jL>?#f15e zh`aVoeQ6)(JdXP;>bp+`;6$ZLJ5T-I?JsrQ{X0wuuP*+rM?=AqHOegWCV= zFQ|k^E6C8h{B0b*d)5P*o_$R=JnZ3`;T_#moa`w~2{^Up@ED|Lh!~q=bR3;>a!O%J zznnluy6BWxlznxM$iN=NqoFFUbz+Jr2MPQw(zckBK(0-L z8nYC5uqg)@UlHAbQ|i?EUkVtCI{ z32EB7ssQ#}ex5oG!Rm^sUAuQfjcFvNIOUU(O-ybV%;8K$KN~#G zKA_`r{}S_n{!awV%zx^)84{mIR8(B+Ot7E%tzc=E>-R~q z?$aM2u^!N0=DcRODSj)CTdZCaNUvUI-yc{y(A%23+FXu?M+X7IXkR7^XLnD$xn#@H z3s##sOl+P;X?3sb*GJ=)KP%F|~ay zp!a~#$PoBSyAQ5UE+N=+%|`JSNH@xkQdFJ;jQ5Hc1kfpR47RZ=fF`J#gAM#m*!H;V z1|4_n4P3qZvB{hL2E!9|9tBM|py}2ZHqn;=%T^O$87x1l!X$mvYLZO;-E_iWYGzWE zhClG(%{zXziN8eyR%7s*qH={!PcYQ8KN#viO)@lXhQg4mXL^FAo+V(ZWP)UA!Zc(6 z557QNL7Iv_NbO0wiP2BiM@W975+vi|tRuPB4zp@o$g0Sa+!oqJsSuQu)&y-o3X@$R zd6%w`yz6mN^5c3(C3k_9nOz71eExP5s9@7 zN>&~h#>o;RrY6ETnYjZhtJPFWALM9~7%)*?Rl?{76eju{r{rH(aBVj)M z#pc!Kj5(;Ca{)$mXXF$9fRHreo=u-aA7 zc7bt>+hr6b7&Rq9a2veWE2X~Id%qWj_j?;<0X4Jz)ffgfrKsgJsO7Y!;%sy2Dq&nL zR-3EDQ$kEPpAzp1l)fwA6HYg8H|{f#d(61rMDBX?1{1j(%nv=teduZSaLZ;-I05-^ z!i@>2y)oflFLL*KAM%34L*PwJrx9U43JYCmlJ4>)xq?6F`xEinpZIMe6@Hr-f?%4A z(V?Lulz~~C(t5N;?a4G?NV|`Gxcjh<8S|H}#zOH|^5OofKdE#6Ngd=Sf!srhcs-Q( zNFo#xAI>=5RA zlH^;8>~Ibt6v;c2ke!E%v&H+bza7gK945?G55=vOW%*8XE z58E4kI@61`&lNM`!4e#j9de_{WBd*l2*~15l%YEflCokATo6%|U3xkYB8D>EDT~9~ z!NHpuZN;Qj+Pz?1z4~El>o=YaOr2Ib>civa%n=n> z!YjBr7W)RYL!KK+-=7#z`X>zoF>R(e$DH{RNH?8pR_oX6;v6~UsDK^-dN9Y$H1~>c z@UkZ*1yw#E^4HUr>|FCr@eXDZ7FmJlPlKudDk08Ajlj55>W=TfSi?WH(W$p{*}Yoe zZzg7CD*3^D;eE9^a9{$%A~oX|w_;vgOIK&`+j$_Ai+vHfjZwSrEgc%AAQdv*V%qyP z7bxxA6+lQv?u#NgUeHs#hokppw8o%;I-1HTPjXlKlKA zDENq?`}9ZD<;T^qj2e1ZJ-+l7vp3wpeP|<@sUg-Nn;!O_KZ?~@h0N86 zc>)Cr7=_%9#xeAxt}8>Im(9Z{m^WW`_x#1k zEM890tmRHL>o3Sjfo&Y|2xg58DIrVX>YH){Dckz(J=D;b|Z>?O~y>Dl!?ojKPtO0=q{1GzYfZRXDOt}05vaPQs*rZ49B;p4c!NC>5Mw$nQMlti>%N{CLGL`lNQ2CG6tciFVV z#Sxa-))mjDxiNcaVuc2XZmTiXA{a+Wn$Jd@;*_gXd#-*`8*V${j=0?8nVoXoCYj5= zWt=e^DGO9{&n2T)M?RLR=s2hFySq|fpq#w9r;uFov}5DJ9Wj&EpeVGGIQUZ$nNZa{aooQdIlTY>$k#jM0*LGSV`M^c;z_ z3`n2Tsq#5O9&@!E{BG0~-u|;~yNrCY!!s5QMn9R+OY3H~Co|hz;NXnD-N>gfau_pX zp5kDJO*7^xlJS|6@hMUpwCKS;RR96TX$v7MYSlTiFkyh5>?SAfZ>!Q|#bL95YS;xXEoQH`rE-5D`v z`uON5G%s8nP4h@{H-R~%kB2c_xEMBo(G-V~xI+E8R1Y(Z=O!K8WcSCLFnhs9HUuhJ#wspY#(B7l8@PmwhHWWR3jXfCGi05c9J||wKvHB|dT+)9} zz>g}2A9W86(tB|BD$5)64XW4X&~wo!GyN41rGja|vs@!~&`PsIB1ge{gmtQem4gew z8jUJANbR?Rdmy=5=D;HKNzn>g8&-%Lg+5<1Z^R)N*WD`~Ame>nY?t}%;&aM>F1{l0 zufz|O`$4S4$-AcG+5jFzQ4|t#;vPmpIzmdO_cVfH^8KSAKfsX77k+AnHVd_~jbt3_Ob0{hSTfb_;jn@tX0obhLZkG!4`h7BN1%`k7d_Vp z9vFfxDY{DGh=n;)a18^lVe+u9ZB?19Vk9mOh50XHaf8rZyGfI5?>oP5!b{5WS&PARdXrsH#}UXO!R$Bjkj zj2&xK;E-x;sZjy%ymUH>reDZK7h?9E3RmbmdVMFB=?t+fKI+g+ukGodl9_&+etRdp)^>UeFE)_)AH|l&B6>7-EA8&K!evBv z!<~Iatnr{=jiz`bT3RI>-;v#UG`@G8$^o*2@{pm-Z;gJ*w&0=GY*CQ-@OB;K2d z3zSi|?@fF#F_Cj%?ZL!H6Jt0ZO?+I*J)Zc0SHbgucL!(7r1U$y@2c5%y&riAk*xg4 z`?=cmb8mH0tg<>OoJ4r1NZ4>vB#AV>KtSE3*GSR>YW)LAFLTz``pZe*CaL0loAeV` z+r9Tw((>eFV52M{Tb{fkIn~ZrE~O4#fdQk4wLSU0Q_^La8>`1`fUfL^|u{XbW~ff=(wt5RP(Dk-qw-mPG_ULt>Y6NsU>p6^%ETraiz>1 z>KL|EJYnl5D~jhP>kcc1=ML*BOTqJ$^*&chJnviobm94@^-~PbPu8-u7@lQmccm$K z?n>Lml@iacv^`vD_sE{Ka5}--h7nGWq*H%ZM*B1U4{H4%(jQ4DLK4>_=}&Z`#x|}e zI<4ibtp}Q(Vzu@b)EV8%Kt6%I_K`j~V%LgW*nhs2+~HZ~6Z$5d>TXHBi-Xu-r(K_p zJTOI<8Aj*Y86@)tW62G2V{wC6!x-0!O$cn6Ym6Oy!2gBumEpZYE9g^6cgemot~IIB z`*$yqdp7NjG!7QirkOYQysMO3Xd8<*Q!gyny7_3?ERdACmSB#eto;V;~aG&03RP);{%%x`XO?Qs*m*8O=I3M;B2nLTO@mg;D~t^eAnUVJK^oU&o7UTqpEt zQ20A^i>A!HGx@1xX_HZdU>jAoTuE(uKB>K)r)K7@N%tlZI)&R#ZJW0(VT6!Ac8>;( zM>)dZ=p`!`nyv~+^g3iihBffLqzo<%SUUcFDJzqDB&MlMM|Z~tbu2#CxFh3owd~CJ z%pVd$iS1(UYE&4(ja6HT}#0vaH-gma#foc)*BMVviueC+{6JvwcZlrGvlEYfllDzpU@?al2CKB+PB z|G4oIUvS%FeoglYzc%mkQ1Rce)4*GzvVRpuXm&m%@FfaHukkGP>9fXTCT@fA_36Kx z*U`AX+w(47JV%k%mF68$dkv0O%W$;%zA)f@neQX2#S?baIC~gy)u=W1n9}-mv^)I_ zZgs{ir2lR*9c6@}sl6;X-K{id`7(x};gF$>WavnwTTEQ=gW!kE|Dv}#N+cr+dR%!` z$8$HsJDB|j6uR_IS7XmO3qRNVfdZ+kakjuM;bO!A5(a?s3&8v)aK@|*&*QwS(1*Re z{%%UoD6;hcnI6Yu_jde9j1=7X8hHvbr;KB=)I@>hsCEXbvya1IR3=-mUQFUBQ4(AL zjdlwi8M%PTN@4+(xF}x-eS*ZL5H^%cO!!mmu2bvGxDZyRx-`Wti^;Kj;byWG-K#2; z5Zy67T`)$v{E=o99Mi=f1?*h(|GCf5joW7^6JhGX1-x3&uu*DK zy*TIOyRYW+@UUXJ?c$?Y&K5*z_kTw5|K2EQEI;+jXfd05oYr>!;c zo8(foLjN;OT7Smgx$`y?z+EcxuQi1hIaW;rRS5ip0L993?KJ80<1h z3E|@vm|0Npp#B6oC{IX&D1VYjVD(anD9Eods^+gYuX$1!i}8L^Gzi=tDKCP<)Ii^c zQst+(tjvi&>Tl_kh+ zuswGnvJcZ1auSsM4|@GW>=91&D!pDuQN4bb?iAgn-%myNqi55Mh zZ=|A)`c|ii?J{nS^(ItGHoRRh$CuAasF2xNziqqS%K^HaeC%{d!HPoq;!lsOH?fy! z?Ow$0JD~4fgyiTlu9V4+q z+(LscHX;;s$!zH<_OMt+Yy2`{Cy{=#7HLurk2^3B^ zoFSBwD=}qyrNLVa~$=l*X zk#vy@rm3YK+yU1JPi<=5f-mdA_1^pRW>@CptyT z6W>Zi(OdWo$|>5P*h)pMi7UNM(Ms>#UKHK!eZnbHL{bF7Kb_c$&6xSAhZ7GZtWDs= z>k0pmiPedJP2`S@sYq#V;cKXfc?)l`c6#>ndu76{2`Ju{useYkG{peA$Gb=U|eV7^|tvXy{bK{Jb2yXc@i(RwL1E7`E06rzj%(c{ekfw}tfkrhb#`gQ}v13w09RjEr;_CIDXe8Oc8!G&Wjc8x@V z?SaCgyo$VF|Kf(gW87;7+^r9fX z2n^nVwH&Xv^pENFO2Qr*^8b}^BaP|%6C3FDkHqisVr?po^H5Wt_!!j~v6TXyfWN^) z6cx}cRoLDqVtk%RabrhV1+p9Jy7i~2vHw64b>K+;)G@jc!FrFO%ORtIV)Sp?N8b;1 z{e}_xVIv6rhza(WlnM)^z?BJiv=h+pZWfK{vl@R5WtYw=HbJFjciL!EE#7TAhOFsP z{dsbOo|kvpl$%hU$(q!>BoG(sXk+q)=!2K(ua{s<<8QRso#fJeC(nE~`A*r6$*;xj zI9b>}lQ-*sBqsle4;b7gpWKegvsJ|XC@Lv~GRtOR?Sn%+ix%q3Lv(JBTu5NnMc7U} zD)cJNd=!^EWd%hXQSp>`mMfmcSrk{$j1j+~!*}22ta*Czf^meZ7wxLJ>H=_AFbM`Y$@ydovr0UUPEb{+ zu5i|=4_$=0c>$VFqh{^j#LYuQ2KA28Tw zus`>1R6!WTXb4UvTg64qGV=zUEF*IZ1#Br6@Mkh!@_pkbU5dDNWxt@mqd5gBopf~V zZtF-X9|C<236lBzf84!yU{yu>|9@`goI?Tx5|Yr7P!tprKo9`~(uGh&MGfj2LK@MK z#1sg)3zueDLBU=Tdjr>Au&l1@iX9vF70c@G+Oe+gy1(}`^US$(pKy0S-}3w8_dAe# zXI|$y&ph)?J#*&XEA7`$cJs&Jx}w?pP>9bU`<;e!wv&p8o{#w%?GI+ z*DY>Af>ibn??_iIZsC;LHm<&KMSi~Cd5-7a`9tvx98bXM!wy47;PVsuY50ioxz{mWhv=e?*-WKzH&n zym~mD!ge>RZq6y~_70Oxy^;MNJ^0AVvQEhAm2g6@e0Bn*Brl^|lg!Mp#ke7}J1%Q6 zzU$rxmm%Dy7Z{972G4^V+`}<*C@dMOckRLF{`jfL(A1f_e$JeyYiDw%aJs_)lV4|w zj-Jk)OANq;v^!W-Y}5R*u-4Q<516uY@wEg9Q7Pv3Wr|XadV(p*TBjJTu8&O(9o4qT z^cPEDE1rFxo|K7lWVHNE9<|<#eM*h?B!HzKUBIto{P zO;@4tb*s}a&Q;oqK6obC=K>mY-o(Q~M82jyg7hh6EW_!;xuOqDA>m_93S`%3Y^c7> z+=M$9R6{S_utT9}pVd&J&zPUgbFRsxA&pMqoytj*J9Tr zdVTy&JPnaqn20P)Wbd}E1K1zo4<4!=IrIeX#Y8osN}kpkophgm`o5^&u(suU7!o-Q z_t9-yH?0V)#GGQju0~F#x=^=-Gt}<=L>;5Aosv1wLsKi?bvi2ALwEaDx0X03vyrjv zJe{#Ty7$r#T2Um9h^`LVss;5u`zU0xxs@t7RX0?GBsp{r*}4rvZa&Gi*`tEC;U6&z z*Naj|BO$KaAM)jBrQ2$JmmTWM5wj@Mrhz&Fx-oU`N@896Qq|Da*;Ubw&SmwY@u?rF zXfwc?_K5QG51ZV!&=hP|o#8)z!seu{Phk?pg^^@<9xlu4L+AWH`R3~XjVY39$!}B8 zI@P{Ts=6j5yX}AeIaTM;@BfECZu%MOg(>)`AJ$%Z*!qUw58}?t;#M2rcl+h|7Jp|0 z{60m*2Z(q-5jWGa1e}%#>QKIjH;VXp5pNOk5h9LnL{B>IN{V@2xrmpExc#1Vi|sdv z+jzbW@Oz$!OZop6A1bczE8<;5T^0%Lmx8KWeai$IMyQt2dZ0}b5NL*;Ydl_B7IWR%Owvb2D@VNDvG&4n{6L6WY zCi+w7`)~Ol``Ed)EpSg}`MKTwUVUBU`~8Pw>&K@t?11~D^1r~BnfYt{^L+mif0yrn z=Ks^jO8r@ZzcIKdNmbzI4DbgXs`p#@wAsp4y3cfmkMYMGi_g#eLHQ5+k0r@>_g6k2 z^IwX+ANwy_l;5s{KWwfmH4w8UH?I61ef;B5IF7Hz(Y(?HSooqHYcA5NhvVm_*~9SV z{(27X5_oq%H$Dc>^W3=d!+d|tc!B!(4J|H$mJ)wqxr_Gq3upNAXn~gx-|Oe&H%Od# zn%^$djc58@`?>MO{uNko_TR>GGZvda4gIz4E@+4E-+QQ?|0$N4T~SQZu)OS&Z2xut zhQPllye3S>l61$nj=hmo;|BXv{uT2tO53jd=l#F<{=LETf&T`+$k6{s@Y^J9+hhKd zv3Jw_f2oUnNdC+B&k8OJ{9A$@NmZ+z-{~a(4gaI0Dw2)IHJ2q36j-)v>li$b+m+@& zpg`uFyF#V@@9?x5TL-?3;ky8yPSV`o&*|oO>*+Tv_g8I7(xUu=;ePWnd{f4iNxYBV z9ECRWmn7WN?>F7Q7%RMP%Ks~}_!T;ZU3c!~K%w70*F}NfKGV~cm8GJWubp%_+aQintoS?e|P&Q+q+6~@#%tdlUhNVpONEt zwYO%vKlS|{gWQYoq56l_^9s7J3H{r``$GTnj5Y23H`{-cq`SM`N$y_Q&Tl^xd)jZ! za3TDf@MDq(V}5$JpIhqcrLNzNABEQWWDHH=FERf{d^@TCMey&yzdL*~skn0SO`q>3 zwH|n_i=S8Qs`CB(LN^}zb4%QKfK|cdcxI;GDaS>gA9nP+4Rld^zjKneWEBkku)TjJ z_SRsx(uGOw4SnT7{vi`xW$Av`K5iV1y{8-3%2vl7jG^H@mQ)_0-@CV;KiCz9B}o@U zS-yKd!|&0oj-K8 zf7E$WP0}@+? z^zr)*bFVng-;?%ZQo#g%*Ca=s5%YK96Se+&|NNxpeH6Zb%YQ8PVv_mY{GNUNzWx2` z)7qSZgOV)K4(pD|&;G7G(nkT-ZvE`8uE^^#{|Eeffr~Dq$-`GM^FwtsTt6I(=6bh= zQLr@%y_q=wT_QLoIzJ!^^@~FDqR_M`lplo}qtN&$)Dne8M4{PH=&nS<f z$0(FYE-zwH8Z>4GSf$kru^1ow+hw~IoVQ79t{b@bkt>Y+iWd-Oe0*q#4#r%ZaI`#Jm} z&=>bn$wykOZ(8T}$xPq>=@-2mmw3@)`gS%J6nPnDu?|q}Ns9$RPEOD@%nPzk3xaS0 zBHd%*?jR#vn-g>ji-PXqj3Br!CrA%tLEp_mk0SKyX?|=bgiAH-u^`AZBqKaI$c0!& zI7`3BW+SK?iy2nu1VLV#OHR;ddyWrsXF_^>kjDywMM5;a1m~AV(&v$M&WtFoug<{( zK>QRk?ya0mUJ>X_<^YGCgKV;O0K&(b;4%cu6Tu-i7=*L60U6=upbKp3RE!)ignK%K zsyY_r6$k06M=^|88w9yQU$VLg60`GyzLY}OP%Xn}`Y@TzD5AGbL1vgXFh?yQTUhae zpeHVC*AnE-AmzD1M??>Vv{D0#gO2J~Ma3o>ssV65slnAJ>Z`j^EWA*^mcW$xF)A}v z5DPNZ(f=)t0XmI?D2?NghK`2npje=_YO})0)kH-V{BccXlAgA7z5xRdHedU4Q;j7~4quFF&l+p0@)$Qshqm$RRv z^9vmVR724gxHNqW>h3lKu7>t;K{h2aE(lasdKc{`?X)o6T&K%mbdEadza+_JkWcZi{m1mMQn~;z69Qft9|E4vzwuJJsKb!zZ7|U)6f=7~dz3!@lmvka(u)W~R zAmdS20;!v7)RDZV-K@*O)DM)Y_Lxo>1}2|M!|ttEaERMhKJ_RUIe9@Rx(+A9OcR_$ z@#f$Fys5A%7^2@}!ASj{&*CH#7UMOoFxXFJtTCCK5bQV3gl1FzTZ?nhirUA5o+NJU zqibS?!C<_paA$C^evbu(^j;hc)X`#dffhyW48puNYBmK2Y0;`k!!V=4m|f&1xhaU{ znK&#;!=}9$l6QKIIJLE$U~rg?q$UJ8D6$|NVf~UDb~3?4yhB$m5%W&h$oI^@ZrXV?j3hs*IeV3sIA`Eo@=>nqUN|f{-zS zB9sSI!bA0mLiaK{lU5b`&?dn_17 z?$EoP1C+`OQHHvUIGiqBLobD#n6128Jr%4;GImnX zu^`gFpR4~7qd(|Km&8In37{g5l?1tXOA9yYu`dlJuSHNrf``Z>#@2&eT~BCTrdC<; zn2GA49*r9+6}iOdGJQA7#SFBzL*r|ZRCjGcv(}MDvxr&2ey}7qEEqPEW(Md@IocPK zT_m5>XYVxjL_I1hc!}}ccJ|jyr))YwQ`eH9BW7g_Fm4v3heH$5 zP66#Qi45WH5bdatbGJ=<5XEeo1_n*?tM&xhJ}cqHr0TR=E6m|32kK*1&~bu^>AF&d zVojfgyu`HI{BWi@S*+)u3R$Ef4y{J_t>FojXjl>@tX!_40LN7KHNq*=!nrOJS^xq zKFFFy&3qVomaL%tY`o0~GG^(1CN>v}R|oxIJGEZ=HWRA6`@8DC*Xp#stgt1>9!itX z^x66%s<;_pwW~Rpt6L$(EpIYFLHbQWV_pzdbJ2iYZh0Ew;D4x!mPjVGA( z^)(|)%(N#=B5kjKtkbcOd|zlN3VJW*jy~wG%`Ojy8uJTH*^F@w!fzw#%8IMU92TUH z$DDiHs=`8UE!DdErJ?3Ums7 zvK_-C!e60-EjAYAxfb0Di@ISp7$U#(w(Uazzb!xlgGgW$J_|-pPSSvjNFc~!J!WJd z9t=KChft}4@c2jt-SNe8f-(34z&4t;8%;xDa{i{^#HygCWFP6>{@)D34`fBj?bG5r zjDXJb=zV1gpLo?-%El`dm9`l#q3%cuk)dPs`J^nz){$6AFs&QTs}&ey*=wBTX=m;u zGk2MkaPJwMoQ@BywDAc(#Z8J1ATz3hkyXK@efb2ROO28{WM4VD*9aiXL0*xbNMT5u zg0H@9li+>-NwSw)b?#KUGWSB8N=H==!gut|T@YSFYb>aCbl-yROI@@os`UZxUDI(c z@%}W&lixeSgFAzuM|6^A6I2%IRluS8ix*Sqembc;v~H7NfEAN04EUH!Wu3T%T0r}q zIYAb76oxwxL&pjQom6|YFSQg+BonhuEcsw`f|(QWo>i=G98PocF^-MknwOGXGGsjb>b^@L7RIi3YwWf6w<}o z5DRy1gY_x{j-cg;(!odnk!{=eRkdyTRHe3V3)hIF^}Wb9Zm9cK?Vva^gEi6}tnT0=)E=axV`}TF=AaMacE+}KRZ)Q*75}o&QBl3`RgrC5R~3`< z-iNAgy7;&2;x_{*s{ZRVBj?$sx?xA#g z3Aw?6m~y5sH7$|4?{(<^a01O26R{#9#Q@;NG)1RQRw^4pe57O592X;hD6O?U;;N zrkSAznEzG=!K`q>TB;mt3JaSlWBAfvR{`B;(>fTv(^wG0xUr^iN6EIJD|s_ii@~UE zm`4O^F?H+Ri}G!>gypuIiMLsr!U$HhrlhE38~z!mb#a|`HJp}f2Kyn+W^B%J&u_(f zERPm@PDQ>_Cx^O+z+pVnFrHq+2^$syFqU!y(N3m1Yqp@2v6cWFnDuwVQ`{ay>d%1v^Y2sYw@%~nii@bU>?n(aK}Th zIEObMnV>sKA8X&e@Q$LuFA^Xvv{H)B)P_e%7q(4Wiya?ijOBv5J;3;dr2)D|&#q_a z*)?W#1N2hvx|RD-Lbv5<7t%6}p3Wvs_HQ>$U<~RcJ!`@f4>An%xG=NOl*T0T%TK?t zg@H_`=My?JZH;a&R0bJXyUL_ht92UDJ$??R0v(SF&&D~`xF*O^d4;_CRHj$MXr7c2 zPSj18%7ChI&*Gr{FjFYw5k=c+MTbpAgjERwMHsuQbq|+aq*n-ISox+UU8C)A(zX|7 zbkk<&H6?UhdeIS=Q@>zV;ZcrH+mfr^7S%dkfV><)>rKeEu2D4EKM}(Wm-QJKE$me9 zKVO1!$K#iaAy2(>Tbs%9k4!%GO{A_QoAg{B-qp#`RA)0p@`4UW@ROQQE&mABKF?I5 zBS=Xx*N@dyKQhCvrgBU-m7~f?=(==mFv!HTBTDo_XBvDojojTMpq0He8cRrP8rBj! zp=Z$#Mo_(D>~gbWjWj!6Z#wD8Zu@@0)Tqg(Mnx5Ba@dJ0lwRF@yKRNKDQ%4z)=Z7c z3Nk08tWnKt@DMELzt^ZC|L@l*b4TsgsLKCPqu$ft*L0a>c-R2wsDiwEmW@)Q@5O+A9O<)H^H;i&8HWIC)4!n6<^;-CJko) zusW_`9bH28j@EHKbaX)&qprIC^x{xYf8W5VT(#B6K1NF4G)nuqhW*kMYR*^3#h-OfKGsa+>a7DVkQ; z@VRZg=owgH5SwJi7ra{z|U9Cq3Tzxg&NzKH)X)8@N zk2dEa+i8mlc7iA^B-!_jaBH|F$PZOj`nK>Roa#q&iJ&iTX%+-Sv36YyHY>=_(i2v! z4QGUB;WiE3R!3TfzZw;a)Mb&_)j=-=vuUjruE2d`F7D0A(2Qb}xb9m+WC)!kp2=$; z;EIvti(<3bMmL3XF^R|NyrrK@(Ja8|(ja2}dsW-xH%7Cak!2vZ4U z4BJ-52V-W^!et(Y;elCtO=DmY+3}M1kscpW$Svw+*L-aB&=#oMU=!vXyH{3WtimN1 zBTh30su$`rbBUR`X!)H%7T)O@Ql?s-vRZ4=uB7h5*rwfK72OtRt5YzU%2cE1?s9g1 zaWN`CBB({XF^Sg+Zs+>E8CCWi?lw%?;Z?jyYUUY9&#qcWZ9wS+2JIFRh6zGcz{J2dMomfSshmD1}ZUhT$sGl{*|cN(gZ?<>N=d( z2~Yv1h4kPqO)o6zq-k9vEnKT#;Q*?-^eiV+y%D7YE4^}T4QLp{a^;XK6Fb^ z7>icZGjNvLWCnJcOwk7K3FgHD>H?#AcQ4hl1C1(&R^Q-etbOBx*yNp9bSEiw%_>@( z&cm)NMD;AE822FKN-(hIs1-51^;Ws`28X@|55S?+5;?%jSl9#x^B|})hIo*X*s*%OGwpDavn^kDG4Hsk?W5^m zgVfxnPYQO@3pa_hFqdK>?F&O7rl#2j(_Q7nP+<+t!~#)zPI?zCQ=7TK6Q@%PLFN61 zp1)`tvDT9{+$4s(N`p3%&Sqo`3me($c#M4*<{w2_Wpw9hL#1jdO3R|>21&as@x}Fe zrdR~6!MdPmM$maq5X=mU;)sUZ^*tB{%N$yAF&h_?9x1-ZmlvAL3-#rNSFn=8luG)$ z^Rd6NlK!Mb-Lo9qRn&8s7XnhgUKcSbFfQHd({&qd8<%B#bd=5Hc za|UcyBDtZu1rsjJoAu=QC5Yv!n^^4@GuM3Cs5Q%x zxo#$YG+J}D)*GNTSG^5m)OGY^7X1^nzfB?v=Zr3LJxr`JZk_KcTiuRI1u8Bx$p`a8 z^^v~%N_b~J!tjo|a{0DUuaED+DNK)z>{)tYRg+8D_^MFD4>Y{k5&KY+i;~4Cp=?Uu zEOm`=4ZFe^MicoF=LPB52zUt!pbQUGa&@23Fx@&l=wIrNO zIo%2sW(j!#Uv&}mDP&?F>;=TOLNs%31&1B zwi+{3i@veQ*$K%{jHId0X0wIb3}HJ@GRar#(M>0R;3izPJLxJUdmS#(q9HbalBaiT zd(B-@Pq%1aha}H^dmc}#$pI>vDF?bOdyF-!N882JA6kpd?cam#@FX67Gh=wKAU4v3 zXx1A$*`$)R7P6-u`ZM&2G$SMBG`UJcriz45>>`-swj6Leg5L64;M& z6&`DZP^JrD-EGFYrAQjx`OA=`^=Y#RT5Q&!5Fd&edY2Fndxx&lkIL}4i;T_)f6%p@ zR=()6F6y*qx<6Y3;h1TsGu^^mo4yY>?+ntn(iAM&&ve62bPU+icO9W>*HWYV){t+0Mx1SlBbrp5*m*?a12nF7RVw*C5LL zf>z!Ts6KB;JH67$ahupQcq+WD=;)S>CsK$OiVfO-a|>vyQ@3bo2E@UUv2g3BsO z>p&yl!LEz8q(}8#7Z+wiRBCz|f`&WQ9`kTiC3NLF1;&k($9ncq>lPi-()Ta}IJm&`(w8akHz{BLdob z5S~g^ZqG6*06j9IEZ}$b!7@A%%1B>Bb?y@AG|lx$UE|47G(N;;>E~)y=pE=jh8|Hv zx;tk0=>kD2wIM~IyUD0xhuVrsu4lf5J_|6OO# zSGc=3D;1#eb|Kx`tqc&Q)8mZK)WgW8+4;x!sl{B|)bNh4h>z9)x5mHYOg1ST(i#ZG~2hYG^0kpqBJO zfHSb8g2!&X?h>j8T6dy`tEbR6c0@!khbC<%Zq_EIQDT$_>bR;Ylh5D^Y-XYDgbRXx zI2{{b9Q4B)VQhiEPm5uY|CK4~q?zHl+5|M`5i}mJ4>D&3BN~Ftxxt7rxG%xmN_q{N zf)V&U$$I^96!HNol$kkF=c_9-Hp-~=rUla^F{XY@kHG~Q;YM8pGtf`tgFe)Y=%?If z_MEhT@G4_UFc$X{nX`+6vBw3OOM|hEhy-KDAxz0m(Dgbg)qgD29g<0PT}EshRhC=+ zL#jzzu5U4Aa1bq*jxTC1piA!9n(b^7r% zhPyv6<*By0XCPnVPR3kgZ?3jz_?DySYF3mL{uasLmOtpKmK9>rsT1;%S%iVOS?|of z9?HHn*HL3Q50?e%>!n3lC8Jpyv9rMN1RK}T!%l3~ry)Y^<%{q{d;w@{b?h7l0PMf6G&LVVau-)V%ZvJ*K+lB0Ka~Ru^j&;_)LuZO0 z^+qrptN(qE3=Y6k>G462nPBudA5(@EbZItP4W-c)vwOOi)%6JzB`gn8(Zlhyewc}y z9Jm%`64H|?oOs6DvCZbIyzoEe>$h$4^#l5Oq=iaNi;$H1iR!FYY^lk^-Ugh@2kDK4 z;6luPXX^B^K?EK;6H|2Sq2elBtbR5lhSmc+^#cr4Br-MfI=06D2e04J#=4iEK`UIp;n`v}~P`h!b&~-Dp8N21FDq^fU5Kow3JJu6n`!FyVelwnu z@rAnAS8ue#!wu|V*PSS2va$UONp51Gdf4@P-eaE5cNWMpN`!7db4kHk?7qScSus4R zKTA|(XO(mDAu7_EYxZLtr|(x#e0rT*jl}J`PSkvKm-e#i!tJISo7e0$%jlUwxAYb) zo_0ZkYl5L_b?YWfe6;nUdKYfjBRaGod)Gxi)cgAWjI)8-s+|P^z4s#ti0 zX4=6J+|t7*v{)LOVV?6Fnjs}VC`T^;)38w3u zy16}`P~&55X7o<_%+6a0Q-)wJfc<(y&B`4jWxmCV|# z+by;k#n$DcfToeyy+Q1G{EVx-8~!kn-oYllgWdGr+(&v>{x7DNv?uN}lU@!DhO}D> zAJB&fX~SWz+7|f6BCDyGq=yY(V9K2@!6$m;RTEhBw4KF&`M zr!itdCkmotl};zNu1YUKv|yVLIYr+IWKDqt4NJ5?IlxD>GcJQMR~ci1e%P%_3974G z^xBtMw^VO~nf1Hw=9**v(>1W*9WqC!N&CwtA#-{8g(YSdJKW9Qh7L##gCo_j;m0B2 z%`N%CaP?(-s@Kbq$_8VZnGSy5zU_1{=~nD~txjtzMBTm`N@(VBrsM0qFpW~9^$pri zhuPrz7CRfHWb~tFrTq?(T(!=*_&Z!ow}Y`^<{?&0t=LA{#68PoQ@@iw?sP@Moc(F8 zy9SxtU;Uypw+^a*X*RjvKLo^GRe#9D;rWCaKLL}aCQ+=-=~d<-LF`16T)ykfn@w^w zxRMw?5lbIspfN5$DO1K-ikj9dy*fDbu%K6qzO%z>5hC>H20t2s zm!e=2y`>)jwO7z8p^k=dg5H`KK8xW%&x2b~C0kH&(u?pRUmD(VrxKnR9E9h*7<6fk zmll|GJ<**bt@AzYA>6O>kH&uFdmCG9Ho4o3V()y~Z zrpjiop|NtQx3sLZwx+4stEjK5^lF+~D{881wCc*TC0;{W^I}S-t{l3XYAfr!W%Z2} zO-QC;`7ub=tEjB2Ujogo&0bk^Wh<_%s9B11p}+D}Z^@|@HH}_veO-0Fet}i6wz{se zBENhw%q(BrQgnl(oP4$h< zUg?VZrqZV6P32{^wIjSzq+Q?ATv}CA3r~?rOCbw3Xl5sQZ;JZb& z_2s9+2KEuTOPf>0g^$u*g3CR9c^wsp%}r@pV>SLN%Z#&PEO{X;U4isxw^%H$ls?nhMm2(&;m2Pt*0zYg&wa=O?E}`D&`9 zs!4@G1zporR@<<+jIvBtn>vP~)MZ?y^GHS5u-sc*wj321UVtcSol%378&E?~U(4r} zRe1Ha6{cdcfrwAmo@=geF01uOUlWx(e2HNVr`0qW zk5=ilO+mLTg)8dFZBRL9NIJKCh7LAU9jI5(Jp^K0@C|yUWHZLiwt|?D)>%lO- ztQIcxN=*k+TCN+`611TDa!*&Z#>(o_rn04#rh@BUrb;`gwz8_Zw5%3A3be3KE1N6p z;6(I$2g&)|p&HW_-V(~%qUFt~AJn6A580^uAKlrQ_O9JnQ>UFA6Lscw>#Mv)l~whP zmDJPd0;w{6G_?YFthNT7G569-O%1N{mfCIrnXj~c9V#h0J?i`6Z`)#ZKcce@gQ!l{ zLGL0|;3d@USD6kBrGtuCi&83UZfWvbnksciuxfp#B+6<6RrK-8ImP}}@Xld{k>9%>q z++*{nj2MwWVpQS5g~Rg;mW&>SmohkbeCwFe1v*w&Uod9)$T5fF1cFPJw3+~_bQCz) zgc>U=3RYlfL#&)iu6BGARjvGj>imKuAp>*Aqp2djf-3SWa$AQ1181wC8L{$H3mO{h zn=8wk>l*mql@^E^~FqkMW@nZ3MN4 z#`Ch8IwXiLgGV!rQ`O`W>JDpb7L^y4d&3sjFR2_>QC3$|SzEBUYzaD?VHK52hs~+4 zYc6X!^~hs}!A5jxOPYq^3i|usyQZ*k82;DPmDjdZRQ^xZG}qVHqIuNR4MV*ttJM}% zmzNJiX_by1RZv-5HO!QyRqVA*W*Dl9%`E2`C4*2Q%|9w?>dkvaWfgZyLZHtDdX}7qhcM%waUxouY8NG!0)NIX2p7SY?qEN?Dew1 z?H9<~A6^&*Hk{@4k1tH>lyyehjLdNAxoV?7XG5@-cK@}1V)Ap7pP&4~q(4E`Ec#M? z(q+G_-~LDgv0t+;`$G+s-X>=H#~RY!rv#>FhBHZ*)^i7+pTh5?=Z_q=UwBUJ4QBoL zo!Dm_p64+A(^rU3bC~@hfqaJz*58BYd5sP$VrGBXfzq&Fns0yHfmj90*&lcy*2-+Q zqi+K<`$g^~x0PABBX%)HWIx&2$r#|w{oA#;CU*Knd0D|PY zGjp#Ux(_ySA8bCep-vi$nb{wOpfrBXtcxSJX&?HoXXfVZ0l`Y317B{f5B)9D&c&CzZwP0sZI||$t(=oz)YbEd4o!kg?a&~|5gi6Y>tOwLhg^@X?kOA8S}bz4 ze`4BmT3vAm?a%ovYwHnRznGb>XGzz$PL0AIveP!I4gT67?Kxwf zzF>NXMNVvHhY}}-mPbC`!@ANDri0kG%&2YBK`hOg(c zK>m_?{ev8~JWkVHlI|m?^7*M`vpv7coTsv{;Pd(iJM4Mn=}1(bdA9H&myf?qEUSOa zL(3Z9ocyOJs0;K4LgpTpA=@JV86VBG>PYS#meW7{5v6;snz7NJwjr3hHiXP<{P`5X zp??shn0$_Qa|HG2)TO6gYMr9%+_VlSCdH?BNLy$Mi$6&hO#t^beBk>pIzlr+1hqG#$|)Z6c3T ztQ&f%erNHws7^$f4q^kDQQf73nEj?(Tjz<*W;y%ACd3x*gEcd=KX@_^a%VByj<9FT zV5Z{>nS)6kW{W({>af5T#ETy3eCPkNPRh@4?nn+GJ8@8*pUO=C0Ly%?KW;fbFD#wj zVS#jhKJ;v0J^F`R=CU623ro1}(|B?R%iM-=)YfKD-#7!E<7p-#?i@*e56kNxd(m}e zF6JvNztj=uzB`M5wZt8h1`3uByJq^glpU3 zQIYV>qz*?V%8=U9R@N~E;V8elEw$RFuIuAYmUHXlJAw^{im#aIAD^*pN9)4}ZO9ny zyD56i>&@Q>Ont1$Bl=b3adCT{$ypta)3$mOae4;J>L01GZJzTwZ9_P9Z96!7s-75f zgFB+boJ3Z&jt&2ZI`U!1F4i#w;mBXGxsuCe2Jp8mqvu)H2f9v7T^lT<(R}jr+6_}X zR68|fYKOE*aL_b!4tnzOH<1U?RP|suWGYxj{{WBmjp?tOBbOI2wL{%j$?19QMb4hz(@M-HGY{ zm}P2dbM0(qT*b89LT3EN(rw>pTIzdW|GVuO`fWnL4W?$D=J1DEq7U9<-er+I*>4Y( z_$5J$^tn3Me_V)z%k|ZOTqi6;JO!Sb57}5RU3@*+P^IXQJ(D$l20pd zP%_|{@~E~aA0rWsd6!1ew!jIRUmo#^9HM?T=3UW-6Hwkk5qAU18|p@*u<{C{up3a( z>Vr#3|G~+CW6Gmkv^|ubeeR1N(iVa!PyQ625mVlwDdedaZhI(iY+DGTym2XbQ3`HX z9Fk8fk4mTQq4emwFFv^~1W|fc*Y=e1rniA~ZxQn@NpPntQu=N-amt32H`4}_-j!FJ z6t@xO&9cFycje7Vira|tjz+8JyrUBlC#1Y%oLC~Ly!nZU6H;w{V$qyb z-f@mpBB;FM6A>q*^j(evIHCVCeZ(TL_}O(iFTb1usv* zD^u|56nt?CUXy~Kl7iQ!;7d~Qx)i)21#fJ_$zw5ZV}hq8g34=7M4XWFmO8OSPA>-y{Q$DQ{&8zN!tUl8Sj7B5ub80?Jz*aW|m6wQf|0Tksn5 z&WU)kp7jxT1IjzojYeVRofU=Mfb!0EqfuBn=|&I%<>V`wJLSkH;&_jF7e-=s1SQFs zcTpQoBWBFIxD6+u96gbM<_SH?JfSCY8LOJ8OgOP4xo}A3d+O~&s#&`Fu@~(2D zQCN9bM`1Uhysd6D3M=pWDC`E5ccUAP!pgfj3cCU2-Qq@dm^9$Kb0ThA)OO7i^Y%Ac z;vUTt^Y-tU(|tx4FOy?fGvm+`nkVM%1DHRfd1BsfXM`_lo)|3WvHTmFC+6+bnD5a% zF>lXj{)y&^dAse2zSKOIr998h7{1p$FXAm6`bG1u5g);!AV&G^7V+5}%GA7j#P8xz zcg=f5yqrS=HSZbmG7iz2Q5P>UZ?~Qrsd-}F-jU@G)4WflC!a&KcG<<5x7!@j+DjK_ z-fs0Aqq)7>i+-H;M9up}`mKDq=81XxP?k7Vb6eV8loCE)fOH2&dTh!4TJ!xQZuPIz zJTY&#exPSUU7UHl zF;BKLl?9s5h~y9C(C1qJ%!u0>|DEQEdGE0-p|qYkk-T2BM~r@2p^G!`wI0dRJTdRJ z&nLTTesrYgCl2+|JTdQ`$b5k2^CNlN@({}J%@gxp>aXdTuK5Wjf>+CDX`Yz(+7@-R<|jq+)bG$yqWP~(1g~>BRH}Jm-g_JK zlQl1kGc^e63Yu+5mH*u(&=1be~ z_L{dwdf2{eH5%+C~H;wFb*Ojwl&e^}@?7K{C#7o)*me~Gj_s-d^V`h(*OtbNRu{q=}l|Ax6JPT(p!G@?m`IBW)r>bo>MeM@B`w;yX*Qzr7GSfB)a<^DcHY^|f~J76N0v7a@~&*& zCCpoG?bbftOx$F4eVJ`?W@oh7TgMxx%?@I-Syyia)!QqP5^dnqm54X7Qk7R+@6jAQ z>WghVurm&thuA)vMC~0C&G=n@4V}axIq9W?c%Vb(#m+M3$srz?L(3jS3JZuOCQWAV3~)3f8c$BK-rbzW z+L6&?HF$K5%5}l@YZSAr5 z)smN_(6dl*+b$Lh{&JwR?==W+(``+`<0<%ADflMAZMv5VZqwbGg5R8i-;shpkb*y! zg6|aEwu@bYTRY!K!S@Jm+r?i6w|c%3{6-q!aQrB^mDiIAoTt-nJxS;O-f~<2hm$fK zr1nny*>TFQNJ*Yf!Z>LBTCPLp2`ADsqftvckLO#^5p*#4%3^R+gQ%su=W+5r+JReBgO4O_2eY{eZ;_)yX6;907sU2 z)AfzyEw|+(4^F{t{gisfq>!JOg3n07=cVA*hc?~2x&15@^0~|x3vT_;kb<|S z;PDjvEWvHMn*_J%UYde$O~G$Y!S6`HA4tI;OTl-h;JZ@rHw3?j>)Rf|ZGQhMcnw$d zuLQU9Kc?WiBIBGbcU%7lvpltzMh?^4Ob$z4or3?b%4et0V^^|ey7ejKxti+X;WWtD z{v?m(>AcH7XHNawdi+`Wc3cj`&vba6!(DlL9z_SqyZ!KHhr9i-Yi9$lvFi-h4|JZm zwMWWZo(>s0DBaueXE`xCDEY){u}1oZ__9Pa3K4IOVu|&1>Q@u1yk7K#miSuy{f+0D zc3qzMI{ewguFqJ`w{>1m79hSFf0yw<;f}LwtP;H2b!VIIV0MhX@3#DBzJ}%obXacF zv*WepJB(Pg!^rY;xqVTePlx5UotGON@rS4&a4a@&5nsvvv1MWPT*~q`kCtD;`8r$Z z`8yR5j&sdhq~H4GYQb-3`>AfwVfA-lJ%2Pf;_q|8*#64O+xGmHkhk}D9|=y+zv%eF zyhZwd=KR|7vHD9`&bB+tH?SW2Jk|22*q?UY!}9Z4eg@~y@`E_tV+Ehh@{0ujh5gnb z`1h=bo-@;7^^f8BIf4&oJr@Z+n(e<)a62D*T=1Jsa^9&nsw6~J&&Tp+MmJsde@OZ-H7N{dX7Yg zm4B1-^^GIH!iebCe+u~?Tu%;VyR4p7oUfw=x8qPLb4t#=f3csFw0f>*JuQyB+yAc= z`tN4@&vE44am0SM((1p7?Z3v6w|yXG_zs7Epgmw-e(&&S9R7mC-*@;Y%x$`pxLoXK zG0ALqK9j@et$Z>2p|9ZYb9^YjlOCEs*y9i(Px~wAnC!^YIBt(wLjD4dALq#1xeZ;j z)RB)Po<$JH3c>Aj&Q(H><<~p%YjOQY?62QB@^xCXdHIu&zntY?75rkp=2M5q!GAUTEdLp^0*vqx!80E2yW9I=kPeLKbDl>m>~EZgT1K^CqIni_GZhU>`6S; z_q;M8-;w1T1V4iL3g)Ee6i$=gHVXNxxt?6$$k#aXw+i`_*#GxBJP!GL_?ia=-^Tn= zhf{vXb9;Q+;c@T}S^jy!-)3&tt8BU0e*0Y^--Yw_zToYdf9!Cw=K$9KwZr3JQ(68y z!R~vT?t(>1O%t=p`mM||P9r-xWFYJdy1^=G;5rPj=M$at4`!b); z+@@>SIZt-vS3|`@uFt19oa`xPJ#~W5V7^Rn+fSY@cnQnL9qy)kw!X4M4-0v_PWqI?;}AWM^*<~4natl7dz{Mvr?PUe)~GPaT4UUuZ;h+o2bb_>3d z`Nx7k$^7qvKf?S=hm$><$N(HaIXsT|7cBpa;2$&Z$v616zUA=vy`SKnnGa%a^SeJK ziep3yK2zv9koC+Fd=T^F1TSKKg5YDAmkVCQyjt)|<}HGs!F;*kYnZPQ{2JzG2!1v5 zjSi=Bd5GKHrOe513-M=<2Oar1-~+7Z5y5X~{=DEHGk;0&KQrI$aJQbk?{G@DGml?? z5xkupr!ygh!`72g%mcxPGw;OQ=GWGf?kV_ip{JPjj1t_=&&CUGzsqNm;3cf5Sn#u% z&lP+<^OFR>k@-TwuVr2%_~Xo%2>yHK%?_t>xkYLBdaIa|-%d72zg{fl?Y!t3hsPoL z2J62;@E4fh>TpVTGuO|19Zq_D9-kf*{1?`(7Q7daf8PoIGt0N<4-8P0>?z}Ua-PE} z-7T!Ahu{}7A0+q}%!dg64D;a*r*s$MkB%|StsnZcJ(C>y)sSoAe!ke@+J2?gc*i219ua&b^QVNKv8?~46#R3c=Xlohwcy7x|5@m1W&QZw34PdnS>A^^)wekQ>^RX+ z@Kag;V24w=*mag7hm)SQEI(23)y!uyxAv^%dUA{-Px|eApxoi4-;P7of^TB|rwRRM zvHsH?dD4Fnk0a+eob>;J^_(a8W6UoT`tM@>*QMYO2|c@6&+i3)mHD3>PIjKl_5Ur0 zlbxTj{GSE?EAtN>PI~P3B7Wy^(v!yh(LV&YUoob5Lr)B{)E|C=KRPl~@co(FdX>Za z4-~vJ^WhFBJ0IixPIP!2d?d?H6?_EqIn1q{^f`1oj!(gxgr2FaXPMv=nXeXlzGwaC zIPzrw%WVH84v&LZv7RdgU&Q<-p?@B(q2taJ{CT104A%3K;Om&bE%ZEP)O&wP!T%-n z*l{<;6DM1~cAQRk_*&>*!v4u&PSF!NO?o?2$iL3(9+MpT6CC;ZLVhCmZznkN3mo|s zj(i;aG4{hs!FMn}Q|QTMU7J(z?LyC=S63yVBJ_-8{bL0m!F;mdQM|=tE$r0R+_x%KakLC9l{7vSC%x!(RloG`; z#*rsG=W)M1!{Kr8pIFaq!GC0atl%BE-&`Pg2J=ON+wrnW@Bu7eFZd|tO@bfH{MUj{ zV;&cL67%&Acl~e?bL+Q9;0rpg7V-<(Z?`!-4!LsH|2x6Um_H!69giOt{4|z-Qt)-m zpB8)#^Ops`fcb90H#2`v@N1cWDEL<9e|0$d&HCYMhm#-PVEOL^-_1Oh2O%8R&qGbr z>yUyEVs88APgu_o!R@#{O7MTM{20N%V?IgnTpmBB3EqYIk<6|Ali2M0VPX+~WtWLqN5_*2mdae=t5$3lF{vz|+1-J9L zN0?iCy0bk`rQjb5J%3~Up9ua5^REQ|iTSsJ|HxeN1_^7=XtpOa1>c{!t#27T-XAD< zJLU%q-h=sQ!Mib^>~NYV@UomYhdG&d92FoR6+*s+dA-BqV1=x|N$>-h|61s0_jqSG z@~fd|D35pNJDl_%#d8q*zuDn#x_3I9(zWx~djv_cC zk*9o>@H~9JBR}7mpkJ3d@^O%_S^o;bKV^Ql;C4Q`QSeM2*Ux88c2c_5&fhumWM>*b z@7UqU9|LZWmxO#BpLoaNaj-)<-93U2WB!%kCo}(6@Dk>VA8_09vh&4ug0Eot{RO|4 z`GJC8$^2mEKmrQpvquMzxN=FQBlJq56dj%x&8Zm{>N zqle1zpL8LP_Z<174brdQ3wc|Pv7ScA`uPL42R|yT56j}1oX?!HL-scqGrW^i@D)POY}UV0@M7j?3Vssva|Azu`NhnwJ!Ndq6N2Bu{Ch_a zm5be{7wcsNNY7C$OK<&{TR+!xy88=W!~7tJlYU!{hd4YAZs(U{1wV)N%oO}S=5qwU zhxySCr*sRsfBTigDcvVozD#gCPd&xqt0CW;$FE9_m&|K@PgQ^@n4&jmk#`Cf;+ z<@+y(lb&jpkM%|bhpj($p4wUP8(BU_@GZ={JDkepBcA9S;P5#3D=dGI;LkH3$(;O8 z{=5T!bc}c8N&k1Of2PCZ;C5a(NANFL|IrS2^^`lD^knfopjz-w%o~_nJIinl9V;Ap z(x1Wg^S2IDV3w|8)I|XlKey`vS%pVneHS<3R zzKZ!%4tM>q%i-jQ%`E?_;O8-akGb{p1Gt8cABFr@mj9>VTbPG^%ym}JP80QdF}L;q zcP!sm@a@b8r_gh73SKPqJj#0J3jQ$j;{|_#`H6x*$Gpnn)ZVKogE&rOPSGPZF)tSi zdE5VAC-@Jn=XOWWTu0B7Lf-1xF(PZazf=B0w)#eA{gPcW|){Bh=|Io$Qb3Wt*)USau_ zg4_Fqvzc2z+xgqILjD8RbEDw){^1UxXA{z<<4Ga^HS2j=@GqIaoI=k#DfqWSkNruf z9|iw~^~d;&3vE5j;(2x;cqita9Zv22Eq-p;S8&@NPZoTu(r)l7n3JDp8>C-P7xGrm zZv}sh@B1!t^vrVfY!mWU&qIPg%k#iLIeLm6J?{#6tLHPp$MJri9~?b19X*}$O@#dH z&Wrj8zMJzkm^t}p2L9}Ec)kfqJ#z)WoiA9xob(*QjNTgZO+@NhDY)I|f40NpxYXXS zZWO!^*OT)dPWiR_{I?1|8RzJ@EnmMy^T0W*=XZk7WPY#1Nq;f>?J?$*-|6_X#|QZ) zMEQ;5ub%aMoR0_&%gdO5ny<0uq~Femzjt^X`~jB#N$@+E|J&iDC;D7yKSXfYbU$JF zPJ+M7Jjdar$JWFB93BVn&-ZWr116yqe`l3trBAy5RQyvsiF@ zzj~y@S3`d-k5eTMckL;6IN5Upr&}%f)y$U({tENc1%HuwoOukosh-$*{^de`FUxNc z{O`T@R^UrLzAz^38P9xELA zIN(>TXQkkKn6Gs>>9_sTxeg~i)A>H+e8ERCzr^9B$M#FtI-K-e!}2!@zM1)L4ktaf zU%JoXq~{lw-y!%H%zy82(qsFjXB&6 zUcv2s#rJ|g$MQcroa#?sPS+oR2o7o&ZoWD@JdSgpv7Q{kKW08a@V-2s8zguS=7$JA zf%#a$?ft+6hm-wd+5XuMCp%AOJ@W)FVZMO5t>@q48am1ydD3rxySv`uaq#7=r%CW- z%v&Aq>RIh@(qrfG>jgiP^_xVBKPJZaf^WD9Iw`cy7 z!`DLoJ?;;Kfr#KB|4-E+^U|BSZ7;Sz8RYOdMEkM+A%gc{K8m^3&&zM#1V^6o)tSq8 zuERbQ_pkJBJ``I@UY#G_*7{0oQ2fo{t;e6QfwF#o~fq-Q-j97o#z=H1R)KFl}qQ1CaIcW^lA zS&?SsyE;4$)UTi6Jq6EZ-q+!zXLMlX4{~@M=v0<36x_~_iv-`q@)HH$z8aZCzcFZA5ZdYS~kk@>F$-@!aC_sytKj)8-&gPv%m)Z==h+3!t^Iq9dETKZ_%TAy zMAm=2;1ifH6#N+G6@nkhyotHB=kILKZ&L8fgq}LqbCuw=%x@HY74usK{|)oI1-J9{ z`vu>?@{cpO_NP+-$FnK;CqmCvtmjj~uVDVI;I}jXQSfcdV}nfkw!S^WJP`aL=AD^a z`v9OA*^_;^=kG&t*CHM^1^Colhv+K7H9r@Kb_gRP+9N#!Rj(8>O`9W}d z|KJU#s}k#GEiCT~Ztov59PXyugSoYnrw87E6#Q^UPaNrQy7Vi-Nz* z{0+fBV*ZZcA29!vxwXgEt?yFs>;sLxw!iv@_2dcuHS_%hS6m_b3m#)$z}(t%tVzy0 z%#mLUc5j+FG2M}$$g=cS<;ceo@5}m65xfWUCZVSjDZ%j@M}9TpZ*u!O+u?EW@vLW~ z;D<54SnxT_FBg0k^Q#^1rn}AIuQ8$C=x7=X2W6I`ZVt2e};Ib~xFS%k$&+1n*$MDTIU7YlwO^IE}=Wxhf1<;*t;zKr=r z%&ncaU;Vu!Px%_a{(r`iKg?zd@1HsHam0Vi`o9wV4CdcCob=muY>x*%@+axJf#rR{ z?fqV^;E%C1|yYHgbkth2bxSwC<$d3iL$7POu9PkU) zf0f`LGr!j1u79>Wob=fHv^xdwb^uP|cwF#{nLjD`4Cb#2zMJP^ZwUSe=AQ_DJCDbo z3Oyo0nV#Ye!?qpe28L$~PQS}cM{kG65x0KN7rchk+uz~j&lc7{+~K6h z`el^hce9?c4ktZj3NJXOIy{c}NRH1CycfskIGp@&GPkcXhm)SGI9@6EWgKr2{85fC z7yKTMuXH%sKb-x4w!)8HF98UUov7fJXIOz`y@q*(dppCwuyF{Xf{@Zao?A zaMFJm$0rGXD94Wy{1}cOE4YeB&t%CoS<2UiUwI_q^`GX@*_So-beOJia@5lZm1^>6B zhwQ(K<7zk}IBdPTf_X>5?`NJR_}$EVFt_$E+Q$f>aS+~Qw5JRpPPbL z3H_T`|0#l>%e+Z&TYk#~-^%i9m|HtPF>z# zF9mPU@gD`x=lDMb@6GXc2SX_iYtKX-!+LxQUclV;pNF!Z5rQAWe4OAjnNJY>2<9`G zNA~c2(9w=O`R6hA&mxD%!B1d4Re~SKyk77X%$o#X%KSIXtv&YlM9xgXuM>JMW<56v zej)Qa1izX2J%ZoJ{88rCo>SOAI~{rQ&ui?TR~=6Nd5HDAA^82w-xvIC=6@0Vb>^Qt zoW_eYc>eGWbFz~WLfE%EQolvx?>|`20fK+ce2BwIzx|zy(GHJ;590L33EqeK;SMJ~ zc79mwaMDxC@^b}0p7}8j*ZF1tuN2(gci-r6(!YiE+$#84%zx)_(*GDa5XTOO#}WTG z%RegkKbSw^aMDx2dR}li>6yy$mjy54`0EZQJxbvP#|I9NBYrx^KNkE{j(__9w00lh zQB`T&z(=Y|F?0*a(2*KCD$3Bi^rmPMk_kjZ5>r?Z%SA-R8qr;_px75(+=z9qkN|E;V5Tgt^JfbvMi!_$)6B(%f;k@D;J=AsquW2SCLDe ztKChOTgd}g*0?*2Uxe}>vDSW8x;nCKqIk*w$53Ojt>l5Tr&0ck@h4G!6Kl=CKpwbd zH^oc-hM~I8b;tu}+fd$b{8g0yYh3gGYxP%P~Bcr=rCcX}?s~?jGE;Nbq=f<_)2aVr=@-N1(M!EjLsD14Z{<~zX z30#lkQoNqFBbWaC33)mhe*oUg__Of7#-D_rYWzL;P~-2wN0Q6&)qYMQmwtYR_^HM} zfzN~MIOz2%m*OS=H5i9ta>;)Hc@`W00e*?`qfuUAydKI|l5@MiCztc^CA>en6)ye9 zL-)x2T|19gQXa{li~ZV89ysrc@($yjQGVa}V3a>JJ`m-P$+_Jh;M)J@uI+Half-%# zxSXFd4*EGsPm0H-Buq2X#Lva^ah4}Olj5bH`gfX&$ph!p(C@{@&wwu@=R8-EOaHsz zIIcB*Caweb8lQ^(SHNX_F!gbd-0$I<_yIV+Z;}Ttt`z-z+js$d7rFHFC%rC_OCCM{ zJ~w^?@_a=ud2}9okX-WYLi{hrUxGIr?1J2)^PI_uOEW$I9yk6xyrc2Q;QfvN3Lk9z zTli_@a(qw6IFBZm{e2DNHkCYZJ_zNR#`~e1fwdmr2Cj}Qc@!`0c0s#kjMK zD6b@!e%6tO+)@sgBHoIGe!q#=^J)`$;38L{yxI5)l%FE!^Xg@C$y0&y-;Cdf@*CuQ zUhO28Jo{1pr*Vz@kMSJ*P*>~e`8HmE*o(FL+bGvD{x_7H8*hnUv^v)Kk?=O;a(tWO z_;x1e^QsTI?C&h(8DM-Gd?;Lxubx-qC|=q`l{X>J^~Udl-(mbm_}#|8hCgEbMEp|2gG=YhYF2hNuuPc7`9o`*&7qsV3aKXf-) zn!{zk@R4`u!^rU?rO=Zm&+B;I(c>?9@FUPLel^7hPX2-Z{K5D$@LS2HpZfbD_mNAU zk@)4B2aNZHKWY3<__M};4}abGXYe7b>TmFk&`q>S?RQ0j(#&A7P<#2Vg{H3Vb*k{JK!H>ZCqT|q49=PRr*D-N|Kt_Z|_3 z#|DxIj`kt`6yv+#XKF|H*hdye_U=dj8LcA7y+FyeYY~ zdkFnMfn3_X9`S9BuYz}k%dp6K(j3pIo-d5ghaWOt3jfu3F}!Xf+`aJU{&GGHsuPvF7+;Tm z_JPZJE92H5<9`}?;CvP_TKAN2KOePOv?*BO>e!B5n;q#1dgJ&Aw3eO{#c1NTC z#pHqWgNR>j{5$wExE{wjbzCL4+?)tkRpW_|y<>O7b==xuJRczsT-DB_+?`tJ5fPuGg_; z$a9SGrSM~oUk7h({7QHy<6Gb-8Lxo%gCF6#CFgTrJwGXaxpVzIc@lZxd?)fuHU2Jq z7CGn1AeTH}AU@mpC-8jZtwy^_ZYeT;416j1au;_#j_;-9f%8)lf4T8N@T=iRxY*9_ z_7lcO=RxXe^1v+>h}ZLW1=_774_w?#+!nS_?sC2Rd6|8ouU-s{YDf2D*CyF;yY6OZWBKV@t;t9 z2a2zc*H1m}o3LLEj6Vc#LN5Dz4$kM+a4EK@{0S4E#I`eyFRB}DNWvvg9I>)mN%4Uz zzlQ#-GX4sDwI|PQ9@lx0j>BH$dD`Uv7yMa2uRrRI;cehL&haqX{k+%XLrtEx$TPxt8~6lrIZp=RJWLwb z-vcayOMgy8Uu0EI@qsI+BmYgtd%$lYmv-CMb$4#Lm-4rXD7=4(;sYlWkms+)$H3np zm;4`xS={dd8Go-%w4pYxb29!C-x}V=_%k?91{vR7FUm6U1fWHXW ze&WZPvE9ZeyIivTOnKzE>%FQbO^8E7ut>>i!?I@mlcZ+n->+20mXli~eNiVvKe zj6DA|J^=nDx#a&L%;Nrk54q%-j`*LAPlDIR{vGXNCC_vmm&S1EXLEPcr6&H%_%ubnqg`O& zZvR5LrSW%B?qIz2#HfWX##_SE;kG}gP`r%8o#_8aa_P@_ zq%eK#1mi8??Z~B{k6?Z{$GAR+v*FVJ#!>3<{!)q$oJ>Xj%Z*Qh-)Q`N_!{F$_&Rdw z=S#RwzF@qKbj&T=;8JWDN_1BX`E)$*K)IgrTTyOid<)90jBiG{lkvZxe3J2(Q0_&( z+;wp|`hO~U;CwgALyhl1c>=kN&&PPZn+2D&4ctwawGCD;Tw#%fp0W^ z0(=wPw)?7y?}hj`jCX^-O)mRYg!dyKQhqs>x*Ra^`aG(Ie0to+pj^-R2$WkG&q4V( zG9ow@=?a`MY*N%7f?Rl__HW?F}?@o z?#6ebJlJ^cDN#R%7(ayaIOAOqKgoCp_*upi@C@Ta;05G-J}f1d<2V?{@hbAb`CR0= z&iG9DP2`fNBl6ryE_sR(ug5`;R}uVA6fgbUgV&eG;ga@9chlu{6R+2qcgX`6d>QiZ zHXgwLZTvR)SH{=Ee=`0AJchOI*CQx5GQJz-CdPN5e4_C>Q=>g@Z~P~e(~WmUe1GE| z;lqt5;G>NXflnak^Jf;h9QWop?hD9e{Le(5^Ni1cpHD7%jzFG^$tBM+#9wN>48Dq7 z`uR+4cjuP3jK7NcRU_1s<0bL;qTN%Cmm*I#T*4Z;n=UIUK5)1WGFe#-;MG?E5yj>B-2?>3%5`5~+&f9o)FboY1iw&edYd8VVh-}q#d4`MC(+fkkt z_<@@Ca}nZ>GoA+xpNpj%|c@uNJx!ni(%ANTl?=$Pie1aa+*XNA3R-&bnu`TvI=|CKx!Ib_upug}tL_6f$Dq0ZSJUuImd(dFbxPigxw)Ypl;QdHNzgX?!5O%;Q&)=O9*A4}1I# z<4sZLUmpL`xQ^S;9zPbJyU6}#Be$$_JigiE2aW4>p%vbbO8#QXpYHLo9?$Z)e$SKk zU!T7(Q@rfg612V7<1O(zsd=tNe0$^f!UvGghskQXah+2pJ-*a<2jp5yo<;dz_PBmt zEB)vA=D6^xpMqT7jCX>c=JBb<_5X97Yh3dzHm>nk7}xk4jcfc}9)E&76MdJ}Cmv73 zhafTznG%R)fN>q43C6XbXM6m7G3tj$A^34zR$_yj~mA{CiXh{Jmip7 zYkcS|7yukPp_+`d# zsU5Yq#`p^OR^$5r1m7{f3GrVT*YA0&-T3hRnkhTzmgdHHMjY#J`~$>~GX4;JfpLw$ z!uUkwUuXOg`19m*VY2$bcyny~+T%YPZ;$wf`0zryE&0`3d%O$z9ORHye{#vw8{0;D zd@{M@!QqN6B%h5OvfAYFea7{9+Bhw$Ec+#S&WMV!bmQ~k(~O@BFEG9uzQ^N-Jl+r! z5ABcU>Fn_V9v|)T9OKWRpQRpO>G7L9ez(WpGyWOc{nU65_(6}?!v$Q&M~`C*k9Raa z4Eg(ee1vg5AEtRc+vCN?^*OM@_1ENBr>~?@c}{>PmP&%{ZEiB|Uz=aeZG9c>D(Pnbht>9^dZqZ^-qvSJE}dg;aK* zeK7fSYB$5<%RPR#@pR;R*7y+k>mL7zd>ZxV82mDp^q;-2@f_5dWV{G|w#SRer&7B& zdHiwX-B9Oc<98G)Nb#VQ9<^XeWAziH$D!zb{M|_{=IRHZ-Nt5+r13& zapPI={vJQe<0T%y)Z?p-Yk%%Ez6JfMF#dP=bH-nQzhV3h_~+z$O_fdEkBtIkeAv%4 zJ|hZ?n2A|FlhPk8)Ok9WWifux_DXQA=a(8e{!XT$F^{s4T3$G`P>SNsr2`=jUEDIT9~ zT>E*h$FK4DTI0H38;ooI&BitUCF2^u!??zOO+E@^CaW=cLn`CIewW98_4uSVQBuvL z*Tn0LXGUSMzZ<_6{)O=!@HqZchvruw>hTQY`;g}X<3GT!GX69CX5%IBjUM0O@jb?M zf4?%W`G4_vBm9t0$3gS7Cm)GE%4(ExysX4#dwdo72+H$?$H&E^AnA|9caDm&KgFX5 z*7th6eY@20`Nnm8ZZN(isvg^5{B-zJ9{-E+v50@$GrkKx!{eF8KSz9_#{-XFZ~Qytx!rhe*>Si0+2hX|Z;JSB#@oT) z_xOJD;Zc*}ebWw6L8`O&_4p)@=X!jx$Cr~2qyAh+F8$Hh+1ow7(YU^!ea7RjkW0JG z(Dqy8((WnneIEbLxQ<;NbXdnz{TOm-7t@tkoIHUxWp%p8&oJH?b@IrEQl2Y3ezWmL zsB^DzeV_QK$6xgLyX2=)e-0Ygd0mT6QIL$A#H)Amcslt~%0HA`->b>yc^<#OxE`;o z$xo&H_j&wzkMA_zAGy9FKbi8!@d6>^z`n-g?{tYO+j#yv;GAa(o*)|kD%yV8cr6KV zi~gNWjc*9Qy=Syt{mZ&hS^v(X^)Ec}^?MzjN59{%t?~QubF6fak2YS3_&FXw&-e>< zqJFG2{yO{)<4y5%qo+Oortvu9_j>$C<3sC3{b|>#IfBpMzy1yF#gNfJgxoy{d+WR21NCAztqPX*S{B&OFk&78s4uq zj<;#CjmBG{&T}4r)wt%_XrCrH`%W&)kkH_Ri!Yvx#H7dsR?=`4r!>5^e z{1`oU6*->zVU349{lW!4T6oIZCv|*zH#mUBjmV*hy6d`asA$J zX_w>2VUnueEX?Bmu8#4}@T-hZhu>=a7WkixKL_7IjwxQaWfUgg(jWG_jAI%c``q|R z$lo1r9yE_0eNX@52QbmCy0u%7K!MAx*p$d#^X^~Y@G2Ncp-X15KP6thH{PJBKPV5}Qea%aNB7Uh_4i@E zHm=_vd%}n)pXS#*`u%3sM^gNH{$0lPdpp1M_#tx6f9&YP_v_@^QF(~*U1)cf@uPA6 zEcUp5kCUDcZ4v*7Cw{BP-!iUwJ~jSd9Ipmr4&Sf(*sqg~p9G&{T)#(gp>d60MUE!J z20!(9v$56vk@%ydVk}P1$7K>Z$6sz-UymLk?||I0+D0BH-{B{7>W+^h zB#(GnRE)JYu5+M~{rVHR#!40aJ{IXGdz}eULiJ0cVywOK zRdD^D6NzsDlhq80XD=adPVv{0%N$UT<%7nxihl2hw5xMQ+4d2|H`QRbZZk0|NW6GF zD#j)l2gmsPG&s*i6yHRH-TL<)ukiS*9{-eF&mC!|?WCwN>AyHGKe1`#jiZq8zS!gI z$s1Dq6CQuXkiSoU9QgtAWfpw7F-^)r{xJ>C#+z-7NAzs7g; zct4MiFs}WXX1oaf%rd?PUS#}ocwk)P>tVZGOD@J^S*78z>=)0|;^bOaD4m?o+XOj3 z-*NhfpCj>gER*8-`lUwg1{#KC3ky}=k z+=)C5C%t%Q@;G@n@^td<N01IX)@~y_|eB&a(Rd6@k*4RHohI@_l)b$f4?%Gz;#o9uSfbL z^Q-oVYa!qk^`22N)`?u^T{;dY8`tN?cygJK9fv$;8`pEI$haPxOUWA|hpbkUA47f@ zInSdWBjD!^i|cCuf8U7Aqcnaf#fxkF404`FEg<%^e?~F(H*%Rjk>`?^lIJGTsoj<2ZOE@D=g--0Cg;!D?k0~@o{i+~$oYG9+LJ#|@g2xtCGSZ77I`P~ z56C-{e@@?+LzgSaW&WVcYVx^?ZhafMzGsrn_mQ7P@fGCqI4OiDX-B4HkQk3=DtNu28u!;WyWxW<@ z{C9A@wx}P1>vLbdjtsb4CPu69^Bw8G{{J|A?r6MzzNY84dVA#2b5gxKJk#VK2-kB! z<4=R@I7zV<>dI;cw#%v>c~B=Ri1R$~s=C#VDOd=n)B6i)TbEmD{)|gtxaj~nPVLa= z(7X<(M(FpDH`e5C&GW8f$a#Lk^FV&x=Xv16bbaP|;3#_D@jS3Towqy>oJG#_z$eIC zQa`h>GcwLR5B!Fl=Yf}DCncWef%lU0Jn&`m6R6!Z42tC8dEh{Do(Ik*=Xu}~a-Ij? zOdh9pYh%!*-FD<1$@RG}o5z#$Jn%g7j+E#3m||@mJs=iFYTz z1_w#J2l;ODp5**}GI}kN&3$n2Bu{UOpGuxi{s9iE#P=cZgM%c_^T3rj7~(t+yoH?S zf&U~QNcsE4qk`laME-m{Du|yPJq+)+omB05%@%2h!IgNP2Tmo|Yp=vtk`EQY(h>(l z@+1`9I-6Xt)w1~~a-IjigM%b_MoJ)-({PZ)M=84XJvd0>qsjk*gCwr61+sZK4w5*} z0~e8xlRzw2laE()>nF)4kbg$5uQjr{DNX`ucM`>aO@0RXXE-?|elmG?oE+j)$Zs4F z6~w2Kx5P;!K8^fNa-Ija!pS7@GbnyAInM*}`^jgM55`F;`R9_~LC*8Qr^)p- zSE}tK=Xv1H)1#!4pXY%|oUG!RQAl|I7I_x=s6ilix(n z^Oir7ccl2I$$8$got)<_J!(glWpg*mGlv{6ec{%8@*WhwmK-mM;a2@QrsT&?w`!tuvTVhC|^ykv#mpByhup^qi!dCNR`Uj+cngUm?e( zJ@l@1!&TUSp0|u8=XuLQa-O%`M9%Y;E#!%)sqlUe`Ec?haZ!`;A3;8cd?a~5K8kz` z`DpUyc)uq3dEU~MoaZe=$a&r}g?v2a&mre|%M$X56n_ml&s+A8^Sq@$F7DEQp0|u8 z=XpyxInP@jCFgm|UUHtdG{=jFv^#_P-<6!_Eknq8-jYeq^A>%dEBWVA{?{m;=Pi54 zdEW9JInP`4=Ngiq=PeWQ;wPR-?Us{gk?$a%Pkw+rn|vf*L?wR?`9ks}`EBI6ENuJLdW!%bYu=BA1dD`jVuBoAw$;5XvZA#q+wR({b!H>6=R zc_le{3-Yv4+2)3=YFkNBR<`bXUP)O|QL;LV8*wTX73URHXDKQx$gVCeIlPJN0yk7u zO)MzRDq4tAX(2aJT8Jj-U}WXz7iRO}ar>21<_<`8Om1<~b-B8n{hK<)Nl-WfS@YeQ z6+X(ZewbBSmI0FRonKg@XOlZhSveSp+}{=Fl~!#iNhZ(J4W-F^9qZiO{IU`~QF4=o zxf+u!E-uuwGCMz+RqQ~wxdY_RmT(v8VOaqca^2&@E-ksyU{JqxsH$mkZVF35C2C<8 zij#CDDNYt-Es_&8x@Ng&mfMXK;~QidcgNL}JBs;6XjZD~6WIoKo ze%J#O9g6C`!{Mj~9Jp%8r-QWUyqr88@RHK1D^_mwxVlc3Ydd^~R9(c+FL6&2?tXDr zfjcLvtCU0#b~-0dpJ@5v!|EvaTy-Z&bmf-{8Lo3KjeEuDl@T?XkzH6OSErD2b25qw z^@xYHN=kFWb0$owQdp*4Q^|8@bJR*yAtO2zRpe?|$}50`Er%z={4#fZq_`kMUc|-~ z$e2Xs>KC%8Jijdayy)bN&XM6WM@$$sBV+owGe=F2>bPzf6k^13lJmEN2%k zT9lNL4x3Ca%*ZX4ll=d~BJXYGtuChNRT&dij}CtzvAoE8Q8BzNtJ;*PdUW^$Tfd9j zEK}Y8*ZT6FPICJC|Fql2OPmYtaSwNsw`2)-_t(D%a9e|alo`+uRc zRP6_n)GbS*Rn>jG>QVGym0P8%AH(~~_e^};lyCLa*Y~C0n|NFQ4{m#^{ZC;3^)n#Z zf0?@5{omj$RsZ+kfQ)e4d5LquJ>KkYsp_ZWePag`&!4Hurmdd(nW*2chWgt*_1B|5 zzXz36ak-=AefKz3|M#Olzn7P4w*UY3)K6fb<5GzhTmOLDma6?s)GwyG@-S}7^<3pt z_4lAYe>NgjZTs?fR#VlFaRzA{>(|L+V*>S>g)SzxxVlc=Yo4Y z#8W?k_raS?e8Q9`dFtyg{AZhZ+y4wt{UW?i&Z?n)zNh{k)aTEpC6#Ue0#E(@sE@bD zRlTtFuk_T{|AZZ{su?}7_5a|hA4kLdS+-=b_2qMH_wTpYf7Gw3|C>DZ_49(7>Q{Q| zXa0}vzwW7Dg!($e?98+XXBE@*8jJsekJNRt!g=XVC(kh7ds@Kmsc>O#e zJLeX8{p{*)xU5#qe@^kVA2g9#vDm_BRdsL2Z-A%$DWy@v>8L5~cXKz}{zOmv`_X=L z^j|w*+aKd;KS29qsr?=)+MnlXKMghDK=T*kHfZ=U}*cD2K89j)b z@@<~_+i~GuSHt*i@YFBD4<&1=|G1}q8h%Jw+w|9t-;195an!Hr`tb(p+xIWMQQx+w z!=dZXJnbh?-;TGpFZ>f85km8d@x^FU1y*9Tb3|1(Ur|5sxFD-kEpKlzmpyZ`e4Ri!U(7!F&G z?GP39{}Jg%t*_;DPy1sRM|-G$-(1>HxSQ73@t=tH?f&cE!Qh{XN}Klh%m1rQwf{@- z3jx}Wv@gffwx8)~zo?V6cKGjBN!hl4mZ$yl6zz|6^=)pdRUDxmMIO8VZ+P0Da#2)~&)-QY z+TZ7Ce@fS=!NF)(SGN5l4|_QQNM9j%h3bd{%lYE^{6k;8(wVvY)}0uGFf&@P4x>r_4Plq;W7{< zbN9CWWuE%!J)s=BxJuW>dSG+a5vk2g{S?@-ciM6sIM#A{)3+OUrEt^R*Lpt z^t8VR?br1F{W(wj2U4`3oud7BJnicjsnpc|o1XT&UMxqo`umV&{FClx$Nytb`*9q& z4w8fxJO1B#>Q6!a2~jtyGT8d^IlY9~*Y8{L!DUVJ-xE;ZUcW0)e?(Qw(F5ClOIP3R z!f&2G35*|hBfO5B>*DPA$?wfc|84uaLp5E$2YTwKVL)oCFTbCcs{J_X50;&yB~Fj! z;Rw(E7o|9V3S7M1|1;5kEt7W+Cf@pYXmqpI*YZqH`&VlF7yvnci`-4?>+5QPXa6(t zOONxkA*}83&-1juetC4_YCFI^7XkWh9EY%)GtE)n&v+rxcaHaFEhpXUE+FSpT9qN+Fy_M*F;Z4Z~wpcv|o<)<(!d4p1;f8 z&9-0PUAR-7KZzky%Y9!#w5qzd<5$boPqqI?u82C$=g$h4#kSwd(|#t}$B#k1?Kkzb zKN#)v^Jis>_B(jmFGBl`s#=a7*!}P6sUM)e`S|HK=kIBr`sJu!)6YK!d-i`yiv3@e zV*kgYefv6l8`?K7pW(6QA(`vy%Q3a}*Z+_8^J}OdGcT-hZrnY-*wcS)U(YwKX3I-m z{Z!}g!W83oolBzi^}H(gwEsu!zsCF8zuMD&kfQza6z$*TX}{usY=51n{k19DU!9`; zhdk|9qW!MA<57Y}Nx|FNqt$H<<)f&SuxdBWTNe_uoW znw~#(-39Ld*1l=}H`k9AHPlbIed8t01^2k4t1nk-^E8X;{wGX)+>}rDv|owwt7-qo zp?#A!ru8TDf3k2v(W^>z^hj4f)$_Lk=kI#?1tGV{>)$$eld+aXpT9Fb?H?m2yIVeW zudlq=_7{5U|F|ZqbZdw>3vY=sZb1bZKlv<8>+ADR`eECa+?jKu9%ka_zz;b~HGh-&mA!UL{mO!<{(nqz b-EWDJMSe{oRsHzgQAPbe#W2|2+4}znwo#ViCjtMesMixd!=H?rf)-ghv&DR*)uQ4)izsAV)iW30+%?zyo delta 39 qcmX>wo#ViCjtMesh870KhGrX;)-ghv&DR*)uQ4)izsAV)iW30*Pz;*@ diff --git a/programs/lib/tls/libtls.a b/programs/lib/tls/libtls.a index 34f6ed5bdf61da9751d564933bcf3e0cd8de7450..0981a3f220de98250bdab3fc8ddc8e9c2033ac6a 100644 GIT binary patch delta 35 lcmZoV#nyC+ZGsG&k%f_wx%oyV2SzBfIfHR~1|#FF#Q?mf3Nruz delta 35 lcmZoV#nyC+ZGsG&p`nq9k-entry_names[idx], ".elf")) { montauk::spawn(fullpath); - } else if (fm->desktop) { - open_texteditor_with_file(fm->desktop, fullpath); + } else { + montauk::spawn("0:/apps/texteditor/texteditor.elf", fullpath); } } diff --git a/programs/src/desktop/apps/app_texteditor.cpp b/programs/src/desktop/apps/app_texteditor.cpp index f83627e..6554ce9 100644 --- a/programs/src/desktop/apps/app_texteditor.cpp +++ b/programs/src/desktop/apps/app_texteditor.cpp @@ -1,1039 +1,22 @@ /* * app_texteditor.cpp - * MontaukOS Desktop - Text Editor application - * Single-buffer text editor with line numbers, cursor, scrolling, file I/O + * MontaukOS Desktop - Text Editor launcher + * Spawns standalone texteditor.elf Window Server app * Copyright (c) 2026 Daniel Hammer */ #include "apps_common.hpp" // ============================================================================ -// Text Editor state -// ============================================================================ - -static constexpr int TE_TOOLBAR_H = 36; -static constexpr int TE_PATHBAR_H = 32; -static constexpr int TE_STATUS_H = 24; -static constexpr int TE_LINE_NUM_W = 48; -static constexpr int TE_SCROLLBAR_W = 12; -static constexpr int TE_INIT_CAP = 4096; -static constexpr int TE_MAX_CAP = 262144; // 256KB -static constexpr int TE_MAX_LINES = 16384; -static constexpr int TE_TAB_WIDTH = 4; - -struct TextEditorState { - char* buffer; - int buf_len; - int buf_cap; - int* line_offsets; - int line_count; - int cursor_pos; // byte position in buffer - int cursor_line; - int cursor_col; - int scroll_y; // first visible line - int scroll_x; // horizontal scroll in pixels - bool modified; - bool cursor_moved; // set when cursor moves, triggers scroll-to-cursor - char filepath[256]; - char filename[64]; - DesktopState* desktop; - - // Selection - int sel_anchor; // byte position where selection started - int sel_end; // byte position where selection extends to - bool has_selection; - bool mouse_selecting; - - // Scrollbar - Scrollbar scrollbar; - - bool show_pathbar; - char pathbar_text[256]; - int pathbar_cursor; - int pathbar_len; -}; - -// ============================================================================ -// Line index management -// ============================================================================ - -static void te_recompute_lines(TextEditorState* te) { - if (!te->line_offsets) { - te->line_offsets = (int*)montauk::malloc(TE_MAX_LINES * sizeof(int)); - } - - te->line_count = 0; - te->line_offsets[te->line_count++] = 0; - - for (int i = 0; i < te->buf_len; i++) { - if (te->buffer[i] == '\n' && te->line_count < TE_MAX_LINES) { - te->line_offsets[te->line_count++] = i + 1; - } - } -} - -static void te_update_cursor_pos(TextEditorState* te) { - // Find which line the cursor is on - te->cursor_line = 0; - for (int i = te->line_count - 1; i >= 0; i--) { - if (te->cursor_pos >= te->line_offsets[i]) { - te->cursor_line = i; - break; - } - } - te->cursor_col = te->cursor_pos - te->line_offsets[te->cursor_line]; - te->cursor_moved = true; -} - -static int te_line_length(TextEditorState* te, int line) { - if (line < 0 || line >= te->line_count) return 0; - int start = te->line_offsets[line]; - int end; - if (line + 1 < te->line_count) { - end = te->line_offsets[line + 1] - 1; // exclude newline - } else { - end = te->buf_len; - } - return end - start; -} - -// ============================================================================ -// Buffer operations -// ============================================================================ - -static void te_ensure_capacity(TextEditorState* te, int needed) { - if (te->buf_len + needed <= te->buf_cap) return; - int new_cap = te->buf_cap * 2; - if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP; - if (new_cap < te->buf_len + needed) new_cap = te->buf_len + needed; - te->buffer = (char*)montauk::realloc(te->buffer, new_cap); - te->buf_cap = new_cap; -} - -static void te_insert_char(TextEditorState* te, char c) { - if (te->buf_len >= TE_MAX_CAP - 1) return; - te_ensure_capacity(te, 1); - - // Shift everything after cursor right - for (int i = te->buf_len; i > te->cursor_pos; i--) { - te->buffer[i] = te->buffer[i - 1]; - } - te->buffer[te->cursor_pos] = c; - te->buf_len++; - te->cursor_pos++; - te->modified = true; - - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -static void te_insert_string(TextEditorState* te, const char* s, int len) { - if (te->buf_len + len >= TE_MAX_CAP) return; - te_ensure_capacity(te, len); - - for (int i = te->buf_len - 1; i >= te->cursor_pos; i--) { - te->buffer[i + len] = te->buffer[i]; - } - for (int i = 0; i < len; i++) { - te->buffer[te->cursor_pos + i] = s[i]; - } - te->buf_len += len; - te->cursor_pos += len; - te->modified = true; - - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -static void te_backspace(TextEditorState* te) { - if (te->cursor_pos <= 0) return; - te->cursor_pos--; - for (int i = te->cursor_pos; i < te->buf_len - 1; i++) { - te->buffer[i] = te->buffer[i + 1]; - } - te->buf_len--; - te->modified = true; - - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -static void te_delete_char(TextEditorState* te) { - if (te->cursor_pos >= te->buf_len) return; - for (int i = te->cursor_pos; i < te->buf_len - 1; i++) { - te->buffer[i] = te->buffer[i + 1]; - } - te->buf_len--; - te->modified = true; - - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -// ============================================================================ -// Cursor movement -// ============================================================================ - -static void te_move_up(TextEditorState* te) { - if (te->cursor_line <= 0) return; - int target_col = te->cursor_col; - int prev_line = te->cursor_line - 1; - int prev_len = te_line_length(te, prev_line); - if (target_col > prev_len) target_col = prev_len; - te->cursor_pos = te->line_offsets[prev_line] + target_col; - te_update_cursor_pos(te); -} - -static void te_move_down(TextEditorState* te) { - if (te->cursor_line >= te->line_count - 1) return; - int target_col = te->cursor_col; - int next_line = te->cursor_line + 1; - int next_len = te_line_length(te, next_line); - if (target_col > next_len) target_col = next_len; - te->cursor_pos = te->line_offsets[next_line] + target_col; - te_update_cursor_pos(te); -} - -static void te_move_left(TextEditorState* te) { - if (te->cursor_pos > 0) { - te->cursor_pos--; - te_update_cursor_pos(te); - } -} - -static void te_move_right(TextEditorState* te) { - if (te->cursor_pos < te->buf_len) { - te->cursor_pos++; - te_update_cursor_pos(te); - } -} - -static void te_move_home(TextEditorState* te) { - te->cursor_pos = te->line_offsets[te->cursor_line]; - te_update_cursor_pos(te); -} - -static void te_move_end(TextEditorState* te) { - te->cursor_pos = te->line_offsets[te->cursor_line] + te_line_length(te, te->cursor_line); - te_update_cursor_pos(te); -} - -// ============================================================================ -// Selection -// ============================================================================ - -static void te_clear_selection(TextEditorState* te) { - te->has_selection = false; - te->sel_anchor = 0; - te->sel_end = 0; -} - -static void te_sel_range(TextEditorState* te, int* out_start, int* out_end) { - if (te->sel_anchor < te->sel_end) { - *out_start = te->sel_anchor; - *out_end = te->sel_end; - } else { - *out_start = te->sel_end; - *out_end = te->sel_anchor; - } -} - -static void te_start_selection(TextEditorState* te) { - if (!te->has_selection) { - te->sel_anchor = te->cursor_pos; - te->sel_end = te->cursor_pos; - te->has_selection = true; - } -} - -static void te_update_selection(TextEditorState* te) { - te->sel_end = te->cursor_pos; - if (te->sel_anchor == te->sel_end) - te->has_selection = false; -} - -static void te_delete_selection(TextEditorState* te) { - if (!te->has_selection) return; - - int sel_s, sel_e; - te_sel_range(te, &sel_s, &sel_e); - int count = sel_e - sel_s; - if (count <= 0) { te_clear_selection(te); return; } - - // Remove bytes [sel_s, sel_e) from buffer - for (int i = sel_s; i < te->buf_len - count; i++) - te->buffer[i] = te->buffer[i + count]; - te->buf_len -= count; - - te->cursor_pos = sel_s; - te->modified = true; - te_clear_selection(te); - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -// ============================================================================ -// Scrolling -// ============================================================================ - -static void te_ensure_cursor_visible(TextEditorState* te, int visible_lines, int text_area_w) { - if (te->cursor_line < te->scroll_y) { - te->scroll_y = te->cursor_line; - } - if (te->cursor_line >= te->scroll_y + visible_lines) { - te->scroll_y = te->cursor_line - visible_lines + 1; - } - - // Clamp scroll_y - int max_scroll_y = te->line_count - visible_lines; - if (max_scroll_y < 0) max_scroll_y = 0; - if (te->scroll_y > max_scroll_y) te->scroll_y = max_scroll_y; - if (te->scroll_y < 0) te->scroll_y = 0; - - // Horizontal scroll - int cell_w = mono_cell_width(); - int cursor_px = te->cursor_col * cell_w; - int view_w = text_area_w - TE_LINE_NUM_W - TE_SCROLLBAR_W; - if (cursor_px - te->scroll_x > view_w - cell_w * 2) { - te->scroll_x = cursor_px - view_w + cell_w * 4; - } - if (cursor_px < te->scroll_x) { - te->scroll_x = cursor_px - cell_w * 2; - if (te->scroll_x < 0) te->scroll_x = 0; - } -} - -// ============================================================================ -// File I/O -// ============================================================================ - -static void te_load_file(TextEditorState* te, const char* path) { - int fd = montauk::open(path); - if (fd < 0) return; - - uint64_t size = montauk::getsize(fd); - if (size > TE_MAX_CAP) size = TE_MAX_CAP; - - if ((int)size >= te->buf_cap) { - int new_cap = (int)size + 1024; - if (new_cap > TE_MAX_CAP) new_cap = TE_MAX_CAP; - te->buffer = (char*)montauk::realloc(te->buffer, new_cap); - te->buf_cap = new_cap; - } - - montauk::read(fd, (uint8_t*)te->buffer, 0, size); - montauk::close(fd); - - te->buf_len = (int)size; - te->cursor_pos = 0; - te->scroll_y = 0; - te->scroll_x = 0; - te->modified = false; - - montauk::strncpy(te->filepath, path, 255); - - // Extract filename from path - int last_slash = -1; - for (int i = 0; path[i]; i++) { - if (path[i] == '/') last_slash = i; - } - if (last_slash >= 0) { - montauk::strncpy(te->filename, path + last_slash + 1, 63); - } else { - montauk::strncpy(te->filename, path, 63); - } - - te_recompute_lines(te); - te_update_cursor_pos(te); -} - -static void te_save_file(TextEditorState* te) { - if (te->filepath[0] == '\0') return; - - int fd = montauk::fcreate(te->filepath); - if (fd < 0) return; - - montauk::fwrite(fd, (const uint8_t*)te->buffer, 0, te->buf_len); - montauk::close(fd); - - te->modified = false; -} - -// ============================================================================ -// Drawing -// ============================================================================ - -static void texteditor_on_draw(Window* win, Framebuffer& fb) { - TextEditorState* te = (TextEditorState*)win->app_data; - if (!te) return; - - Canvas c(win); - c.fill(colors::WINDOW_BG); - - int cell_w = mono_cell_width(); - int cell_h = mono_cell_height(); - - // ---- Toolbar (36px) ---- - c.fill_rect(0, 0, c.w, TE_TOOLBAR_H, Color::from_rgb(0xF5, 0xF5, 0xF5)); - - // Open button - Color btn_bg = Color::from_rgb(0xE8, 0xE8, 0xE8); - c.fill_rounded_rect(4, 6, 24, 24, 3, btn_bg); - if (te->desktop && te->desktop->icon_folder.pixels) - c.icon(8, 10, te->desktop->icon_folder); - - // Save button - c.fill_rounded_rect(32, 6, 24, 24, 3, btn_bg); - if (te->desktop && te->desktop->icon_save.pixels) - c.icon(36, 10, te->desktop->icon_save); - - // Vertical separator - c.vline(60, 4, 28, colors::BORDER); - - // Filename + modified flag - char toolbar_label[128]; - if (te->filename[0]) { - snprintf(toolbar_label, 128, "%s%s", te->filename, te->modified ? " [modified]" : ""); - } else { - snprintf(toolbar_label, 128, "Untitled%s", te->modified ? " [modified]" : ""); - } - int sfh = system_font_height(); - c.text(68, (TE_TOOLBAR_H - sfh) / 2, toolbar_label, colors::TEXT_COLOR); - - // Toolbar bottom separator - c.hline(0, TE_TOOLBAR_H - 1, c.w, colors::BORDER); - - // ---- Path bar (conditional, 32px) ---- - int editor_y_start = TE_TOOLBAR_H; - - if (te->show_pathbar) { - int pb_y = TE_TOOLBAR_H; - c.fill_rect(0, pb_y, c.w, TE_PATHBAR_H, Color::from_rgb(0xF0, 0xF0, 0xF0)); - - // Text input box - int inp_x = 8; - int inp_y = pb_y + 4; - int btn_w = 56; - int inp_w = c.w - inp_x - btn_w - 12; - int inp_h = 24; - - c.fill_rect(inp_x, inp_y, inp_w, inp_h, colors::WHITE); - c.rect(inp_x, inp_y, inp_w, inp_h, colors::ACCENT); - - // Path text - int text_y = inp_y + (inp_h - sfh) / 2; - c.text(inp_x + 4, text_y, te->pathbar_text, colors::TEXT_COLOR); - - // Blinking cursor in path input - char prefix[256]; - int plen = te->pathbar_cursor; - if (plen > 255) plen = 255; - for (int i = 0; i < plen; i++) prefix[i] = te->pathbar_text[i]; - prefix[plen] = '\0'; - int cx = inp_x + 4 + text_width(prefix); - c.fill_rect(cx, inp_y + 3, 2, inp_h - 6, colors::ACCENT); - - // Open button - int ob_x = inp_x + inp_w + 6; - c.button(ob_x, inp_y, btn_w, inp_h, "Open", colors::ACCENT, colors::WHITE, 3); - - // Path bar bottom separator - c.hline(0, pb_y + TE_PATHBAR_H - 1, c.w, colors::BORDER); - - editor_y_start = TE_TOOLBAR_H + TE_PATHBAR_H; - } - - // ---- Editor area ---- - int text_area_h = c.h - editor_y_start - TE_STATUS_H; - int visible_lines = text_area_h / cell_h; - if (visible_lines < 1) visible_lines = 1; - int text_area_w = c.w - TE_SCROLLBAR_W; - - // Only scroll-to-cursor when the cursor actually moved - if (te->cursor_moved) { - te_ensure_cursor_visible(te, visible_lines, c.w); - te->cursor_moved = false; - } - - // Update scrollbar - te->scrollbar.bounds = {c.w - TE_SCROLLBAR_W, editor_y_start, TE_SCROLLBAR_W, text_area_h}; - te->scrollbar.content_height = te->line_count * cell_h; - te->scrollbar.view_height = text_area_h; - te->scrollbar.scroll_offset = te->scroll_y * cell_h; - { - int ms = te->scrollbar.max_scroll(); - if (te->scrollbar.scroll_offset > ms) te->scrollbar.scroll_offset = ms; - if (te->scrollbar.scroll_offset < 0) te->scrollbar.scroll_offset = 0; - } - - // Line number gutter background - c.fill_rect(0, editor_y_start, TE_LINE_NUM_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0)); - - // Gutter separator - c.vline(TE_LINE_NUM_W, editor_y_start, text_area_h, colors::BORDER); - - // Selection range - int sel_s = 0, sel_e = 0; - if (te->has_selection) te_sel_range(te, &sel_s, &sel_e); - Color sel_bg = Color::from_rgb(0xB0, 0xD0, 0xF0); - - // Draw lines - Color linenum_color = Color::from_rgb(0x99, 0x99, 0x99); - Color cursor_line_color = Color::from_rgb(0xFF, 0xFD, 0xE8); - Color text_color = colors::TEXT_COLOR; - - int text_start_x = TE_LINE_NUM_W + 4; - - for (int vis = 0; vis < visible_lines + 1; vis++) { - int line = te->scroll_y + vis; - if (line >= te->line_count) break; - - int py = editor_y_start + vis * cell_h; - if (py >= editor_y_start + text_area_h) break; - - // Cursor line highlighting (only when no selection) - if (line == te->cursor_line && !te->has_selection) { - int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py); - if (hl_h > 0) - c.fill_rect(TE_LINE_NUM_W + 1, py, text_area_w - TE_LINE_NUM_W - 1, hl_h, cursor_line_color); - } - - // Line number - char num_str[8]; - snprintf(num_str, 8, "%4d", line + 1); - c.text_mono(4, py, num_str, linenum_color); - - // Line text (per-character rendering with horizontal scroll clipping) - int line_start = te->line_offsets[line]; - int line_len = te_line_length(te, line); - - for (int ci = 0; ci < line_len; ci++) { - int px = text_start_x + ci * cell_w - te->scroll_x; - if (px + cell_w <= TE_LINE_NUM_W + 1) continue; - if (px >= text_area_w) break; - - int byte_pos = line_start + ci; - char ch = te->buffer[byte_pos]; - - // Draw selection highlight behind text - if (te->has_selection && byte_pos >= sel_s && byte_pos < sel_e) { - int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py); - if (hl_h > 0) - c.fill_rect(px, py, cell_w, hl_h, sel_bg); - } - - if (ch >= 32 || ch < 0) { - if (fonts::mono && fonts::mono->valid) { - GlyphCache* gc = fonts::mono->get_cache(fonts::TERM_SIZE); - fonts::mono->draw_char_to_buffer( - c.pixels, c.w, c.h, px, py + gc->ascent, - ch, text_color, gc); - } else { - // bitmap fallback - uint32_t text_px = text_color.to_pixel(); - const uint8_t* glyph = &font_data[(unsigned char)ch * FONT_HEIGHT]; - for (int fy = 0; fy < FONT_HEIGHT && py + fy < editor_y_start + text_area_h; fy++) { - uint8_t bits = glyph[fy]; - for (int fx = 0; fx < FONT_WIDTH; fx++) { - if (bits & (0x80 >> fx)) { - int dx = px + fx; - int dy = py + fy; - if (dx > TE_LINE_NUM_W && dx < text_area_w && dy >= 0 && dy < c.h) - c.pixels[dy * c.w + dx] = text_px; - } - } - } - } - } - } - - // Draw selection highlight for newline at end of line (visual feedback) - if (te->has_selection && line + 1 < te->line_count) { - int nl_pos = line_start + line_len; // the newline byte - if (nl_pos >= sel_s && nl_pos < sel_e) { - int px = text_start_x + line_len * cell_w - te->scroll_x; - if (px > TE_LINE_NUM_W && px < text_area_w) { - int hl_h = gui_min(cell_h, editor_y_start + text_area_h - py); - if (hl_h > 0) - c.fill_rect(px, py, cell_w / 2, hl_h, sel_bg); - } - } - } - - // Draw cursor - if (line == te->cursor_line) { - int cx = text_start_x + te->cursor_col * cell_w - te->scroll_x; - if (cx > TE_LINE_NUM_W && cx + 2 <= text_area_w) { - int cur_h = gui_min(cell_h, editor_y_start + text_area_h - py); - if (cur_h > 0) - c.fill_rect(cx, py, 2, cur_h, colors::ACCENT); - } - } - } - - // ---- Scrollbar ---- - if (te->scrollbar.content_height > te->scrollbar.view_height) { - Color sb_fg = (te->scrollbar.hovered || te->scrollbar.dragging) - ? te->scrollbar.hover_fg : te->scrollbar.fg; - int sbx = te->scrollbar.bounds.x; - int sby = te->scrollbar.bounds.y; - int sbw = te->scrollbar.bounds.w; - int sbh = te->scrollbar.bounds.h; - c.fill_rect(sbx, sby, sbw, sbh, colors::SCROLLBAR_BG); - int th = te->scrollbar.thumb_height(); - int tty = te->scrollbar.thumb_y(); - c.fill_rect(sbx + 1, tty, sbw - 2, th, sb_fg); - } - - // ---- Status bar ---- - int status_y = c.h - TE_STATUS_H; - c.fill_rect(0, status_y, c.w, TE_STATUS_H, Color::from_rgb(0x2B, 0x3E, 0x50)); - - // Cursor position (right side) - char status_right[48]; - if (te->has_selection) { - int ss, se; - te_sel_range(te, &ss, &se); - snprintf(status_right, 48, "%d selected Ln %d, Col %d ", se - ss, te->cursor_line + 1, te->cursor_col + 1); - } else { - snprintf(status_right, 48, "Ln %d, Col %d ", te->cursor_line + 1, te->cursor_col + 1); - } - int sr_w = text_width(status_right); - int status_text_y = status_y + (TE_STATUS_H - sfh) / 2; - c.text(c.w - sr_w - 4, status_text_y, status_right, colors::PANEL_TEXT); - - // Filename + modified flag (left side) - char status_left[128]; - if (te->filename[0]) { - snprintf(status_left, 128, " %s%s", te->filename, te->modified ? " [modified]" : ""); - } else { - snprintf(status_left, 128, " Untitled%s", te->modified ? " [modified]" : ""); - } - c.text(4, status_text_y, status_left, colors::PANEL_TEXT); -} - -// ============================================================================ -// Mouse handling -// ============================================================================ - -static int te_hit_test(TextEditorState* te, int local_x, int local_y, int editor_y_start) { - int cell_w = mono_cell_width(); - int cell_h = mono_cell_height(); - - int clicked_line = te->scroll_y + (local_y - editor_y_start) / cell_h; - if (clicked_line >= te->line_count) clicked_line = te->line_count - 1; - if (clicked_line < 0) clicked_line = 0; - - int clicked_col = (local_x - TE_LINE_NUM_W - 4 + te->scroll_x + cell_w / 2) / cell_w; - if (clicked_col < 0) clicked_col = 0; - int line_len = te_line_length(te, clicked_line); - if (clicked_col > line_len) clicked_col = line_len; - - return te->line_offsets[clicked_line] + clicked_col; -} - -static void texteditor_on_mouse(Window* win, MouseEvent& ev) { - TextEditorState* te = (TextEditorState*)win->app_data; - if (!te) return; - - Rect cr = win->content_rect(); - int local_x = ev.x - cr.x; - int local_y = ev.y - cr.y; - - int cell_h = mono_cell_height(); - - int editor_y_start = TE_TOOLBAR_H + (te->show_pathbar ? TE_PATHBAR_H : 0); - int text_area_h = cr.h - editor_y_start - TE_STATUS_H; - - // ---- Scrollbar ---- - { - MouseEvent local_ev = ev; - local_ev.x = local_x; - local_ev.y = local_y; - bool was_dragging = te->scrollbar.dragging; - te->scrollbar.handle_mouse(local_ev); - if (te->scrollbar.dragging || was_dragging) { - // Convert pixel scroll offset back to line index - if (cell_h > 0) - te->scroll_y = te->scrollbar.scroll_offset / cell_h; - return; - } - } - - // ---- Toolbar clicks ---- - if (ev.left_pressed() && local_y < TE_TOOLBAR_H) { - // Open button - if (local_x >= 4 && local_x < 28 && local_y >= 6 && local_y < 30) { - te->show_pathbar = !te->show_pathbar; - if (te->show_pathbar) { - montauk::strncpy(te->pathbar_text, te->filepath, 255); - te->pathbar_len = montauk::slen(te->pathbar_text); - te->pathbar_cursor = te->pathbar_len; - } - return; - } - // Save button - if (local_x >= 32 && local_x < 56 && local_y >= 6 && local_y < 30) { - te_save_file(te); - return; - } - return; - } - - // ---- Path bar clicks ---- - if (te->show_pathbar && local_y >= TE_TOOLBAR_H && local_y < TE_TOOLBAR_H + TE_PATHBAR_H) { - if (ev.left_pressed()) { - int btn_w = 56; - int inp_w = cr.w - 8 - btn_w - 12; - int ob_x = 8 + inp_w + 6; - if (local_x >= ob_x && local_x < ob_x + btn_w) { - if (te->pathbar_text[0]) { - te_load_file(te, te->pathbar_text); - char title[64]; - snprintf(title, 64, "%s - Editor", te->filename); - montauk::strncpy(win->title, title, 63); - te->show_pathbar = false; - } - } - } - return; - } - - // ---- Editor area: mouse press - start selection or place cursor ---- - if (ev.left_pressed() && local_y >= editor_y_start && local_y < editor_y_start + text_area_h - && local_x > TE_LINE_NUM_W && local_x < cr.w - TE_SCROLLBAR_W) { - int pos = te_hit_test(te, local_x, local_y, editor_y_start); - te->cursor_pos = pos; - te_update_cursor_pos(te); - - te->sel_anchor = pos; - te->sel_end = pos; - te->has_selection = false; - te->mouse_selecting = true; - return; - } - - // ---- Editor area: mouse drag - extend selection ---- - if (te->mouse_selecting && ev.left_held() && local_y >= editor_y_start - 20) { - int pos = te_hit_test(te, local_x, local_y, editor_y_start); - te->sel_end = pos; - te->cursor_pos = pos; - te_update_cursor_pos(te); - te->has_selection = (te->sel_anchor != te->sel_end); - return; - } - - // ---- Mouse release: end selection ---- - if (te->mouse_selecting && ev.left_released()) { - te->mouse_selecting = false; - return; - } - - // ---- Scroll ---- - if (ev.scroll != 0 && local_y >= editor_y_start && local_y < editor_y_start + text_area_h) { - te->scroll_y -= ev.scroll * 3; - if (te->scroll_y < 0) te->scroll_y = 0; - int visible_lines = text_area_h / cell_h; - int max_scroll = te->line_count - visible_lines; - if (max_scroll < 0) max_scroll = 0; - if (te->scroll_y > max_scroll) te->scroll_y = max_scroll; - } -} - -// ============================================================================ -// Keyboard handling -// ============================================================================ - -static void texteditor_on_key(Window* win, const Montauk::KeyEvent& key) { - TextEditorState* te = (TextEditorState*)win->app_data; - if (!te || !key.pressed) return; - - // ---- Path bar input mode ---- - if (te->show_pathbar) { - if (key.ascii == '\n' || key.ascii == '\r') { - if (te->pathbar_text[0]) { - te_load_file(te, te->pathbar_text); - char title[64]; - snprintf(title, 64, "%s - Editor", te->filename); - montauk::strncpy(win->title, title, 63); - te->show_pathbar = false; - } - return; - } - if (key.scancode == 0x01) { // Escape - te->show_pathbar = false; - return; - } - if (key.ascii == '\b' || key.scancode == 0x0E) { - if (te->pathbar_cursor > 0) { - for (int i = te->pathbar_cursor - 1; i < te->pathbar_len - 1; i++) - te->pathbar_text[i] = te->pathbar_text[i + 1]; - te->pathbar_len--; - te->pathbar_cursor--; - te->pathbar_text[te->pathbar_len] = '\0'; - } - return; - } - if (key.scancode == 0x4B) { // Left - if (te->pathbar_cursor > 0) te->pathbar_cursor--; - return; - } - if (key.scancode == 0x4D) { // Right - if (te->pathbar_cursor < te->pathbar_len) te->pathbar_cursor++; - return; - } - if (key.ascii >= 32 && key.ascii < 127 && te->pathbar_len < 254) { - for (int i = te->pathbar_len; i > te->pathbar_cursor; i--) - te->pathbar_text[i] = te->pathbar_text[i - 1]; - te->pathbar_text[te->pathbar_cursor] = key.ascii; - te->pathbar_cursor++; - te->pathbar_len++; - te->pathbar_text[te->pathbar_len] = '\0'; - return; - } - return; - } - - // ---- Normal editor mode ---- - - // Ctrl+S: save - if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) { - te_save_file(te); - return; - } - - // Ctrl+O: open - if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) { - te->show_pathbar = !te->show_pathbar; - if (te->show_pathbar) { - montauk::strncpy(te->pathbar_text, te->filepath, 255); - te->pathbar_len = montauk::slen(te->pathbar_text); - te->pathbar_cursor = te->pathbar_len; - } - return; - } - - // Ctrl+A: select all - if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) { - te->sel_anchor = 0; - te->sel_end = te->buf_len; - te->has_selection = (te->buf_len > 0); - te->cursor_pos = te->buf_len; - te_update_cursor_pos(te); - return; - } - - // Arrow keys (Shift extends selection) - if (key.scancode == 0x48) { // Up - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - te_move_up(te); - if (key.shift) te_update_selection(te); - return; - } - if (key.scancode == 0x50) { // Down - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - te_move_down(te); - if (key.shift) te_update_selection(te); - return; - } - if (key.scancode == 0x4B) { // Left - if (key.shift) { - te_start_selection(te); - te_move_left(te); - te_update_selection(te); - } else if (te->has_selection) { - int s, e; te_sel_range(te, &s, &e); - te->cursor_pos = s; - te_update_cursor_pos(te); - te_clear_selection(te); - } else { - te_move_left(te); - } - return; - } - if (key.scancode == 0x4D) { // Right - if (key.shift) { - te_start_selection(te); - te_move_right(te); - te_update_selection(te); - } else if (te->has_selection) { - int s, e; te_sel_range(te, &s, &e); - te->cursor_pos = e; - te_update_cursor_pos(te); - te_clear_selection(te); - } else { - te_move_right(te); - } - return; - } - - // Home - if (key.scancode == 0x47) { - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - te_move_home(te); - if (key.shift) te_update_selection(te); - return; - } - // End - if (key.scancode == 0x4F) { - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - te_move_end(te); - if (key.shift) te_update_selection(te); - return; - } - - // Page Up - if (key.scancode == 0x49) { - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - int cell_h = mono_cell_height(); - int visible = (te->scrollbar.view_height > 0 && cell_h > 0) - ? te->scrollbar.view_height / cell_h : 20; - for (int i = 0; i < visible; i++) te_move_up(te); - if (key.shift) te_update_selection(te); - return; - } - // Page Down - if (key.scancode == 0x51) { - if (key.shift) te_start_selection(te); - else if (te->has_selection) te_clear_selection(te); - int cell_h = mono_cell_height(); - int visible = (te->scrollbar.view_height > 0 && cell_h > 0) - ? te->scrollbar.view_height / cell_h : 20; - for (int i = 0; i < visible; i++) te_move_down(te); - if (key.shift) te_update_selection(te); - return; - } - - // Delete - if (key.scancode == 0x53) { - if (te->has_selection) te_delete_selection(te); - else te_delete_char(te); - return; - } - - // Backspace - if (key.ascii == '\b' || key.scancode == 0x0E) { - if (te->has_selection) te_delete_selection(te); - else te_backspace(te); - return; - } - - // Enter - if (key.ascii == '\n' || key.ascii == '\r') { - if (te->has_selection) te_delete_selection(te); - te_insert_char(te, '\n'); - return; - } - - // Tab - if (key.ascii == '\t') { - if (te->has_selection) te_delete_selection(te); - for (int i = 0; i < TE_TAB_WIDTH; i++) { - te_insert_char(te, ' '); - } - return; - } - - // Printable characters - if (key.ascii >= 32 && key.ascii < 127) { - if (te->has_selection) te_delete_selection(te); - te_insert_char(te, key.ascii); - return; - } -} - -static void texteditor_on_close(Window* win) { - TextEditorState* te = (TextEditorState*)win->app_data; - if (te) { - if (te->buffer) montauk::mfree(te->buffer); - if (te->line_offsets) montauk::mfree(te->line_offsets); - montauk::mfree(te); - win->app_data = nullptr; - } -} - -// ============================================================================ -// Text Editor launchers +// Text Editor launchers (spawn standalone Window Server app) // ============================================================================ void open_texteditor(DesktopState* ds) { - int idx = desktop_create_window(ds, "Text Editor", 180, 60, 600, 450); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState)); - montauk::memset(te, 0, sizeof(TextEditorState)); - - te->buffer = (char*)montauk::malloc(TE_INIT_CAP); - te->buf_cap = TE_INIT_CAP; - te->buf_len = 0; - te->modified = false; - te->desktop = ds; - te->show_pathbar = false; - te->pathbar_text[0] = '\0'; - te->pathbar_cursor = 0; - te->pathbar_len = 0; - te->has_selection = false; - te->mouse_selecting = false; - te->sel_anchor = 0; - te->sel_end = 0; - te->cursor_moved = true; - te->scrollbar.init(0, 0, TE_SCROLLBAR_W, 100); - - te_recompute_lines(te); - te_update_cursor_pos(te); - - win->app_data = te; - win->on_draw = texteditor_on_draw; - win->on_mouse = texteditor_on_mouse; - win->on_key = texteditor_on_key; - win->on_close = texteditor_on_close; + (void)ds; + montauk::spawn("0:/apps/texteditor/texteditor.elf"); } void open_texteditor_with_file(DesktopState* ds, const char* path) { - const char* name = path; - for (int i = 0; path[i]; i++) { - if (path[i] == '/') name = path + i + 1; - } - - char title[64]; - snprintf(title, 64, "%s - Editor", name); - - int idx = desktop_create_window(ds, title, 180, 60, 600, 450); - if (idx < 0) return; - - Window* win = &ds->windows[idx]; - TextEditorState* te = (TextEditorState*)montauk::malloc(sizeof(TextEditorState)); - montauk::memset(te, 0, sizeof(TextEditorState)); - - te->buffer = (char*)montauk::malloc(TE_INIT_CAP); - te->buf_cap = TE_INIT_CAP; - te->buf_len = 0; - te->modified = false; - te->desktop = ds; - te->show_pathbar = false; - te->pathbar_text[0] = '\0'; - te->pathbar_cursor = 0; - te->pathbar_len = 0; - te->has_selection = false; - te->mouse_selecting = false; - te->sel_anchor = 0; - te->sel_end = 0; - te->scrollbar.init(0, 0, TE_SCROLLBAR_W, 100); - - te_recompute_lines(te); - te_update_cursor_pos(te); - - te_load_file(te, path); - - win->app_data = te; - win->on_draw = texteditor_on_draw; - win->on_mouse = texteditor_on_mouse; - win->on_key = texteditor_on_key; - win->on_close = texteditor_on_close; + (void)ds; + montauk::spawn("0:/apps/texteditor/texteditor.elf", path); } diff --git a/programs/src/doom/Makefile b/programs/src/doom/Makefile index 1a29d14..eb21720 100644 --- a/programs/src/doom/Makefile +++ b/programs/src/doom/Makefile @@ -20,6 +20,7 @@ endif DOOM_SRC := ../../../doomgeneric/doomgeneric LIBC_INC := ../../include/libc PROG_INC := ../../include +LIBDIR := ../../lib LINK_LD := ../../link.ld BINDIR := ../../bin OBJDIR := obj @@ -153,7 +154,7 @@ DOOM_SRCS := \ z_zone.c # Local source files -LOCAL_SRCS := doomgeneric_montauk.c libc.c +LOCAL_SRCS := doomgeneric_montauk.c # ---- Object files ---- @@ -171,7 +172,7 @@ all: $(TARGET) $(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile mkdir -p $(BINDIR)/apps/doom - $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@ + $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) $(LIBDIR)/libc/liblibc.a -o $@ # DOOM source files (from doomgeneric directory) $(OBJDIR)/%.o: $(DOOM_SRC)/%.c Makefile diff --git a/programs/src/doom/libc.c b/programs/src/doom/libc.c deleted file mode 100644 index 42549d4..0000000 --- a/programs/src/doom/libc.c +++ /dev/null @@ -1,1124 +0,0 @@ -/* - * libc.c - * Minimal C standard library for MontaukOS userspace (DOOM port) - * Copyright (c) 2025 Daniel Hammer -*/ - -#include -#include -#include -#include - -/* ======================================================================== - Raw syscall wrappers (C versions matching kernel ABI) - ======================================================================== */ - -static inline long _zos_syscall0(long nr) { - long ret; - __asm__ volatile("syscall" : "=a"(ret) : "a"(nr) - : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); - return ret; -} - -static inline long _zos_syscall1(long nr, long a1) { - long ret; - __asm__ volatile( - "mov %[a1], %%rdi\n\t" - "syscall" - : "=a"(ret) - : "a"(nr), [a1] "r"(a1) - : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); - return ret; -} - -static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) { - long ret; - __asm__ volatile( - "mov %[a1], %%rdi\n\t" - "mov %[a2], %%rsi\n\t" - "mov %[a3], %%rdx\n\t" - "mov %[a4], %%r10\n\t" - "syscall" - : "=a"(ret) - : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4) - : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); - return ret; -} - -/* Syscall numbers */ -#define SYS_EXIT 0 -#define SYS_PRINT 4 -#define SYS_PUTCHAR 5 -#define SYS_OPEN 6 -#define SYS_READ 7 -#define SYS_GETSIZE 8 -#define SYS_CLOSE 9 -#define SYS_ALLOC 11 -#define SYS_FREE 12 - -/* ======================================================================== - errno - ======================================================================== */ - -int errno = 0; - -/* ======================================================================== - string.h functions - ======================================================================== */ - -void *memcpy(void *dest, const void *src, size_t n) { - unsigned char *d = (unsigned char *)dest; - const unsigned char *s = (const unsigned char *)src; - for (size_t i = 0; i < n; i++) - d[i] = s[i]; - return dest; -} - -void *memset(void *s, int c, size_t n) { - unsigned char *p = (unsigned char *)s; - for (size_t i = 0; i < n; i++) - p[i] = (unsigned char)c; - return s; -} - -void *memmove(void *dest, const void *src, size_t n) { - unsigned char *d = (unsigned char *)dest; - const unsigned char *s = (const unsigned char *)src; - if (s < d && d < s + n) { - for (size_t i = n; i > 0; i--) - d[i - 1] = s[i - 1]; - } else { - for (size_t i = 0; i < n; i++) - d[i] = s[i]; - } - return dest; -} - -int memcmp(const void *s1, const void *s2, size_t n) { - const unsigned char *a = (const unsigned char *)s1; - const unsigned char *b = (const unsigned char *)s2; - for (size_t i = 0; i < n; i++) { - if (a[i] != b[i]) - return a[i] < b[i] ? -1 : 1; - } - return 0; -} - -size_t strlen(const char *s) { - size_t len = 0; - while (s[len]) len++; - return len; -} - -int strcmp(const char *a, const char *b) { - while (*a && *a == *b) { a++; b++; } - return (unsigned char)*a - (unsigned char)*b; -} - -int strncmp(const char *a, const char *b, size_t n) { - for (size_t i = 0; i < n; i++) { - if (a[i] != b[i] || a[i] == 0) - return (unsigned char)a[i] - (unsigned char)b[i]; - } - return 0; -} - -char *strcpy(char *dest, const char *src) { - char *d = dest; - while ((*d++ = *src++)); - return dest; -} - -char *strncpy(char *dest, const char *src, size_t n) { - size_t i; - for (i = 0; i < n && src[i]; i++) - dest[i] = src[i]; - for (; i < n; i++) - dest[i] = '\0'; - return dest; -} - -char *strcat(char *dest, const char *src) { - char *d = dest + strlen(dest); - while ((*d++ = *src++)); - return dest; -} - -char *strncat(char *dest, const char *src, size_t n) { - char *d = dest + strlen(dest); - size_t i; - for (i = 0; i < n && src[i]; i++) - d[i] = src[i]; - d[i] = '\0'; - return dest; -} - -char *strchr(const char *s, int c) { - while (*s) { - if (*s == (char)c) return (char *)s; - s++; - } - return (c == 0) ? (char *)s : NULL; -} - -char *strrchr(const char *s, int c) { - const char *last = NULL; - while (*s) { - if (*s == (char)c) last = s; - s++; - } - if (c == 0) return (char *)s; - return (char *)last; -} - -static int _tolower(int c) { - return (c >= 'A' && c <= 'Z') ? c + 32 : c; -} - -int strcasecmp(const char *a, const char *b) { - while (*a && _tolower((unsigned char)*a) == _tolower((unsigned char)*b)) { a++; b++; } - return _tolower((unsigned char)*a) - _tolower((unsigned char)*b); -} - -int strncasecmp(const char *a, const char *b, size_t n) { - for (size_t i = 0; i < n; i++) { - int ca = _tolower((unsigned char)a[i]); - int cb = _tolower((unsigned char)b[i]); - if (ca != cb || ca == 0) return ca - cb; - } - return 0; -} - -char *strstr(const char *haystack, const char *needle) { - if (!*needle) return (char *)haystack; - size_t nlen = strlen(needle); - while (*haystack) { - if (strncmp(haystack, needle, nlen) == 0) - return (char *)haystack; - haystack++; - } - return NULL; -} - -/* ======================================================================== - ctype.h functions - ======================================================================== */ - -int isalpha(int c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } -int isdigit(int c) { return c >= '0' && c <= '9'; } -int isalnum(int c) { return isalpha(c) || isdigit(c); } -int isspace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } -int isupper(int c) { return c >= 'A' && c <= 'Z'; } -int islower(int c) { return c >= 'a' && c <= 'z'; } -int isprint(int c) { return c >= 0x20 && c <= 0x7E; } -int ispunct(int c) { return isprint(c) && !isalnum(c) && c != ' '; } -int isxdigit(int c) { return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } -int iscntrl(int c) { return (c >= 0 && c < 0x20) || c == 0x7F; } -int isgraph(int c) { return c > 0x20 && c <= 0x7E; } -int toupper(int c) { return (c >= 'a' && c <= 'z') ? c - 32 : c; } -int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } - -/* ======================================================================== - Heap allocator (free-list, backed by SYS_ALLOC) - ======================================================================== */ - -#define HEAP_MAGIC 0x5A484541ULL /* "ZHEA" */ - -struct HeapHeader { - uint64_t magic; - uint64_t size; -} __attribute__((packed)); - -struct FreeNode { - uint64_t size; - struct FreeNode *next; -}; - -static struct FreeNode g_heapHead = { 0, NULL }; -static int g_heapInit = 0; - -static void heap_insert_free(void *ptr, uint64_t size) { - struct FreeNode *node = (struct FreeNode *)ptr; - node->size = size; - node->next = g_heapHead.next; - g_heapHead.next = node; -} - -static void heap_grow(uint64_t bytes) { - uint64_t pages = (bytes + 0xFFF) / 0x1000; - if (pages < 4) pages = 4; - void *mem = (void *)_zos_syscall1(SYS_ALLOC, (long)(pages * 0x1000)); - if (mem != NULL) - heap_insert_free(mem, pages * 0x1000); -} - -void *malloc(size_t size) { - if (!g_heapInit) { - heap_grow(16 * 0x1000); - g_heapInit = 1; - } - - uint64_t needed = size + sizeof(struct HeapHeader); - needed = (needed + 15) & ~15ULL; - - struct FreeNode *prev = &g_heapHead; - struct FreeNode *cur = g_heapHead.next; - - while (cur != NULL) { - if (cur->size >= needed) { - uint64_t blockSize = cur->size; - prev->next = cur->next; - - if (blockSize > needed + sizeof(struct FreeNode) + 16) { - void *rest = (void *)((uint8_t *)cur + needed); - heap_insert_free(rest, blockSize - needed); - } - - struct HeapHeader *hdr = (struct HeapHeader *)cur; - hdr->magic = HEAP_MAGIC; - hdr->size = size; - return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader)); - } - prev = cur; - cur = cur->next; - } - - heap_grow(needed); - return malloc(size); -} - -void free(void *ptr) { - if (ptr == NULL) return; - - struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); - uint64_t blockSize = hdr->size + sizeof(struct HeapHeader); - blockSize = (blockSize + 15) & ~15ULL; - heap_insert_free((void *)hdr, blockSize); -} - -void *calloc(size_t nmemb, size_t size) { - size_t total = nmemb * size; - void *p = malloc(total); - if (p) memset(p, 0, total); - return p; -} - -void *realloc(void *ptr, size_t size) { - if (ptr == NULL) return malloc(size); - if (size == 0) { free(ptr); return NULL; } - - struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); - uint64_t old = hdr->size; - - void *newp = malloc(size); - if (newp == NULL) return NULL; - - size_t copySize = old < size ? old : size; - memcpy(newp, ptr, copySize); - free(ptr); - return newp; -} - -char *strdup(const char *s) { - size_t len = strlen(s) + 1; - char *d = (char *)malloc(len); - if (d) memcpy(d, s, len); - return d; -} - -/* ======================================================================== - stdlib.h functions - ======================================================================== */ - -int abs(int x) { return x < 0 ? -x : x; } -long labs(long x) { return x < 0 ? -x : x; } - -int atoi(const char *s) { - int neg = 0, val = 0; - while (isspace((unsigned char)*s)) s++; - if (*s == '-') { neg = 1; s++; } - else if (*s == '+') { s++; } - while (isdigit((unsigned char)*s)) { - val = val * 10 + (*s - '0'); - s++; - } - return neg ? -val : val; -} - -long atol(const char *s) { - long neg = 0, val = 0; - while (isspace((unsigned char)*s)) s++; - if (*s == '-') { neg = 1; s++; } - else if (*s == '+') { s++; } - while (isdigit((unsigned char)*s)) { - val = val * 10 + (*s - '0'); - s++; - } - return neg ? -val : val; -} - -long strtol(const char *nptr, char **endptr, int base) { - const char *s = nptr; - long val = 0; - int neg = 0; - - while (isspace((unsigned char)*s)) s++; - if (*s == '-') { neg = 1; s++; } - else if (*s == '+') { s++; } - - if (base == 0) { - if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } - else if (*s == '0') { base = 8; s++; } - else base = 10; - } else if (base == 16 && *s == '0' && (s[1] == 'x' || s[1] == 'X')) { - s += 2; - } - - while (*s) { - int digit; - if (*s >= '0' && *s <= '9') digit = *s - '0'; - else if (*s >= 'a' && *s <= 'z') digit = *s - 'a' + 10; - else if (*s >= 'A' && *s <= 'Z') digit = *s - 'A' + 10; - else break; - if (digit >= base) break; - val = val * base + digit; - s++; - } - - if (endptr) *endptr = (char *)s; - return neg ? -val : val; -} - -unsigned long strtoul(const char *nptr, char **endptr, int base) { - return (unsigned long)strtol(nptr, endptr, base); -} - -void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)) { - /* Simple insertion sort — sufficient for doom's usage */ - char *arr = (char *)base; - char tmp[256]; /* doom's qsort elements are small */ - for (size_t i = 1; i < nmemb; i++) { - memcpy(tmp, arr + i * size, size); - size_t j = i; - while (j > 0 && compar(arr + (j - 1) * size, tmp) > 0) { - memcpy(arr + j * size, arr + (j - 1) * size, size); - j--; - } - memcpy(arr + j * size, tmp, size); - } -} - -static unsigned int _rand_seed = 1; - -int rand(void) { - _rand_seed = _rand_seed * 1103515245 + 12345; - return (int)((_rand_seed >> 16) & 0x7FFF); -} - -void srand(unsigned int seed) { - _rand_seed = seed; -} - -char *getenv(const char *name) { - (void)name; - return NULL; -} - -static void (*_atexit_funcs[32])(void); -static int _atexit_count = 0; - -int atexit(void (*func)(void)) { - if (_atexit_count >= 32) return -1; - _atexit_funcs[_atexit_count++] = func; - return 0; -} - -void exit(int status) { - for (int i = _atexit_count - 1; i >= 0; i--) - _atexit_funcs[i](); - _zos_syscall1(SYS_EXIT, (long)status); - __builtin_unreachable(); -} - -void abort(void) { - _zos_syscall1(SYS_PRINT, (long)"abort() called\n"); - _zos_syscall1(SYS_EXIT, 1); - __builtin_unreachable(); -} - -/* ======================================================================== - assert.h support - ======================================================================== */ - -void __assert_fail(const char *expr, const char *file, int line, const char *func) { - _zos_syscall1(SYS_PRINT, (long)"Assertion failed: "); - _zos_syscall1(SYS_PRINT, (long)expr); - _zos_syscall1(SYS_PRINT, (long)" at "); - _zos_syscall1(SYS_PRINT, (long)file); - _zos_syscall1(SYS_PRINT, (long)"\n"); - (void)line; (void)func; - abort(); -} - -/* ======================================================================== - printf family — vsnprintf core - ======================================================================== */ - -/* Internal: write a char to buffer if space remains */ -struct _pf_state { - char *buf; - size_t pos; - size_t max; -}; - -static void _pf_putc(struct _pf_state *st, char c) { - if (st->pos < st->max) - st->buf[st->pos] = c; - st->pos++; -} - -static void _pf_puts(struct _pf_state *st, const char *s) { - while (*s) _pf_putc(st, *s++); -} - -static void _pf_putnum(struct _pf_state *st, unsigned long val, int base, int upper, int width, char pad, int neg, int precision) { - char tmp[24]; - int i = 0; - const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; - - if (val == 0) { - tmp[i++] = '0'; - } else { - while (val > 0) { - tmp[i++] = digits[val % base]; - val /= base; - } - } - - /* Precision: minimum number of digits (zero-pad the digits themselves) */ - int digitCount = i; - int precPad = 0; - if (precision > digitCount) - precPad = precision - digitCount; - - /* Total output length: sign + precPad + digits */ - int total = (neg ? 1 : 0) + precPad + digitCount; - - /* Width padding (if pad is '0' and no precision, treat as digit padding) */ - if (neg && pad == '0' && precision < 0) { - _pf_putc(st, '-'); - } - if (precision < 0 && pad == '0') { - /* Old behavior: '0' flag pads digits */ - for (int w = total; w < width; w++) - _pf_putc(st, '0'); - } else { - /* Space padding on the left */ - for (int w = total; w < width; w++) - _pf_putc(st, ' '); - } - if (neg && !(pad == '0' && precision < 0)) { - _pf_putc(st, '-'); - } - - /* Precision zero-padding */ - for (int p = 0; p < precPad; p++) - _pf_putc(st, '0'); - - while (i > 0) - _pf_putc(st, tmp[--i]); -} - -int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) { - struct _pf_state st; - st.buf = buf; - st.pos = 0; - st.max = size > 0 ? size - 1 : 0; - - while (*fmt) { - if (*fmt != '%') { - _pf_putc(&st, *fmt++); - continue; - } - fmt++; /* skip '%' */ - - /* Flags */ - char pad = ' '; - int left_align = 0; - while (*fmt == '0' || *fmt == '-' || *fmt == '+' || *fmt == ' ') { - if (*fmt == '0') pad = '0'; - if (*fmt == '-') { left_align = 1; pad = ' '; } - fmt++; - } - - /* Width */ - int width = 0; - if (*fmt == '*') { - width = va_arg(ap, int); - fmt++; - } else { - while (*fmt >= '0' && *fmt <= '9') { - width = width * 10 + (*fmt - '0'); - fmt++; - } - } - - /* Precision (consumed but mostly ignored) */ - int precision = -1; - if (*fmt == '.') { - fmt++; - precision = 0; - if (*fmt == '*') { - precision = va_arg(ap, int); - fmt++; - } else { - while (*fmt >= '0' && *fmt <= '9') { - precision = precision * 10 + (*fmt - '0'); - fmt++; - } - } - } - - /* Length modifier */ - int is_long = 0; - if (*fmt == 'l') { is_long = 1; fmt++; if (*fmt == 'l') { is_long = 2; fmt++; } } - else if (*fmt == 'h') { fmt++; if (*fmt == 'h') fmt++; } - else if (*fmt == 'z') { is_long = 1; fmt++; } - - /* Conversion */ - switch (*fmt) { - case 'd': case 'i': { - long val; - if (is_long >= 1) val = va_arg(ap, long); - else val = va_arg(ap, int); - int neg = 0; - unsigned long uval; - if (val < 0) { neg = 1; uval = (unsigned long)(-val); } - else uval = (unsigned long)val; - if (left_align) { - size_t before = st.pos; - _pf_putnum(&st, uval, 10, 0, 0, pad, neg, precision); - size_t len = st.pos - before; - for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); - } else { - _pf_putnum(&st, uval, 10, 0, width, pad, neg, precision); - } - break; - } - case 'u': { - unsigned long val; - if (is_long >= 1) val = va_arg(ap, unsigned long); - else val = va_arg(ap, unsigned int); - if (left_align) { - size_t before = st.pos; - _pf_putnum(&st, val, 10, 0, 0, ' ', 0, precision); - size_t len = st.pos - before; - for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); - } else { - _pf_putnum(&st, val, 10, 0, width, pad, 0, precision); - } - break; - } - case 'x': case 'X': { - unsigned long val; - if (is_long >= 1) val = va_arg(ap, unsigned long); - else val = va_arg(ap, unsigned int); - int upper = (*fmt == 'X'); - if (left_align) { - size_t before = st.pos; - _pf_putnum(&st, val, 16, upper, 0, pad, 0, precision); - size_t len = st.pos - before; - for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); - } else { - _pf_putnum(&st, val, 16, upper, width, pad, 0, precision); - } - break; - } - case 'o': { - unsigned long val; - if (is_long >= 1) val = va_arg(ap, unsigned long); - else val = va_arg(ap, unsigned int); - _pf_putnum(&st, val, 8, 0, width, pad, 0, precision); - break; - } - case 'p': { - void *val = va_arg(ap, void *); - _pf_puts(&st, "0x"); - _pf_putnum(&st, (unsigned long)val, 16, 0, 0, '0', 0, -1); - break; - } - case 's': { - const char *s = va_arg(ap, const char *); - if (s == NULL) s = "(null)"; - int slen = (int)strlen(s); - if (precision >= 0 && precision < slen) slen = precision; - if (!left_align) { - for (int w = slen; w < width; w++) _pf_putc(&st, ' '); - } - for (int i = 0; i < slen; i++) _pf_putc(&st, s[i]); - if (left_align) { - for (int w = slen; w < width; w++) _pf_putc(&st, ' '); - } - break; - } - case 'c': { - char c = (char)va_arg(ap, int); - _pf_putc(&st, c); - break; - } - case '%': - _pf_putc(&st, '%'); - break; - default: - _pf_putc(&st, '%'); - _pf_putc(&st, *fmt); - break; - } - if (*fmt) fmt++; - } - - /* Null terminate */ - if (size > 0) { - if (st.pos < size) - st.buf[st.pos] = '\0'; - else - st.buf[size - 1] = '\0'; - } - - return (int)st.pos; -} - -int vsprintf(char *buf, const char *fmt, va_list ap) { - return vsnprintf(buf, (size_t)-1, fmt, ap); -} - -int snprintf(char *buf, size_t size, const char *fmt, ...) { - va_list ap; - va_start(ap, fmt); - int ret = vsnprintf(buf, size, fmt, ap); - va_end(ap); - return ret; -} - -int sprintf(char *buf, const char *fmt, ...) { - va_list ap; - va_start(ap, fmt); - int ret = vsnprintf(buf, (size_t)-1, fmt, ap); - va_end(ap); - return ret; -} - -/* Print buffer for printf/fprintf */ -static char _printbuf[4096]; - -int vprintf(const char *fmt, va_list ap) { - (void)fmt; (void)ap; - return 0; -} - -int printf(const char *fmt, ...) { - (void)fmt; - return 0; -} - -int puts(const char *s) { - (void)s; - return 0; -} - -int putchar(int c) { - (void)c; - return c; -} - -/* ======================================================================== - FILE I/O - ======================================================================== */ - -/* Simple FILE struct */ -struct _FILE { - int handle; - uint64_t pos; - uint64_t size; - int eof; - int error; - int is_std; /* 1 = stdout, 2 = stderr */ - int ungetc_buf; /* -1 if empty */ -}; - -typedef struct _FILE FILE; - -/* Up to 16 open files */ -#define MAX_FILES 16 -static FILE _file_pool[MAX_FILES]; -static int _file_pool_init = 0; - -/* Standard streams */ -static FILE _stdout_file = { -1, 0, 0, 0, 0, 1, -1 }; -static FILE _stderr_file = { -1, 0, 0, 0, 0, 2, -1 }; -static FILE _stdin_file = { -1, 0, 0, 0, 0, 3, -1 }; - -FILE *stdout = &_stdout_file; -FILE *stderr = &_stderr_file; -FILE *stdin = &_stdin_file; - -static FILE *alloc_file(void) { - if (!_file_pool_init) { - memset(_file_pool, 0, sizeof(_file_pool)); - for (int i = 0; i < MAX_FILES; i++) { - _file_pool[i].handle = -1; - _file_pool[i].ungetc_buf = -1; - } - _file_pool_init = 1; - } - for (int i = 0; i < MAX_FILES; i++) { - if (_file_pool[i].handle == -1 && _file_pool[i].is_std == 0) { - return &_file_pool[i]; - } - } - return NULL; -} - -FILE *fopen(const char *path, const char *mode) { - (void)mode; - - /* Build VFS path: prepend "0:/" if not already prefixed */ - char vfspath[256]; - if (path[0] == '0' && path[1] == ':') { - /* Already has VFS prefix */ - size_t len = strlen(path); - if (len >= sizeof(vfspath)) len = sizeof(vfspath) - 1; - memcpy(vfspath, path, len); - vfspath[len] = '\0'; - } else { - /* Prepend "0:/" */ - const char *prefix = "0:/"; - int i = 0; - while (prefix[i]) { vfspath[i] = prefix[i]; i++; } - int j = 0; - while (path[j] && i < 254) { vfspath[i++] = path[j++]; } - vfspath[i] = '\0'; - } - - int handle = (int)_zos_syscall1(SYS_OPEN, (long)vfspath); - if (handle < 0) { - errno = 2; /* ENOENT */ - return NULL; - } - - FILE *fp = alloc_file(); - if (fp == NULL) { - _zos_syscall1(SYS_CLOSE, (long)handle); - errno = 12; /* ENOMEM */ - return NULL; - } - - fp->handle = handle; - fp->pos = 0; - fp->size = (uint64_t)_zos_syscall1(SYS_GETSIZE, (long)handle); - fp->eof = 0; - fp->error = 0; - fp->is_std = 0; - fp->ungetc_buf = -1; - - return fp; -} - -int fclose(FILE *fp) { - if (fp == NULL || fp->is_std) return -1; - _zos_syscall1(SYS_CLOSE, (long)fp->handle); - fp->handle = -1; - fp->is_std = 0; - return 0; -} - -size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) { - if (fp == NULL || fp->is_std || size == 0 || nmemb == 0) return 0; - - size_t total = size * nmemb; - size_t remaining = 0; - if (fp->pos < fp->size) - remaining = (size_t)(fp->size - fp->pos); - - if (total > remaining) total = remaining; - if (total == 0) { fp->eof = 1; return 0; } - - int bytes = (int)_zos_syscall4(SYS_READ, - (long)fp->handle, (long)ptr, (long)fp->pos, (long)total); - - if (bytes <= 0) { fp->eof = 1; return 0; } - - fp->pos += (uint64_t)bytes; - if (fp->pos >= fp->size) fp->eof = 1; - - return (size_t)bytes / size; -} - -size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) { - if (fp == NULL) return 0; - - /* stdout/stderr: discard (no terminal in windowed mode) */ - if (fp->is_std == 1 || fp->is_std == 2) { - return nmemb; - } - - /* Read-only filesystem, no-op */ - return 0; -} - -int fseek(FILE *fp, long offset, int whence) { - if (fp == NULL || fp->is_std) return -1; - - uint64_t newpos; - switch (whence) { - case 0: /* SEEK_SET */ - newpos = (uint64_t)offset; - break; - case 1: /* SEEK_CUR */ - newpos = fp->pos + (uint64_t)offset; - break; - case 2: /* SEEK_END */ - newpos = fp->size + (uint64_t)offset; - break; - default: - return -1; - } - - fp->pos = newpos; - fp->eof = 0; - return 0; -} - -long ftell(FILE *fp) { - if (fp == NULL || fp->is_std) return -1; - return (long)fp->pos; -} - -int fflush(FILE *fp) { - (void)fp; - return 0; -} - -int feof(FILE *fp) { - if (fp == NULL) return 1; - return fp->eof; -} - -int ferror(FILE *fp) { - if (fp == NULL) return 1; - return fp->error; -} - -void clearerr(FILE *fp) { - if (fp == NULL) return; - fp->eof = 0; - fp->error = 0; -} - -int fgetc(FILE *fp) { - if (fp == NULL) return -1; - if (fp->ungetc_buf >= 0) { - int c = fp->ungetc_buf; - fp->ungetc_buf = -1; - return c; - } - unsigned char c; - size_t n = fread(&c, 1, 1, fp); - return n == 1 ? (int)c : -1; -} - -int getc(FILE *fp) { - return fgetc(fp); -} - -int ungetc(int c, FILE *fp) { - if (fp == NULL || c == -1) return -1; - fp->ungetc_buf = c; - fp->eof = 0; - return c; -} - -char *fgets(char *s, int size, FILE *fp) { - if (size <= 0) return NULL; - int i = 0; - while (i < size - 1) { - int c = fgetc(fp); - if (c == -1) break; - s[i++] = (char)c; - if (c == '\n') break; - } - if (i == 0) return NULL; - s[i] = '\0'; - return s; -} - -int fputs(const char *s, FILE *fp) { - (void)s; (void)fp; - return 0; -} - -int fprintf(FILE *fp, const char *fmt, ...) { - (void)fp; (void)fmt; - return 0; -} - -int vfprintf(FILE *fp, const char *fmt, va_list ap) { - (void)fp; (void)fmt; (void)ap; - return 0; -} - -int sscanf(const char *str, const char *fmt, ...) { - /* Minimal sscanf: supports %d, %s, %x, %u only */ - va_list ap; - va_start(ap, fmt); - int count = 0; - const char *s = str; - - while (*fmt && *s) { - if (*fmt == '%') { - fmt++; - if (*fmt == 'd' || *fmt == 'i') { - int *out = va_arg(ap, int *); - int neg = 0; - int val = 0; - while (isspace((unsigned char)*s)) s++; - if (*s == '-') { neg = 1; s++; } - else if (*s == '+') s++; - if (!isdigit((unsigned char)*s)) break; - while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } - *out = neg ? -val : val; - count++; - fmt++; - } else if (*fmt == 'u') { - unsigned int *out = va_arg(ap, unsigned int *); - unsigned int val = 0; - while (isspace((unsigned char)*s)) s++; - if (!isdigit((unsigned char)*s)) break; - while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } - *out = val; - count++; - fmt++; - } else if (*fmt == 'x' || *fmt == 'X') { - unsigned int *out = va_arg(ap, unsigned int *); - unsigned int val = 0; - while (isspace((unsigned char)*s)) s++; - if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; - while (isxdigit((unsigned char)*s)) { - int d; - if (*s >= '0' && *s <= '9') d = *s - '0'; - else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; - else d = *s - 'A' + 10; - val = val * 16 + d; - s++; - } - *out = val; - count++; - fmt++; - } else if (*fmt == 's') { - char *out = va_arg(ap, char *); - while (isspace((unsigned char)*s)) s++; - while (*s && !isspace((unsigned char)*s)) *out++ = *s++; - *out = '\0'; - count++; - fmt++; - } else { - break; - } - } else if (isspace((unsigned char)*fmt)) { - while (isspace((unsigned char)*s)) s++; - while (isspace((unsigned char)*fmt)) fmt++; - } else { - if (*s != *fmt) break; - s++; - fmt++; - } - } - - va_end(ap); - return count; -} - -void perror(const char *s) { - (void)s; -} - -int rename(const char *old, const char *new_) { - (void)old; (void)new_; - return -1; -} - -int remove(const char *path) { - (void)path; - return -1; -} - -FILE *tmpfile(void) { - return NULL; -} - -char *tmpnam(char *s) { - (void)s; - return NULL; -} - -/* ======================================================================== - Stubs for unneeded functions - ======================================================================== */ - -int mkdir(const char *path, unsigned int mode) { - (void)path; (void)mode; - return -1; -} - -/* math.h stubs — doom's fixed-point code doesn't use libm heavily, - but z_zone.c references some functions. Provide no-ops. */ -double fabs(double x) { return x < 0 ? -x : x; } -double floor(double x) { return (double)(long)x - (x < (double)(long)x ? 1.0 : 0.0); } -double ceil(double x) { double f = floor(x); return (x > f) ? f + 1.0 : f; } -double fmod(double x, double y) { if (y == 0.0) return 0.0; return x - (double)((long)(x/y)) * y; } -double sqrt(double x) { - if (x <= 0.0) return 0.0; - double guess = x; - for (int i = 0; i < 20; i++) - guess = (guess + x / guess) * 0.5; - return guess; -} -double pow(double base, double exp) { - if (exp == 0.0) return 1.0; - if (exp == 1.0) return base; - /* Integer exponent fast path */ - if (exp == (double)(long)exp && exp > 0) { - double r = 1.0; - long e = (long)exp; - while (e-- > 0) r *= base; - return r; - } - return 0.0; -} -double sin(double x) { (void)x; return 0.0; } -double cos(double x) { (void)x; return 1.0; } -double atan2(double y, double x) { (void)y; (void)x; return 0.0; } -double log(double x) { (void)x; return 0.0; } -double exp(double x) { (void)x; return 1.0; } -double round(double x) { return floor(x + 0.5); } -double atof(const char *s) { (void)s; return 0.0; } -float floorf(float x) { return (float)floor((double)x); } -float ceilf(float x) { return (float)ceil((double)x); } -float fabsf(float x) { return x < 0.0f ? -x : x; } - -/* ======================================================================== - Misc stubs that doom might reference - ======================================================================== */ - -/* Some doom code calls I_Error which ends up doing fprintf + exit */ -/* The signal/system stuff is behind ifdefs, but just in case: */ - -unsigned int sleep(unsigned int seconds) { - (void)seconds; - return 0; -} - -int system(const char *command) { - (void)command; - return -1; -} diff --git a/programs/src/texteditor/Makefile b/programs/src/texteditor/Makefile new file mode 100644 index 0000000..14866df --- /dev/null +++ b/programs/src/texteditor/Makefile @@ -0,0 +1,86 @@ +# Makefile for texteditor (standalone Window Server app) on MontaukOS +# Copyright (c) 2026 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CXX := $(TOOLCHAIN_PREFIX)g++ +else + CXX := g++ +endif + +# ---- Paths ---- + +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj +LIBDIR := ../../lib + +# ---- Compiler flags ---- + +CXXFLAGS := \ + -std=gnu++20 \ + -g -O2 -pipe \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-function \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -fno-rtti \ + -fno-exceptions \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -I $(PROG_INC) \ + -isystem $(PROG_INC)/libc \ + -isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \ + -isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include + +# ---- Linker flags ---- + +LDFLAGS := \ + -nostdlib \ + -static \ + -Wl,--build-id=none \ + -Wl,--gc-sections \ + -Wl,-m,elf_x86_64 \ + -z max-page-size=0x1000 \ + -T $(LINK_LD) + +# ---- Source files ---- + +SRCS := main.cpp stb_truetype_impl.cpp +OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) + +# ---- Target ---- + +TARGET := $(BINDIR)/apps/texteditor/texteditor.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR)/apps/texteditor + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBDIR)/libc/liblibc.a -o $@ + +$(OBJDIR)/%.o: %.cpp Makefile + mkdir -p $(OBJDIR) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/texteditor/main.cpp b/programs/src/texteditor/main.cpp new file mode 100644 index 0000000..1ad2702 --- /dev/null +++ b/programs/src/texteditor/main.cpp @@ -0,0 +1,1171 @@ +/* + * main.cpp + * MontaukOS Text Editor - standalone Window Server app + * Single-buffer text editor with line numbers, cursor, scrolling, file I/O, + * syntax highlighting for C files + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +#include +#include +} + +using namespace gui; + +#include "syntax_highlight.hpp" + +// ============================================================================ +// Constants +// ============================================================================ + +static constexpr int INIT_W = 600; +static constexpr int INIT_H = 450; +static constexpr int TOOLBAR_H = 36; +static constexpr int PATHBAR_H = 32; +static constexpr int STATUS_H = 24; +static constexpr int LINE_NUM_W = 48; +static constexpr int SCROLLBAR_W = 12; +static constexpr int INIT_CAP = 4096; +static constexpr int MAX_CAP = 262144; // 256KB +static constexpr int MAX_LINES = 16384; +static constexpr int TAB_WIDTH = 4; +static constexpr int FONT_SIZE = 16; + +// Colors +static constexpr Color BG_COLOR = Color::from_rgb(0xFF, 0xFF, 0xFF); +static constexpr Color TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5); +static constexpr Color BTN_BG = Color::from_rgb(0xE8, 0xE8, 0xE8); +static constexpr Color BORDER_COLOR = Color::from_rgb(0xD0, 0xD0, 0xD0); +static constexpr Color LINENUM_COLOR = Color::from_rgb(0x99, 0x99, 0x99); +static constexpr Color LINENUM_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); +static constexpr Color CURSOR_LINE_BG = Color::from_rgb(0xFF, 0xFD, 0xE8); +static constexpr Color TEXT_COLOR = Color::from_rgb(0x1E, 0x1E, 0x1E); +static constexpr Color SEL_BG = Color::from_rgb(0xB0, 0xD0, 0xF0); +static constexpr Color ACCENT = Color::from_rgb(0x36, 0x7B, 0xF0); +static constexpr Color STATUS_BG = Color::from_rgb(0x2B, 0x3E, 0x50); +static constexpr Color STATUS_TEXT = Color::from_rgb(0xE0, 0xE0, 0xE0); +static constexpr Color PATHBAR_BG = Color::from_rgb(0xF0, 0xF0, 0xF0); +static constexpr Color WHITE = Color::from_rgb(0xFF, 0xFF, 0xFF); + +// ============================================================================ +// Global state +// ============================================================================ + +int g_win_w = INIT_W; +int g_win_h = INIT_H; + +TrueTypeFont* g_font = nullptr; // UI font (Roboto) +TrueTypeFont* g_font_mono = nullptr; // Editor font (JetBrains Mono) + +SvgIcon g_icon_folder = {}; +SvgIcon g_icon_save = {}; + +// Buffer +char* g_buffer = nullptr; +int g_buf_len = 0; +int g_buf_cap = 0; + +// Line index +int* g_line_offsets = nullptr; +int g_line_count = 0; + +// Cursor +int g_cursor_pos = 0; // byte position +int g_cursor_line = 0; +int g_cursor_col = 0; +int g_scroll_y = 0; // first visible line +int g_scroll_x = 0; // horizontal scroll in pixels +bool g_modified = false; +bool g_cursor_moved = true; + +// File +char g_filepath[256] = {}; +char g_filename[64] = {}; + +// Selection +int g_sel_anchor = 0; +int g_sel_end = 0; +bool g_has_selection = false; +bool g_mouse_selecting = false; + +// Pathbar +bool g_pathbar_open = false; +bool g_pathbar_save = false; +char g_pathbar_text[256] = {}; +int g_pathbar_len = 0; +int g_pathbar_cursor = 0; + +// Syntax highlighting +bool g_syntax_active = false; + +// ============================================================================ +// Pixel drawing helpers +// ============================================================================ + +static void px_fill(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, Color c) { + uint32_t v = c.to_pixel(); + int x0 = x < 0 ? 0 : x, y0 = y < 0 ? 0 : y; + int x1 = x + w > bw ? bw : x + w; + int y1 = y + h > bh ? bh : y + h; + for (int row = y0; row < y1; row++) + for (int col = x0; col < x1; col++) + px[row * bw + col] = v; +} + +static void px_hline(uint32_t* px, int bw, int bh, + int x, int y, int w, Color c) { + if (y < 0 || y >= bh) return; + uint32_t v = c.to_pixel(); + int x0 = x < 0 ? 0 : x; + int x1 = x + w > bw ? bw : x + w; + for (int col = x0; col < x1; col++) + px[y * bw + col] = v; +} + +static void px_vline(uint32_t* px, int bw, int bh, + int x, int y, int h, Color c) { + if (x < 0 || x >= bw) return; + uint32_t v = c.to_pixel(); + int y0 = y < 0 ? 0 : y; + int y1 = y + h > bh ? bh : y + h; + for (int row = y0; row < y1; row++) + px[row * bw + x] = v; +} + +static void px_fill_rounded(uint32_t* px, int bw, int bh, + int x, int y, int w, int h, int r, Color c) { + uint32_t v = c.to_pixel(); + for (int row = 0; row < h; row++) { + int py = y + row; + if (py < 0 || py >= bh) continue; + int inset = 0; + if (row < r) { + int dy = r - 1 - row; + if (r == 3) { + if (dy >= 2) inset = 2; + else if (dy >= 1) inset = 1; + } + } else if (row >= h - r) { + int dy = row - (h - r); + if (r == 3) { + if (dy >= 2) inset = 2; + else if (dy >= 1) inset = 1; + } + } + int x0 = x + inset; + int x1 = x + w - inset; + if (x0 < 0) x0 = 0; + if (x1 > bw) x1 = bw; + for (int col = x0; col < x1; col++) + px[py * bw + col] = v; + } +} + +static void px_icon(uint32_t* px, int bw, int bh, + int x, int y, const SvgIcon& ic) { + if (!ic.pixels) return; + for (int row = 0; row < ic.height; row++) { + int dy = y + row; + if (dy < 0 || dy >= bh) continue; + for (int col = 0; col < ic.width; col++) { + int dx = x + col; + if (dx < 0 || dx >= bw) continue; + uint32_t src = ic.pixels[row * ic.width + col]; + uint8_t sa = (src >> 24) & 0xFF; + if (sa == 0) continue; + if (sa == 255) { + px[dy * bw + dx] = src; + } else { + uint32_t dst = px[dy * bw + dx]; + uint8_t sr = (src >> 16) & 0xFF, sg = (src >> 8) & 0xFF, sb = src & 0xFF; + uint8_t dr = (dst >> 16) & 0xFF, dg = (dst >> 8) & 0xFF, db = dst & 0xFF; + uint32_t a = sa, inv = 255 - sa; + uint32_t rr = (a * sr + inv * dr + 128) / 255; + uint32_t gg = (a * sg + inv * dg + 128) / 255; + uint32_t bb = (a * sb + inv * db + 128) / 255; + px[dy * bw + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; + } + } + } +} + +// ============================================================================ +// Font metrics +// ============================================================================ + +static constexpr int MONO_SIZE = 18; + +static int font_cell_w() { + if (g_font_mono && g_font_mono->valid) { + GlyphCache* gc = g_font_mono->get_cache(MONO_SIZE); + CachedGlyph* g = g_font_mono->get_glyph(gc, 'M'); + if (g) return g->advance; + } + return 8; +} + +static int font_cell_h() { + if (g_font_mono && g_font_mono->valid) + return g_font_mono->get_line_height(MONO_SIZE); + return 16; +} + +// ============================================================================ +// Line index management +// ============================================================================ + +static void recompute_lines() { + if (!g_line_offsets) + g_line_offsets = (int*)montauk::malloc(MAX_LINES * sizeof(int)); + + g_line_count = 0; + g_line_offsets[g_line_count++] = 0; + for (int i = 0; i < g_buf_len; i++) { + if (g_buffer[i] == '\n' && g_line_count < MAX_LINES) + g_line_offsets[g_line_count++] = i + 1; + } +} + +static void update_cursor_pos() { + g_cursor_line = 0; + for (int i = g_line_count - 1; i >= 0; i--) { + if (g_cursor_pos >= g_line_offsets[i]) { + g_cursor_line = i; + break; + } + } + g_cursor_col = g_cursor_pos - g_line_offsets[g_cursor_line]; + g_cursor_moved = true; +} + +static int line_length(int line) { + if (line < 0 || line >= g_line_count) return 0; + int start = g_line_offsets[line]; + int end = (line + 1 < g_line_count) ? g_line_offsets[line + 1] - 1 : g_buf_len; + return end - start; +} + +// ============================================================================ +// Buffer operations +// ============================================================================ + +static void ensure_capacity(int needed) { + if (g_buf_len + needed <= g_buf_cap) return; + int new_cap = g_buf_cap * 2; + if (new_cap > MAX_CAP) new_cap = MAX_CAP; + if (new_cap < g_buf_len + needed) new_cap = g_buf_len + needed; + g_buffer = (char*)montauk::realloc(g_buffer, new_cap); + g_buf_cap = new_cap; +} + +static void delete_range(int start, int count) { + for (int i = start; i < g_buf_len - count; i++) + g_buffer[i] = g_buffer[i + count]; + g_buf_len -= count; +} + +static void insert_char(char c) { + if (g_buf_len >= MAX_CAP - 1) return; + ensure_capacity(1); + for (int i = g_buf_len; i > g_cursor_pos; i--) + g_buffer[i] = g_buffer[i - 1]; + g_buffer[g_cursor_pos] = c; + g_buf_len++; + g_cursor_pos++; + g_modified = true; + recompute_lines(); + update_cursor_pos(); +} + +static void insert_string(const char* s, int len) { + if (g_buf_len + len >= MAX_CAP) return; + ensure_capacity(len); + for (int i = g_buf_len - 1; i >= g_cursor_pos; i--) + g_buffer[i + len] = g_buffer[i]; + for (int i = 0; i < len; i++) + g_buffer[g_cursor_pos + i] = s[i]; + g_buf_len += len; + g_cursor_pos += len; + g_modified = true; + recompute_lines(); + update_cursor_pos(); +} + +static void backspace() { + if (g_cursor_pos <= 0) return; + g_cursor_pos--; + delete_range(g_cursor_pos, 1); + g_modified = true; + recompute_lines(); + update_cursor_pos(); +} + +static void delete_char() { + if (g_cursor_pos >= g_buf_len) return; + delete_range(g_cursor_pos, 1); + g_modified = true; + recompute_lines(); + update_cursor_pos(); +} + +// ============================================================================ +// Cursor movement +// ============================================================================ + +static void move_up() { + if (g_cursor_line <= 0) return; + int col = g_cursor_col; + int prev = g_cursor_line - 1; + int len = line_length(prev); + if (col > len) col = len; + g_cursor_pos = g_line_offsets[prev] + col; + update_cursor_pos(); +} + +static void move_down() { + if (g_cursor_line >= g_line_count - 1) return; + int col = g_cursor_col; + int next = g_cursor_line + 1; + int len = line_length(next); + if (col > len) col = len; + g_cursor_pos = g_line_offsets[next] + col; + update_cursor_pos(); +} + +static void move_left() { + if (g_cursor_pos > 0) { g_cursor_pos--; update_cursor_pos(); } +} + +static void move_right() { + if (g_cursor_pos < g_buf_len) { g_cursor_pos++; update_cursor_pos(); } +} + +static void move_home() { + g_cursor_pos = g_line_offsets[g_cursor_line]; + update_cursor_pos(); +} + +static void move_end() { + g_cursor_pos = g_line_offsets[g_cursor_line] + line_length(g_cursor_line); + update_cursor_pos(); +} + +// ============================================================================ +// Selection +// ============================================================================ + +static void clear_selection() { + g_has_selection = false; + g_sel_anchor = 0; + g_sel_end = 0; +} + +static void sel_range(int* out_s, int* out_e) { + if (g_sel_anchor < g_sel_end) { *out_s = g_sel_anchor; *out_e = g_sel_end; } + else { *out_s = g_sel_end; *out_e = g_sel_anchor; } +} + +static void start_selection() { + if (!g_has_selection) { + g_sel_anchor = g_cursor_pos; + g_sel_end = g_cursor_pos; + g_has_selection = true; + } +} + +static void update_selection() { + g_sel_end = g_cursor_pos; + if (g_sel_anchor == g_sel_end) + g_has_selection = false; +} + +static void delete_selection() { + if (!g_has_selection) return; + int ss, se; + sel_range(&ss, &se); + int count = se - ss; + if (count <= 0) { clear_selection(); return; } + delete_range(ss, count); + g_cursor_pos = ss; + g_modified = true; + clear_selection(); + recompute_lines(); + update_cursor_pos(); +} + +// ============================================================================ +// Scrolling +// ============================================================================ + +static void ensure_cursor_visible(int visible_lines, int text_area_w) { + if (g_cursor_line < g_scroll_y) + g_scroll_y = g_cursor_line; + if (g_cursor_line >= g_scroll_y + visible_lines) + g_scroll_y = g_cursor_line - visible_lines + 1; + + int max_scroll_y = g_line_count - visible_lines; + if (max_scroll_y < 0) max_scroll_y = 0; + if (g_scroll_y > max_scroll_y) g_scroll_y = max_scroll_y; + if (g_scroll_y < 0) g_scroll_y = 0; + + int cell_w = font_cell_w(); + int cursor_px = g_cursor_col * cell_w; + int view_w = text_area_w - LINE_NUM_W - SCROLLBAR_W; + if (cursor_px - g_scroll_x > view_w - cell_w * 2) + g_scroll_x = cursor_px - view_w + cell_w * 4; + if (cursor_px < g_scroll_x) { + g_scroll_x = cursor_px - cell_w * 2; + if (g_scroll_x < 0) g_scroll_x = 0; + } +} + +// ============================================================================ +// File I/O +// ============================================================================ + +static void load_file(const char* path) { + int fd = montauk::open(path); + if (fd < 0) return; + + uint64_t size = montauk::getsize(fd); + if (size > (uint64_t)MAX_CAP) size = MAX_CAP; + + if ((int)size >= g_buf_cap) { + int new_cap = (int)size + 1024; + if (new_cap > MAX_CAP) new_cap = MAX_CAP; + g_buffer = (char*)montauk::realloc(g_buffer, new_cap); + g_buf_cap = new_cap; + } + + montauk::read(fd, (uint8_t*)g_buffer, 0, size); + montauk::close(fd); + + g_buf_len = (int)size; + g_cursor_pos = 0; + g_scroll_y = 0; + g_scroll_x = 0; + g_modified = false; + + montauk::strncpy(g_filepath, path, 255); + + int last_slash = -1; + for (int i = 0; path[i]; i++) + if (path[i] == '/') last_slash = i; + if (last_slash >= 0) + montauk::strncpy(g_filename, path + last_slash + 1, 63); + else + montauk::strncpy(g_filename, path, 63); + + g_syntax_active = syn_is_c_file(g_filepath); + + recompute_lines(); + update_cursor_pos(); +} + +static void save_file() { + if (g_filepath[0] == '\0') return; + + int fd = montauk::fcreate(g_filepath); + if (fd < 0) return; + + montauk::fwrite(fd, (const uint8_t*)g_buffer, 0, g_buf_len); + montauk::close(fd); + + g_modified = false; +} + +// ============================================================================ +// Hit testing +// ============================================================================ + +static int hit_test(int mx, int my, int editor_y) { + int cell_w = font_cell_w(); + int cell_h = font_cell_h(); + + int clicked_line = g_scroll_y + (my - editor_y) / cell_h; + if (clicked_line >= g_line_count) clicked_line = g_line_count - 1; + if (clicked_line < 0) clicked_line = 0; + + int clicked_col = (mx - LINE_NUM_W - 4 + g_scroll_x + cell_w / 2) / cell_w; + if (clicked_col < 0) clicked_col = 0; + int ll = line_length(clicked_line); + if (clicked_col > ll) clicked_col = ll; + + return g_line_offsets[clicked_line] + clicked_col; +} + +// ============================================================================ +// Rendering +// ============================================================================ + +static void render(uint32_t* pixels) { + int W = g_win_w, H = g_win_h; + int cell_w = font_cell_w(); + int cell_h = font_cell_h(); + + // Clear + px_fill(pixels, W, H, 0, 0, W, H, BG_COLOR); + + // ---- Toolbar ---- + px_fill(pixels, W, H, 0, 0, W, TOOLBAR_H, TOOLBAR_BG); + + // Open button + px_fill_rounded(pixels, W, H, 4, 6, 24, 24, 3, BTN_BG); + px_icon(pixels, W, H, 8, 10, g_icon_folder); + + // Save button + px_fill_rounded(pixels, W, H, 32, 6, 24, 24, 3, BTN_BG); + px_icon(pixels, W, H, 36, 10, g_icon_save); + + // Separator + px_vline(pixels, W, H, 60, 4, 28, BORDER_COLOR); + + // Filename + modified + char label[128]; + if (g_filename[0]) + snprintf(label, 128, "%s%s", g_filename, g_modified ? " [modified]" : ""); + else + snprintf(label, 128, "Untitled%s", g_modified ? " [modified]" : ""); + if (g_font) + g_font->draw_to_buffer(pixels, W, H, 68, (TOOLBAR_H - FONT_SIZE) / 2, label, TEXT_COLOR, FONT_SIZE); + + // Toolbar bottom line + px_hline(pixels, W, H, 0, TOOLBAR_H - 1, W, BORDER_COLOR); + + // ---- Pathbar (conditional) ---- + int editor_y = TOOLBAR_H; + + if (g_pathbar_open) { + int pb_y = TOOLBAR_H; + px_fill(pixels, W, H, 0, pb_y, W, PATHBAR_H, PATHBAR_BG); + + int btn_w = 56; + int inp_x = 8; + int inp_y = pb_y + 4; + int inp_w = W - inp_x - btn_w - 12; + int inp_h = 24; + + // Input box + px_fill(pixels, W, H, inp_x, inp_y, inp_w, inp_h, WHITE); + // Input border (top, bottom, left, right) + px_hline(pixels, W, H, inp_x, inp_y, inp_w, ACCENT); + px_hline(pixels, W, H, inp_x, inp_y + inp_h - 1, inp_w, ACCENT); + px_vline(pixels, W, H, inp_x, inp_y, inp_h, ACCENT); + px_vline(pixels, W, H, inp_x + inp_w - 1, inp_y, inp_h, ACCENT); + + // Path text + if (g_font && g_pathbar_text[0]) + g_font->draw_to_buffer(pixels, W, H, inp_x + 4, inp_y + (inp_h - FONT_SIZE) / 2, + g_pathbar_text, TEXT_COLOR, FONT_SIZE); + + // Cursor in pathbar + char prefix[256]; + int plen = g_pathbar_cursor > 255 ? 255 : g_pathbar_cursor; + for (int i = 0; i < plen; i++) prefix[i] = g_pathbar_text[i]; + prefix[plen] = '\0'; + int cx = inp_x + 4 + (g_font ? g_font->measure_text(prefix, FONT_SIZE) : 0); + px_fill(pixels, W, H, cx, inp_y + 3, 2, inp_h - 6, ACCENT); + + // Button + int ob_x = inp_x + inp_w + 6; + px_fill_rounded(pixels, W, H, ob_x, inp_y, btn_w, inp_h, 3, ACCENT); + const char* btn_label = g_pathbar_save ? "Save" : "Open"; + if (g_font) { + int tw = g_font->measure_text(btn_label, FONT_SIZE); + g_font->draw_to_buffer(pixels, W, H, ob_x + (btn_w - tw) / 2, + inp_y + (inp_h - FONT_SIZE) / 2, btn_label, WHITE, FONT_SIZE); + } + + // Pathbar bottom line + px_hline(pixels, W, H, 0, pb_y + PATHBAR_H - 1, W, BORDER_COLOR); + + editor_y = TOOLBAR_H + PATHBAR_H; + } + + // ---- Editor area ---- + int text_area_h = H - editor_y - STATUS_H; + int visible_lines = text_area_h / cell_h; + if (visible_lines < 1) visible_lines = 1; + int text_area_w = W - SCROLLBAR_W; + + if (g_cursor_moved) { + ensure_cursor_visible(visible_lines, W); + g_cursor_moved = false; + } + + // Line number gutter + px_fill(pixels, W, H, 0, editor_y, LINE_NUM_W, text_area_h, LINENUM_BG); + px_vline(pixels, W, H, LINE_NUM_W, editor_y, text_area_h, BORDER_COLOR); + + // Selection range + int sel_s = 0, sel_e = 0; + if (g_has_selection) sel_range(&sel_s, &sel_e); + + int text_start_x = LINE_NUM_W + 4; + + // Syntax highlighting: compute block comment state up to first visible line + bool syn_block_comment = false; + SynToken syn_line_buf[1024]; + if (g_syntax_active) { + for (int i = 0; i < g_scroll_y && i < g_line_count; i++) { + int ls = g_line_offsets[i]; + int ll = line_length(i); + if (ll > 1024) ll = 1024; + syn_highlight_line(g_buffer + ls, ll, syn_line_buf, syn_block_comment); + } + } + + GlyphCache* gc = (g_font_mono && g_font_mono->valid) ? g_font_mono->get_cache(MONO_SIZE) : nullptr; + + for (int vis = 0; vis < visible_lines + 1; vis++) { + int line = g_scroll_y + vis; + if (line >= g_line_count) break; + + int py = editor_y + vis * cell_h; + if (py >= editor_y + text_area_h) break; + + // Cursor line highlight + if (line == g_cursor_line && !g_has_selection) { + int hl_h = cell_h; + if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; + if (hl_h > 0) + px_fill(pixels, W, H, LINE_NUM_W + 1, py, text_area_w - LINE_NUM_W - 1, hl_h, CURSOR_LINE_BG); + } + + // Line number + char num_str[8]; + snprintf(num_str, 8, "%4d", line + 1); + if (g_font_mono) + g_font_mono->draw_to_buffer(pixels, W, H, 4, py, num_str, LINENUM_COLOR, MONO_SIZE); + + // Line text + int line_start = g_line_offsets[line]; + int line_len = line_length(line); + + // Syntax highlight this line + int syn_len = line_len > 1024 ? 1024 : line_len; + if (g_syntax_active) + syn_highlight_line(g_buffer + line_start, syn_len, syn_line_buf, syn_block_comment); + + for (int ci = 0; ci < line_len; ci++) { + int px_x = text_start_x + ci * cell_w - g_scroll_x; + if (px_x + cell_w <= LINE_NUM_W + 1) continue; + if (px_x >= text_area_w) break; + + int byte_pos = line_start + ci; + char ch = g_buffer[byte_pos]; + + // Selection highlight + if (g_has_selection && byte_pos >= sel_s && byte_pos < sel_e) { + int hl_h = cell_h; + if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; + if (hl_h > 0) + px_fill(pixels, W, H, px_x, py, cell_w, hl_h, SEL_BG); + } + + Color char_color = TEXT_COLOR; + if (g_syntax_active && ci < syn_len) + char_color = syn_color(syn_line_buf[ci]); + + if (ch >= 32 || ch < 0) { + if (gc) { + g_font_mono->draw_char_to_buffer(pixels, W, H, px_x, py + gc->ascent, + ch, char_color, gc); + } + } + } + + // Selection highlight for newline at end of line + if (g_has_selection && line + 1 < g_line_count) { + int nl_pos = line_start + line_len; + if (nl_pos >= sel_s && nl_pos < sel_e) { + int px_x = text_start_x + line_len * cell_w - g_scroll_x; + if (px_x > LINE_NUM_W && px_x < text_area_w) { + int hl_h = cell_h; + if (py + hl_h > editor_y + text_area_h) hl_h = editor_y + text_area_h - py; + if (hl_h > 0) + px_fill(pixels, W, H, px_x, py, cell_w / 2, hl_h, SEL_BG); + } + } + } + + // Cursor + if (line == g_cursor_line) { + int cx = text_start_x + g_cursor_col * cell_w - g_scroll_x; + if (cx > LINE_NUM_W && cx + 2 <= text_area_w) { + int cur_h = cell_h; + if (py + cur_h > editor_y + text_area_h) cur_h = editor_y + text_area_h - py; + if (cur_h > 0) + px_fill(pixels, W, H, cx, py, 2, cur_h, ACCENT); + } + } + } + + // ---- Scrollbar ---- + int content_h = g_line_count * cell_h; + if (content_h > text_area_h) { + int sb_x = W - SCROLLBAR_W; + px_fill(pixels, W, H, sb_x, editor_y, SCROLLBAR_W, text_area_h, Color::from_rgb(0xF0, 0xF0, 0xF0)); + int max_scr = g_line_count - visible_lines; + if (max_scr < 1) max_scr = 1; + int thumb_h = (text_area_h * text_area_h) / content_h; + if (thumb_h < 20) thumb_h = 20; + int range = text_area_h - thumb_h; + int thumb_y = editor_y + (g_scroll_y * range) / max_scr; + px_fill(pixels, W, H, sb_x + 1, thumb_y, SCROLLBAR_W - 2, thumb_h, Color::from_rgb(0xAA, 0xAA, 0xAA)); + } + + // ---- Status bar ---- + int status_y = H - STATUS_H; + px_fill(pixels, W, H, 0, status_y, W, STATUS_H, STATUS_BG); + + int sty = status_y + (STATUS_H - FONT_SIZE) / 2; + + // Left: filename + char status_left[128]; + if (g_filename[0]) + snprintf(status_left, 128, " %s%s", g_filename, g_modified ? " [modified]" : ""); + else + snprintf(status_left, 128, " Untitled%s", g_modified ? " [modified]" : ""); + if (g_font) + g_font->draw_to_buffer(pixels, W, H, 4, sty, status_left, STATUS_TEXT, FONT_SIZE); + + // Right: cursor position + char status_right[48]; + if (g_has_selection) { + int ss, se; + sel_range(&ss, &se); + snprintf(status_right, 48, "%d selected Ln %d, Col %d ", se - ss, g_cursor_line + 1, g_cursor_col + 1); + } else { + snprintf(status_right, 48, "Ln %d, Col %d ", g_cursor_line + 1, g_cursor_col + 1); + } + if (g_font) { + int rw = g_font->measure_text(status_right, FONT_SIZE); + g_font->draw_to_buffer(pixels, W, H, W - rw - 4, sty, status_right, STATUS_TEXT, FONT_SIZE); + } +} + +// ============================================================================ +// Pathbar actions +// ============================================================================ + +static void pathbar_confirm(int win_id) { + if (g_pathbar_text[0] == '\0') return; + + if (g_pathbar_save) { + montauk::strncpy(g_filepath, g_pathbar_text, 255); + const char* name = g_pathbar_text; + for (int i = 0; g_pathbar_text[i]; i++) + if (g_pathbar_text[i] == '/') name = g_pathbar_text + i + 1; + montauk::strncpy(g_filename, name, 63); + g_syntax_active = syn_is_c_file(g_filepath); + save_file(); + } else { + load_file(g_pathbar_text); + } + + g_pathbar_open = false; +} + +static void open_pathbar_open() { + g_pathbar_open = true; + g_pathbar_save = false; + montauk::strncpy(g_pathbar_text, g_filepath, 255); + g_pathbar_len = montauk::slen(g_pathbar_text); + g_pathbar_cursor = g_pathbar_len; +} + +static void open_pathbar_save() { + g_pathbar_open = true; + g_pathbar_save = true; + g_pathbar_text[0] = '\0'; + g_pathbar_len = 0; + g_pathbar_cursor = 0; +} + +// ============================================================================ +// Keyboard handler +// ============================================================================ + +static bool handle_key(Montauk::KeyEvent& key, int win_id) { + if (!key.pressed) return false; + + // ---- Pathbar mode ---- + if (g_pathbar_open) { + if (key.ascii == '\n' || key.ascii == '\r') { + pathbar_confirm(win_id); + return true; + } + if (key.scancode == 0x01) { // Escape + g_pathbar_open = false; + return true; + } + if (key.ascii == '\b' || key.scancode == 0x0E) { + if (g_pathbar_cursor > 0) { + for (int i = g_pathbar_cursor - 1; i < g_pathbar_len - 1; i++) + g_pathbar_text[i] = g_pathbar_text[i + 1]; + g_pathbar_len--; + g_pathbar_cursor--; + g_pathbar_text[g_pathbar_len] = '\0'; + } + return true; + } + if (key.scancode == 0x4B) { if (g_pathbar_cursor > 0) g_pathbar_cursor--; return true; } + if (key.scancode == 0x4D) { if (g_pathbar_cursor < g_pathbar_len) g_pathbar_cursor++; return true; } + if (key.ascii >= 32 && key.ascii < 127 && g_pathbar_len < 254) { + for (int i = g_pathbar_len; i > g_pathbar_cursor; i--) + g_pathbar_text[i] = g_pathbar_text[i - 1]; + g_pathbar_text[g_pathbar_cursor] = key.ascii; + g_pathbar_cursor++; + g_pathbar_len++; + g_pathbar_text[g_pathbar_len] = '\0'; + return true; + } + return false; + } + + // ---- Editor mode ---- + + // Ctrl+S: save + if (key.ctrl && (key.ascii == 's' || key.ascii == 'S')) { + if (g_filepath[0] == '\0') + open_pathbar_save(); + else + save_file(); + return true; + } + + // Ctrl+O: open + if (key.ctrl && (key.ascii == 'o' || key.ascii == 'O')) { + if (g_pathbar_open) g_pathbar_open = false; + else open_pathbar_open(); + return true; + } + + // Ctrl+A: select all + if (key.ctrl && (key.ascii == 'a' || key.ascii == 'A')) { + g_sel_anchor = 0; + g_sel_end = g_buf_len; + g_has_selection = (g_buf_len > 0); + g_cursor_pos = g_buf_len; + update_cursor_pos(); + return true; + } + + // Arrow keys + if (key.scancode == 0x48) { // Up + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + move_up(); + if (key.shift) update_selection(); + return true; + } + if (key.scancode == 0x50) { // Down + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + move_down(); + if (key.shift) update_selection(); + return true; + } + if (key.scancode == 0x4B) { // Left + if (key.shift) { + start_selection(); move_left(); update_selection(); + } else if (g_has_selection) { + int s, e; sel_range(&s, &e); + g_cursor_pos = s; update_cursor_pos(); clear_selection(); + } else move_left(); + return true; + } + if (key.scancode == 0x4D) { // Right + if (key.shift) { + start_selection(); move_right(); update_selection(); + } else if (g_has_selection) { + int s, e; sel_range(&s, &e); + g_cursor_pos = e; update_cursor_pos(); clear_selection(); + } else move_right(); + return true; + } + + // Home + if (key.scancode == 0x47) { + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + move_home(); + if (key.shift) update_selection(); + return true; + } + // End + if (key.scancode == 0x4F) { + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + move_end(); + if (key.shift) update_selection(); + return true; + } + + // Page Up + if (key.scancode == 0x49) { + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + int cell_h = font_cell_h(); + int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; + for (int i = 0; i < vis; i++) move_up(); + if (key.shift) update_selection(); + return true; + } + // Page Down + if (key.scancode == 0x51) { + if (key.shift) start_selection(); else if (g_has_selection) clear_selection(); + int cell_h = font_cell_h(); + int vis = cell_h > 0 ? (g_win_h - TOOLBAR_H - STATUS_H) / cell_h : 20; + for (int i = 0; i < vis; i++) move_down(); + if (key.shift) update_selection(); + return true; + } + + // Delete + if (key.scancode == 0x53) { + if (g_has_selection) delete_selection(); else delete_char(); + return true; + } + + // Backspace + if (key.ascii == '\b' || key.scancode == 0x0E) { + if (g_has_selection) delete_selection(); else backspace(); + return true; + } + + // Enter + if (key.ascii == '\n' || key.ascii == '\r') { + if (g_has_selection) delete_selection(); + insert_char('\n'); + return true; + } + + // Tab + if (key.ascii == '\t') { + if (g_has_selection) delete_selection(); + for (int i = 0; i < TAB_WIDTH; i++) insert_char(' '); + return true; + } + + // Printable + if (key.ascii >= 32 && key.ascii < 127) { + if (g_has_selection) delete_selection(); + insert_char(key.ascii); + return true; + } + + return false; +} + +// ============================================================================ +// Mouse handler +// ============================================================================ + +static bool handle_mouse(Montauk::WinEvent& ev, int win_id) { + int mx = ev.mouse.x; + int my = ev.mouse.y; + uint8_t btns = ev.mouse.buttons; + uint8_t prev = ev.mouse.prev_buttons; + bool clicked = (btns & 1) && !(prev & 1); + bool held = (btns & 1) != 0; + bool released = !(btns & 1) && (prev & 1); + int scroll = ev.mouse.scroll; + int cell_h = font_cell_h(); + + int editor_y = TOOLBAR_H + (g_pathbar_open ? PATHBAR_H : 0); + int text_area_h = g_win_h - editor_y - STATUS_H; + + // ---- Toolbar clicks ---- + if (clicked && my < TOOLBAR_H) { + // Open button + if (mx >= 4 && mx < 28 && my >= 6 && my < 30) { + if (g_pathbar_open) g_pathbar_open = false; + else open_pathbar_open(); + return true; + } + // Save button + if (mx >= 32 && mx < 56 && my >= 6 && my < 30) { + if (g_filepath[0] == '\0') + open_pathbar_save(); + else + save_file(); + return true; + } + return false; + } + + // ---- Pathbar clicks ---- + if (g_pathbar_open && my >= TOOLBAR_H && my < TOOLBAR_H + PATHBAR_H) { + if (clicked) { + int btn_w = 56; + int inp_w = g_win_w - 8 - btn_w - 12; + int ob_x = 8 + inp_w + 6; + if (mx >= ob_x && mx < ob_x + btn_w) + pathbar_confirm(win_id); + } + return true; + } + + // ---- Editor click ---- + if (clicked && my >= editor_y && my < editor_y + text_area_h + && mx > LINE_NUM_W && mx < g_win_w - SCROLLBAR_W) { + int pos = hit_test(mx, my, editor_y); + g_cursor_pos = pos; + update_cursor_pos(); + g_sel_anchor = pos; + g_sel_end = pos; + g_has_selection = false; + g_mouse_selecting = true; + return true; + } + + // ---- Editor drag ---- + if (g_mouse_selecting && held && my >= editor_y - 20) { + int pos = hit_test(mx, my, editor_y); + g_sel_end = pos; + g_cursor_pos = pos; + update_cursor_pos(); + g_has_selection = (g_sel_anchor != g_sel_end); + return true; + } + + // ---- Mouse release ---- + if (g_mouse_selecting && released) { + g_mouse_selecting = false; + return true; + } + + // ---- Scroll ---- + if (scroll != 0 && my >= editor_y && my < editor_y + text_area_h) { + int old_scroll = g_scroll_y; + g_scroll_y -= scroll * 3; + if (g_scroll_y < 0) g_scroll_y = 0; + int visible_lines = text_area_h / cell_h; + int max_scroll = g_line_count - visible_lines; + if (max_scroll < 0) max_scroll = 0; + if (g_scroll_y > max_scroll) g_scroll_y = max_scroll; + + // Move cursor with scroll to keep it visible + int delta = g_scroll_y - old_scroll; + if (delta != 0) { + int new_line = g_cursor_line + delta; + if (new_line < g_scroll_y) new_line = g_scroll_y; + if (new_line >= g_scroll_y + visible_lines) new_line = g_scroll_y + visible_lines - 1; + if (new_line < 0) new_line = 0; + if (new_line >= g_line_count) new_line = g_line_count - 1; + int col = g_cursor_col; + int ll = line_length(new_line); + if (col > ll) col = ll; + g_cursor_pos = g_line_offsets[new_line] + col; + g_cursor_line = new_line; + g_cursor_col = col; + } + return true; + } + + return false; +} + +// ============================================================================ +// Entry point +// ============================================================================ + +extern "C" void _start() { + // Initialize buffer + g_buffer = (char*)montauk::malloc(INIT_CAP); + g_buf_cap = INIT_CAP; + g_buf_len = 0; + + recompute_lines(); + update_cursor_pos(); + + // Load fonts + auto load_font = [](const char* path) -> TrueTypeFont* { + TrueTypeFont* f = (TrueTypeFont*)montauk::malloc(sizeof(TrueTypeFont)); + if (!f) return nullptr; + montauk::memset(f, 0, sizeof(TrueTypeFont)); + if (!f->init(path)) { montauk::mfree(f); return nullptr; } + return f; + }; + g_font = load_font("0:/fonts/Roboto-Medium.ttf"); + g_font_mono = load_font("0:/fonts/JetBrainsMono-Regular.ttf"); + + // Load toolbar icons + Color icon_color = Color::from_rgb(0x44, 0x44, 0x44); + g_icon_folder = svg_load("0:/icons/folder.svg", 16, 16, icon_color); + g_icon_save = svg_load("0:/icons/document-save-symbolic.svg", 16, 16, icon_color); + + // Check for file argument + char args[512] = {}; + int arglen = montauk::getargs(args, sizeof(args)); + if (arglen > 0 && args[0]) + load_file(args); + + // Build window title + char title[64] = "Text Editor"; + if (g_filename[0]) { + snprintf(title, 64, "%s - Editor", g_filename); + } + + // Create window + Montauk::WinCreateResult wres; + if (montauk::win_create(title, INIT_W, INIT_H, &wres) < 0 || wres.id < 0) + montauk::exit(1); + + int win_id = wres.id; + uint32_t* pixels = (uint32_t*)(uintptr_t)wres.pixelVa; + + render(pixels); + montauk::win_present(win_id); + + while (true) { + Montauk::WinEvent ev; + int r = montauk::win_poll(win_id, &ev); + + if (r < 0) break; + if (r == 0) { montauk::sleep_ms(16); continue; } + + // Close + if (ev.type == 3) break; + + // Resize + if (ev.type == 2) { + g_win_w = ev.resize.w; + g_win_h = ev.resize.h; + pixels = (uint32_t*)(uintptr_t)montauk::win_resize(win_id, g_win_w, g_win_h); + render(pixels); + montauk::win_present(win_id); + continue; + } + + bool redraw = false; + + // Keyboard + if (ev.type == 0) + redraw = handle_key(ev.key, win_id); + + // Mouse + if (ev.type == 1) + redraw = handle_mouse(ev, win_id); + + if (redraw) { + render(pixels); + montauk::win_present(win_id); + } + } + + // Cleanup + if (g_buffer) montauk::mfree(g_buffer); + if (g_line_offsets) montauk::mfree(g_line_offsets); + if (g_font) montauk::mfree(g_font); + if (g_font_mono) montauk::mfree(g_font_mono); + montauk::win_destroy(win_id); + montauk::exit(0); +} diff --git a/programs/src/texteditor/stb_truetype_impl.cpp b/programs/src/texteditor/stb_truetype_impl.cpp new file mode 100644 index 0000000..9ce2266 --- /dev/null +++ b/programs/src/texteditor/stb_truetype_impl.cpp @@ -0,0 +1,35 @@ +/* + * stb_truetype_impl.cpp + * Single compilation unit for stb_truetype in MontaukOS freestanding environment + * Copyright (c) 2026 Daniel Hammer + */ + +#include +#include +#include +#include +#include + +// Override all stb_truetype dependencies before including the implementation + +#define STBTT_ifloor(x) ((int) stb_floor(x)) +#define STBTT_iceil(x) ((int) stb_ceil(x)) +#define STBTT_sqrt(x) stb_sqrt(x) +#define STBTT_pow(x,y) stb_pow(x,y) +#define STBTT_fmod(x,y) stb_fmod(x,y) +#define STBTT_cos(x) stb_cos(x) +#define STBTT_acos(x) stb_acos(x) +#define STBTT_fabs(x) stb_fabs(x) + +#define STBTT_malloc(x,u) ((void)(u), montauk::malloc(x)) +#define STBTT_free(x,u) ((void)(u), montauk::mfree(x)) + +#define STBTT_memcpy(d,s,n) montauk::memcpy(d,s,n) +#define STBTT_memset(d,v,n) montauk::memset(d,v,n) + +#define STBTT_strlen(x) montauk::slen(x) + +#define STBTT_assert(x) ((void)(x)) + +#define STB_TRUETYPE_IMPLEMENTATION +#include diff --git a/programs/src/texteditor/syntax_highlight.hpp b/programs/src/texteditor/syntax_highlight.hpp new file mode 100644 index 0000000..f5a1c67 --- /dev/null +++ b/programs/src/texteditor/syntax_highlight.hpp @@ -0,0 +1,287 @@ +/* + * syntax_highlight.hpp + * C syntax highlighting for the MontaukOS text editor + * Activated for .c and .h files + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once + +#include + +using namespace gui; + +// ============================================================================ +// Token types +// ============================================================================ + +enum SynToken : uint8_t { + SYN_NORMAL, + SYN_KEYWORD, + SYN_TYPE, + SYN_PREPROCESSOR, + SYN_STRING, + SYN_CHAR, + SYN_COMMENT, + SYN_NUMBER, + SYN_OPERATOR, +}; + +// ============================================================================ +// Colors for each token type +// ============================================================================ + +inline Color syn_color(SynToken tok) { + switch (tok) { + case SYN_KEYWORD: return Color::from_rgb(0xC5, 0x62, 0x8C); // magenta-pink + case SYN_TYPE: return Color::from_rgb(0x2E, 0x86, 0xAB); // teal + case SYN_PREPROCESSOR: return Color::from_rgb(0x8B, 0x6E, 0xB5); // purple + case SYN_STRING: return Color::from_rgb(0x6A, 0x9F, 0x3A); // green + case SYN_CHAR: return Color::from_rgb(0x6A, 0x9F, 0x3A); // green + case SYN_COMMENT: return Color::from_rgb(0x7A, 0x7A, 0x7A); // gray + case SYN_NUMBER: return Color::from_rgb(0xC9, 0x7E, 0x2A); // orange + case SYN_OPERATOR: return Color::from_rgb(0x40, 0x40, 0x40); // dark gray + default: return Color::from_rgb(0x1E, 0x1E, 0x1E); // near-black + } +} + +// ============================================================================ +// Keyword / type lookup +// ============================================================================ + +inline bool syn_is_alpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +inline bool syn_is_alnum(char c) { + return syn_is_alpha(c) || (c >= '0' && c <= '9'); +} + +inline bool syn_is_digit(char c) { + return c >= '0' && c <= '9'; +} + +inline bool syn_is_hex(char c) { + return syn_is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +inline bool syn_streq(const char* buf, int len, const char* kw) { + int i = 0; + while (i < len && kw[i]) { + if (buf[i] != kw[i]) return false; + i++; + } + return i == len && kw[i] == '\0'; +} + +inline SynToken syn_classify_word(const char* buf, int len) { + // C keywords + static const char* keywords[] = { + "auto", "break", "case", "const", "continue", "default", "do", + "else", "enum", "extern", "for", "goto", "if", "inline", + "register", "restrict", "return", "sizeof", "static", "struct", + "switch", "typedef", "union", "volatile", "while", + "NULL", "true", "false", "nullptr", + }; + // C types + static const char* types[] = { + "void", "char", "short", "int", "long", "float", "double", + "signed", "unsigned", "bool", "_Bool", + "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", + "FILE", + }; + + for (int i = 0; i < (int)(sizeof(keywords) / sizeof(keywords[0])); i++) { + if (syn_streq(buf, len, keywords[i])) return SYN_KEYWORD; + } + for (int i = 0; i < (int)(sizeof(types) / sizeof(types[0])); i++) { + if (syn_streq(buf, len, types[i])) return SYN_TYPE; + } + return SYN_NORMAL; +} + +// ============================================================================ +// Per-line highlighter +// ============================================================================ +// +// Fills `out[]` with SynToken values for each character in the line. +// `in_block_comment` is the multi-line comment state carried across lines: +// - pass in the state from the previous line +// - on return, updated for the next line +// +// `line` points to the first char of the line, `len` is its length +// (excluding the newline). `out` must have room for `len` entries. + +inline void syn_highlight_line(const char* line, int len, SynToken* out, bool& in_block_comment) { + int i = 0; + + while (i < len) { + // ---- Block comment continuation ---- + if (in_block_comment) { + while (i < len) { + if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') { + out[i] = SYN_COMMENT; + out[i + 1] = SYN_COMMENT; + i += 2; + in_block_comment = false; + break; + } + out[i] = SYN_COMMENT; + i++; + } + continue; + } + + char c = line[i]; + + // ---- Line comment ---- + if (c == '/' && i + 1 < len && line[i + 1] == '/') { + while (i < len) out[i++] = SYN_COMMENT; + break; + } + + // ---- Block comment start ---- + if (c == '/' && i + 1 < len && line[i + 1] == '*') { + in_block_comment = true; + out[i] = SYN_COMMENT; + out[i + 1] = SYN_COMMENT; + i += 2; + continue; + } + + // ---- Preprocessor directive ---- + if (c == '#') { + // Check that only whitespace precedes the # + bool is_pp = true; + for (int j = 0; j < i; j++) { + if (line[j] != ' ' && line[j] != '\t') { is_pp = false; break; } + } + if (is_pp) { + while (i < len) { + // Handle line-comment inside preprocessor + if (i + 1 < len && line[i] == '/' && line[i + 1] == '/') { + while (i < len) out[i++] = SYN_COMMENT; + break; + } + // Handle block comment start inside preprocessor + if (i + 1 < len && line[i] == '/' && line[i + 1] == '*') { + in_block_comment = true; + out[i] = SYN_COMMENT; + out[i + 1] = SYN_COMMENT; + i += 2; + // Continue consuming as comment + while (i < len) { + if (i + 1 < len && line[i] == '*' && line[i + 1] == '/') { + out[i] = SYN_COMMENT; + out[i + 1] = SYN_COMMENT; + i += 2; + in_block_comment = false; + break; + } + out[i] = SYN_COMMENT; + i++; + } + continue; + } + out[i] = SYN_PREPROCESSOR; + i++; + } + continue; + } + } + + // ---- String literal ---- + if (c == '"') { + out[i++] = SYN_STRING; + while (i < len) { + if (line[i] == '\\' && i + 1 < len) { + out[i] = SYN_STRING; + out[i + 1] = SYN_STRING; + i += 2; + continue; + } + if (line[i] == '"') { + out[i++] = SYN_STRING; + break; + } + out[i++] = SYN_STRING; + } + continue; + } + + // ---- Character literal ---- + if (c == '\'') { + out[i++] = SYN_CHAR; + while (i < len) { + if (line[i] == '\\' && i + 1 < len) { + out[i] = SYN_CHAR; + out[i + 1] = SYN_CHAR; + i += 2; + continue; + } + if (line[i] == '\'') { + out[i++] = SYN_CHAR; + break; + } + out[i++] = SYN_CHAR; + } + continue; + } + + // ---- Numbers ---- + if (syn_is_digit(c) || (c == '.' && i + 1 < len && syn_is_digit(line[i + 1]))) { + // Hex + if (c == '0' && i + 1 < len && (line[i + 1] == 'x' || line[i + 1] == 'X')) { + out[i] = SYN_NUMBER; out[i + 1] = SYN_NUMBER; + i += 2; + while (i < len && syn_is_hex(line[i])) out[i++] = SYN_NUMBER; + } else { + // Decimal / float + while (i < len && (syn_is_digit(line[i]) || line[i] == '.')) + out[i++] = SYN_NUMBER; + } + // Suffixes: u, l, f, etc. + while (i < len && (line[i] == 'u' || line[i] == 'U' || + line[i] == 'l' || line[i] == 'L' || + line[i] == 'f' || line[i] == 'F')) + out[i++] = SYN_NUMBER; + continue; + } + + // ---- Identifiers / keywords / types ---- + if (syn_is_alpha(c)) { + int start = i; + while (i < len && syn_is_alnum(line[i])) i++; + SynToken tok = syn_classify_word(line + start, i - start); + for (int j = start; j < i; j++) out[j] = tok; + continue; + } + + // ---- Operators ---- + if (c == '+' || c == '-' || c == '*' || c == '/' || c == '%' || + c == '=' || c == '!' || c == '<' || c == '>' || c == '&' || + c == '|' || c == '^' || c == '~' || c == '?' || c == ':') { + out[i++] = SYN_OPERATOR; + continue; + } + + // ---- Everything else (whitespace, braces, parens, etc.) ---- + out[i++] = SYN_NORMAL; + } +} + +// ============================================================================ +// Extension check +// ============================================================================ + +inline bool syn_is_c_file(const char* filepath) { + if (!filepath || filepath[0] == '\0') return false; + int len = 0; + while (filepath[len]) len++; + if (len >= 2 && filepath[len - 2] == '.' && + (filepath[len - 1] == 'c' || filepath[len - 1] == 'h')) + return true; + return false; +}