From fb8d16ee7bbd52d7625be680021c59d443458a38 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Wed, 18 Feb 2026 15:13:53 +0100 Subject: [PATCH] feat: expand user mode, add DOOM game, add manpages --- .gitignore | 3 + docs/userspace.html | 1062 ++++++++++++++++++++ doomgeneric | 1 + kernel/src/Api/Syscall.cpp | 80 +- kernel/src/Api/Syscall.hpp | 14 + kernel/src/Drivers/PS2/Keyboard.cpp | 43 +- kernel/src/Graphics/Cursor.cpp | 5 + kernel/src/Graphics/Cursor.hpp | 5 + kernel/src/Main.cpp | 17 + kernel/src/Memory/PageFrameAllocator.cpp | 9 +- kernel/src/Sched/ElfLoader.cpp | 13 +- kernel/src/Sched/Scheduler.cpp | 39 +- kernel/src/Sched/Scheduler.hpp | 6 +- kernel/src/Terminal/Terminal.hpp | 8 +- programs/GNUmakefile | 12 +- programs/bin/hello.elf | Bin 12360 -> 12224 bytes programs/bin/shell.elf | Bin 38744 -> 45392 bytes programs/include/Api/Syscall.hpp | 14 + programs/include/libc/assert.h | 23 + programs/include/libc/ctype.h | 28 + programs/include/libc/errno.h | 29 + programs/include/libc/fcntl.h | 22 + programs/include/libc/inttypes.h | 27 + programs/include/libc/limits.h | 31 + programs/include/libc/math.h | 31 + programs/include/libc/stdio.h | 77 ++ programs/include/libc/stdlib.h | 66 ++ programs/include/libc/string.h | 35 + programs/include/libc/strings.h | 17 + programs/include/libc/sys/stat.h | 24 + programs/include/libc/sys/time.h | 24 + programs/include/libc/sys/types.h | 23 + programs/include/libc/unistd.h | 20 + programs/include/zenith/heap.h | 133 +++ programs/include/zenith/syscall.h | 22 + programs/man/file.2 | 74 ++ programs/man/framebuffer.2 | 62 ++ programs/man/intro.1 | 49 + programs/man/malloc.3 | 57 ++ programs/man/man.1 | 47 + programs/man/shell.1 | 54 + programs/man/spawn.2 | 69 ++ programs/man/syscalls.2 | 137 +++ programs/obj/hello/main.o | Bin 8416 -> 7928 bytes programs/obj/shell/main.o | Bin 74968 -> 92944 bytes programs/src/doom/Makefile | 187 ++++ programs/src/doom/doomgeneric_zenith.c | 230 +++++ programs/src/doom/libc.c | 1158 ++++++++++++++++++++++ programs/src/man/main.cpp | 374 +++++++ programs/src/shell/main.cpp | 66 +- 50 files changed, 4484 insertions(+), 43 deletions(-) create mode 100644 docs/userspace.html create mode 160000 doomgeneric create mode 100644 programs/include/libc/assert.h create mode 100644 programs/include/libc/ctype.h create mode 100644 programs/include/libc/errno.h create mode 100644 programs/include/libc/fcntl.h create mode 100644 programs/include/libc/inttypes.h create mode 100644 programs/include/libc/limits.h create mode 100644 programs/include/libc/math.h create mode 100644 programs/include/libc/stdio.h create mode 100644 programs/include/libc/stdlib.h create mode 100644 programs/include/libc/string.h create mode 100644 programs/include/libc/strings.h create mode 100644 programs/include/libc/sys/stat.h create mode 100644 programs/include/libc/sys/time.h create mode 100644 programs/include/libc/sys/types.h create mode 100644 programs/include/libc/unistd.h create mode 100644 programs/include/zenith/heap.h create mode 100644 programs/man/file.2 create mode 100644 programs/man/framebuffer.2 create mode 100644 programs/man/intro.1 create mode 100644 programs/man/malloc.3 create mode 100644 programs/man/man.1 create mode 100644 programs/man/shell.1 create mode 100644 programs/man/spawn.2 create mode 100644 programs/man/syscalls.2 create mode 100644 programs/src/doom/Makefile create mode 100644 programs/src/doom/doomgeneric_zenith.c create mode 100644 programs/src/doom/libc.c create mode 100644 programs/src/man/main.cpp diff --git a/.gitignore b/.gitignore index 60d33ff..7e88317 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ qemu.log toolchain/local/ toolchain/build/ toolchain/src/ +programs/bin/ +programs/obj/ +programs/src/doom/obj/ \ No newline at end of file diff --git a/docs/userspace.html b/docs/userspace.html new file mode 100644 index 0000000..3fe51f6 --- /dev/null +++ b/docs/userspace.html @@ -0,0 +1,1062 @@ + + + + + +ZenithOS — Userspace Application Development + + + + + + + + + + + + +
+ + +

Userspace Developer's Handbook

+

+ ZenithOS is a hobbyist 64-bit operating system written in C++20. + Userspace programs run in Ring 3, are loaded as static ELF64 + binaries, and communicate with the kernel through the x86-64 + SYSCALL/SYSRET mechanism. +

+

+ This document covers everything you need to write, build, and run + userspace applications — from a minimal "Hello World" to using + every available syscall — as well as how to extend the kernel + with new syscalls. +

+ + +

Getting Started

+ +

Project Structure

+
programs/
+├── GNUmakefile          # Build system (C++ programs)
+├── link.ld              # Linker script (base address 0x400000)
+├── include/
+│   ├── Api/
+│   │   └── Syscall.hpp      # Syscall numbers & data structures
+│   ├── zenith/
+│   │   ├── syscall.h        # Inline asm wrappers & typed API
+│   │   └── heap.h           # Userspace heap allocator (malloc/mfree/realloc)
+│   └── libc/                # Minimal C standard library headers
+│       ├── stdio.h          # printf, FILE I/O
+│       ├── stdlib.h         # malloc, free, atoi, exit
+│       ├── string.h         # memcpy, strlen, strcmp, etc.
+│       └── ...              # ctype.h, errno.h, assert.h, etc.
+├── src/
+│   ├── hello/
+│   │   └── main.cpp         # Hello world example
+│   ├── shell/
+│   │   └── main.cpp         # Interactive shell
+│   ├── man/
+│   │   └── main.cpp         # Manual page viewer
+│   └── doom/
+│       ├── Makefile             # DOOM build system
+│       ├── doomgeneric_zenith.c # ZenithOS platform layer
+│       └── libc.c               # C library implementation
+├── bin/                     # Compiled .elf binaries
+└── obj/                     # Intermediate object files
+

+ Each subdirectory under src/ is treated as a separate + program. The build system discovers them automatically. +

+ +

Toolchain

+

+ Programs are compiled with a freestanding x86_64-elf + cross-compiler. The build system looks for one in + toolchain/local/bin/; if not found, it falls back to the + host g++. +

+

Key compiler flags:

+ +
+ SSE: The default C++ build disables SSE. Programs that need + floating-point support (e.g. C programs using float/double) + should compile with -msse -msse2 instead. The kernel enables SSE in + CR0/CR4 at boot, so SSE instructions are safe in userspace. +
+ +

Build System

+

To build all programs:

+
cd programs/
+make          # or: make -j$(nproc)
+

+ This produces one ELF binary per program in bin/ + (e.g. bin/hello.elf, bin/shell.elf). + The binaries are then typically packed into the ramdisk + (ramdisk.tar) and loaded by the kernel at boot. +

+

To add a new program, create src/<name>/main.cpp and run make.

+ +

Linker Script

+

programs/link.ld

+

+ All userspace programs are linked at virtual address + 0x400000. The linker script defines four standard + sections: +

+ + + + + + +
SectionContentsAlignment
.textExecutable code— (base)
.rodataRead-only data, string literals4 KiB
.dataInitialized read/write data4 KiB
.bssZero-initialized data
+

+ Debug frames (.eh_frame), notes, and comments are + discarded to keep binaries small. +

+ + +

Program Anatomy

+ +

Entry Point: _start

+

+ Because there is no C runtime, every program must define + extern "C" void _start() as its entry point. There is no + main(), no argc/argv, and no + atexit handlers. +

+
+ Auto-exit: If _start() returns normally, + the kernel's exit stub (mapped at 0x3FF000) automatically + calls SYS_EXIT(0). You can also call + zenith::exit(code) explicitly at any point. +
+ +

Hello World

+

programs/src/hello/main.cpp

+
// Minimal ZenithOS userspace program
+#include <zenith/syscall.h>
+
+extern "C" void _start() {
+    zenith::print("Hello from userspace!\n");
+}
+

+ Include <zenith/syscall.h> for the full typed API. + That header pulls in <Api/Syscall.hpp> for + constants and data structures. +

+ + +

Syscall Architecture

+ +

Calling Convention

+

+ ZenithOS uses the hardware SYSCALL instruction on + x86-64. The kernel sets up the required MSRs + (IA32_STAR, IA32_LSTAR, + IA32_FMASK) during boot. +

+ + + + + + + + + +
RegisterPurpose
RAXSyscall number (in) / return value (out)
RDIArgument 1
RSIArgument 2
RDXArgument 3
R10Argument 4 (not RCX — SYSCALL clobbers it)
R8Argument 5
R9Argument 6
+

+ The SYSCALL instruction saves RIP in + RCX and RFLAGS in R11; both + registers are clobbered. The kernel masks IF on entry + (via IA32_FMASK) so interrupts are disabled during the + transition. +

+

+ On the kernel side, SyscallEntry (assembly) saves + callee-saved registers and all arguments into a + SyscallFrame, then calls + SyscallDispatch(SyscallFrame*) which dispatches by + syscall number. +

+ +

Raw Syscall Wrappers

+

programs/include/zenith/syscall.h

+

+ Seven inline functions cover 0–6 argument syscalls: +

+
int64_t syscall0(uint64_t nr);
+int64_t syscall1(uint64_t nr, uint64_t a1);
+int64_t syscall2(uint64_t nr, uint64_t a1, uint64_t a2);
+int64_t syscall3(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3);
+int64_t syscall4(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4);
+int64_t syscall5(uint64_t nr, uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, uint64_t a5);
+int64_t syscall6(uint64_t nr, uint64_t a1, ..., uint64_t a6);
+

+ Each wrapper uses inline assembly with explicit register moves and a + full clobber list to ensure correctness. You can use these directly if + you need a syscall not yet covered by the typed API. +

+ +

Typed API Wrappers

+

+ The zenith:: namespace provides type-safe wrappers around + the raw syscalls. These are the recommended interface for application + code. The full reference follows below. +

+ + +

Syscall Reference

+

+ ZenithOS v0.1.0 exposes 26 syscalls (numbers + 0–25), organized into 10 categories. +

+ + +

Process Process Management

+ +

SYS_EXIT (0) — Terminate the current process

+
[[noreturn]] void zenith::exit(int code = 0);
+

+ Terminates the calling process. The exit code is currently unused by + the kernel but reserved for future wait/status support. + Control never returns to the caller. +

+ +

SYS_YIELD (1) — Yield the CPU

+
void zenith::yield();
+

+ Voluntarily yields the remainder of the current time slice, allowing + the scheduler to run another ready process immediately. +

+ +

SYS_SLEEP_MS (2) — Sleep for a duration

+
void zenith::sleep_ms(uint64_t ms);
+

+ Suspends the calling process for at least ms + milliseconds. Resolution depends on the APIC timer tick rate. +

+ +

SYS_GETPID (3) — Get process ID

+
int zenith::getpid();
+

+ Returns the PID of the calling process. Returns -1 if + called from the idle context (should not happen in userspace). +

+ +

SYS_SPAWN (20) — Spawn a new process

+
int zenith::spawn(const char* path, const char* args = nullptr);
+

+ Loads the ELF binary at path (a VFS path like + "0:/hello.elf") and spawns it as a new process. + Returns the new process's PID on success, or -1 on + failure (no free slots, invalid ELF, file not found). + The optional args string (up to 255 characters) is + copied into the new process and can be retrieved with + SYS_GETARGS. +

+ +

SYS_WAITPID (23) — Wait for a process to exit

+
void zenith::waitpid(int pid);
+

+ Blocks the calling process until the process identified by + pid has exited. Internally yields the CPU in a loop + until the target process is no longer alive. This is used by the + shell to wait for foreground processes (e.g. run + command) so that keyboard input and the terminal are not shared + simultaneously. +

+ +

SYS_GETARGS (25) — Get process arguments

+
int zenith::getargs(char* buf, uint64_t maxLen);
+

+ Copies the argument string passed to the current process (via + spawn()) into buf, writing at most + maxLen - 1 characters plus a null terminator. + Returns the number of characters copied, or -1 on error. + If no arguments were provided at spawn time, the buffer will be empty. +

+ + +

Console Console I/O

+ +

SYS_PRINT (4) — Print a string

+
void zenith::print(const char* text);
+

+ Writes a null-terminated string to the kernel terminal. Supports + newlines (\n) and standard printable ASCII. +

+ +

SYS_PUTCHAR (5) — Print a single character

+
void zenith::putchar(char c);
+

+ Writes a single character to the kernel terminal. Useful for building + output character by character (e.g. printing integers). +

+ + +

File I/O File I/O

+ +

SYS_OPEN (6) — Open a file

+
int zenith::open(const char* path);
+

+ Opens a file on the VFS. Paths use the format + "<device>:/<name>" (e.g. "0:/hello.elf" + for the ramdisk). Returns a non-negative handle on success, or a + negative value on error. +

+ +

SYS_READ (7) — Read from a file

+
int zenith::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size);
+

+ Reads up to size bytes from the file at the given byte + offset into buf. Returns the number of bytes + actually read, or a negative value on error. Does not maintain an + implicit file position — the offset is explicit on every call. +

+ +

SYS_GETSIZE (8) — Get file size

+
uint64_t zenith::getsize(int handle);
+

Returns the total size (in bytes) of the file associated with handle.

+ +

SYS_CLOSE (9) — Close a file

+
void zenith::close(int handle);
+

Closes the file handle and releases associated kernel resources.

+ +

SYS_READDIR (10) — List directory entries

+
int zenith::readdir(const char* path, const char** names, int max);
+

+ Reads up to max directory entries from path. + Entry name pointers are written into the names array. + The kernel allocates a user-accessible page for the string data + automatically. Returns the number of entries read, or + ≤ 0 on error/empty directory. Maximum 64 entries per call. +

+ + +

Memory Memory Management

+ +

+ Memory allocation has two layers: low-level syscalls + that map pages from the kernel, and a userspace heap + that provides malloc/mfree on top. +

+ +

Userspace Heap (recommended)

+

programs/include/zenith/heap.h

+

+ Include <zenith/heap.h> for a proper free-list + allocator that runs entirely in userspace. It calls + SYS_ALLOC internally to obtain pages and manages + sub-page allocations with a linked free list — adapted from + the kernel's own HeapAllocator. +

+ +
void* zenith::malloc(uint64_t size);
+

+ Allocates size bytes from the userspace free list. + Returns a 16-byte-aligned pointer, or nullptr on failure. + When the free list is exhausted, it transparently requests more pages + from the kernel via SYS_ALLOC (minimum 16 KiB growth). +

+ +
void zenith::mfree(void* ptr);
+

+ Returns the block to the userspace free list. + No syscall is made — the memory stays mapped + and is immediately available for future malloc calls. + Passing nullptr is a safe no-op. +

+ +
void* zenith::realloc(void* ptr, uint64_t size);
+

+ Resizes the allocation at ptr to size bytes. + Allocates a new block, copies the smaller of old/new sizes, and frees + the old block. If ptr is nullptr, behaves + like malloc. +

+ +

Low-Level Page Syscalls

+ +

SYS_ALLOC (11) — Map pages

+
void* zenith::alloc(uint64_t size);
+

+ Maps size bytes of zeroed physical pages into the + process's address space (starting at 0x40000000). + The size is rounded up to the nearest page boundary (4 KiB). + Returns a pointer to the mapped region, or nullptr on + failure. This is the backing primitive for + zenith::malloc — most programs should use the heap + API instead of calling this directly. +

+ +

SYS_FREE (12) — Unmap pages (no-op)

+
void zenith::free(void* ptr);
+

+ Reserved for future page-level unmapping. Currently a no-op — + pages are reclaimed when the process exits. Use + zenith::mfree for heap allocations. +

+ + +

Time Timekeeping

+ +

SYS_GETTICKS (13) — Get tick count

+
uint64_t zenith::get_ticks();
+

+ Returns the number of APIC timer ticks since boot. The tick rate + depends on the hardware and APIC timer calibration. +

+ +

SYS_GETMILLISECONDS (14) — Get milliseconds since boot

+
uint64_t zenith::get_milliseconds();
+

+ Returns wall-clock milliseconds elapsed since boot. Useful for + calculating uptime or measuring durations. +

+ + +

System System Information

+ +

SYS_GETINFO (15) — Get OS information

+
void zenith::get_info(Zenith::SysInfo* info);
+

+ Fills in a SysInfo structure + with the OS name, version string, API version number, and maximum + process count. +

+ + +

Keyboard Keyboard Input

+ +

SYS_ISKEYAVAILABLE (16) — Check for pending key

+
bool zenith::is_key_available();
+

+ Returns true if a key event is available in the PS/2 + keyboard buffer. Non-blocking. +

+ +

SYS_GETKEY (17) — Get a key event

+
void zenith::getkey(Zenith::KeyEvent* out);
+

+ Fills in a KeyEvent + structure with the next keyboard event (press or release), including + scancode, ASCII translation, and modifier state (Shift, Ctrl, Alt). +

+ +

SYS_GETCHAR (18) — Read a character (blocking)

+
char zenith::getchar();
+

+ Blocks until a printable character key-press is available, then + returns the ASCII character. This is the simplest way to read + interactive text input. +

+ + +

Network Networking

+ +

SYS_PING (19) — Send an ICMP echo request

+
int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs = 3000);
+

+ Sends an ICMP echo request to ip and waits up to + timeoutMs milliseconds for a reply. The IP address is in + little-endian byte order (e.g. 10.0.2.2 → + 0x0202000A). + Returns the round-trip time in milliseconds on success, or + -1 on timeout. +

+ + +

Framebuffer Framebuffer Access

+ +

SYS_FBINFO (21) — Get framebuffer information

+
void zenith::fb_info(Zenith::FbInfo* info);
+

+ Fills in a FbInfo structure + with the framebuffer dimensions, pitch (bytes per scanline), and + bits per pixel. Call this before SYS_FBMAP to learn the + framebuffer geometry. +

+ +

SYS_FBMAP (22) — Map framebuffer into process memory

+
void* zenith::fb_map();
+

+ Maps the physical framebuffer into the calling process's address + space at 0x50000000 and returns the user virtual address. + The mapped region covers height × pitch bytes. Each + pixel is a 32-bit value in 0xAARRGGBB format (blue in + the low byte). Writing to this memory directly updates the screen. +

+
+ Note: After mapping, the cursor overlay is not composited + automatically. Programs that use the framebuffer take full control of + screen output for the mapped region. +
+ + +

Terminal Terminal

+ +

SYS_TERMSIZE (24) — Get terminal dimensions

+
void zenith::termsize(int* cols, int* rows);
+

+ Returns the current terminal dimensions (character grid) via the + two output pointers. Columns are packed in the low 32 bits and + rows in the high 32 bits of the raw return value; the typed + wrapper unpacks them for you. Either pointer may be + nullptr if you only need one dimension. +

+ + +

Data Structures

+ +

Zenith::SysInfo

+

programs/include/Api/Syscall.hpp

+
struct SysInfo {
+    char     osName[32];       // e.g. "ZenithOS"
+    char     osVersion[32];    // e.g. "0.1.0"
+    uint32_t apiVersion;       // Current: 2
+    uint32_t maxProcesses;     // Current: 16
+};
+ +

Zenith::FbInfo

+
struct FbInfo {
+    uint64_t width;       // Framebuffer width in pixels
+    uint64_t height;      // Framebuffer height in pixels
+    uint64_t pitch;       // Bytes per scanline
+    uint64_t bpp;         // Bits per pixel (always 32)
+    uint64_t userAddr;    // Reserved (0 until mapped via SYS_FBMAP)
+};
+ +

Zenith::KeyEvent

+
struct KeyEvent {
+    uint8_t scancode;   // Raw PS/2 scancode
+    char    ascii;      // Translated ASCII character (0 if non-printable)
+    bool    pressed;    // true = key down, false = key up
+    bool    shift;      // Shift modifier active
+    bool    ctrl;       // Ctrl modifier active
+    bool    alt;        // Alt modifier active
+};
+ +

Zenith::SyscallFrame (kernel only)

+
struct SyscallFrame {
+    uint64_t r15, r14, r13, r12, rbp, rbx;   // callee-saved
+    uint64_t arg6, arg5, arg4, arg3, arg2, arg1;
+    uint64_t syscall_nr;
+    uint64_t user_rflags, user_rip, user_rsp;
+};
+

+ This is the stack frame pushed by SyscallEntry.asm and + passed to SyscallDispatch. Userspace code never sees this + directly. +

+ + +

Shell Application Walkthrough

+

programs/src/shell/main.cpp

+

+ The built-in shell is the best example of a real ZenithOS application. + It demonstrates most of the available syscalls. +

+ +

Initialization

+
extern "C" void _start() {
+    zenith::print("\n  ZenithOS Shell v0.1\n");
+    zenith::print("  Type 'help' for available commands.\n\n");
+
+    char line[256];
+    int pos = 0;
+    prompt();
+
+    while (true) {
+        char c = zenith::getchar();       // blocking read
+        // ... handle input, echo, backspace ...
+    }
+}
+

+ The shell uses zenith::getchar() in a loop for blocking + character-by-character input, manually handling echo and backspace. +

+ +

Shell Commands & Syscalls Used

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CommandDescriptionSyscalls Used
helpPrint available commandsSYS_PRINT
infoShow OS name, version, API versionSYS_GETINFO, SYS_PRINT, SYS_PUTCHAR
man <topic>Fullscreen manual page viewerSYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_ALLOC, SYS_TERMSIZE, SYS_GETKEY, SYS_PRINT, SYS_PUTCHAR
lsList ramdisk filesSYS_READDIR, SYS_PRINT
cat <file>Display file contents in 512-byte chunksSYS_OPEN, SYS_GETSIZE, SYS_READ, SYS_CLOSE, SYS_PRINT
run <file>Spawn a new process and wait for it to exitSYS_SPAWN, SYS_WAITPID, SYS_PRINT
ping <ip>Send 4 ICMP echo requestsSYS_PING, SYS_SLEEP_MS, SYS_PRINT, SYS_PUTCHAR
uptimeShow uptime in minutes, seconds, msSYS_GETMILLISECONDS, SYS_PRINT, SYS_PUTCHAR
clearScroll past visible contentSYS_PUTCHAR
exitTerminate the shellSYS_PRINT, SYS_EXIT
+ +

Pattern: Reading a File

+

The shell's cat implementation shows the standard file-reading pattern:

+
// 1. Build VFS path
+char path[128];
+// ... copy "0:/" + filename into path ...
+
+// 2. Open
+int handle = zenith::open(path);
+if (handle < 0) { /* error */ return; }
+
+// 3. Get size
+uint64_t size = zenith::getsize(handle);
+
+// 4. Read in chunks
+uint8_t buf[512];
+uint64_t offset = 0;
+while (offset < size) {
+    uint64_t chunk = size - offset;
+    if (chunk > sizeof(buf) - 1) chunk = sizeof(buf) - 1;
+    int bytesRead = zenith::read(handle, buf, offset, chunk);
+    if (bytesRead <= 0) break;
+    buf[bytesRead] = '\0';
+    zenith::print((const char*)buf);
+    offset += bytesRead;
+}
+
+// 5. Close
+zenith::close(handle);
+ +

Pattern: IP Networking

+
// Parse "10.0.2.2" into uint32_t in little-endian order
+uint32_t ip;
+parse_ip("10.0.2.2", &ip);  // ip = 0x0202000A
+
+// Send 4 pings with 1-second intervals
+for (int i = 0; i < 4; i++) {
+    int32_t rtt = zenith::ping(ip, 3000);
+    if (rtt < 0) { /* timeout */ }
+    else { /* reply in rtt ms */ }
+    if (i < 3) zenith::sleep_ms(1000);
+}
+ + +

Adding New Syscalls

+

+ Adding a syscall requires changes in 3 files + (kernel-side) and 2 files (userspace-side). Follow + these steps: +

+ +

Step 1: Assign a Syscall Number

+

+ Add a new constant in both copies of the syscall + number definitions. The numbers must match exactly. +

+

kernel/src/Api/Syscall.hpp

+

programs/include/Api/Syscall.hpp

+
static constexpr uint64_t SYS_MYFUNC = 26;  // next available number
+
+ Keep both files in sync. The kernel and userspace + headers define syscall numbers independently. If they disagree, the + wrong handler runs. +
+ +

Step 2: Implement the Kernel Handler

+

kernel/src/Api/Syscall.cpp

+

Add a static function implementing your syscall's logic:

+
static int64_t Sys_MyFunc(uint64_t arg1, const char* arg2) {
+    // Your kernel-side implementation here.
+    // You have full access to kernel subsystems.
+    return 0;
+}
+ +

Step 3: Add the Dispatch Case

+

kernel/src/Api/Syscall.cpp — SyscallDispatch()

+
case SYS_MYFUNC:
+    return (int64_t)Sys_MyFunc(frame->arg1, (const char*)frame->arg2);
+

+ Arguments are accessed through frame->arg1 through + frame->arg6, corresponding to RDI, RSI, RDX, R10, R8, + R9 respectively. +

+ +

Step 4: Add a Typed Userspace Wrapper

+

programs/include/zenith/syscall.h

+
inline int64_t my_func(uint64_t arg1, const char* arg2) {
+    return syscall2(Zenith::SYS_MYFUNC, arg1, (uint64_t)arg2);
+}
+

+ Choose the appropriate syscallN variant based on the + number of arguments. Cast pointer types to uint64_t. +

+ +

Step 5: Update the Log Message (optional)

+

kernel/src/Api/Syscall.cpp — InitializeSyscalls()

+

+ Update the boot log to reflect the new syscall count: +

+
// Change "26 syscalls" to "27 syscalls"
+Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR="
+    << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 27 syscalls)";
+ +

Complete Checklist

+ + + + + + + +
#FileChange
1kernel/src/Api/Syscall.hppAdd SYS_MYFUNC constant (+ any new structs)
2kernel/src/Api/Syscall.cppAdd Sys_MyFunc() implementation + dispatch case
3programs/include/Api/Syscall.hppAdd matching SYS_MYFUNC constant (+ any new structs)
4programs/include/zenith/syscall.hAdd typed wrapper in zenith:: namespace
5(optional)Update syscall count in boot log
+ + +

Process Memory Model

+

+ Each process gets its own PML4 page table. The kernel half + (upper 256 entries) is shared; the lower half is per-process. +

+ + + + + + + +
RegionVirtual AddressSizePurpose
Exit stub0x3FF0004 KiBAuto-exit trampoline (calls SYS_EXIT(0) if _start returns)
Program code0x400000+VariesELF .text, .rodata, .data, .bss
User heap0x40000000+Grows upPage pool (SYS_ALLOC); managed by userspace free-list heap
Framebuffer0x50000000+height × pitchMapped by SYS_FBMAP; direct pixel access (32-bit ARGB)
User stack0x7FFFFEF0000x7FFFFFF00016 KiB (4 pages)Grows down from 0x7FFFFFF000
+

+ The ELF loader maps PT_LOAD segments with user-accessible page flags. + BSS is zero-initialized automatically (pages are allocated zeroed). +

+ + +

Current Limitations

+ + +
+

+ ZenithOS Documentation — Copyright © 2025-2026 Daniel Hammer +

+ +
+ + diff --git a/doomgeneric b/doomgeneric new file mode 160000 index 0000000..fc60163 --- /dev/null +++ b/doomgeneric @@ -0,0 +1 @@ +Subproject commit fc601639494e089702a1ada082eb51aaafc03722 diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index a6522b6..e72d142 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include "../Libraries/flanterm/src/flanterm.h" // Assembly entry point extern "C" void SyscallEntry(); @@ -175,6 +177,47 @@ namespace Zenith { static uint16_t g_pingSeq = 0; static constexpr uint16_t PING_ID = 0x2E01; // "ZE" + static void Sys_FbInfo(FbInfo* out) { + if (out == nullptr) return; + out->width = Graphics::Cursor::GetFramebufferWidth(); + out->height = Graphics::Cursor::GetFramebufferHeight(); + out->pitch = Graphics::Cursor::GetFramebufferPitch(); + out->bpp = 32; + out->userAddr = 0; + } + + static void Sys_WaitPid(int pid) { + while (Sched::IsAlive(pid)) { + Sched::Schedule(); // yield until the process exits + } + } + + static uint64_t Sys_FbMap() { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr) return 0; + + uint32_t* fbBase = Graphics::Cursor::GetFramebufferBase(); + if (fbBase == nullptr) return 0; + + uint64_t fbPhys = Memory::SubHHDM((uint64_t)fbBase); + uint64_t fbSize = Graphics::Cursor::GetFramebufferHeight() + * Graphics::Cursor::GetFramebufferPitch(); + uint64_t numPages = (fbSize + 0xFFF) / 0x1000; + + // Map at a fixed user VA + constexpr uint64_t userVa = 0x50000000ULL; + + for (uint64_t i = 0; i < numPages; i++) { + Memory::VMM::Paging::MapUserIn( + proc->pml4Phys, + fbPhys + i * 0x1000, + userVa + i * 0x1000 + ); + } + + return userVa; + } + static int32_t Sys_Ping(uint32_t ipAddr, uint32_t timeoutMs) { uint16_t seq = g_pingSeq++; @@ -192,6 +235,27 @@ namespace Zenith { return (int32_t)(Timekeeping::GetMilliseconds() - start); } + static int Sys_Spawn(const char* path, const char* args) { + return Sched::Spawn(path, args); + } + + static int Sys_GetArgs(char* buf, uint64_t maxLen) { + auto* proc = Sched::GetCurrentProcessPtr(); + if (proc == nullptr || buf == nullptr || maxLen == 0) return -1; + int i = 0; + for (; i < (int)maxLen - 1 && proc->args[i]; i++) { + buf[i] = proc->args[i]; + } + buf[i] = '\0'; + return i; + } + + static uint64_t Sys_TermSize() { + size_t cols = 0, rows = 0; + flanterm_get_dimensions(Kt::ctx, &cols, &rows); + return (rows << 32) | (cols & 0xFFFFFFFF); + } + // ---- Dispatch ---- extern "C" int64_t SyscallDispatch(SyscallFrame* frame) { @@ -248,6 +312,20 @@ namespace Zenith { return (int64_t)Sys_GetChar(); case SYS_PING: return (int64_t)Sys_Ping((uint32_t)frame->arg1, (uint32_t)frame->arg2); + case SYS_SPAWN: + return (int64_t)Sys_Spawn((const char*)frame->arg1, (const char*)frame->arg2); + case SYS_WAITPID: + Sys_WaitPid((int)frame->arg1); + return 0; + case SYS_FBINFO: + Sys_FbInfo((FbInfo*)frame->arg1); + return 0; + case SYS_FBMAP: + return (int64_t)Sys_FbMap(); + case SYS_TERMSIZE: + return (int64_t)Sys_TermSize(); + case SYS_GETARGS: + return (int64_t)Sys_GetArgs((char*)frame->arg1, frame->arg2); default: return -1; } @@ -274,7 +352,7 @@ namespace Zenith { Hal::WriteMSR(Hal::IA32_FMASK, 0x200); Kt::KernelLogStream(Kt::OK, "Syscall") << "SYSCALL/SYSRET initialized (LSTAR=" - << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 20 syscalls)"; + << kcp::hex << (uint64_t)SyscallEntry << kcp::dec << ", 26 syscalls)"; } } diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index 59c4c96..43447a3 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -31,6 +31,20 @@ namespace Zenith { static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETCHAR = 18; static constexpr uint64_t SYS_PING = 19; + static constexpr uint64_t SYS_SPAWN = 20; + static constexpr uint64_t SYS_FBINFO = 21; + static constexpr uint64_t SYS_FBMAP = 22; + static constexpr uint64_t SYS_WAITPID = 23; + static constexpr uint64_t SYS_TERMSIZE = 24; + static constexpr uint64_t SYS_GETARGS = 25; + + struct FbInfo { + uint64_t width; + uint64_t height; + uint64_t pitch; // bytes per scanline + uint64_t bpp; // bits per pixel (32) + uint64_t userAddr; // filled by SYS_FBMAP (0 until mapped) + }; struct SysInfo { char osName[32]; diff --git a/kernel/src/Drivers/PS2/Keyboard.cpp b/kernel/src/Drivers/PS2/Keyboard.cpp index e597d3a..4a4b061 100644 --- a/kernel/src/Drivers/PS2/Keyboard.cpp +++ b/kernel/src/Drivers/PS2/Keyboard.cpp @@ -176,39 +176,66 @@ namespace Drivers::PS2::Keyboard { return; } - // Handle modifier keys + // Handle modifier keys (update state, but still push event to buffer) + bool isModifier = false; switch (keycode) { case ScLeftShift: g_Modifiers.LeftShift = !released; - return; + isModifier = true; + break; case ScRightShift: g_Modifiers.RightShift = !released; - return; + isModifier = true; + break; case ScLeftCtrl: g_Modifiers.LeftCtrl = !released; - return; + isModifier = true; + break; case ScLeftAlt: g_Modifiers.LeftAlt = !released; - return; + isModifier = true; + break; case ScCapsLock: if (!released) { g_Modifiers.CapsLock = !g_Modifiers.CapsLock; } - return; + isModifier = true; + break; case ScNumLock: if (!released) { g_Modifiers.NumLock = !g_Modifiers.NumLock; } - return; + isModifier = true; + break; case ScScrollLock: if (!released) { g_Modifiers.ScrollLock = !g_Modifiers.ScrollLock; } - return; + isModifier = true; + break; default: break; } + // Modifiers still need events in the buffer (for apps like doom) + // but lock keys (caps/num/scroll) don't need buffer events + if (isModifier && keycode != ScCapsLock && keycode != ScNumLock && keycode != ScScrollLock) { + KeyEvent event = { + .Scancode = scancode, + .Ascii = 0, + .Pressed = !released, + .Shift = g_Modifiers.LeftShift || g_Modifiers.RightShift, + .Ctrl = g_Modifiers.LeftCtrl || g_Modifiers.RightCtrl, + .Alt = g_Modifiers.LeftAlt || g_Modifiers.RightAlt, + .CapsLock = g_Modifiers.CapsLock + }; + g_BufferLock.Acquire(); + BufferPush(event); + g_BufferLock.Release(); + return; + } + if (isModifier) return; + // Translate scancode to ASCII char ascii = 0; if (keycode < 128) { diff --git a/kernel/src/Graphics/Cursor.cpp b/kernel/src/Graphics/Cursor.cpp index fd560b7..296fd40 100644 --- a/kernel/src/Graphics/Cursor.cpp +++ b/kernel/src/Graphics/Cursor.cpp @@ -162,4 +162,9 @@ namespace Graphics::Cursor { g_OldY = newY; } + uint32_t* GetFramebufferBase() { return g_FbBase; } + uint64_t GetFramebufferWidth() { return g_FbWidth; } + uint64_t GetFramebufferHeight() { return g_FbHeight; } + uint64_t GetFramebufferPitch() { return g_FbPitch; } + }; diff --git a/kernel/src/Graphics/Cursor.hpp b/kernel/src/Graphics/Cursor.hpp index 86d26f1..aa2b91c 100644 --- a/kernel/src/Graphics/Cursor.hpp +++ b/kernel/src/Graphics/Cursor.hpp @@ -13,4 +13,9 @@ namespace Graphics::Cursor { void Initialize(limine_framebuffer* framebuffer); void Update(); + uint32_t* GetFramebufferBase(); + uint64_t GetFramebufferWidth(); + uint64_t GetFramebufferHeight(); + uint64_t GetFramebufferPitch(); + }; diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index e56ac4f..7fcd286 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -93,6 +93,23 @@ extern "C" void kmain() { #if defined (__x86_64__) Hal::PrepareGDT(); Hal::BridgeLoadGDT(); + + // Enable SSE/SSE2 — required for userspace programs compiled with SSE + // CR0: clear EM (bit 2), set MP (bit 1) + { + uint64_t cr0; + asm volatile("mov %%cr0, %0" : "=r"(cr0)); + cr0 &= ~(1ULL << 2); // Clear EM + cr0 |= (1ULL << 1); // Set MP + asm volatile("mov %0, %%cr0" :: "r"(cr0)); + + // CR4: set OSFXSR (bit 9) and OSXMMEXCPT (bit 10) + uint64_t cr4; + asm volatile("mov %%cr4, %0" : "=r"(cr4)); + cr4 |= (1ULL << 9); // OSFXSR + cr4 |= (1ULL << 10); // OSXMMEXCPT + asm volatile("mov %0, %%cr4" :: "r"(cr4)); + } #endif uint64_t hhdm_offset = hhdm_request.response->offset; diff --git a/kernel/src/Memory/PageFrameAllocator.cpp b/kernel/src/Memory/PageFrameAllocator.cpp index 27d41df..316833b 100644 --- a/kernel/src/Memory/PageFrameAllocator.cpp +++ b/kernel/src/Memory/PageFrameAllocator.cpp @@ -77,12 +77,17 @@ namespace Memory { }; } + // Allocate() returns pages from the top of a free region in descending + // order, so 'first' is the highest address. The contiguous block + // actually starts (n-1) pages below 'first'. + void* base = (void*)((uint64_t)first - (uint64_t)(n - 1) * 0x1000); + if (ptr != nullptr) { - memcpy(first, ptr, n); + memcpy(base, ptr, (uint64_t)n * 0x1000); Free(ptr); } - return first; + return base; } void PageFrameAllocator::Free(void* ptr) { diff --git a/kernel/src/Sched/ElfLoader.cpp b/kernel/src/Sched/ElfLoader.cpp index 97521fb..d2e4d84 100644 --- a/kernel/src/Sched/ElfLoader.cpp +++ b/kernel/src/Sched/ElfLoader.cpp @@ -52,8 +52,6 @@ namespace Sched { } uint64_t ElfLoad(const char* vfsPath, uint64_t pml4Phys) { - Kt::KernelLogStream(Kt::INFO, "ELF") << "Loading " << vfsPath; - int handle = Fs::Vfs::VfsOpen(vfsPath); if (handle < 0) { Kt::KernelLogStream(Kt::ERROR, "ELF") << "Failed to open " << vfsPath; @@ -78,6 +76,10 @@ namespace Sched { Fs::Vfs::VfsRead(handle, fileData, 0, fileSize); Fs::Vfs::VfsClose(handle); + // Prevent the optimizer from reordering the VfsRead store past the + // header validation reads that follow. + asm volatile("" ::: "memory"); + // Validate ELF header Elf64Header* hdr = (Elf64Header*)fileData; if (!ValidateElfHeader(hdr)) { @@ -85,9 +87,6 @@ namespace Sched { return 0; } - Kt::KernelLogStream(Kt::OK, "ELF") << "Entry point: " << kcp::hex << hdr->e_entry << kcp::dec - << ", " << (uint64_t)hdr->e_phnum << " program header(s)"; - // Process program headers for (uint16_t i = 0; i < hdr->e_phnum; i++) { Elf64ProgramHeader* phdr = (Elf64ProgramHeader*)(fileData + hdr->e_phoff + i * hdr->e_phentsize); @@ -100,9 +99,6 @@ namespace Sched { continue; } - Kt::KernelLogStream(Kt::INFO, "ELF") << "PT_LOAD: vaddr=" << kcp::hex << phdr->p_vaddr - << " filesz=" << phdr->p_filesz << " memsz=" << phdr->p_memsz << kcp::dec; - // Allocate pages and map them in the process PML4 with User bit uint64_t segBase = phdr->p_vaddr & ~0xFFFULL; uint64_t segEnd = (phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFULL; @@ -147,7 +143,6 @@ namespace Sched { uint64_t entryPoint = hdr->e_entry; Memory::g_heap->Free(fileData); - Kt::KernelLogStream(Kt::OK, "ELF") << "Loaded successfully, entry=" << kcp::hex << entryPoint << kcp::dec; return entryPoint; } diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index 7aa0e93..6aeb8f9 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -75,6 +75,7 @@ namespace Sched { processTable[i].kernelStackTop = 0; processTable[i].userStackTop = 0; processTable[i].heapNext = 0; + processTable[i].args[0] = '\0'; } currentPid = -1; @@ -85,7 +86,7 @@ namespace Sched { << " process slots, " << (uint64_t)TimeSliceMs << " ms time slice)"; } - void Spawn(const char* vfsPath) { + int Spawn(const char* vfsPath, const char* args) { int slot = -1; for (int i = 0; i < MaxProcesses; i++) { if (processTable[i].state == ProcessState::Free) { @@ -96,7 +97,7 @@ namespace Sched { if (slot < 0) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "No free process slots"; - return; + return -1; } // Create per-process PML4 with kernel-half copied @@ -106,20 +107,20 @@ namespace Sched { uint64_t entry = ElfLoad(vfsPath, pml4Phys); if (entry == 0) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to load ELF: " << vfsPath; - return; + return -1; } // Allocate kernel stack (used during syscalls and interrupts) void* firstPage = Memory::g_pfa->AllocateZeroed(); if (firstPage == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for kernel stack"; - return; + return -1; } void* stackMem = Memory::g_pfa->ReallocConsecutive(firstPage, StackPages); if (stackMem == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Failed to allocate contiguous kernel stack"; Memory::g_pfa->Free(firstPage); - return; + return -1; } uint8_t* kernelStackBase = (uint8_t*)stackMem; @@ -132,7 +133,7 @@ namespace Sched { void* page = Memory::g_pfa->AllocateZeroed(); if (page == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for user stack"; - return; + return -1; } uint64_t physAddr = Memory::SubHHDM((uint64_t)page); Memory::VMM::Paging::MapUserIn(pml4Phys, physAddr, userStackBase + i * 0x1000); @@ -145,7 +146,7 @@ namespace Sched { void* stubPage = Memory::g_pfa->AllocateZeroed(); if (stubPage == nullptr) { Kt::KernelLogStream(Kt::ERROR, "Sched") << "Out of memory for exit stub"; - return; + return -1; } uint64_t stubPhys = Memory::SubHHDM((uint64_t)stubPage); Memory::VMM::Paging::MapUserIn(pml4Phys, stubPhys, ExitStubAddr); @@ -189,11 +190,17 @@ namespace Sched { proc.userStackTop = UserStackTop - 8; // account for pushed exit stub return address proc.heapNext = UserHeapBase; - Kt::KernelLogStream(Kt::OK, "Sched") << "Spawned process " << (uint64_t)proc.pid - << " (" << vfsPath << ") entry=" << kcp::hex << entry - << " kstack=" << (uint64_t)kernelStackBase << "-" << kernelStackTop - << " ustack=" << userStackBase << "-" << UserStackTop - << " pml4=" << pml4Phys << kcp::dec; + // Copy arguments string into process + proc.args[0] = '\0'; + if (args != nullptr) { + int i = 0; + for (; i < 255 && args[i]; i++) { + proc.args[i] = args[i]; + } + proc.args[i] = '\0'; + } + + return proc.pid; } void Schedule() { @@ -271,8 +278,6 @@ namespace Sched { return; } - Kt::KernelLogStream(Kt::OK, "Sched") << "Process " << (uint64_t)processTable[currentPid].pid << " terminated"; - processTable[currentPid].state = ProcessState::Terminated; int next = -1; @@ -305,4 +310,10 @@ namespace Sched { } } + bool IsAlive(int pid) { + if (pid < 0 || pid >= MaxProcesses) return false; + return processTable[pid].state == ProcessState::Ready + || processTable[pid].state == ProcessState::Running; + } + } diff --git a/kernel/src/Sched/Scheduler.hpp b/kernel/src/Sched/Scheduler.hpp index 05cabbd..d617953 100644 --- a/kernel/src/Sched/Scheduler.hpp +++ b/kernel/src/Sched/Scheduler.hpp @@ -38,10 +38,11 @@ namespace Sched { uint64_t kernelStackTop; // Top of kernel stack (for TSS RSP0 / SYSCALL) uint64_t userStackTop; // User-space stack top uint64_t heapNext; // Simple bump allocator for user heap + char args[256]; // Command-line arguments (set by parent via Spawn) }; void Initialize(); - void Spawn(const char* vfsPath); + int Spawn(const char* vfsPath, const char* args = nullptr); void Schedule(); // Called from the APIC timer handler on every tick. @@ -56,4 +57,7 @@ namespace Sched { // Called by terminated processes to mark themselves done void ExitProcess(); + // Check if a process is still alive (Ready or Running) + bool IsAlive(int pid); + } diff --git a/kernel/src/Terminal/Terminal.hpp b/kernel/src/Terminal/Terminal.hpp index 83e1a21..829b444 100644 --- a/kernel/src/Terminal/Terminal.hpp +++ b/kernel/src/Terminal/Terminal.hpp @@ -165,4 +165,10 @@ namespace Kt }; }; -extern Kt::KernelErrorStream kerr; \ No newline at end of file +extern Kt::KernelErrorStream kerr; + +// Forward-declare flanterm context for syscall access +struct flanterm_context; +namespace Kt { + extern flanterm_context *ctx; +} \ No newline at end of file diff --git a/programs/GNUmakefile b/programs/GNUmakefile index f9edb1c..93a810a 100644 --- a/programs/GNUmakefile +++ b/programs/GNUmakefile @@ -59,9 +59,19 @@ PROGRAMS := $(notdir $(wildcard src/*)) # Build targets: one ELF per program. TARGETS := $(addprefix $(BINDIR)/,$(addsuffix .elf,$(PROGRAMS))) +# Man pages source directory. +MANDIR := man +MANSRC := $(wildcard $(MANDIR)/*.*) +MANDST := $(patsubst $(MANDIR)/%,$(BINDIR)/man/%,$(MANSRC)) + .PHONY: all clean -all: $(TARGETS) +all: $(TARGETS) $(MANDST) + +# Copy man pages into bin/man/ so mkramdisk.sh picks them up. +$(BINDIR)/man/%: $(MANDIR)/% + mkdir -p $(BINDIR)/man + cp $< $@ # Build each program from its source files. # For now each program is a single .cpp file compiled and linked directly. diff --git a/programs/bin/hello.elf b/programs/bin/hello.elf index 844af02ec53ec942b17284bcbdaae6c184f86c96..2db5c72a385fc10ebf840c33cba75169d39ed9ad 100755 GIT binary patch delta 1612 zcmZuxU1%It6uxJEc4qf(c4vRGlif7gY?Ew$Hz9VLwMk8~p^ZssW9TNN1tpEB7{rvu z5FdhF>WdY9NO~Vc6iY!zjxmS9xr?N6jr5l|8+l>rDdEeL+yRSPkI0WP3V#YlQ~m`f?E;3*vr0SQ zVTMGxGM8ek5dlUR>hum^l%YW@fH8(9ZDL-Wp^r*{)eJ5A8-@f!n;fVvLqBDp)-Vjv zHeiyWq#s~NF$~fzsI}*XdLt7q4+Gb+9Hu?QdWH_g@n+HtBlI=qH870QZ-5zwF`B@% zgAC(z1F(@{HT7az6T<{O4sSCQ1Iq*1mvQyTRcmho3pz zLBMNYMs@2&1f8^BAjNJ1lRqNF5DH*RUWD{EFB2F?xR=s`fE0`xC5Rzl=7F>KIgB(E zt-Nx`n1`ALX~JzRg7gC9amOD*s9y0V;o&~Ztp$#OChnpUZ6$I6N-Ma@qHs^MdLW(# z83Z@^i}M1uZ(l%CMv>wsl9Fq!G#=yjgyb5Ra0eF_MV4m;=X%5;2C2oTmxNXq;d=O} z&96{JC#R1G&G`&fbi=vEgKfRSqGP_Rb{dOiQ;gv-`4U18^=0&ac|Ld9u-O zvV4i;!xH;DLhwphGm_v9)KO3D6n!!*r`^-?jGTc-cG07;hK$bnpovBMGFpG;K&5z9 zgqlM!si$kx)6;H*92bwiNk};$b)55XjHJX`SOUu~gt&&|_2&s*ANc6dg5qjGg#^gv5=(O`1FZxlV6QrCI;qO-EuL#+_+)W%>;3lUh*DdTP8Wk} z%MFsN=5qzV+rV2D;#0K&_I+c8y{vA7m6|K;glg+5XjhumlU)P;@Akz>wb51Zr}+Tk rx~^`6{kYGDR9km}>eXELAdRZ^?m=y}L;ch}5LoZ1*xwZDYOel29IV!F delta 1800 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 CW%~C3 literal 38744 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 F{|{q!r4;}G diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index a66e7a9..7230c21 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -31,6 +31,20 @@ namespace Zenith { static constexpr uint64_t SYS_GETKEY = 17; static constexpr uint64_t SYS_GETCHAR = 18; static constexpr uint64_t SYS_PING = 19; + static constexpr uint64_t SYS_SPAWN = 20; + static constexpr uint64_t SYS_FBINFO = 21; + static constexpr uint64_t SYS_FBMAP = 22; + static constexpr uint64_t SYS_WAITPID = 23; + static constexpr uint64_t SYS_TERMSIZE = 24; + static constexpr uint64_t SYS_GETARGS = 25; + + struct FbInfo { + uint64_t width; + uint64_t height; + uint64_t pitch; // bytes per scanline + uint64_t bpp; // bits per pixel (32) + uint64_t userAddr; // filled by SYS_FBMAP (0 until mapped) + }; struct SysInfo { char osName[32]; diff --git a/programs/include/libc/assert.h b/programs/include/libc/assert.h new file mode 100644 index 0000000..b1f703e --- /dev/null +++ b/programs/include/libc/assert.h @@ -0,0 +1,23 @@ +#ifndef _LIBC_ASSERT_H +#define _LIBC_ASSERT_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +void __assert_fail(const char *expr, const char *file, int line, const char *func); + +#ifdef NDEBUG +#define assert(expr) ((void)0) +#else +#define assert(expr) \ + ((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__, __func__)) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_ASSERT_H */ diff --git a/programs/include/libc/ctype.h b/programs/include/libc/ctype.h new file mode 100644 index 0000000..efca6ed --- /dev/null +++ b/programs/include/libc/ctype.h @@ -0,0 +1,28 @@ +#ifndef _LIBC_CTYPE_H +#define _LIBC_CTYPE_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +int isalpha(int c); +int isdigit(int c); +int isalnum(int c); +int isspace(int c); +int isupper(int c); +int islower(int c); +int isprint(int c); +int ispunct(int c); +int isxdigit(int c); +int iscntrl(int c); +int isgraph(int c); +int toupper(int c); +int tolower(int c); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_CTYPE_H */ diff --git a/programs/include/libc/errno.h b/programs/include/libc/errno.h new file mode 100644 index 0000000..cdbda4d --- /dev/null +++ b/programs/include/libc/errno.h @@ -0,0 +1,29 @@ +#ifndef _LIBC_ERRNO_H +#define _LIBC_ERRNO_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +extern int errno; + +#define ENOENT 2 +#define EIO 5 +#define ENOMEM 12 +#define EACCES 13 +#define EINVAL 22 +#define ERANGE 34 +#define ENOSYS 38 +#define EISDIR 21 +#define ENOTDIR 20 +#define EEXIST 17 +#define EBADF 9 +#define EPERM 1 + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_ERRNO_H */ diff --git a/programs/include/libc/fcntl.h b/programs/include/libc/fcntl.h new file mode 100644 index 0000000..46890eb --- /dev/null +++ b/programs/include/libc/fcntl.h @@ -0,0 +1,22 @@ +#ifndef _LIBC_FCNTL_H +#define _LIBC_FCNTL_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define O_RDONLY 0 +#define O_WRONLY 1 +#define O_RDWR 2 +#define O_CREAT 0x40 +#define O_TRUNC 0x200 + +int open(const char *path, int flags, ...); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_FCNTL_H */ diff --git a/programs/include/libc/inttypes.h b/programs/include/libc/inttypes.h new file mode 100644 index 0000000..398a5fc --- /dev/null +++ b/programs/include/libc/inttypes.h @@ -0,0 +1,27 @@ +#ifndef _LIBC_INTTYPES_H +#define _LIBC_INTTYPES_H + +#include + +#define PRId8 "d" +#define PRId16 "d" +#define PRId32 "d" +#define PRId64 "ld" +#define PRIi8 "i" +#define PRIi16 "i" +#define PRIi32 "i" +#define PRIi64 "li" +#define PRIu8 "u" +#define PRIu16 "u" +#define PRIu32 "u" +#define PRIu64 "lu" +#define PRIx8 "x" +#define PRIx16 "x" +#define PRIx32 "x" +#define PRIx64 "lx" +#define PRIX8 "X" +#define PRIX16 "X" +#define PRIX32 "X" +#define PRIX64 "lX" + +#endif /* _LIBC_INTTYPES_H */ diff --git a/programs/include/libc/limits.h b/programs/include/libc/limits.h new file mode 100644 index 0000000..c9e9c6b --- /dev/null +++ b/programs/include/libc/limits.h @@ -0,0 +1,31 @@ +#ifndef _LIBC_LIMITS_H +#define _LIBC_LIMITS_H + +#pragma once + +#define CHAR_BIT 8 + +#define SCHAR_MIN (-128) +#define SCHAR_MAX 127 +#define UCHAR_MAX 255 + +#define SHRT_MIN (-32768) +#define SHRT_MAX 32767 +#define USHRT_MAX 65535 + +#define INT_MIN (-2147483647 - 1) +#define INT_MAX 2147483647 +#define UINT_MAX 4294967295U + +#define LONG_MIN (-9223372036854775807L - 1) +#define LONG_MAX 9223372036854775807L +#define ULONG_MAX 18446744073709551615UL + +#define LLONG_MIN (-9223372036854775807LL - 1) +#define LLONG_MAX 9223372036854775807LL +#define ULLONG_MAX 18446744073709551615ULL + +#define PATH_MAX 4096 +#define NAME_MAX 256 + +#endif /* _LIBC_LIMITS_H */ diff --git a/programs/include/libc/math.h b/programs/include/libc/math.h new file mode 100644 index 0000000..b4522b2 --- /dev/null +++ b/programs/include/libc/math.h @@ -0,0 +1,31 @@ +#ifndef _LIBC_MATH_H +#define _LIBC_MATH_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#define HUGE_VAL __builtin_huge_val() +#define INFINITY __builtin_inff() +#define NAN __builtin_nanf("") + +double fabs(double x); +double floor(double x); +double ceil(double x); +double sqrt(double x); +double sin(double x); +double cos(double x); +double atan2(double y, double x); +double pow(double base, double exp); +double log(double x); +double exp(double x); +double fmod(double x, double y); +double round(double x); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_MATH_H */ diff --git a/programs/include/libc/stdio.h b/programs/include/libc/stdio.h new file mode 100644 index 0000000..93bb94b --- /dev/null +++ b/programs/include/libc/stdio.h @@ -0,0 +1,77 @@ +#ifndef _LIBC_STDIO_H +#define _LIBC_STDIO_H + +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define EOF (-1) +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 +#define BUFSIZ 1024 +#define FILENAME_MAX 256 + +typedef struct _FILE { + int handle; + unsigned long pos; + unsigned long size; + int eof; + int error; + int is_std; + int ungetc_buf; +} FILE; + +extern FILE *stdin; +extern FILE *stdout; +extern FILE *stderr; + +int printf(const char *fmt, ...); +int fprintf(FILE *stream, const char *fmt, ...); +int sprintf(char *str, const char *fmt, ...); +int snprintf(char *str, size_t size, const char *fmt, ...); +int vprintf(const char *fmt, va_list ap); +int vfprintf(FILE *stream, const char *fmt, va_list ap); +int vsprintf(char *str, const char *fmt, va_list ap); +int vsnprintf(char *str, size_t size, const char *fmt, va_list ap); + +int puts(const char *s); +int putchar(int c); + +FILE *fopen(const char *path, const char *mode); +int fclose(FILE *stream); +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); +int fseek(FILE *stream, long offset, int whence); +long ftell(FILE *stream); +int fflush(FILE *stream); + +int rename(const char *oldpath, const char *newpath); +int remove(const char *path); + +int sscanf(const char *str, const char *fmt, ...); + +int feof(FILE *stream); +int ferror(FILE *stream); +void clearerr(FILE *stream); + +int fgetc(FILE *stream); +int getc(FILE *stream); +int ungetc(int c, FILE *stream); +char *fgets(char *s, int size, FILE *stream); +int fputs(const char *s, FILE *stream); + +void perror(const char *s); +FILE *tmpfile(void); +char *tmpnam(char *s); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_STDIO_H */ diff --git a/programs/include/libc/stdlib.h b/programs/include/libc/stdlib.h new file mode 100644 index 0000000..0ffaf2a --- /dev/null +++ b/programs/include/libc/stdlib.h @@ -0,0 +1,66 @@ +#ifndef _LIBC_STDLIB_H +#define _LIBC_STDLIB_H + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef NULL +#define NULL ((void *)0) +#endif + +#define EXIT_SUCCESS 0 +#define EXIT_FAILURE 1 +#define RAND_MAX 0x7fffffff + +typedef struct { + int quot; + int rem; +} div_t; + +typedef struct { + long quot; + long rem; +} ldiv_t; + +void *malloc(size_t size); +void free(void *ptr); +void *calloc(size_t nmemb, size_t size); +void *realloc(void *ptr, size_t size); + +int atoi(const char *s); +long atol(const char *s); +double atof(const char *s); + +int abs(int j); +long labs(long j); + +void exit(int status); +void abort(void); +int atexit(void (*func)(void)); + +char *getenv(const char *name); + +void qsort(void *base, size_t nmemb, size_t size, + int (*compar)(const void *, const void *)); + +long strtol(const char *nptr, char **endptr, int base); +unsigned long strtoul(const char *nptr, char **endptr, int base); + +int rand(void); +void srand(unsigned int seed); + +div_t div(int numer, int denom); +ldiv_t ldiv(long numer, long denom); + +int system(const char *command); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_STDLIB_H */ diff --git a/programs/include/libc/string.h b/programs/include/libc/string.h new file mode 100644 index 0000000..180247f --- /dev/null +++ b/programs/include/libc/string.h @@ -0,0 +1,35 @@ +#ifndef _LIBC_STRING_H +#define _LIBC_STRING_H + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void *memcpy(void *dest, const void *src, size_t n); +void *memset(void *s, int c, size_t n); +void *memmove(void *dest, const void *src, size_t n); +int memcmp(const void *s1, const void *s2, size_t n); + +size_t strlen(const char *s); +int strcmp(const char *s1, const char *s2); +int strncmp(const char *s1, const char *s2, size_t n); +char *strcpy(char *dest, const char *src); +char *strncpy(char *dest, const char *src, size_t n); +char *strcat(char *dest, const char *src); +char *strncat(char *dest, const char *src, size_t n); +char *strdup(const char *s); +char *strchr(const char *s, int c); +char *strrchr(const char *s, int c); +int strcasecmp(const char *s1, const char *s2); +int strncasecmp(const char *s1, const char *s2, size_t n); +char *strstr(const char *haystack, const char *needle); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_STRING_H */ diff --git a/programs/include/libc/strings.h b/programs/include/libc/strings.h new file mode 100644 index 0000000..fba3df7 --- /dev/null +++ b/programs/include/libc/strings.h @@ -0,0 +1,17 @@ +#ifndef _LIBC_STRINGS_H +#define _LIBC_STRINGS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int strcasecmp(const char *s1, const char *s2); +int strncasecmp(const char *s1, const char *s2, size_t n); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_STRINGS_H */ diff --git a/programs/include/libc/sys/stat.h b/programs/include/libc/sys/stat.h new file mode 100644 index 0000000..e0f9f5b --- /dev/null +++ b/programs/include/libc/sys/stat.h @@ -0,0 +1,24 @@ +#ifndef _LIBC_SYS_STAT_H +#define _LIBC_SYS_STAT_H + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct stat { + unsigned long st_size; +}; + +int mkdir(const char *path, unsigned int mode); +int stat(const char *path, struct stat *buf); +int fstat(int fd, struct stat *buf); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_SYS_STAT_H */ diff --git a/programs/include/libc/sys/time.h b/programs/include/libc/sys/time.h new file mode 100644 index 0000000..33de4ae --- /dev/null +++ b/programs/include/libc/sys/time.h @@ -0,0 +1,24 @@ +#ifndef _LIBC_SYS_TIME_H +#define _LIBC_SYS_TIME_H + +/* Stub header for ZenithOS */ + +#ifdef __cplusplus +extern "C" { +#endif + +struct timeval { + long tv_sec; + long tv_usec; +}; + +struct timezone { + int tz_minuteswest; + int tz_dsttime; +}; + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_SYS_TIME_H */ diff --git a/programs/include/libc/sys/types.h b/programs/include/libc/sys/types.h new file mode 100644 index 0000000..350e0fb --- /dev/null +++ b/programs/include/libc/sys/types.h @@ -0,0 +1,23 @@ +#ifndef _LIBC_SYS_TYPES_H +#define _LIBC_SYS_TYPES_H + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +typedef unsigned long size_t; +typedef long ssize_t; +typedef long off_t; +typedef int pid_t; +typedef unsigned int mode_t; +typedef unsigned int uid_t; +typedef unsigned int gid_t; +typedef long time_t; + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_SYS_TYPES_H */ diff --git a/programs/include/libc/unistd.h b/programs/include/libc/unistd.h new file mode 100644 index 0000000..fe56976 --- /dev/null +++ b/programs/include/libc/unistd.h @@ -0,0 +1,20 @@ +#ifndef _LIBC_UNISTD_H +#define _LIBC_UNISTD_H + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +int read(int fd, void *buf, size_t count); +int write(int fd, const void *buf, size_t count); +int close(int fd); + +#ifdef __cplusplus +} +#endif + +#endif /* _LIBC_UNISTD_H */ diff --git a/programs/include/zenith/heap.h b/programs/include/zenith/heap.h new file mode 100644 index 0000000..cefd9a9 --- /dev/null +++ b/programs/include/zenith/heap.h @@ -0,0 +1,133 @@ +/* + * heap.h + * Userspace heap allocator for ZenithOS programs + * Free-list allocator backed by SYS_ALLOC page requests. + * Adapted from the kernel HeapAllocator. + * Copyright (c) 2025 Daniel Hammer +*/ + +#pragma once +#include + +namespace zenith { +namespace heap_detail { + + static constexpr uint64_t HEADER_MAGIC = 0x5A484541; // "ZHEA" + + struct Header { + uint64_t magic; + uint64_t size; // user-requested size + } __attribute__((packed)); + + struct FreeNode { + uint64_t size; // total size of this free block (including node) + FreeNode* next; + }; + + // Per-process heap state (single TU per program, so static is fine) + static FreeNode g_head{0, nullptr}; + static bool g_initialized = false; + + static inline Header* get_header(void* block) { + return (Header*)((uint8_t*)block - sizeof(Header)); + } + + static inline void insert_free(void* ptr, uint64_t size) { + auto* node = (FreeNode*)ptr; + node->size = size; + node->next = g_head.next; + g_head.next = node; + } + + static inline void grow(uint64_t bytes) { + uint64_t pages = (bytes + 0xFFF) / 0x1000; + if (pages < 4) pages = 4; // grow at least 16 KiB at a time + + void* mem = zenith::alloc(pages * 0x1000); + if (mem != nullptr) + insert_free(mem, pages * 0x1000); + } + +} // namespace heap_detail + + // ---- Public API ---- + + inline void* malloc(uint64_t size) { + using namespace heap_detail; + + if (!g_initialized) { + grow(16 * 0x1000); // seed with 64 KiB + g_initialized = true; + } + + uint64_t needed = size + sizeof(Header); + needed = (needed + 15) & ~15ULL; // 16-byte alignment + + FreeNode* prev = &g_head; + FreeNode* current = g_head.next; + + while (current != nullptr) { + if (current->size >= needed) { + uint64_t blockSize = current->size; + + // Unlink + prev->next = current->next; + + // Split if worthwhile + if (blockSize > needed + sizeof(FreeNode) + 16) { + void* rest = (void*)((uint8_t*)current + needed); + uint64_t restSize = blockSize - needed; + insert_free(rest, restSize); + } + + // Write allocation header + Header* header = (Header*)current; + header->magic = HEADER_MAGIC; + header->size = size; + + return (void*)((uint8_t*)header + sizeof(Header)); + } + + prev = current; + current = current->next; + } + + // No fit — grow and retry + grow(needed); + return malloc(size); + } + + inline void mfree(void* ptr) { + using namespace heap_detail; + + if (ptr == nullptr) return; + + Header* header = get_header(ptr); + + uint64_t blockSize = header->size + sizeof(Header); + blockSize = (blockSize + 15) & ~15ULL; + + insert_free((void*)header, blockSize); + } + + inline void* realloc(void* ptr, uint64_t size) { + if (ptr == nullptr) return malloc(size); + + auto* header = heap_detail::get_header(ptr); + uint64_t old = header->size; + + void* newBlock = malloc(size); + if (newBlock == nullptr) return nullptr; + + // Copy the smaller of old/new sizes + uint64_t copySize = (old < size) ? old : size; + uint8_t* dst = (uint8_t*)newBlock; + uint8_t* src = (uint8_t*)ptr; + for (uint64_t i = 0; i < copySize; i++) + dst[i] = src[i]; + + mfree(ptr); + return newBlock; + } + +} // namespace zenith diff --git a/programs/include/zenith/syscall.h b/programs/include/zenith/syscall.h index 1ba0705..51cd40c 100644 --- a/programs/include/zenith/syscall.h +++ b/programs/include/zenith/syscall.h @@ -117,6 +117,9 @@ namespace zenith { inline void yield() { syscall0(Zenith::SYS_YIELD); } inline void sleep_ms(uint64_t ms) { syscall1(Zenith::SYS_SLEEP_MS, ms); } inline int getpid() { return (int)syscall0(Zenith::SYS_GETPID); } + inline int spawn(const char* path, const char* args = nullptr) { + return (int)syscall2(Zenith::SYS_SPAWN, (uint64_t)path, (uint64_t)args); + } // Console inline void print(const char* text) { syscall1(Zenith::SYS_PRINT, (uint64_t)text); } @@ -154,4 +157,23 @@ namespace zenith { return (int32_t)syscall2(Zenith::SYS_PING, (uint64_t)ip, (uint64_t)timeoutMs); } + // Process management + inline void waitpid(int pid) { syscall1(Zenith::SYS_WAITPID, (uint64_t)pid); } + + // Framebuffer + inline void fb_info(Zenith::FbInfo* info) { syscall1(Zenith::SYS_FBINFO, (uint64_t)info); } + inline void* fb_map() { return (void*)syscall0(Zenith::SYS_FBMAP); } + + // Arguments + inline int getargs(char* buf, uint64_t maxLen) { + return (int)syscall2(Zenith::SYS_GETARGS, (uint64_t)buf, maxLen); + } + + // Terminal + inline void termsize(int* cols, int* rows) { + uint64_t r = (uint64_t)syscall0(Zenith::SYS_TERMSIZE); + if (cols) *cols = (int)(r & 0xFFFFFFFF); + if (rows) *rows = (int)(r >> 32); + } + } diff --git a/programs/man/file.2 b/programs/man/file.2 new file mode 100644 index 0000000..dc72f17 --- /dev/null +++ b/programs/man/file.2 @@ -0,0 +1,74 @@ +.TH FILE 2 +.SH NAME + open, read, getsize, close, readdir - file I/O system calls + +.SH SYNOPSIS +.BI int zenith::open(const char* path); +.BI int zenith::read(int handle, uint8_t* buf, uint64_t offset, uint64_t size); +.BI uint64_t zenith::getsize(int handle); +.BI void zenith::close(int handle); +.BI int zenith::readdir(const char* path, const char** names, int max); + +.SH DESCRIPTION + ZenithOS provides a simple read-only Virtual File System (VFS) + backed by the boot ramdisk. Files are accessed via paths in the + format ":/", where drive 0 is the ramdisk. + +.SS open + Opens a file and returns a non-negative handle on success, or a + negative value on error (file not found, no free handles). + + int h = zenith::open("0:/shell.elf"); + +.SS read + Reads up to 'size' bytes starting at 'offset' into 'buf'. + Returns the number of bytes actually read, or negative on error. + There is no implicit file position -- the offset is explicit on + every call. + + uint8_t buf[512]; + int n = zenith::read(h, buf, 0, 512); + +.SS getsize + Returns the total size in bytes of the file. + + uint64_t sz = zenith::getsize(h); + +.SS close + Closes the file handle and frees kernel resources. + + zenith::close(h); + +.SS readdir + Lists entries in a directory. Up to 'max' entry names (max 64) + are written to the 'names' array. The kernel allocates a user- + accessible page for the string data automatically. + + const char* entries[64]; + int count = zenith::readdir("0:/", entries, 64); + +.SH READING PATTERN + The standard pattern for reading a file: + + int h = zenith::open("0:/myfile.txt"); + uint64_t size = zenith::getsize(h); + uint8_t buf[512]; + uint64_t off = 0; + while (off < size) { + uint64_t chunk = size - off; + if (chunk > 511) chunk = 511; + int n = zenith::read(h, buf, off, chunk); + if (n <= 0) break; + buf[n] = '\0'; + zenith::print((const char*)buf); + off += n; + } + zenith::close(h); + +.SH NOTES + The filesystem is read-only. There are no write or create calls. + All files live on the ramdisk which is loaded at boot from a + USTAR tar archive. + +.SH SEE ALSO + syscalls(2), spawn(2), malloc(3) diff --git a/programs/man/framebuffer.2 b/programs/man/framebuffer.2 new file mode 100644 index 0000000..856dc46 --- /dev/null +++ b/programs/man/framebuffer.2 @@ -0,0 +1,62 @@ +.TH FRAMEBUFFER 2 +.SH NAME + fb_info, fb_map - direct framebuffer access + +.SH SYNOPSIS +.BI void zenith::fb_info(Zenith::FbInfo* info); +.BI void* zenith::fb_map(); + +.SH DESCRIPTION + These syscalls allow userspace programs to access the linear + framebuffer directly for graphical output. + +.SS fb_info + Fills in an FbInfo structure with the framebuffer geometry: + + Zenith::FbInfo fb; + zenith::fb_info(&fb); + // fb.width, fb.height, fb.pitch, fb.bpp + + The pitch is the number of bytes per scanline (may be larger + than width * 4 due to alignment). bpp is always 32. + +.SS fb_map + Maps the physical framebuffer into the process address space at + a fixed virtual address (0x50000000) and returns that address. + + uint32_t* pixels = (uint32_t*)zenith::fb_map(); + + Each pixel is a 32-bit value in 0xAARRGGBB format (blue in the + low byte). Writing to this memory directly updates the screen. + +.SH PIXEL FORMAT + Bits 31-24: Alpha (unused, typically 0xFF) + Bits 23-16: Red + Bits 15-8: Green + Bits 7-0: Blue + + Example: red = 0x00FF0000, green = 0x0000FF00, blue = 0x000000FF + +.SH EXAMPLE + Fill the screen with blue: + + Zenith::FbInfo fb; + zenith::fb_info(&fb); + uint32_t* pixels = (uint32_t*)zenith::fb_map(); + + for (uint64_t y = 0; y < fb.height; y++) { + uint32_t* row = (uint32_t*)((uint8_t*)pixels + y * fb.pitch); + for (uint64_t x = 0; x < fb.width; x++) { + row[x] = 0x000000FF; + } + } + +.SH NOTES + After mapping, the cursor overlay is not composited. Programs + that use the framebuffer take full control of screen output. + + Only one mapping per process is supported. Calling fb_map() + multiple times returns the same address. + +.SH SEE ALSO + syscalls(2), malloc(3) diff --git a/programs/man/intro.1 b/programs/man/intro.1 new file mode 100644 index 0000000..6c581cb --- /dev/null +++ b/programs/man/intro.1 @@ -0,0 +1,49 @@ +.TH INTRO 1 +.SH NAME + intro - introduction to ZenithOS userspace + +.SH DESCRIPTION + ZenithOS is a hobbyist 64-bit operating system written in C++20. + Userspace programs run in Ring 3, are loaded as static ELF64 + binaries, and communicate with the kernel through the x86-64 + SYSCALL/SYSRET mechanism. + + Programs are compiled with a freestanding cross-compiler and + linked at virtual address 0x400000. There is no standard C + library for C++ programs -- all system interaction goes through + the zenith:: syscall wrappers. + +.SH GETTING STARTED + To write a new program, create a directory under programs/src/ + with a main.cpp file. The entry point is: + + extern "C" void _start() { ... } + + There is no argc/argv. Include for the full + typed syscall API. Include for malloc/mfree. + + Build with: + + cd programs && make + + The resulting ELF binary appears in programs/bin/. + +.SH SHELL + The built-in shell is the primary way to interact with ZenithOS. + Type 'help' at the shell prompt for a list of commands. Use + 'man shell' for detailed shell documentation. + +.SH MAN PAGES + The following man pages are available: + + intro(1) This page + shell(1) Shell commands reference + man(1) The man command itself + syscalls(2) Overview of all syscalls + spawn(2) Process spawning + file(2) File I/O syscalls + framebuffer(2) Framebuffer access + malloc(3) Memory allocation + +.SH SEE ALSO + shell(1), syscalls(2), malloc(3) diff --git a/programs/man/malloc.3 b/programs/man/malloc.3 new file mode 100644 index 0000000..46687c9 --- /dev/null +++ b/programs/man/malloc.3 @@ -0,0 +1,57 @@ +.TH MALLOC 3 +.SH NAME + malloc, mfree, realloc - userspace heap allocation + +.SH SYNOPSIS +.BI void* zenith::malloc(uint64_t size); +.BI void zenith::mfree(void* ptr); +.BI void* zenith::realloc(void* ptr, uint64_t size); + +.SH DESCRIPTION + The userspace heap provides dynamic memory allocation on top of + the kernel's page-mapping syscall (SYS_ALLOC). Include the + header to use these functions. + +.SS malloc + Allocates 'size' bytes from the free list. Returns a 16-byte + aligned pointer, or nullptr on failure. When the free list is + empty, it requests more pages from the kernel via SYS_ALLOC + (minimum 16 KiB growth, initial seed of 64 KiB). + + char* buf = (char*)zenith::malloc(1024); + +.SS mfree + Returns the block to the userspace free list. No syscall is + made -- the memory stays mapped and is immediately reusable. + Passing nullptr is a safe no-op. + + zenith::mfree(buf); + +.SS realloc + Resizes the allocation to 'size' bytes. Allocates a new block, + copies the smaller of old/new sizes, and frees the old block. + If ptr is nullptr, behaves like malloc. + + buf = (char*)zenith::realloc(buf, 2048); + +.SH IMPLEMENTATION + The allocator uses a linked free-list with first-fit search. + Blocks larger than needed are split. The allocation header is + 16 bytes (magic + size). All allocations are 16-byte aligned. + + The heap grows by requesting pages from the kernel via + SYS_ALLOC. These pages are never returned to the kernel (since + SYS_FREE is currently a no-op), but mfree makes them available + for future malloc calls within the process. + +.SH LOW-LEVEL PAGE API + For large allocations or when direct page control is needed: + + void* zenith::alloc(uint64_t size); // SYS_ALLOC + void zenith::free(void* ptr); // SYS_FREE (no-op) + + alloc() maps zeroed pages starting at 0x40000000 and growing + upward. Size is rounded up to 4 KiB page boundaries. + +.SH SEE ALSO + syscalls(2), file(2) diff --git a/programs/man/man.1 b/programs/man/man.1 new file mode 100644 index 0000000..ec65e0d --- /dev/null +++ b/programs/man/man.1 @@ -0,0 +1,47 @@ +.TH MAN 1 +.SH NAME + man - display manual pages + +.SH SYNOPSIS +.BI man topic +.BI man section topic + +.SH DESCRIPTION + The man command displays manual pages from the ramdisk in a + fullscreen pager. Pages are stored as plain text files with + simple formatting directives. + + If no section is specified, sections 1, 2, and 3 are searched + in order. If a section number is given, only that section is + checked. + +.SH KEY BINDINGS + +.B Navigation + j, Down Arrow Scroll down one line + k, Up Arrow Scroll up one line + Space, Page Down Scroll down one page + b, Page Up Scroll up one page + g, Home Go to top + G, End Go to bottom + q Quit + +.SH SECTIONS + 1 User commands (shell built-ins) + 2 System calls (kernel interface) + 3 Library functions (userspace libraries) + +.SH FILES + Man pages are stored on the ramdisk at: + + 0:/man/.
+ + For example, man intro reads 0:/man/intro.1 + +.SH EXAMPLES + man intro View the introduction + man 2 syscalls View syscall overview (section 2) + man malloc View malloc documentation + +.SH SEE ALSO + intro(1), shell(1), syscalls(2) diff --git a/programs/man/shell.1 b/programs/man/shell.1 new file mode 100644 index 0000000..0207f8b --- /dev/null +++ b/programs/man/shell.1 @@ -0,0 +1,54 @@ +.TH SHELL 1 +.SH NAME + shell - ZenithOS interactive command shell + +.SH DESCRIPTION + The ZenithOS shell is a simple command interpreter that runs as + the first userspace process. It provides basic file inspection, + process management, networking, and documentation access. + +.SH COMMANDS + +.SS help + Display a list of available commands. + +.SS info + Show the OS name, version, and syscall API version number. + +.SS man + Open a manual page in the fullscreen pager. See man(1). + +.SS ls + List all files on the ramdisk (drive 0:/). + +.SS cat + Print the contents of a ramdisk file to the terminal. + Example: cat hello.elf + +.SS run + Spawn a new process from an ELF binary on the ramdisk and wait + for it to exit. The shell blocks until the child process + terminates. + Example: run hello.elf + +.SS ping + Send 4 ICMP echo requests to the given IP address and display + round-trip times. Timeout is 3 seconds per request. + Example: ping 10.0.2.2 + +.SS uptime + Display the system uptime in minutes, seconds, and milliseconds. + +.SS clear + Clear the terminal screen. + +.SS exit + Terminate the shell process. + +.SH INPUT + The shell reads input character by character using SYS_GETCHAR. + Backspace is supported. Lines are limited to 255 characters. + There is no command history or tab completion. + +.SH SEE ALSO + man(1), intro(1), syscalls(2) diff --git a/programs/man/spawn.2 b/programs/man/spawn.2 new file mode 100644 index 0000000..2be6d18 --- /dev/null +++ b/programs/man/spawn.2 @@ -0,0 +1,69 @@ +.TH SPAWN 2 +.SH NAME + spawn, waitpid - create and wait for processes + +.SH SYNOPSIS +.BI int zenith::spawn(const char* path, const char* args = nullptr); +.BI void zenith::waitpid(int pid); +.BI int zenith::getargs(char* buf, uint64_t maxLen); + +.SH DESCRIPTION + +.SS spawn + Loads the ELF64 binary at the given VFS path and creates a new + process. The path must include the drive prefix, for example: + + int pid = zenith::spawn("0:/hello.elf"); + + An optional second argument passes a string to the child: + + int pid = zenith::spawn("0:/man.elf", "intro"); + + The new process gets its own PML4 page table, a 16 KiB stack + (at 0x7FFFFEF000-0x7FFFFFF000), and begins executing at the + ELF entry point (_start). + + Returns the new process's PID on success, or -1 on failure. + Failure occurs when there are no free process slots (max 16), + the file cannot be found, or the ELF is invalid. + +.SS waitpid + Blocks the calling process until the process with the given PID + has exited. Internally, this yields the CPU in a loop: + + zenith::waitpid(pid); + + This is how the shell implements foreground process execution -- + it spawns a child and waits for it to complete before showing + the next prompt. + +.SH EXAMPLES + Spawn a program and wait for it: + + int pid = zenith::spawn("0:/hello.elf"); + if (pid < 0) { + zenith::print("spawn failed\n"); + } else { + zenith::waitpid(pid); + zenith::print("child exited\n"); + } + +.SS getargs + Copies the argument string into buf (up to maxLen bytes, always + null-terminated). Returns the number of characters copied, or + -1 on error. + + char args[256]; + zenith::getargs(args, sizeof(args)); + + The argument string is set by the parent when calling spawn(). + If no arguments were provided, the buffer will be empty. + +.SH NOTES + The _start() entry point receives no argc/argv. Use getargs() + to retrieve the argument string passed by the parent process. + + Process exit codes are not yet collected by waitpid. + +.SH SEE ALSO + syscalls(2), file(2) diff --git a/programs/man/syscalls.2 b/programs/man/syscalls.2 new file mode 100644 index 0000000..32085fe --- /dev/null +++ b/programs/man/syscalls.2 @@ -0,0 +1,137 @@ +.TH SYSCALLS 2 +.SH NAME + syscalls - overview of ZenithOS system calls + +.SH DESCRIPTION + ZenithOS provides 26 system calls (numbers 0-25) for userspace + programs. Syscalls use the x86-64 SYSCALL instruction with the + following register convention: + + RAX Syscall number (in) / return value (out) + RDI Argument 1 + RSI Argument 2 + RDX Argument 3 + R10 Argument 4 + R8 Argument 5 + R9 Argument 6 + + Include for typed wrappers in the zenith:: + namespace. + +.SH PROCESS MANAGEMENT +.B SYS_EXIT (0) + Terminate the calling process. + void zenith::exit(int code = 0); + +.B SYS_YIELD (1) + Yield the remainder of the time slice. + void zenith::yield(); + +.B SYS_SLEEP_MS (2) + Sleep for at least the given number of milliseconds. + void zenith::sleep_ms(uint64_t ms); + +.B SYS_GETPID (3) + Return the PID of the calling process. + int zenith::getpid(); + +.B SYS_SPAWN (20) + Spawn a new process from an ELF binary on the VFS. + int zenith::spawn(const char* path, const char* args = nullptr); + +.B SYS_WAITPID (23) + Block until the given process has exited. + void zenith::waitpid(int pid); + +.SH CONSOLE I/O +.B SYS_PRINT (4) + Write a null-terminated string to the terminal. + void zenith::print(const char* text); + +.B SYS_PUTCHAR (5) + Write a single character to the terminal. + void zenith::putchar(char c); + +.SH FILE I/O +.B SYS_OPEN (6) + Open a file. Returns a handle or negative on error. + int zenith::open(const char* path); + +.B SYS_READ (7) + Read bytes from a file at a given offset. + int zenith::read(int h, uint8_t* buf, uint64_t off, uint64_t sz); + +.B SYS_GETSIZE (8) + Get the size of an open file in bytes. + uint64_t zenith::getsize(int handle); + +.B SYS_CLOSE (9) + Close a file handle. + void zenith::close(int handle); + +.B SYS_READDIR (10) + List directory entries (max 64 per call). + int zenith::readdir(const char* path, const char** names, int max); + +.SH MEMORY +.B SYS_ALLOC (11) + Map zeroed pages into the process address space. + void* zenith::alloc(uint64_t size); + +.B SYS_FREE (12) + Reserved (currently a no-op). + void zenith::free(void* ptr); + +.SH TIMEKEEPING +.B SYS_GETTICKS (13) + Get APIC timer ticks since boot. + uint64_t zenith::get_ticks(); + +.B SYS_GETMILLISECONDS (14) + Get milliseconds elapsed since boot. + uint64_t zenith::get_milliseconds(); + +.SH SYSTEM +.B SYS_GETINFO (15) + Get OS name, version, and configuration. + void zenith::get_info(Zenith::SysInfo* info); + +.SH KEYBOARD +.B SYS_ISKEYAVAILABLE (16) + Check if a key event is pending (non-blocking). + bool zenith::is_key_available(); + +.B SYS_GETKEY (17) + Get the next key event (press or release). + void zenith::getkey(Zenith::KeyEvent* out); + +.B SYS_GETCHAR (18) + Block until a printable character is typed. + char zenith::getchar(); + +.SH NETWORKING +.B SYS_PING (19) + Send an ICMP echo request and wait for reply. + int32_t zenith::ping(uint32_t ip, uint32_t timeoutMs); + +.SH FRAMEBUFFER +.B SYS_FBINFO (21) + Get framebuffer dimensions and format. + void zenith::fb_info(Zenith::FbInfo* info); + +.B SYS_FBMAP (22) + Map the framebuffer into process memory at 0x50000000. + void* zenith::fb_map(); + +.SH TERMINAL +.B SYS_TERMSIZE (24) + Get terminal dimensions (columns and rows). + void zenith::termsize(int* cols, int* rows); + +.SH ARGUMENTS +.B SYS_GETARGS (25) + Get the argument string passed to this process at spawn time. + int zenith::getargs(char* buf, uint64_t maxLen); + +.SH SEE ALSO + spawn(2), file(2), framebuffer(2), malloc(3) diff --git a/programs/obj/hello/main.o b/programs/obj/hello/main.o index 781ee69cd2d4de0e7526ec2dcc11328a0de192e0..bead10b8ff0d9f61a030b76b05ec116d8323a853 100644 GIT binary patch delta 2506 zcmZuzYiv|i5I*NVcK7bx_ucO8ZXer)Qd?0L%fQW$_H8e3*DuL3* zt_4F%&(b!ro_Ye|Kh5WlU$>M{tlt z2KrLv36*=uP!*jXAx-Ze*vdTnFV+nIN&Lr)}vAtI|9p`w_G(N1vnRWhQlY<-S-nz{KiS~2fq=I6vQNa*Gr&Y;MD&2lnl zZ9x$i>TFWWgj!2#La1{{wS`(oYB_9Bo-Zn=5mCM;ae>4Y5`UqPEqk{sq@!TWBEgL) zJ!qs#560i~;N^xYN);|*+oMFT%TiJ+2MAM3YDVhm# zC?lefdbD){nJv&t5!HsePWvGQHtQS3lFS(LsEDMXj?7+KLxCpc7{^~=V35^ORG_bk zEhHrc_1e~=&N{JoPlBnT){MH?Y%$69)(O}b6tK2fCwQEjkD9ncEgZQfQZpt;uD^;m zS!?*ocC*)BXRbHbBFCJ^pRj5&D)|FC9)s45vN%&&N*;{zIU&nb(-rC7UOUPo*j=cW znP$#Z@%)4>Zkjk3PJz=7geXTu)F%K@2V+sJ!dIdYA?9PEB(oe13r6+wzPQPIF>_fn zYeFnBkGCad-I#PqlA`SU!A@%lu3mWg`g+DOI>~T46^X;QVr)*8Y~I^gN?U@(osAaV)#*tYXoDV?R*(rwk-&R zEBV{d5N=F4r4Gmav_q!%pMr(&L`@H#iiw`M2-|W=P3BdELdi3wYR) zAoIWL^1cL3=#9(#6E5!{^r3f3dVg_wXTg5-J|yS8>GEEI>4@E#ld(QIztfi?am$7O zDDi3+{sv^CHa`k2(MDc?{%B)zfn;uSF>k=}sGYnh(QPj8BDjnhx*-~CxR#jTUvYcOWn$r~>3 zBhq^dj$1a2#{)&W$O|OuERoaEW?8vYse`x6%AF(a;G>e?ApH*BFRPgE!e3MwwZ~Ww z^p)Agy)O4%8Fs*hKPmBdT=+L~`hza~gv38|;fH0OPfK|0-nIQe27Cra;w^~S<1jzr zn>cSDQ@IUe6>a$Dn6AhgUyFY%^kwIjSz}l(;wi|ZbwT=kFossM`1&%o6Qb(sg3{`+F5uN!W4x?1!zRe1wNv`Og0bqXat#Bj zb4Fgimfr-P&MN2p(3;Ni`LHA1W)uRY=*M9iU7KyVlg=sQ0cfqsDIW!4xF%=3Ccfy5 J-Gb><`ah(b9?$>) delta 3053 zcmbVOYitx%6ux&JyE8lcnCWbHI=kD);!+f`rM98a!j?h^G{IM)NEL+&UBMWFZ7o`3 zHaw!mpt8A%LBtdkj0IxEn#jYb=npV4M2rsz8s!IS#0L_^N22kZnY%a+k{E9?_ulh; z-#O>L=Fa>&=1OMUJoJnc?@TK!ce31kQdyDMuA7tunj-%^77>=Vu}A1{@gdSI5;Wi8gawOe5^0y60^2?-{M z7;VXjt=cpVZKc+M)rbR2tun&HRGSJep=lJf8b~u51+AvhE(;d5i8O!}siSkvnv94{ zRED$Dk`Y=nY4l$XhuNUxqoyWSr>eUwgX-X?XCXpP6X8f3Ce<{^0>L5S>L|u2b4KO> z&IkxGA;02EP!Ly$GZ?J`jB=KfD+-CrCW=ClG4 zVFK<{9W&L0^TC-rCK-k$mf?NDBDn7)c4>kgm#u3OLemJ5{)Q7F*f6(@c;yPJ4aKKc z*hM)@Tj1@s3ipCnDUY+E7zGZyL<3Sd0TeI)6-r9UabB;u6i*Z9TEIXpni>YTtP(+@rXxkCV_3XiDy}cg-W4Xa?93|d|mF!5=vR67+4F#O?o|@zz*v5b}d&f^C?d=YFPGhjt=_RIBPKK>0H(Wbt5Yp z)~dI;;($;2CATN?Jf)F;$L%RT{0!H(`tS?fp2c>SS@aI9?Yu{WM?TEgZjhY-cF4m9 zJnRO?ce95@-)io=l{J`_z1_p2ZM*cwxpl z9RJXRW3C&SVVVW(&F6|USBUtH#}gxVas5RPj&brF|J{eL;JC=o9_|h5;RnO{#*v_p z7~l?J5AIEz^x$|rm+;LT@53|Pp5(*(N}Dg%yO~O-WdR@gET2P;+j*9WTZLzR>{;%$ z%ZImde6J5b$kV^#!(Zh1K_C7EkMov`mtDGUp5hKi*}L&43pepqLlBjv196)N$4Kx2 zb;{km_3PJO0blRIy>W0Wp|GXp>oN(qPZsgE6uWc-=f_Tz9~Ie2TW34M65D4_f}8i0 z-K;K-x*n66oorUOargIFH(0}*+sd8>D-WM6EL+(ru>Ry@cYtP;eBvS=hQ0qgH5`a zUD+)3Ypk;}M?Yf4$~N_Dej7|>r+~WIW@S}5p(esQt8(g0K5)a0SCv&)!mk3BpB?1e Gitu0l12(1r diff --git a/programs/obj/shell/main.o b/programs/obj/shell/main.o index 86b80f664b824af37fbaeeee16f21e84867cbf18..72939d873d995babb02c855a4dc0c0f93aa5607b 100644 GIT binary patch literal 92944 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 diff --git a/programs/src/doom/Makefile b/programs/src/doom/Makefile new file mode 100644 index 0000000..c1485cd --- /dev/null +++ b/programs/src/doom/Makefile @@ -0,0 +1,187 @@ +# Makefile for DOOM (doomgeneric) on ZenithOS +# Copyright (c) 2025 Daniel Hammer + +MAKEFLAGS += -rR +.SUFFIXES: + +# ---- Toolchain ---- + +TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf- +ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),) + CC := $(TOOLCHAIN_PREFIX)gcc + LD := $(TOOLCHAIN_PREFIX)gcc +else + CC := gcc + LD := gcc +endif + +# ---- Paths ---- + +DOOM_SRC := ../../../doomgeneric/doomgeneric +LIBC_INC := ../../include/libc +PROG_INC := ../../include +LINK_LD := ../../link.ld +BINDIR := ../../bin +OBJDIR := obj + +# ---- Compiler flags ---- + +CFLAGS := \ + -std=gnu11 \ + -g -O2 -pipe \ + -Wall \ + -Wno-unused-parameter \ + -Wno-unused-variable \ + -Wno-missing-field-initializers \ + -Wno-sign-compare \ + -Wno-pointer-sign \ + -nostdinc \ + -ffreestanding \ + -fno-stack-protector \ + -fno-stack-check \ + -fno-PIC \ + -ffunction-sections \ + -fdata-sections \ + -m64 \ + -march=x86-64 \ + -msse \ + -msse2 \ + -mno-red-zone \ + -mcmodel=small \ + -DNORMALUNIX \ + -DLINUX \ + -isystem $(LIBC_INC) \ + -I $(PROG_INC) \ + -I $(DOOM_SRC) \ + -isystem $(shell $(CC) -print-file-name=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) + +# ---- DOOM source files (excluding platform-specific ports and sound backends) ---- + +DOOM_SRCS := \ + am_map.c \ + d_event.c \ + d_items.c \ + d_iwad.c \ + d_loop.c \ + d_main.c \ + d_mode.c \ + d_net.c \ + doomdef.c \ + doomgeneric.c \ + doomstat.c \ + dstrings.c \ + dummy.c \ + f_finale.c \ + f_wipe.c \ + g_game.c \ + gusconf.c \ + hu_lib.c \ + hu_stuff.c \ + i_cdmus.c \ + i_endoom.c \ + i_input.c \ + i_joystick.c \ + i_scale.c \ + i_sound.c \ + i_system.c \ + i_timer.c \ + i_video.c \ + info.c \ + m_argv.c \ + m_bbox.c \ + m_cheat.c \ + m_config.c \ + m_controls.c \ + m_fixed.c \ + m_menu.c \ + m_misc.c \ + m_random.c \ + memio.c \ + mus2mid.c \ + p_ceilng.c \ + p_doors.c \ + p_enemy.c \ + p_floor.c \ + p_inter.c \ + p_lights.c \ + p_map.c \ + p_maputl.c \ + p_mobj.c \ + p_plats.c \ + p_pspr.c \ + p_saveg.c \ + p_setup.c \ + p_sight.c \ + p_spec.c \ + p_switch.c \ + p_telept.c \ + p_tick.c \ + p_user.c \ + r_bsp.c \ + r_data.c \ + r_draw.c \ + r_main.c \ + r_plane.c \ + r_segs.c \ + r_sky.c \ + r_things.c \ + s_sound.c \ + sha1.c \ + sounds.c \ + st_lib.c \ + st_stuff.c \ + statdump.c \ + tables.c \ + v_video.c \ + w_checksum.c \ + w_file.c \ + w_file_stdc.c \ + w_main.c \ + w_wad.c \ + wi_stuff.c \ + z_zone.c + +# Local source files +LOCAL_SRCS := doomgeneric_zenith.c libc.c + +# ---- Object files ---- + +DOOM_OBJS := $(addprefix $(OBJDIR)/,$(DOOM_SRCS:.c=.o)) +LOCAL_OBJS := $(addprefix $(OBJDIR)/,$(LOCAL_SRCS:.c=.o)) +ALL_OBJS := $(DOOM_OBJS) $(LOCAL_OBJS) + +# ---- Target ---- + +TARGET := $(BINDIR)/doom.elf + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(ALL_OBJS) $(LINK_LD) Makefile + mkdir -p $(BINDIR) + $(LD) $(CFLAGS) $(LDFLAGS) $(ALL_OBJS) -o $@ + +# DOOM source files (from doomgeneric directory) +$(OBJDIR)/%.o: $(DOOM_SRC)/%.c Makefile + mkdir -p $(OBJDIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# Local source files +$(OBJDIR)/%.o: %.c Makefile + mkdir -p $(OBJDIR) + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -rf $(OBJDIR) $(TARGET) diff --git a/programs/src/doom/doomgeneric_zenith.c b/programs/src/doom/doomgeneric_zenith.c new file mode 100644 index 0000000..ee47db7 --- /dev/null +++ b/programs/src/doom/doomgeneric_zenith.c @@ -0,0 +1,230 @@ +/* + * doomgeneric_zenith.c + * DOOM platform implementation for ZenithOS + * Copyright (c) 2025 Daniel Hammer +*/ + +#include "doomgeneric.h" +#include "doomkeys.h" + +#include +#include + +/* ---- Raw syscall interface (C versions) ---- */ + +static inline long _zos_syscall0(long nr) { + long ret; + __asm__ volatile("syscall" : "=a"(ret) : "a"(nr) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall1(long nr, long a1) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +/* Syscall numbers (must match kernel/src/Api/Syscall.hpp) */ +#define SYS_EXIT 0 +#define SYS_SLEEP_MS 2 +#define SYS_PRINT 4 +#define SYS_GETMILLISECONDS 14 +#define SYS_ISKEYAVAILABLE 16 +#define SYS_GETKEY 17 +#define SYS_FBINFO 21 +#define SYS_FBMAP 22 + +/* FbInfo struct (must match kernel definition) */ +struct FbInfo { + unsigned long width; + unsigned long height; + unsigned long pitch; + unsigned long bpp; + unsigned long userAddr; +}; + +/* KeyEvent struct (must match kernel definition) */ +struct KeyEvent { + unsigned char scancode; + char ascii; + unsigned char pressed; + unsigned char shift; + unsigned char ctrl; + unsigned char alt; +}; + +/* ---- Framebuffer state ---- */ + +static uint32_t* g_fbPtr = 0; +static uint32_t g_fbWidth = 0; +static uint32_t g_fbHeight = 0; +static uint32_t g_fbPitch = 0; /* bytes per scanline */ + +/* ---- Circular key queue ---- */ + +#define KEY_QUEUE_SIZE 64 + +struct KeyQueueEntry { + int pressed; + unsigned char doomkey; +}; + +static struct KeyQueueEntry g_keyQueue[KEY_QUEUE_SIZE]; +static int g_keyQueueRead = 0; +static int g_keyQueueWrite = 0; + +static void key_queue_push(int pressed, unsigned char doomkey) { + int next = (g_keyQueueWrite + 1) % KEY_QUEUE_SIZE; + if (next == g_keyQueueRead) return; /* full, drop */ + g_keyQueue[g_keyQueueWrite].pressed = pressed; + g_keyQueue[g_keyQueueWrite].doomkey = doomkey; + g_keyQueueWrite = next; +} + +static int key_queue_pop(int* pressed, unsigned char* doomkey) { + if (g_keyQueueRead == g_keyQueueWrite) return 0; + *pressed = g_keyQueue[g_keyQueueRead].pressed; + *doomkey = g_keyQueue[g_keyQueueRead].doomkey; + g_keyQueueRead = (g_keyQueueRead + 1) % KEY_QUEUE_SIZE; + return 1; +} + +/* ---- PS/2 scancode to ASCII table (set 1, unshifted) ---- */ + +static const char scancode_to_ascii[128] = { + 0, 27, '1','2','3','4','5','6','7','8','9','0','-','=','\b', + '\t','q','w','e','r','t','y','u','i','o','p','[',']','\n', + 0, 'a','s','d','f','g','h','j','k','l',';','\'','`', + 0, '\\','z','x','c','v','b','n','m',',','.','/', 0, + '*', 0, ' ' +}; + +/* ---- PS/2 scancode to DOOM key mapping ---- */ + +static unsigned char scancode_to_doomkey(unsigned char scancode, char ascii) { + switch (scancode) { + case 0x48: return KEY_UPARROW; + case 0x50: return KEY_DOWNARROW; + case 0x4B: return KEY_LEFTARROW; + case 0x4D: return KEY_RIGHTARROW; + case 0x1C: return KEY_ENTER; + case 0x01: return KEY_ESCAPE; + case 0x39: return ' '; /* Space = use */ + case 0x1D: return KEY_RCTRL; /* LCtrl = fire */ + case 0x2A: return KEY_RSHIFT; /* LShift = run */ + case 0x36: return KEY_RSHIFT; /* RShift = run */ + case 0x38: return KEY_RALT; /* Alt = strafe */ + case 0x0E: return KEY_BACKSPACE; + case 0x0F: return KEY_TAB; + /* F1-F10 */ + case 0x3B: return KEY_F1; + case 0x3C: return KEY_F2; + case 0x3D: return KEY_F3; + case 0x3E: return KEY_F4; + case 0x3F: return KEY_F5; + case 0x40: return KEY_F6; + case 0x41: return KEY_F7; + case 0x42: return KEY_F8; + case 0x43: return KEY_F9; + case 0x44: return KEY_F10; + case 0x57: return KEY_F11; + case 0x58: return KEY_F12; + /* Equals and minus for screen size */ + case 0x0D: return KEY_EQUALS; + case 0x0C: return KEY_MINUS; + default: + /* Pass through printable ASCII as lowercase */ + if (ascii >= 'a' && ascii <= 'z') return (unsigned char)ascii; + if (ascii >= '0' && ascii <= '9') return (unsigned char)ascii; + return 0; + } +} + +/* ---- Poll keyboard and enqueue events ---- */ + +static void poll_keyboard(void) { + while (_zos_syscall0(SYS_ISKEYAVAILABLE)) { + struct KeyEvent evt; + _zos_syscall1(SYS_GETKEY, (long)&evt); + + unsigned char baseSc = evt.scancode & 0x7F; /* strip break bit */ + + char ascii = 0; + if (baseSc < 128) + ascii = scancode_to_ascii[baseSc]; + + unsigned char dk = scancode_to_doomkey(baseSc, ascii); + if (dk != 0) { + key_queue_push(evt.pressed ? 1 : 0, dk); + } + } +} + +/* ---- DG platform functions ---- */ + +void DG_Init(void) { + struct FbInfo info; + _zos_syscall1(SYS_FBINFO, (long)&info); + + g_fbWidth = (uint32_t)info.width; + g_fbHeight = (uint32_t)info.height; + g_fbPitch = (uint32_t)info.pitch; + + g_fbPtr = (uint32_t*)(unsigned long)_zos_syscall0(SYS_FBMAP); + + printf("DOOM: framebuffer %ux%u pitch=%u mapped at %p\n", + g_fbWidth, g_fbHeight, g_fbPitch, (void*)g_fbPtr); +} + +void DG_DrawFrame(void) { + /* Poll keyboard first */ + poll_keyboard(); + + /* Copy DG_ScreenBuffer (DOOMGENERIC_RESX x DOOMGENERIC_RESY) to framebuffer */ + if (g_fbPtr == 0 || DG_ScreenBuffer == 0) return; + + uint32_t copyW = DOOMGENERIC_RESX; + uint32_t copyH = DOOMGENERIC_RESY; + if (copyW > g_fbWidth) copyW = g_fbWidth; + if (copyH > g_fbHeight) copyH = g_fbHeight; + + uint32_t fbStride = g_fbPitch / 4; /* pixels per scanline */ + + for (uint32_t y = 0; y < copyH; y++) { + uint32_t* dst = g_fbPtr + y * fbStride; + uint32_t* src = DG_ScreenBuffer + y * DOOMGENERIC_RESX; + memcpy(dst, src, copyW * sizeof(uint32_t)); + } +} + +void DG_SleepMs(uint32_t ms) { + _zos_syscall1(SYS_SLEEP_MS, (long)ms); +} + +uint32_t DG_GetTicksMs(void) { + return (uint32_t)_zos_syscall0(SYS_GETMILLISECONDS); +} + +int DG_GetKey(int* pressed, unsigned char* doomKey) { + return key_queue_pop(pressed, doomKey); +} + +void DG_SetWindowTitle(const char* title) { + (void)title; +} + +/* ---- Entry point ---- */ + +void _start(void) { + char *argv[] = { "doom", "-iwad", "0:/doom1.wad", 0 }; + doomgeneric_Create(3, argv); + for (;;) { + doomgeneric_Tick(); + } +} diff --git a/programs/src/doom/libc.c b/programs/src/doom/libc.c new file mode 100644 index 0000000..274e04a --- /dev/null +++ b/programs/src/doom/libc.c @@ -0,0 +1,1158 @@ +/* + * libc.c + * Minimal C standard library for ZenithOS userspace (DOOM port) + * Copyright (c) 2025 Daniel Hammer +*/ + +#include +#include +#include +#include + +/* ======================================================================== + Raw syscall wrappers (C versions matching kernel ABI) + ======================================================================== */ + +static inline long _zos_syscall0(long nr) { + long ret; + __asm__ volatile("syscall" : "=a"(ret) : "a"(nr) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall1(long nr, long a1) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +static inline long _zos_syscall4(long nr, long a1, long a2, long a3, long a4) { + long ret; + __asm__ volatile( + "mov %[a1], %%rdi\n\t" + "mov %[a2], %%rsi\n\t" + "mov %[a3], %%rdx\n\t" + "mov %[a4], %%r10\n\t" + "syscall" + : "=a"(ret) + : "a"(nr), [a1] "r"(a1), [a2] "r"(a2), [a3] "r"(a3), [a4] "r"(a4) + : "rcx", "r11", "rdi", "rsi", "rdx", "r8", "r9", "r10", "memory"); + return ret; +} + +/* Syscall numbers */ +#define SYS_EXIT 0 +#define SYS_PRINT 4 +#define SYS_PUTCHAR 5 +#define SYS_OPEN 6 +#define SYS_READ 7 +#define SYS_GETSIZE 8 +#define SYS_CLOSE 9 +#define SYS_ALLOC 11 +#define SYS_FREE 12 + +/* ======================================================================== + errno + ======================================================================== */ + +int errno = 0; + +/* ======================================================================== + string.h functions + ======================================================================== */ + +void *memcpy(void *dest, const void *src, size_t n) { + unsigned char *d = (unsigned char *)dest; + const unsigned char *s = (const unsigned char *)src; + for (size_t i = 0; i < n; i++) + d[i] = s[i]; + return dest; +} + +void *memset(void *s, int c, size_t n) { + unsigned char *p = (unsigned char *)s; + for (size_t i = 0; i < n; i++) + p[i] = (unsigned char)c; + return s; +} + +void *memmove(void *dest, const void *src, size_t n) { + unsigned char *d = (unsigned char *)dest; + const unsigned char *s = (const unsigned char *)src; + if (s < d && d < s + n) { + for (size_t i = n; i > 0; i--) + d[i - 1] = s[i - 1]; + } else { + for (size_t i = 0; i < n; i++) + d[i] = s[i]; + } + return dest; +} + +int memcmp(const void *s1, const void *s2, size_t n) { + const unsigned char *a = (const unsigned char *)s1; + const unsigned char *b = (const unsigned char *)s2; + for (size_t i = 0; i < n; i++) { + if (a[i] != b[i]) + return a[i] < b[i] ? -1 : 1; + } + return 0; +} + +size_t strlen(const char *s) { + size_t len = 0; + while (s[len]) len++; + return len; +} + +int strcmp(const char *a, const char *b) { + while (*a && *a == *b) { a++; b++; } + return (unsigned char)*a - (unsigned char)*b; +} + +int strncmp(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + if (a[i] != b[i] || a[i] == 0) + return (unsigned char)a[i] - (unsigned char)b[i]; + } + return 0; +} + +char *strcpy(char *dest, const char *src) { + char *d = dest; + while ((*d++ = *src++)); + return dest; +} + +char *strncpy(char *dest, const char *src, size_t n) { + size_t i; + for (i = 0; i < n && src[i]; i++) + dest[i] = src[i]; + for (; i < n; i++) + dest[i] = '\0'; + return dest; +} + +char *strcat(char *dest, const char *src) { + char *d = dest + strlen(dest); + while ((*d++ = *src++)); + return dest; +} + +char *strncat(char *dest, const char *src, size_t n) { + char *d = dest + strlen(dest); + size_t i; + for (i = 0; i < n && src[i]; i++) + d[i] = src[i]; + d[i] = '\0'; + return dest; +} + +char *strchr(const char *s, int c) { + while (*s) { + if (*s == (char)c) return (char *)s; + s++; + } + return (c == 0) ? (char *)s : NULL; +} + +char *strrchr(const char *s, int c) { + const char *last = NULL; + while (*s) { + if (*s == (char)c) last = s; + s++; + } + if (c == 0) return (char *)s; + return (char *)last; +} + +static int _tolower(int c) { + return (c >= 'A' && c <= 'Z') ? c + 32 : c; +} + +int strcasecmp(const char *a, const char *b) { + while (*a && _tolower((unsigned char)*a) == _tolower((unsigned char)*b)) { a++; b++; } + return _tolower((unsigned char)*a) - _tolower((unsigned char)*b); +} + +int strncasecmp(const char *a, const char *b, size_t n) { + for (size_t i = 0; i < n; i++) { + int ca = _tolower((unsigned char)a[i]); + int cb = _tolower((unsigned char)b[i]); + if (ca != cb || ca == 0) return ca - cb; + } + return 0; +} + +char *strstr(const char *haystack, const char *needle) { + if (!*needle) return (char *)haystack; + size_t nlen = strlen(needle); + while (*haystack) { + if (strncmp(haystack, needle, nlen) == 0) + return (char *)haystack; + haystack++; + } + return NULL; +} + +/* ======================================================================== + ctype.h functions + ======================================================================== */ + +int isalpha(int c) { return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); } +int isdigit(int c) { return c >= '0' && c <= '9'; } +int isalnum(int c) { return isalpha(c) || isdigit(c); } +int isspace(int c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'; } +int isupper(int c) { return c >= 'A' && c <= 'Z'; } +int islower(int c) { return c >= 'a' && c <= 'z'; } +int isprint(int c) { return c >= 0x20 && c <= 0x7E; } +int ispunct(int c) { return isprint(c) && !isalnum(c) && c != ' '; } +int isxdigit(int c) { return isdigit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } +int iscntrl(int c) { return (c >= 0 && c < 0x20) || c == 0x7F; } +int isgraph(int c) { return c > 0x20 && c <= 0x7E; } +int toupper(int c) { return (c >= 'a' && c <= 'z') ? c - 32 : c; } +int tolower(int c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } + +/* ======================================================================== + Heap allocator (free-list, backed by SYS_ALLOC) + ======================================================================== */ + +#define HEAP_MAGIC 0x5A484541ULL /* "ZHEA" */ + +struct HeapHeader { + uint64_t magic; + uint64_t size; +} __attribute__((packed)); + +struct FreeNode { + uint64_t size; + struct FreeNode *next; +}; + +static struct FreeNode g_heapHead = { 0, NULL }; +static int g_heapInit = 0; + +static void heap_insert_free(void *ptr, uint64_t size) { + struct FreeNode *node = (struct FreeNode *)ptr; + node->size = size; + node->next = g_heapHead.next; + g_heapHead.next = node; +} + +static void heap_grow(uint64_t bytes) { + uint64_t pages = (bytes + 0xFFF) / 0x1000; + if (pages < 4) pages = 4; + void *mem = (void *)_zos_syscall1(SYS_ALLOC, (long)(pages * 0x1000)); + if (mem != NULL) + heap_insert_free(mem, pages * 0x1000); +} + +void *malloc(size_t size) { + if (!g_heapInit) { + heap_grow(16 * 0x1000); + g_heapInit = 1; + } + + uint64_t needed = size + sizeof(struct HeapHeader); + needed = (needed + 15) & ~15ULL; + + struct FreeNode *prev = &g_heapHead; + struct FreeNode *cur = g_heapHead.next; + + while (cur != NULL) { + if (cur->size >= needed) { + uint64_t blockSize = cur->size; + prev->next = cur->next; + + if (blockSize > needed + sizeof(struct FreeNode) + 16) { + void *rest = (void *)((uint8_t *)cur + needed); + heap_insert_free(rest, blockSize - needed); + } + + struct HeapHeader *hdr = (struct HeapHeader *)cur; + hdr->magic = HEAP_MAGIC; + hdr->size = size; + return (void *)((uint8_t *)hdr + sizeof(struct HeapHeader)); + } + prev = cur; + cur = cur->next; + } + + heap_grow(needed); + return malloc(size); +} + +void free(void *ptr) { + if (ptr == NULL) return; + + struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); + uint64_t blockSize = hdr->size + sizeof(struct HeapHeader); + blockSize = (blockSize + 15) & ~15ULL; + heap_insert_free((void *)hdr, blockSize); +} + +void *calloc(size_t nmemb, size_t size) { + size_t total = nmemb * size; + void *p = malloc(total); + if (p) memset(p, 0, total); + return p; +} + +void *realloc(void *ptr, size_t size) { + if (ptr == NULL) return malloc(size); + if (size == 0) { free(ptr); return NULL; } + + struct HeapHeader *hdr = (struct HeapHeader *)((uint8_t *)ptr - sizeof(struct HeapHeader)); + uint64_t old = hdr->size; + + void *newp = malloc(size); + if (newp == NULL) return NULL; + + size_t copySize = old < size ? old : size; + memcpy(newp, ptr, copySize); + free(ptr); + return newp; +} + +char *strdup(const char *s) { + size_t len = strlen(s) + 1; + char *d = (char *)malloc(len); + if (d) memcpy(d, s, len); + return d; +} + +/* ======================================================================== + stdlib.h functions + ======================================================================== */ + +int abs(int x) { return x < 0 ? -x : x; } +long labs(long x) { return x < 0 ? -x : x; } + +int atoi(const char *s) { + int neg = 0, val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + while (isdigit((unsigned char)*s)) { + val = val * 10 + (*s - '0'); + s++; + } + return neg ? -val : val; +} + +long atol(const char *s) { + long neg = 0, val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + while (isdigit((unsigned char)*s)) { + val = val * 10 + (*s - '0'); + s++; + } + return neg ? -val : val; +} + +long strtol(const char *nptr, char **endptr, int base) { + const char *s = nptr; + long val = 0; + int neg = 0; + + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') { s++; } + + if (base == 0) { + if (*s == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } + else if (*s == '0') { base = 8; s++; } + else base = 10; + } else if (base == 16 && *s == '0' && (s[1] == 'x' || s[1] == 'X')) { + s += 2; + } + + while (*s) { + int digit; + if (*s >= '0' && *s <= '9') digit = *s - '0'; + else if (*s >= 'a' && *s <= 'z') digit = *s - 'a' + 10; + else if (*s >= 'A' && *s <= 'Z') digit = *s - 'A' + 10; + else break; + if (digit >= base) break; + val = val * base + digit; + s++; + } + + if (endptr) *endptr = (char *)s; + return neg ? -val : val; +} + +unsigned long strtoul(const char *nptr, char **endptr, int base) { + return (unsigned long)strtol(nptr, endptr, base); +} + +void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)) { + /* Simple insertion sort — sufficient for doom's usage */ + char *arr = (char *)base; + char tmp[256]; /* doom's qsort elements are small */ + for (size_t i = 1; i < nmemb; i++) { + memcpy(tmp, arr + i * size, size); + size_t j = i; + while (j > 0 && compar(arr + (j - 1) * size, tmp) > 0) { + memcpy(arr + j * size, arr + (j - 1) * size, size); + j--; + } + memcpy(arr + j * size, tmp, size); + } +} + +static unsigned int _rand_seed = 1; + +int rand(void) { + _rand_seed = _rand_seed * 1103515245 + 12345; + return (int)((_rand_seed >> 16) & 0x7FFF); +} + +void srand(unsigned int seed) { + _rand_seed = seed; +} + +char *getenv(const char *name) { + (void)name; + return NULL; +} + +static void (*_atexit_funcs[32])(void); +static int _atexit_count = 0; + +int atexit(void (*func)(void)) { + if (_atexit_count >= 32) return -1; + _atexit_funcs[_atexit_count++] = func; + return 0; +} + +void exit(int status) { + for (int i = _atexit_count - 1; i >= 0; i--) + _atexit_funcs[i](); + _zos_syscall1(SYS_EXIT, (long)status); + __builtin_unreachable(); +} + +void abort(void) { + _zos_syscall1(SYS_PRINT, (long)"abort() called\n"); + _zos_syscall1(SYS_EXIT, 1); + __builtin_unreachable(); +} + +/* ======================================================================== + assert.h support + ======================================================================== */ + +void __assert_fail(const char *expr, const char *file, int line, const char *func) { + _zos_syscall1(SYS_PRINT, (long)"Assertion failed: "); + _zos_syscall1(SYS_PRINT, (long)expr); + _zos_syscall1(SYS_PRINT, (long)" at "); + _zos_syscall1(SYS_PRINT, (long)file); + _zos_syscall1(SYS_PRINT, (long)"\n"); + (void)line; (void)func; + abort(); +} + +/* ======================================================================== + printf family — vsnprintf core + ======================================================================== */ + +/* Internal: write a char to buffer if space remains */ +struct _pf_state { + char *buf; + size_t pos; + size_t max; +}; + +static void _pf_putc(struct _pf_state *st, char c) { + if (st->pos < st->max) + st->buf[st->pos] = c; + st->pos++; +} + +static void _pf_puts(struct _pf_state *st, const char *s) { + while (*s) _pf_putc(st, *s++); +} + +static void _pf_putnum(struct _pf_state *st, unsigned long val, int base, int upper, int width, char pad, int neg, int precision) { + char tmp[24]; + int i = 0; + const char *digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + + if (val == 0) { + tmp[i++] = '0'; + } else { + while (val > 0) { + tmp[i++] = digits[val % base]; + val /= base; + } + } + + /* Precision: minimum number of digits (zero-pad the digits themselves) */ + int digitCount = i; + int precPad = 0; + if (precision > digitCount) + precPad = precision - digitCount; + + /* Total output length: sign + precPad + digits */ + int total = (neg ? 1 : 0) + precPad + digitCount; + + /* Width padding (if pad is '0' and no precision, treat as digit padding) */ + if (neg && pad == '0' && precision < 0) { + _pf_putc(st, '-'); + } + if (precision < 0 && pad == '0') { + /* Old behavior: '0' flag pads digits */ + for (int w = total; w < width; w++) + _pf_putc(st, '0'); + } else { + /* Space padding on the left */ + for (int w = total; w < width; w++) + _pf_putc(st, ' '); + } + if (neg && !(pad == '0' && precision < 0)) { + _pf_putc(st, '-'); + } + + /* Precision zero-padding */ + for (int p = 0; p < precPad; p++) + _pf_putc(st, '0'); + + while (i > 0) + _pf_putc(st, tmp[--i]); +} + +int vsnprintf(char *buf, size_t size, const char *fmt, va_list ap) { + struct _pf_state st; + st.buf = buf; + st.pos = 0; + st.max = size > 0 ? size - 1 : 0; + + while (*fmt) { + if (*fmt != '%') { + _pf_putc(&st, *fmt++); + continue; + } + fmt++; /* skip '%' */ + + /* Flags */ + char pad = ' '; + int left_align = 0; + while (*fmt == '0' || *fmt == '-' || *fmt == '+' || *fmt == ' ') { + if (*fmt == '0') pad = '0'; + if (*fmt == '-') { left_align = 1; pad = ' '; } + fmt++; + } + + /* Width */ + int width = 0; + if (*fmt == '*') { + width = va_arg(ap, int); + fmt++; + } else { + while (*fmt >= '0' && *fmt <= '9') { + width = width * 10 + (*fmt - '0'); + fmt++; + } + } + + /* Precision (consumed but mostly ignored) */ + int precision = -1; + if (*fmt == '.') { + fmt++; + precision = 0; + if (*fmt == '*') { + precision = va_arg(ap, int); + fmt++; + } else { + while (*fmt >= '0' && *fmt <= '9') { + precision = precision * 10 + (*fmt - '0'); + fmt++; + } + } + } + + /* Length modifier */ + int is_long = 0; + if (*fmt == 'l') { is_long = 1; fmt++; if (*fmt == 'l') { is_long = 2; fmt++; } } + else if (*fmt == 'h') { fmt++; if (*fmt == 'h') fmt++; } + else if (*fmt == 'z') { is_long = 1; fmt++; } + + /* Conversion */ + switch (*fmt) { + case 'd': case 'i': { + long val; + if (is_long >= 1) val = va_arg(ap, long); + else val = va_arg(ap, int); + int neg = 0; + unsigned long uval; + if (val < 0) { neg = 1; uval = (unsigned long)(-val); } + else uval = (unsigned long)val; + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, uval, 10, 0, 0, pad, neg, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, uval, 10, 0, width, pad, neg, precision); + } + break; + } + case 'u': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, val, 10, 0, 0, ' ', 0, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, val, 10, 0, width, pad, 0, precision); + } + break; + } + case 'x': case 'X': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + int upper = (*fmt == 'X'); + if (left_align) { + size_t before = st.pos; + _pf_putnum(&st, val, 16, upper, 0, pad, 0, precision); + size_t len = st.pos - before; + for (size_t w = len; (int)w < width; w++) _pf_putc(&st, ' '); + } else { + _pf_putnum(&st, val, 16, upper, width, pad, 0, precision); + } + break; + } + case 'o': { + unsigned long val; + if (is_long >= 1) val = va_arg(ap, unsigned long); + else val = va_arg(ap, unsigned int); + _pf_putnum(&st, val, 8, 0, width, pad, 0, precision); + break; + } + case 'p': { + void *val = va_arg(ap, void *); + _pf_puts(&st, "0x"); + _pf_putnum(&st, (unsigned long)val, 16, 0, 0, '0', 0, -1); + break; + } + case 's': { + const char *s = va_arg(ap, const char *); + if (s == NULL) s = "(null)"; + int slen = (int)strlen(s); + if (precision >= 0 && precision < slen) slen = precision; + if (!left_align) { + for (int w = slen; w < width; w++) _pf_putc(&st, ' '); + } + for (int i = 0; i < slen; i++) _pf_putc(&st, s[i]); + if (left_align) { + for (int w = slen; w < width; w++) _pf_putc(&st, ' '); + } + break; + } + case 'c': { + char c = (char)va_arg(ap, int); + _pf_putc(&st, c); + break; + } + case '%': + _pf_putc(&st, '%'); + break; + default: + _pf_putc(&st, '%'); + _pf_putc(&st, *fmt); + break; + } + if (*fmt) fmt++; + } + + /* Null terminate */ + if (size > 0) { + if (st.pos < size) + st.buf[st.pos] = '\0'; + else + st.buf[size - 1] = '\0'; + } + + return (int)st.pos; +} + +int vsprintf(char *buf, const char *fmt, va_list ap) { + return vsnprintf(buf, (size_t)-1, fmt, ap); +} + +int snprintf(char *buf, size_t size, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, size, fmt, ap); + va_end(ap); + return ret; +} + +int sprintf(char *buf, const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, (size_t)-1, fmt, ap); + va_end(ap); + return ret; +} + +/* Print buffer for printf/fprintf */ +static char _printbuf[4096]; + +int vprintf(const char *fmt, va_list ap) { + int ret = vsnprintf(_printbuf, sizeof(_printbuf), fmt, ap); + _zos_syscall1(SYS_PRINT, (long)_printbuf); + return ret; +} + +int printf(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + int ret = vprintf(fmt, ap); + va_end(ap); + return ret; +} + +int puts(const char *s) { + _zos_syscall1(SYS_PRINT, (long)s); + _zos_syscall1(SYS_PUTCHAR, (long)'\n'); + return 0; +} + +int putchar(int c) { + _zos_syscall1(SYS_PUTCHAR, (long)c); + return c; +} + +/* ======================================================================== + FILE I/O + ======================================================================== */ + +/* Simple FILE struct */ +struct _FILE { + int handle; + uint64_t pos; + uint64_t size; + int eof; + int error; + int is_std; /* 1 = stdout, 2 = stderr */ + int ungetc_buf; /* -1 if empty */ +}; + +typedef struct _FILE FILE; + +/* Up to 16 open files */ +#define MAX_FILES 16 +static FILE _file_pool[MAX_FILES]; +static int _file_pool_init = 0; + +/* Standard streams */ +static FILE _stdout_file = { -1, 0, 0, 0, 0, 1, -1 }; +static FILE _stderr_file = { -1, 0, 0, 0, 0, 2, -1 }; +static FILE _stdin_file = { -1, 0, 0, 0, 0, 3, -1 }; + +FILE *stdout = &_stdout_file; +FILE *stderr = &_stderr_file; +FILE *stdin = &_stdin_file; + +static FILE *alloc_file(void) { + if (!_file_pool_init) { + memset(_file_pool, 0, sizeof(_file_pool)); + for (int i = 0; i < MAX_FILES; i++) { + _file_pool[i].handle = -1; + _file_pool[i].ungetc_buf = -1; + } + _file_pool_init = 1; + } + for (int i = 0; i < MAX_FILES; i++) { + if (_file_pool[i].handle == -1 && _file_pool[i].is_std == 0) { + return &_file_pool[i]; + } + } + return NULL; +} + +FILE *fopen(const char *path, const char *mode) { + (void)mode; + + /* Build VFS path: prepend "0:/" if not already prefixed */ + char vfspath[256]; + if (path[0] == '0' && path[1] == ':') { + /* Already has VFS prefix */ + size_t len = strlen(path); + if (len >= sizeof(vfspath)) len = sizeof(vfspath) - 1; + memcpy(vfspath, path, len); + vfspath[len] = '\0'; + } else { + /* Prepend "0:/" */ + const char *prefix = "0:/"; + int i = 0; + while (prefix[i]) { vfspath[i] = prefix[i]; i++; } + int j = 0; + while (path[j] && i < 254) { vfspath[i++] = path[j++]; } + vfspath[i] = '\0'; + } + + int handle = (int)_zos_syscall1(SYS_OPEN, (long)vfspath); + if (handle < 0) { + errno = 2; /* ENOENT */ + return NULL; + } + + FILE *fp = alloc_file(); + if (fp == NULL) { + _zos_syscall1(SYS_CLOSE, (long)handle); + errno = 12; /* ENOMEM */ + return NULL; + } + + fp->handle = handle; + fp->pos = 0; + fp->size = (uint64_t)_zos_syscall1(SYS_GETSIZE, (long)handle); + fp->eof = 0; + fp->error = 0; + fp->is_std = 0; + fp->ungetc_buf = -1; + + return fp; +} + +int fclose(FILE *fp) { + if (fp == NULL || fp->is_std) return -1; + _zos_syscall1(SYS_CLOSE, (long)fp->handle); + fp->handle = -1; + fp->is_std = 0; + return 0; +} + +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp) { + if (fp == NULL || fp->is_std || size == 0 || nmemb == 0) return 0; + + size_t total = size * nmemb; + size_t remaining = 0; + if (fp->pos < fp->size) + remaining = (size_t)(fp->size - fp->pos); + + if (total > remaining) total = remaining; + if (total == 0) { fp->eof = 1; return 0; } + + int bytes = (int)_zos_syscall4(SYS_READ, + (long)fp->handle, (long)ptr, (long)fp->pos, (long)total); + + if (bytes <= 0) { fp->eof = 1; return 0; } + + fp->pos += (uint64_t)bytes; + if (fp->pos >= fp->size) fp->eof = 1; + + return (size_t)bytes / size; +} + +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp) { + if (fp == NULL) return 0; + + /* stdout/stderr: write to terminal */ + if (fp->is_std == 1 || fp->is_std == 2) { + /* Write as string to terminal */ + size_t total = size * nmemb; + const char *s = (const char *)ptr; + char buf[512]; + while (total > 0) { + size_t chunk = total > 511 ? 511 : total; + memcpy(buf, s, chunk); + buf[chunk] = '\0'; + _zos_syscall1(SYS_PRINT, (long)buf); + s += chunk; + total -= chunk; + } + return nmemb; + } + + /* Read-only filesystem, no-op */ + return 0; +} + +int fseek(FILE *fp, long offset, int whence) { + if (fp == NULL || fp->is_std) return -1; + + uint64_t newpos; + switch (whence) { + case 0: /* SEEK_SET */ + newpos = (uint64_t)offset; + break; + case 1: /* SEEK_CUR */ + newpos = fp->pos + (uint64_t)offset; + break; + case 2: /* SEEK_END */ + newpos = fp->size + (uint64_t)offset; + break; + default: + return -1; + } + + fp->pos = newpos; + fp->eof = 0; + return 0; +} + +long ftell(FILE *fp) { + if (fp == NULL || fp->is_std) return -1; + return (long)fp->pos; +} + +int fflush(FILE *fp) { + (void)fp; + return 0; +} + +int feof(FILE *fp) { + if (fp == NULL) return 1; + return fp->eof; +} + +int ferror(FILE *fp) { + if (fp == NULL) return 1; + return fp->error; +} + +void clearerr(FILE *fp) { + if (fp == NULL) return; + fp->eof = 0; + fp->error = 0; +} + +int fgetc(FILE *fp) { + if (fp == NULL) return -1; + if (fp->ungetc_buf >= 0) { + int c = fp->ungetc_buf; + fp->ungetc_buf = -1; + return c; + } + unsigned char c; + size_t n = fread(&c, 1, 1, fp); + return n == 1 ? (int)c : -1; +} + +int getc(FILE *fp) { + return fgetc(fp); +} + +int ungetc(int c, FILE *fp) { + if (fp == NULL || c == -1) return -1; + fp->ungetc_buf = c; + fp->eof = 0; + return c; +} + +char *fgets(char *s, int size, FILE *fp) { + if (size <= 0) return NULL; + int i = 0; + while (i < size - 1) { + int c = fgetc(fp); + if (c == -1) break; + s[i++] = (char)c; + if (c == '\n') break; + } + if (i == 0) return NULL; + s[i] = '\0'; + return s; +} + +int fputs(const char *s, FILE *fp) { + size_t len = strlen(s); + return fwrite(s, 1, len, fp) > 0 ? 0 : -1; +} + +int fprintf(FILE *fp, const char *fmt, ...) { + char buf[4096]; + va_list ap; + va_start(ap, fmt); + int ret = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + if (fp == stdout || fp == stderr || (fp && fp->is_std)) { + _zos_syscall1(SYS_PRINT, (long)buf); + } + return ret; +} + +int vfprintf(FILE *fp, const char *fmt, va_list ap) { + char buf[4096]; + int ret = vsnprintf(buf, sizeof(buf), fmt, ap); + + if (fp == stdout || fp == stderr || (fp && fp->is_std)) { + _zos_syscall1(SYS_PRINT, (long)buf); + } + return ret; +} + +int sscanf(const char *str, const char *fmt, ...) { + /* Minimal sscanf: supports %d, %s, %x, %u only */ + va_list ap; + va_start(ap, fmt); + int count = 0; + const char *s = str; + + while (*fmt && *s) { + if (*fmt == '%') { + fmt++; + if (*fmt == 'd' || *fmt == 'i') { + int *out = va_arg(ap, int *); + int neg = 0; + int val = 0; + while (isspace((unsigned char)*s)) s++; + if (*s == '-') { neg = 1; s++; } + else if (*s == '+') s++; + if (!isdigit((unsigned char)*s)) break; + while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } + *out = neg ? -val : val; + count++; + fmt++; + } else if (*fmt == 'u') { + unsigned int *out = va_arg(ap, unsigned int *); + unsigned int val = 0; + while (isspace((unsigned char)*s)) s++; + if (!isdigit((unsigned char)*s)) break; + while (isdigit((unsigned char)*s)) { val = val * 10 + (*s - '0'); s++; } + *out = val; + count++; + fmt++; + } else if (*fmt == 'x' || *fmt == 'X') { + unsigned int *out = va_arg(ap, unsigned int *); + unsigned int val = 0; + while (isspace((unsigned char)*s)) s++; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + while (isxdigit((unsigned char)*s)) { + int d; + if (*s >= '0' && *s <= '9') d = *s - '0'; + else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; + else d = *s - 'A' + 10; + val = val * 16 + d; + s++; + } + *out = val; + count++; + fmt++; + } else if (*fmt == 's') { + char *out = va_arg(ap, char *); + while (isspace((unsigned char)*s)) s++; + while (*s && !isspace((unsigned char)*s)) *out++ = *s++; + *out = '\0'; + count++; + fmt++; + } else { + break; + } + } else if (isspace((unsigned char)*fmt)) { + while (isspace((unsigned char)*s)) s++; + while (isspace((unsigned char)*fmt)) fmt++; + } else { + if (*s != *fmt) break; + s++; + fmt++; + } + } + + va_end(ap); + return count; +} + +void perror(const char *s) { + if (s && *s) { + _zos_syscall1(SYS_PRINT, (long)s); + _zos_syscall1(SYS_PRINT, (long)": "); + } + _zos_syscall1(SYS_PRINT, (long)"error\n"); +} + +int rename(const char *old, const char *new_) { + (void)old; (void)new_; + return -1; +} + +int remove(const char *path) { + (void)path; + return -1; +} + +FILE *tmpfile(void) { + return NULL; +} + +char *tmpnam(char *s) { + (void)s; + return NULL; +} + +/* ======================================================================== + Stubs for unneeded functions + ======================================================================== */ + +int mkdir(const char *path, unsigned int mode) { + (void)path; (void)mode; + return -1; +} + +/* math.h stubs — doom's fixed-point code doesn't use libm heavily, + but z_zone.c references some functions. Provide no-ops. */ +double fabs(double x) { return x < 0 ? -x : x; } +double floor(double x) { return (double)(long)x - (x < (double)(long)x ? 1.0 : 0.0); } +double ceil(double x) { double f = floor(x); return (x > f) ? f + 1.0 : f; } +double fmod(double x, double y) { if (y == 0.0) return 0.0; return x - (double)((long)(x/y)) * y; } +double sqrt(double x) { + if (x <= 0.0) return 0.0; + double guess = x; + for (int i = 0; i < 20; i++) + guess = (guess + x / guess) * 0.5; + return guess; +} +double pow(double base, double exp) { + if (exp == 0.0) return 1.0; + if (exp == 1.0) return base; + /* Integer exponent fast path */ + if (exp == (double)(long)exp && exp > 0) { + double r = 1.0; + long e = (long)exp; + while (e-- > 0) r *= base; + return r; + } + return 0.0; +} +double sin(double x) { (void)x; return 0.0; } +double cos(double x) { (void)x; return 1.0; } +double atan2(double y, double x) { (void)y; (void)x; return 0.0; } +double log(double x) { (void)x; return 0.0; } +double exp(double x) { (void)x; return 1.0; } +double round(double x) { return floor(x + 0.5); } +double atof(const char *s) { (void)s; return 0.0; } +float floorf(float x) { return (float)floor((double)x); } +float ceilf(float x) { return (float)ceil((double)x); } +float fabsf(float x) { return x < 0.0f ? -x : x; } + +/* ======================================================================== + Misc stubs that doom might reference + ======================================================================== */ + +/* Some doom code calls I_Error which ends up doing fprintf + exit */ +/* The signal/system stuff is behind ifdefs, but just in case: */ + +unsigned int sleep(unsigned int seconds) { + (void)seconds; + return 0; +} + +int system(const char *command) { + (void)command; + return -1; +} diff --git a/programs/src/man/main.cpp b/programs/src/man/main.cpp new file mode 100644 index 0000000..c935e2b --- /dev/null +++ b/programs/src/man/main.cpp @@ -0,0 +1,374 @@ +/* + * main.cpp + * Manual page viewer for ZenithOS + * Fullscreen pager with ANSI formatting and keyboard navigation + * Copyright (c) 2025 Daniel Hammer +*/ + +#include +#include + +// ---- Utility functions ---- + +static bool starts_with(const char* str, const char* prefix) { + while (*prefix) { + if (*str != *prefix) return false; + str++; prefix++; + } + return true; +} + +static const char* skip_spaces(const char* s) { + while (*s == ' ') s++; + return s; +} + +static int slen(const char* s) { + int n = 0; + while (s[n]) n++; + return n; +} + +static void print_int(uint64_t n) { + if (n == 0) { + zenith::putchar('0'); + return; + } + char buf[20]; + int i = 0; + while (n > 0) { + buf[i++] = '0' + (n % 10); + n /= 10; + } + for (int j = i - 1; j >= 0; j--) { + zenith::putchar(buf[j]); + } +} + +// ---- Pager rendering ---- + +static constexpr int MAN_MAX_LINES = 2048; + +static int int_to_buf(char* buf, int n) { + if (n == 0) { buf[0] = '0'; return 1; } + char tmp[12]; + int i = 0; + while (n > 0) { tmp[i++] = '0' + (n % 10); n /= 10; } + for (int j = 0; j < i; j++) buf[j] = tmp[i - 1 - j]; + return i; +} + +static void cursor_to(int row, int col) { + char seq[24] = "\033["; + int p = 2; + p += int_to_buf(seq + p, row); + seq[p++] = ';'; + p += int_to_buf(seq + p, col); + seq[p++] = 'H'; + seq[p] = '\0'; + zenith::print(seq); +} + +struct ManLine { + const char* text; + int len; + bool isSH; + bool isSS; + bool isBold; + bool isTH; +}; + +static void man_render(ManLine* lines, int totalLines, int scroll, int rows, int cols, + const char* name, int section) { + int contentRows = rows - 1; + + for (int r = 0; r < contentRows; r++) { + cursor_to(r + 1, 1); + zenith::print("\033[2K"); + + int idx = scroll + r; + if (idx < 0 || idx >= totalLines) continue; + + ManLine& ln = lines[idx]; + if (ln.isTH) continue; + + if (ln.isSH || ln.isSS || ln.isBold) { + zenith::print("\033[1m"); + } + + if (ln.isSS) { + zenith::print(" "); + } + + int maxW = cols; + if (ln.isSS) maxW -= 3; + int printLen = ln.len; + if (printLen > maxW) printLen = maxW; + for (int c = 0; c < printLen; c++) { + zenith::putchar(ln.text[c]); + } + + if (ln.isSH || ln.isSS || ln.isBold) { + zenith::print("\033[0m"); + } + } + + // Status bar + cursor_to(rows, 1); + zenith::print("\033[7m"); + zenith::print(" Manual page "); + zenith::print(name); + zenith::putchar('('); + print_int((uint64_t)section); + zenith::putchar(')'); + zenith::print(" line "); + print_int((uint64_t)(scroll + 1)); + zenith::putchar('/'); + print_int((uint64_t)totalLines); + + int padCount = cols - 30 - slen(name); + for (int i = 0; i < padCount; i++) zenith::putchar(' '); + + zenith::print("\033[0m"); +} + +// ---- Main ---- + +extern "C" void _start() { + // Get arguments passed by the shell (e.g. "intro" or "2 syscalls") + char argbuf[256]; + zenith::getargs(argbuf, sizeof(argbuf)); + + const char* arg = skip_spaces(argbuf); + if (*arg == '\0') { + zenith::print("Usage: man \n"); + zenith::print(" man
\n"); + zenith::print("Try: man intro\n"); + return; + } + + // Parse optional section number and topic name + int section = 0; + const char* topic = arg; + + if (arg[0] >= '1' && arg[0] <= '9' && arg[1] == ' ') { + section = arg[0] - '0'; + topic = skip_spaces(arg + 2); + } + + // Try to open man page file + int handle = -1; + int foundSection = 0; + char path[128]; + + if (section > 0) { + const char* prefix = "0:/man/"; + int p = 0; + while (prefix[p]) { path[p] = prefix[p]; p++; } + int t = 0; + while (topic[t] && p < 120) { path[p++] = topic[t++]; } + path[p++] = '.'; + path[p++] = '0' + section; + path[p] = '\0'; + + handle = zenith::open(path); + if (handle >= 0) foundSection = section; + } else { + for (int s = 1; s <= 3; s++) { + const char* prefix = "0:/man/"; + int p = 0; + while (prefix[p]) { path[p] = prefix[p]; p++; } + int t = 0; + while (topic[t] && p < 120) { path[p++] = topic[t++]; } + path[p++] = '.'; + path[p++] = '0' + s; + path[p] = '\0'; + + handle = zenith::open(path); + if (handle >= 0) { + foundSection = s; + break; + } + } + } + + if (handle < 0) { + zenith::print("No manual entry for "); + zenith::print(topic); + zenith::putchar('\n'); + return; + } + + // Load entire file into heap + uint64_t fileSize = zenith::getsize(handle); + if (fileSize == 0) { + zenith::close(handle); + zenith::print("Empty manual page.\n"); + return; + } + + char* fileData = (char*)zenith::malloc(fileSize + 1); + if (fileData == nullptr) { + zenith::close(handle); + zenith::print("Out of memory.\n"); + return; + } + + uint64_t offset = 0; + while (offset < fileSize) { + uint64_t chunk = fileSize - offset; + if (chunk > 4096) chunk = 4096; + int bytesRead = zenith::read(handle, (uint8_t*)fileData + offset, offset, chunk); + if (bytesRead <= 0) break; + offset += bytesRead; + } + fileData[offset] = '\0'; + zenith::close(handle); + + // Parse into lines + ManLine* lines = (ManLine*)zenith::malloc(MAN_MAX_LINES * sizeof(ManLine)); + if (lines == nullptr) { + zenith::mfree(fileData); + zenith::print("Out of memory.\n"); + return; + } + + int totalLines = 0; + const char* p = fileData; + while (*p && totalLines < MAN_MAX_LINES) { + const char* lineStart = p; + while (*p && *p != '\n') p++; + int lineLen = (int)(p - lineStart); + if (*p == '\n') p++; + + ManLine& ln = lines[totalLines]; + ln.isSH = false; + ln.isSS = false; + ln.isBold = false; + ln.isTH = false; + + if (starts_with(lineStart, ".TH ")) { + ln.isTH = true; + ln.text = lineStart + 4; + ln.len = lineLen - 4; + } else if (starts_with(lineStart, ".SH ")) { + ln.isSH = true; + ln.text = lineStart + 4; + ln.len = lineLen - 4; + } else if (starts_with(lineStart, ".SS ")) { + ln.isSS = true; + ln.text = lineStart + 4; + ln.len = lineLen - 4; + } else if (starts_with(lineStart, ".B ")) { + ln.isBold = true; + ln.text = lineStart + 3; + ln.len = lineLen - 3; + } else if (starts_with(lineStart, ".BI ")) { + ln.isBold = true; + ln.text = lineStart + 4; + ln.len = lineLen - 4; + } else { + ln.text = lineStart; + ln.len = lineLen; + } + + totalLines++; + } + + if (totalLines == 0) { + zenith::mfree(lines); + zenith::mfree(fileData); + zenith::print("Empty manual page.\n"); + return; + } + + // Get terminal dimensions + int cols = 80, rows = 25; + zenith::termsize(&cols, &rows); + + // Enter alternate screen, hide cursor + zenith::print("\033[?1049h"); + zenith::print("\033[?25l"); + + int scroll = 0; + int maxScroll = totalLines - (rows - 1); + if (maxScroll < 0) maxScroll = 0; + + man_render(lines, totalLines, scroll, rows, cols, topic, foundSection); + + // Event loop — yield while waiting for key input + bool running = true; + while (running) { + while (!zenith::is_key_available()) { + zenith::yield(); + } + + Zenith::KeyEvent ev; + zenith::getkey(&ev); + if (!ev.pressed) continue; + + int contentRows = rows - 1; + + switch (ev.ascii) { + case 'q': + running = false; + break; + case 'j': + if (scroll < maxScroll) scroll++; + break; + case 'k': + if (scroll > 0) scroll--; + break; + case ' ': + scroll += contentRows; + if (scroll > maxScroll) scroll = maxScroll; + break; + case 'b': + scroll -= contentRows; + if (scroll < 0) scroll = 0; + break; + case 'g': + scroll = 0; + break; + case 'G': + scroll = maxScroll; + break; + default: + // Handle scancodes for special keys + switch (ev.scancode) { + case 0x48: // Up arrow + if (scroll > 0) scroll--; + break; + case 0x50: // Down arrow + if (scroll < maxScroll) scroll++; + break; + case 0x49: // Page Up + scroll -= contentRows; + if (scroll < 0) scroll = 0; + break; + case 0x51: // Page Down + scroll += contentRows; + if (scroll > maxScroll) scroll = maxScroll; + break; + case 0x47: // Home + scroll = 0; + break; + case 0x4F: // End + scroll = maxScroll; + break; + } + break; + } + + if (running) { + man_render(lines, totalLines, scroll, rows, cols, topic, foundSection); + } + } + + // Restore screen + zenith::print("\033[?25h"); + zenith::print("\033[?1049l"); + + zenith::mfree(lines); + zenith::mfree(fileData); +} diff --git a/programs/src/shell/main.cpp b/programs/src/shell/main.cpp index bf5079e..5318ba9 100644 --- a/programs/src/shell/main.cpp +++ b/programs/src/shell/main.cpp @@ -44,15 +44,17 @@ static void print_int(uint64_t n) { } static void prompt() { - zenith::print("zenith> "); + zenith::print("% "); } static void cmd_help() { zenith::print("Available commands:\n"); zenith::print(" help Show this help message\n"); zenith::print(" info Show system information\n"); + zenith::print(" man View manual pages\n"); zenith::print(" ls List ramdisk files\n"); zenith::print(" cat Display file contents\n"); + zenith::print(" run Spawn a new process from an ELF file\n"); zenith::print(" ping Send ICMP echo requests\n"); zenith::print(" uptime Show uptime in milliseconds\n"); zenith::print(" clear Clear the screen\n"); @@ -223,11 +225,52 @@ static void cmd_ping(const char* arg) { } } -static void cmd_clear() { - // Print enough newlines to scroll past visible content - for (int i = 0; i < 50; i++) { - zenith::putchar('\n'); +static void cmd_run(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: run \n"); + return; } + + // Build path "0:/" + char path[128]; + const char* prefix = "0:/"; + int i = 0; + while (prefix[i]) { path[i] = prefix[i]; i++; } + int j = 0; + while (arg[j] && i < 126) { path[i++] = arg[j++]; } + path[i] = '\0'; + + int pid = zenith::spawn(path); + if (pid < 0) { + zenith::print("Error: failed to spawn '"); + zenith::print(arg); + zenith::print("'\n"); + } else { + zenith::waitpid(pid); + } +} + +static void cmd_man(const char* arg) { + arg = skip_spaces(arg); + if (*arg == '\0') { + zenith::print("Usage: man \n"); + zenith::print(" man
\n"); + zenith::print("Try: man intro\n"); + return; + } + + int pid = zenith::spawn("0:/man.elf", arg); + if (pid < 0) { + zenith::print("Error: failed to start man viewer\n"); + } else { + zenith::waitpid(pid); + } +} + +static void cmd_clear() { + zenith::print("\033[2J"); // Clear entire screen + zenith::print("\033[H"); // Move cursor to top-left } static void process_command(const char* line) { @@ -241,10 +284,18 @@ static void process_command(const char* line) { cmd_info(); } else if (streq(line, "ls")) { cmd_ls(); + } else if (starts_with(line, "man ")) { + cmd_man(line + 4); + } else if (streq(line, "man")) { + cmd_man(""); } else if (starts_with(line, "cat ")) { cmd_cat(line + 4); } else if (streq(line, "cat")) { cmd_cat(""); + } else if (starts_with(line, "run ")) { + cmd_run(line + 4); + } else if (streq(line, "run")) { + cmd_run(""); } else if (starts_with(line, "ping ")) { cmd_ping(line + 5); } else if (streq(line, "ping")) { @@ -265,7 +316,10 @@ static void process_command(const char* line) { extern "C" void _start() { zenith::print("\n"); - zenith::print(" ZenithOS Shell v0.1\n"); + zenith::print(" ZenithOS\n"); + zenith::print(" Copyright (c) 2025-2026 Daniel Hammer\n"); + zenith::print("\n"); + zenith::print(" Type 'help' for available commands.\n"); zenith::print("\n");