feat: Symmetric Multiprocessing, text editor improvements, merge doom libc, implement math functions
This commit is contained in:
+1
-1
@@ -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)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <Graphics/Cursor.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Hal/MSR.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Api/Syscall.hpp>
|
||||
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,15 +10,24 @@
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+52
-44
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "Vfs.hpp"
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
+10
-1
@@ -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) {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* SmpBoot.cpp
|
||||
* Symmetric Multiprocessing bootstrap and AP entry
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "SmpBoot.hpp"
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/Interrupts.hpp>
|
||||
#include <Hal/IDT.hpp>
|
||||
#include <Hal/MSR.hpp>
|
||||
#include <Hal/Cpu.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <limine.h>
|
||||
|
||||
// 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 <Libraries/Memory.hpp>
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* SmpBoot.hpp
|
||||
* Symmetric Multiprocessing bootstrap and per-CPU data
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <Hal/GDT.hpp>
|
||||
|
||||
// ====================================================================
|
||||
// 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <Fs/FsProbe.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <Api/Syscall.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
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");
|
||||
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
#include <Memory/PageFrameAllocator.hpp>
|
||||
#include <Common/Panic.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+351
-96
@@ -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 <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/GDT.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Api/WinServer.hpp>
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../Libraries/String.hpp"
|
||||
#include "../Libraries/Memory.hpp"
|
||||
#include <CppLib/CString.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
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;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <Libraries/String.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
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<typename T>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "ApicTimer.hpp"
|
||||
#include <Hal/Apic/Apic.hpp>
|
||||
#include <Hal/Apic/Interrupts.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Io/IoPort.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+10
-6
@@ -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/<name>/.
|
||||
@@ -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/<name>/.
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) \
|
||||
|
||||
+279
-13
@@ -11,6 +11,7 @@
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1076,8 +1076,8 @@ static void filemanager_open_entry(FileManagerState* fm, int idx) {
|
||||
montauk::spawn("0:/apps/music/music.elf", fullpath);
|
||||
} else if (str_ends_with(fm->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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* stb_truetype_impl.cpp
|
||||
* Single compilation unit for stb_truetype in MontaukOS freestanding environment
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <montauk/heap.h>
|
||||
#include <montauk/string.h>
|
||||
#include <gui/stb_math.h>
|
||||
|
||||
// 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 <gui/stb_truetype.h>
|
||||
@@ -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 <gui/gui.hpp>
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user