Mean Hamster SoftwareMean Hamster
← HamsterOS

Developer Documentation

HamsterOS Developer SDK

Build external HamsterOS applications with the HAMA app format, or add boot-time hardware support with the HAMD driver format. These docs are synced from the HamsterOS source tree and published here for developers.

HAMA apps

Loadable 32-bit .APP files for tools, games, and user apps.

HAMD drivers

Boot-time .DRV files for storage, audio, and input hardware.

Host APIs

Append-only OS service tables for safe app and driver integration.

HamsterOS Developer SDK

App SDK

Synced Jul 6, 2026

HamsterOS App Developer Guide

Revision: v1 (June 2026) — HAMA App Format v1


Overview

HamsterOS apps are standalone 32-bit i386 binaries loaded from the floppy at runtime. Shipped apps live in four standard folders:

DirectoryPurpose
/APPS/System apps converted from built-in code (e.g. NOTEPAD.APP)
/TOOLS/Hardware tool apps — Disk Copier, CD Player, Floppy Diag, CD ROM Diag, IDE Diag, Format, GFX Diag
/USER/User-installed third-party apps and bundled user apps

The toolbar buttons open /APPS/, /TOOLS/, /USER/, and /GAMES/ as normal file manager windows. Double-clicking a .APP in those folders reads the file from disk, allocates heap memory, copies the code into it, applies relocations, and calls your app_entry() function. When the app closes the heap is freed automatically.

There is no kernel reboot, no relink, no persistent process list. Apps are purely event-driven: the OS calls your draw/input/update functions on demand.

Because floppy and FAT reads are synchronous in the current shell, file-manager launch paths and system-app loaders should paint a small immediate "Loading <name>" toast before starting blocking disk I/O. This keeps the UI from feeling dead during external app loads. While the kernel is in the protected .APP read path, the mouse pointer overlay is intentionally hidden and service callbacks are temporarily suspended so floppy reads are not disturbed. The pointer returns on the next redraw when the app opens or an error dialog appears.

For apps that need large temporary scratch buffers only during one operation (for example BMP load/save staging or a flood-fill queue), HamsterOS also exposes a shared transient workspace API. This lets short-lived work areas come from one reusable pool instead of forcing every app to keep a dedicated persistent heap reservation.


Quick-start: three required files

Every app needs three source files: myapp.c (your logic), myapp_stubs.c (OS call wrappers), and myapp_ext_entry.c (the app_entry() function). See apps/piano_stubs.c and apps/piano_ext_entry.c for complete examples.

myapp_ext_entry.c (always the same shape)

c

#include "app_abi.h"
#include <stddef.h>

/* Forward-declare functions from myapp.c */
void myapp_stubs_set_host(HamsterHostAPI *h);
void myapp_init(void); void myapp_open(void); void myapp_close(void);
bool myapp_is_open(void); bool myapp_is_active(void); bool myapp_has_modal(void);
void myapp_set_active(bool);
bool myapp_scancode(uint8_t);
bool myapp_ptr_down(int16_t, int16_t);
bool myapp_ptr_move(int16_t, int16_t);
bool myapp_ptr_up(int16_t, int16_t);
void myapp_draw(void);
void myapp_bounds(int16_t*, int16_t*, int16_t*, int16_t*);

static AppDescriptor g_desc = {
    myapp_init, myapp_open,
    NULL, NULL, myapp_close,           /* open_file, open_file_at, close */
    myapp_is_open, myapp_is_active, myapp_has_modal, myapp_set_active,
    myapp_scancode,
    myapp_ptr_down, myapp_ptr_move, myapp_ptr_up,
    NULL, NULL,                        /* handle_menu, handle_edit */
    myapp_draw, NULL,                  /* draw, draw_content */
    myapp_bounds, NULL, NULL,          /* get_bounds, content_bounds, status_bounds */
    NULL, NULL,                        /* consume_content_redraw, consume_row */
    NULL, NULL,                        /* update, consume_redraw_request */
    NULL, NULL,                        /* interaction_active, live_interaction_active */
    NULL                               /* open_launch_at */
};

AppDescriptor *app_entry(HamsterHostAPI *host) {
    myapp_stubs_set_host(host);
    return &g_desc;
}

Build toolchain

Requirements: hamsteros-builder Docker image (same one used to build the OS).

sh

CC_FLAGS="-m32 -march=i386 -std=c11 -ffreestanding -fno-builtin -fno-pic -fno-pie \
          -fno-stack-protector -Ikernel -Idrivers -Iapps -Ifs -Wall -Wextra -O2"

gcc $CC_FLAGS -c myapp.c           -o myapp.o
gcc $CC_FLAGS -c myapp_stubs.c     -o myapp_stubs.o
gcc $CC_FLAGS -c myapp_ext_entry.c -o myapp_ext.o

ld -T tools/app.ld -melf_i386 --emit-relocs -nostdlib \
   myapp_ext.o myapp.o myapp_stubs.o -o myapp.elf

python3 tools/mkapp.py --elf myapp.elf --name MYAPP --output MYAPP.APP

Copy MYAPP.APP to /USER/ on the floppy. It appears in the USER file manager folder. For Makefile integration see the PIANO_APP and SLOTS_APP rules in Makefile.

--emit-relocs is required. mkapp.py reads every R_386_32 entry from the ELF .rel.* sections and writes those patch offsets into the HAMA relocation table. These relocations include obvious static data pointers and less-obvious function pointers stored in AppDescriptor. If descriptor relocations are missing, the app may load successfully and then soft-crash when the OS calls init(), open(), or another descriptor callback.


HAMA v1 file format

Offset  Size  Field         Description
------  ----  -----         -----------
0       4     magic         0x414D4148 ("HAMA" little-endian)
4       2     version       1  (loader rejects anything else)
6       2     flags         Bit 0 = HAS_ICON, bit 1 = COMPRESSED, bit 2 = ALIAS
8       4     code_size     Unpacked bytes of code+rodata+data after optional icon
12      4     bss_size      Bytes of zeroed BSS allocated at runtime
16      4     entry_offset  Byte offset of app_entry() in code blob
20      4     reloc_count   Number of uint32 patch offsets after code
24      8     name          Display name: ASCII, null-padded
32      256   icon          Optional 16x16 VGA icon when HAS_ICON is set
32/288  4     packed_size   Present only when COMPRESSED is set
...     ...   code blob     code_size bytes when uncompressed, packed_size bytes when compressed
...     ...   reloc table   reloc_count x uint32_t patch offsets

If HAS_ICON is set, the 256-byte icon is stored immediately after the header and before the code blob. If COMPRESSED is set, a little-endian packed_size field sits after the optional icon and before the packed blob. The loader must skip both the icon block and the optional size field before copying or unpacking code and before locating the relocation table.

Load sequence (kernel/app_loader.c): 1. heap_alloc(file_size) staging buffer, load whole file. 2. Validate magic and version. 3. heap_alloc(code_size + bss_size) runtime region. 4. Skip the optional icon block, then either copy the code blob directly or unpack it into the runtime buffer, then zero BSS. 5. Apply relocations: *(uint32_t*)(base + offset) += (uint32_t)base. 6. heap_free(staging) — staging freed immediately. 7. Call app_entry(host) at base + entry_offset. 8. The owner slot/loader calls desc->init() before first desc->open(). 9. On close: heap_free(code_buf) — all RAM reclaimed.

If the app is launched from a shell surface that performs blocking disk reads on the UI thread, draw and present a loading toast before step 1. Current file-manager and lazy-loader code uses a centered 170x34 light-gray box with a dark-gray border, bold text like Loading SNAKE, and an immediate vga_present_framebuffer(). The pointer overlay is suppressed during the blocking read so users do not see a frozen cursor. This is a visual busy state only; the mouse position is still kept and is redrawn after loading completes.

Load failures must be nonfatal. Missing files, malformed headers, invalid descriptor callbacks, bad 8.3 filenames, out-of-memory conditions, and disk read errors should call shell_show_error() or return an error that the owning shell surface converts into a dialog. Never leave a loading toast on screen as the final state.

Compression and aliases

tools/mkapp.py now supports two build-time space-saving features:

sh

python3 tools/mkapp.py --elf myapp.elf --name MYAPP --output MYAPP.APP
python3 tools/mkapp.py --elf myapp.elf --name MYAPP --no-compress --output MYAPP.APP
python3 tools/mkapp.py --alias-target /TOOLS/MYAPP.APP --name MYAPP --output MYAPP.APP
  • Normal .APP builds automatically compress the code blob when that makes the

file smaller enough to be worth it. The loader inflates the blob after reading it from disk, so floppy launch I/O drops without changing the in-memory app.

  • --no-compress forces the older uncompressed layout.
  • --alias-target creates a tiny shortcut .APP. Alias apps are for shell

surfaces such as /DESKTOP; they point at the real app path and are not executable payloads themselves.

Relocation offsets are flat offsets in the runtime code/data blob because app ELFs are linked as ET_EXEC at address 0. Do not add the target section base again when packing relocations. Doing so drops relocations outside .text, including descriptor callback pointers.


Host services reference (HamsterHostAPI v1)

All services via the HamsterHostAPI* passed to app_entry(). Never call OS functions directly — only through this pointer.

Drawing (VGA)

FieldSignatureNotes
fill_rect(x, y, w, h, color)Fill rectangle
draw_text(x, y, str, color, scale)Bitmap text; scale 1 or 2
draw_border(x, y, w, h, color)1-pixel border

Colors: VGA_COLOR_* constants 0-15 from vga.h.

Filesystem (FAT)

FieldNotes
fat_load(drive, dir, name, buf, max, &sz, &trunc)Load file into buffer
fat_save(drive, dir, name, buf, sz)Save buffer to file
fat_error()Last FAT error string

drive: FAT_DRIVE_BOOT = A:, FAT_DRIVE_B = B:, FAT_DRIVE_HOST = C: (first IDE), FAT_DRIVE_RAM = R:. dir = 0 for root directory.

Loading from a subdirectorydir is a FAT cluster number, not a path string. To reach a nested folder directly, resolve the cluster chain with fat_list_dir. Apps that need assets beside their executable should implement open_launch_at(drive, cluster, name), store that drive/cluster as their asset base, then run their normal open path. Pack those apps with mkapp.py --open-launch-at; older apps without that HAMA flag continue to receive open(). open_file_at(drive, cluster, name) is for opening a user-selected document.

`.APP` files on non-boot drives — the OS shell now supports double-clicking a .APP on any drive (B:, C:, etc.). The loader calls app_loader_load_drive(drive, cluster, filename) so the binary is read from the correct device. No special handling needed in your app.

File dialog

FieldNotes
fd_init(state, x, y)Init a FileDialogState
fd_open(state, mode, drive, default_name, cfg, ctx)Open LOAD or SAVE dialog
fd_close(state)Close without accepting
fd_draw(state, cfg)Draw dialog
fd_scancode(state, sc, shift, cfg, ctx)Returns ACCEPT or CANCEL
fd_pointer_down(state, x, y, cfg, ctx)Mouse click
fd_translate_key(sc, shift)Scancode to ASCII for filename input
fd_current_cluster(state)Directory cluster for fat_load/fat_save
fd_filename_83_valid(name)Validate 8.3 filename

Window manager

Every normal app should use these for its main window. All title bar styling, bevel buttons, active/inactive states, and future OS chrome changes propagate automatically.

FieldNotes
wnd_init(f, x, y, w, h, min_w, min_h)Init WindowFrame
wnd_draw_frame(f, title)Draw title bar and chrome — always use this, never roll your own
wnd_content_rect(f, inset, &x,&y,&w,&h)Get content area (use WINDOW_INSET=8)
wnd_display_bounds(f, &x,&y,&w,&h)Full window bounds
wnd_hit_test(f, x, y)Returns WINDOW_HIT_* (low-level; prefer wnd_handle_pointer_down)
wnd_handle_pointer_down(f, x, y)Dispatch pointer-down on chrome; starts drag/resize, sets chrome_pressed
wnd_handle_pointer_up(f, x, y)Dispatch pointer-up; ends drag/resize, returns hit type for close/maximize
wnd_begin_drag(f, px, py)Start drag (called internally by wnd_handle_pointer_down)
wnd_begin_resize(f, px, py)Start resize (called internally by wnd_handle_pointer_down)
wnd_update_pointer(f, px, py)Call on pointer_move during drag/resize
wnd_end_interaction(f)End drag or resize (called internally by wnd_handle_pointer_up)
wnd_toggle_maximize(f)Toggle maximised state

Window chrome pattern (all apps must follow this):

c

/* pointer_down */
WindowHit hit = window_handle_pointer_down(&frame, x, y);
if (hit == WINDOW_HIT_CLOSE || hit == WINDOW_HIT_MAXIMIZE ||
    hit == WINDOW_HIT_TITLE || hit == WINDOW_HIT_RESIZE)
    return true;
if (hit != WINDOW_HIT_BODY) return false;
/* ... handle body clicks ... */

/* pointer_move */
if (window_update_pointer(&frame, x, y)) return true;

/* pointer_up */
WindowHit release = window_handle_pointer_up(&frame, x, y);
if (release == WINDOW_HIT_CLOSE)    { /* close app */ return true; }
if (release == WINDOW_HIT_MAXIMIZE) { window_toggle_maximize(&frame); return true; }
return release != WINDOW_HIT_NONE;

Standard OS scrollbar

Use draw_scrollbar for any scrollable content. Never implement a custom scrollbar — this ensures consistent look and keyboard/mouse behaviour across all apps.

draw_scrollbar(x, y, height, total, visible, position, up_pressed, down_pressed)

  • x, y, height — position and total height of the 16-px-wide strip
  • total — total scrollable units (e.g. line count)
  • visible — units visible at once (determines thumb size)
  • position — current scroll offset (0 = top)
  • up_pressed / down_pressed — true while the respective arrow button is held

c

g_host->draw_scrollbar(scroll_x, content_y, content_height,
    total_lines, visible_lines, scroll_offset,
    up_btn_pressed, down_btn_pressed);

Constants: VGA_SCROLLBAR_WIDTH = 16, VGA_SCROLLBAR_ARROW_HEIGHT = 16.

Scrollbar hit-testing for clicks:

c

VgaScrollbarMetrics sb;
VgaScrollbarHit hit;

g_host->scrollbar_metrics(scroll_x, content_y, content_height,
    total_lines, visible_lines, scroll_offset, &sb);
hit = g_host->scrollbar_hit_test(&sb, pointer_x, pointer_y);

For automatic mouse-wheel support, expose the scrollable list as the app's primary scroll area in AppDescriptor. The window manager will route wheel events to the focused app. If your custom handle_wheel() is missing or returns false, HamsterOS will use these two hooks to update the scroll position automatically.

c

static bool my_get_primary_scroll(AppScrollArea *out)
{
    if (total_lines <= visible_lines) return false;
    out->total = total_lines;
    out->visible = visible_lines;
    out->position = scroll_offset;
    out->wheel_step = 3; /* 0 also means the OS default of 3 */
    return true;
}

static bool my_set_primary_scroll(uint32_t position)
{
    uint32_t max = total_lines > visible_lines ? total_lines - visible_lines : 0;
    if (position > max) position = max;
    if (position == scroll_offset) return false;
    scroll_offset = position;
    return true;
}

static AppDescriptor g_desc = {
    /* required callbacks ... */
    .get_primary_scroll = my_get_primary_scroll,
    .set_primary_scroll = my_set_primary_scroll
};

Positive wheel deltas mean wheel-up. Apps with several independent scroll areas or modal lists may still implement handle_wheel() directly; return true after consuming the wheel event, or false to let the primary scroll fallback run.

Timing and audio

FieldSignatureNotes
pit_millis()-> uint32_tMilliseconds since boot
speaker_note_on(hz)uint16_t hzPlay square-wave tone on PC speaker
speaker_note_off()noneStop PC speaker tone
sb16_card_present()-> boolTrue if SoundBlaster 16 was detected at boot
sb16_play_pcm(pcm, len, hz)const uint8_t*, uint32_t, uint16_tPlay 8-bit unsigned mono PCM non-blocking via DMA
sb16_audio_stop()noneHalt DMA playback immediately
sb16_opl3_note_on(midi_note)uint8_t midi_notePlay one OPL3 FM note using a MIDI note number
sb16_opl3_note_off()noneStop the active OPL3 FM note

SB16 audio notes for app authors:

  • Always call sb16_card_present() before using SB16 PCM or OPL3 services. Fall back to speaker_note_on/off on machines without a card.
  • Playback is non-blockingsb16_play_pcm returns immediately; DMA runs in background. Do not free the PCM buffer until playback finishes or you have called sb16_audio_stop.
  • Use sounds sparingly. A single WAV file can easily consume 20–40 KB on a 1.44 MB floppy. Keep app sounds under 2 seconds. Prefer 11025 Hz, 8-bit unsigned mono; that yields ~11 KB per second.
  • Call sb16_audio_stop() before starting a new sound so the previous DMA transfer does not overlap.
  • For simple musical notes, prefer sb16_opl3_note_on/off over PCM samples; it costs much less floppy space.

Misc

FieldNotes
prefs_sound()System confirmation beep
serial_str(s)Debug string to serial port
serial_char(ch)Single character to serial
serial_dec16(v)int16_t as decimal to serial
shell_files_changed()Notify shell to refresh file lists
set_toolbar_hidden(bool)Hide/show only the floating tray (no other changes)
set_game_mode(bool)Game Mode — see section below

Game Mode

Game Mode gives an app exclusive ownership of the screen and input. Call it in open() and undo it in close(). One call replaces `set_toolbar_hidden` for games — do not call both.

c

void mm_port_open(void) {
    g_open = g_active = true;
    g_host->set_game_mode(true);   /* enter Game Mode */
    /* load game data, etc. */
}
void mm_port_close(void) {
    g_host->set_game_mode(false);  /* restore full OS UI */
    g_open = g_active = false;
}

What `set_game_mode(true)` does:

EffectDetail
Toolbar tray hiddenTray not drawn; clicks in tray area fall through to game window
Menu-bar header hiddenThe 20 px OS header strip (menus, clock, LED) is not drawn; reclaimed for the game window
Exclusive keyboard inputAll scancodes go only to the front WM app. shell_handle_scancode is completely bypassed — F1, Alt+F, Alt+H, Ctrl+Q, volume keys, and every other shell shortcut are suppressed. The app's handle_scancode() return value is not used as a fallback; the shell never sees the key.
Screensaver disabledThe idle timer is not checked while game mode is active; the screensaver never fires during gameplay

Your `handle_scancode` in Game Mode:

Return true for keys you handle and false for keys you don't — but note that returning false in Game Mode does not pass the key to the shell. It is simply discarded. The only way to exit Game Mode is to call close() (which should call set_game_mode(false)).

Recommended quit pattern — catch ESC or whatever quit key the game uses:

c

bool mm_port_scancode(uint8_t sc) {
    if (sc == 0x01) {          /* ESC — quit game */
        mm_port_close();
        return true;
    }
    /* ... game input handling ... */
    return true;               /* consume everything in game mode */
}

Other ideas that combine well with Game Mode:

  • Position your WindowFrame at (0, 0) with w=640, h=480 to cover the full screen (header is hidden so the top 20 px are available)
  • Call update() returning true every frame for continuous animation
  • Save game state in close() — this fires whether the user closes from your quit key or from a programmatic close
  • set_toolbar_hidden(bool) still exists independently for apps that want only tray suppression without the full Game Mode restrictions

Shared transient workspace

FieldSignatureNotes
workspace_acquire(size, owner, &out_size)Request exclusive scratch space for one transient operation
workspace_release(ptr)Release the shared scratch block when the operation is done

Rules:

  • Treat the workspace as a temporary, exclusive loan, not app-owned memory.
  • Only use it for scratch data that can be discarded and recreated.
  • Release it as soon as the blocking operation or calculation completes.
  • If workspace_acquire() returns NULL, show a normal user-facing low-memory

or busy status and abort that operation cleanly.

  • Do not store live document state, window state, undo history, or any

pointer that must survive beyond the current operation in the workspace.

Shared clipboard

Plain text clipboard v1 is exposed through appended HamsterHostAPI fields:

FieldSignatureNotes
clipboard_set_text(text, length, owner)Copies ASCII text into the OS clipboard; bounded and heap-backed
clipboard_get_text(buffer, max_size)Copies text out; returns the full clipboard size even if truncated
clipboard_text_size()Current text payload length
clipboard_version()Monotonic change counter for apps that cache clipboard state
clipboard_owner()Short owner label from the last writer
clipboard_has_text()True when a non-empty plain-text payload exists

Keep clipboard use small and transient. Larger typed payloads are planned later; v1 apps should treat it as plain text only.


AppDescriptor fields

Return from app_entry(). Set unused fields to NULL.

FieldRequiredWhen called
init()YesAfter app_entry() — init state, call wnd_init()
open()YesUser launches the app when open_launch_at is not implemented
open_file(drive, name)NoRoot-dir file association
open_file_at(drive, cluster, name)NoFile browser document double-click
open_launch_at(drive, cluster, name)NoApp launch with executable folder context; called instead of open() only when the .APP was packed with --open-launch-at, so store context and enter the normal open path. name is the .APP filename
close()NoProgrammatic close
is_open()YesEvery frame — return true while window exists
is_active()YesEvery frame — return true while window has focus
has_modal()YesEvery frame — true if a modal dialog is open
set_active(bool)YesWhen another window gains focus
handle_scancode(sc)YesKey press/release — return true if consumed
handle_pointer_down/move/up(x,y)YesMouse events — return true if consumed
handle_menu_command(cmd)NoMenu item (uint32_t — cast to your enum)
handle_edit_command(cmd)NoEdit menu / clipboard
draw()YesFull window redraw
draw_content()NoPartial redraw (content only)
get_bounds(x,y,w,h)YesWindow bounds for hit-testing and damage tracking
update()NoAnimation/audio tick — return true if redraw needed
consume_redraw_request()NoAnimation dirty flag poll
handle_wheel(wheel)NoSigned wheel input; positive means wheel-up. Return true if consumed
get_primary_scroll(out)NoDescribe the main scrollable list for automatic wheel support
set_primary_scroll(position)NoApply a clamped scroll position for automatic wheel support
cursor_style_at(x,y)NoReturn VGA_CURSOR_RUNTIME_DEFAULT for the normal pointer, or a transient VGA_CURSOR_* shape for the point under the mouse

Use cursor_style_at() for local interaction affordances only. It should not change the user's saved pointer preference; the window manager applies the returned style only while drawing the cursor. Current transient styles include busy, text/I-beam, pencil, eraser, fill, and crosshair.


Writing a stubs file

Every OS function your app calls must be wrapped. The linker reports the missing ones. See apps/piano_stubs.c for the full pattern:

c

/* myapp_stubs.c */
#include "app_abi.h"
#include "window.h"
#include "vga.h"
#include <stdbool.h>
#include <stdint.h>

static HamsterHostAPI *g_host;
void myapp_stubs_set_host(HamsterHostAPI *h) { g_host = h; }

void vga_fill_rect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c)
    { g_host->fill_rect(x, y, w, h, c); }
void vga_draw_text(int16_t x, int16_t y, const char *s, uint8_t c, uint8_t sc)
    { g_host->draw_text(x, y, s, c, sc); }
void vga_draw_border(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t c)
    { g_host->draw_border(x, y, w, h, c); }

void window_init(WindowFrame *f, int16_t x, int16_t y, int16_t w, int16_t h,
                 int16_t mw, int16_t mh) { g_host->wnd_init(f,x,y,w,h,mw,mh); }
void window_draw_frame(const WindowFrame *f, const char *t)
    { g_host->wnd_draw_frame(f, t); }
void window_content_rect(const WindowFrame *f, int16_t i,
                          int16_t *x, int16_t *y, int16_t *w, int16_t *h)
    { g_host->wnd_content_rect(f, i, x, y, w, h); }
void window_display_bounds(const WindowFrame *f,
                            int16_t *x, int16_t *y, int16_t *w, int16_t *h)
    { g_host->wnd_display_bounds(f, x, y, w, h); }
WindowHit window_hit_test(const WindowFrame *f, int16_t x, int16_t y)
    { return g_host->wnd_hit_test(f, x, y); }
void window_begin_drag(WindowFrame *f, int16_t px, int16_t py)
    { g_host->wnd_begin_drag(f, px, py); }
void window_begin_resize(WindowFrame *f, int16_t px, int16_t py)
    { g_host->wnd_begin_resize(f, px, py); }
bool window_update_pointer(WindowFrame *f, int16_t px, int16_t py)
    { return g_host->wnd_update_pointer(f, px, py); }
void window_end_interaction(WindowFrame *f)  { g_host->wnd_end_interaction(f); }
void window_toggle_maximize(WindowFrame *f)  { g_host->wnd_toggle_maximize(f); }

ABI stability contract

  • HamsterHostAPI and AppDescriptor are append-only. New fields are

always added at the end. Never reorder existing fields — doing so breaks every compiled .APP.

  • HAMA version is incremented on breaking changes. Currently: 1.
  • HAMA flags bit 0 means a bundled 16x16 VGA icon is present immediately

after the header. Unknown flag bits should be treated as unsupported until a future version defines them.

  • Shared structs (WindowFrame, FileDialogState) must be compiled from the

OS source tree headers — never copy headers manually.

  • The name[8] field in the HAMA header is the display name used by app

launch surfaces and icon view. Set it with mkapp.py --name MYAPP (max 8 chars).


Memory rules

  • Static variables live in BSS, zeroed at load time.
  • Persistent app state should still be static/BSS-backed in v1.
  • General heap_alloc() / heap_free() are not part of the public app ABI.
  • For large transient scratch work only, use the shared workspace API

instead of inventing a private permanent buffer.

  • BSS is freed when is_open() returns false. Do not store cross-open pointers.
  • Total loaded app footprint = code + BSS. Current examples: Piano is about

5 KB, Notepad about 23 KB, and Paint about 23 KB before their runtime heap buffers.

  • The staging buffer used to load the .APP file is freed before app_entry()

is called, so only your runtime region occupies heap.


Conventions

  • Prefix all symbols with your app name. The linker combines all objects;

duplicate names cause link errors.

  • No libc. Use only <stdbool.h>, <stdint.h>, <stddef.h>.
  • No direct I/O. Never access VGA memory, I/O ports, or kernel globals.

Every OS interaction goes through HamsterHostAPI.

  • Uppercase 8.3 filenames on the FAT12 filesystem.
  • One USER app, game, or tool slot at a time. Loading another closes the

current one and releases the previous .APP image.

  • Show load feedback before blocking reads. For file-manager launches and lazy-loaded

system apps, draw a loading toast before fat_load_file() or app_loader_load() begins. The file manager does this automatically for .APP files double-clicked in icon or list view.

  • Fail visibly and return control. If a load cannot complete, show a

user-facing error dialog and return to the desktop or file manager. Do not spin, panic, or leave the loading toast as the last painted state.

  • Keep `AppDescriptor` initializers explicit. Fill every field through

update and consume_redraw_request, using NULL for unsupported hooks. Short initializers compile, but they hide ABI drift and make relocation review harder.

  • Provide `close()` when the app owns heap buffers or hardware state. The

slots call it before replacing a loaded app/tool, and lazy system-app loaders use it to make teardown idempotent before releasing the .APP image.


File manager icon view, .ICO sidecars, and bundled icons

The file manager supports two view modes per window: By Name (list) and By Icon (grid). Users toggle between them via VIEW → BY NAME / BY ICON in the top menu bar.

Generic file-type icons

drivers/vga.c provides a centralized icon library via:

c

void vga_draw_file_icon(FileIconKind kind, int16_t x, int16_t y, uint8_t color);

FileIconKind values (from drivers/vga.h):

ConstantShown for
FILE_ICON_FOLDERDirectories
FILE_ICON_APP.APP files (fallback when no bundled icon)
FILE_ICON_TEXT.TXT files
FILE_ICON_IMAGE.BMP files
FILE_ICON_UNKNOWNEverything else

Preferred fast path: .ICO sidecars

For built-in apps/tools/games shipped on the floppy, HamsterOS prefers a separate NAME.ICO sidecar next to NAME.APP. This keeps folder browsing fast: the file manager can batch-load a fixed 256-byte icon file without touching the app binary at all.

Use sidecars for anything that appears in normal folder listings on the shipping disk. This is the default pattern for /APPS, /GAMES, /TOOLS, /USER, and desktop shortcuts.

Bundled .APP icons

When an .APP file has HAMA_FLAG_HAS_ICON set in its header, HamsterOS can still read and display that icon by loading only the fixed header/icon block via app_loader_read_icon(). The full app code is never loaded just for icon display.

This path is best treated as compatibility or metadata support, not the primary folder-browsing strategy. If you expect users to browse a directory full of apps, ship .ICO sidecars so icon view stays snappy.

Icon data format: 256 bytes, 16×16 pixels, one byte per pixel (VGA palette index). Color 0 is treated as transparent by vga_draw_palette_icon().

To include an icon in your app, pass --icon assets/myapp_icon.bin to mkapp.py:

sh

python3 tools/mkapp.py --elf myapp.elf --name MYAPP --icon assets/myapp_icon.bin --output MYAPP.APP

The icon binary must be exactly 256 bytes (16×16 VGA palette indices, row-major). See existing game apps (assets/snake_icon.bin etc.) as references.


Existing apps as reference implementations

AppSourceFormatNotes
Notepadapps/notepad.c + apps/notepad_stubs.c/APPS/NOTEPAD.APPFile I/O, file dialog, complex state
Paintapps/paint.c + apps/paint_stubs.c/APPS/PAINT.APPExternal system app, heap canvas, close hook
Pianoapps/piano.c + apps/piano_stubs.c/USER/PIANO.APPAudio (speaker), animation tick
Slotsapps/slot_machine.c + apps/slot_stubs.c/USER/SLOTS.APPAnimation, consume_redraw_request
Gamesapps/*_gentry.c + apps/*_gstubs.c/GAMES/*.APPBundled 16x16 icons via HAMA HAS_ICON
Disk Copierapps/diskcopier.c + apps/diskcopier_tool_stubs.c/TOOLS/DISKCOPY.APPFloppy driver API stubs
CD Playerapps/cdplayer.c + apps/cdplayer_tool_stubs.c/TOOLS/CDPLAY.APPCDROM/ATAPI API stubs, animation tick
Floppy Diagapps/floppydiag.c + apps/floppydiag_tool_stubs.c/TOOLS/FLOPDIAG.APPFull floppy + FAT + shell stubs
CD ROM Diagapps/cdromdiag.c + apps/cdromdiag_tool_stubs.c/TOOLS/CDDIAG.APPCDROM diagnostic stubs
IDE Diagapps/idediag.c + apps/idediag_tool_stubs.c/TOOLS/IDEDIAG.APPIDE diagnostic stubs
Formatapps/format.c + apps/format_tool_stubs.c/TOOLS/FORMAT.APPFAT format + floppy stubs
GFX Diagapps/gfxdiag.c + apps/gfxdiag_tool_stubs.c/TOOLS/GFXDIAG.APPMulti-mode VGA benchmark

HamsterHostAPI — extended fields (tool apps, June 2026)

The following fields were appended after prefs_failure_sound to support the tool apps in /TOOLS/. All new fields follow the same append-only rule.

FieldMaps toUsed by
draw_text_boldvga_draw_text_boldAll tool apps
present_framebuffervga_present_framebufferGFX Diag
get_video_modevga_current_video_modeGFX Diag
pit_accumulatepit_accumulateFloppy Diag, GFX Diag
serial_hex32serial_write_hex32Floppy Diag
fat_deletefat_file_traceFAT extension functionsFloppy Diag, Format
shell_set_boot_readyshell_set_cdrom_readyShell state notificationsFloppy Diag, Format, CD ROM Diag
kernel_servicekernel_serviceDisk Copier
floppy_a_readyfloppy_last_failureFloppy driver functionsDisk Copier, Floppy Diag, Format
cdrom_presentcdrom_ejectCDROM driver functionsCD Player, CD ROM Diag
ide_diagide_run_diagnosticIDE Diag
ata_drive_present, ata_read_rawATA raw sector accessPartition Table Viewer
sb16_card_present, sb16_play_pcm, sb16_audio_stop, sb16_opl3_note_on/offSoundBlaster 16Piano (OPL3), any app that wants digital audio

Hardware access and driver architecture

How drivers work in HamsterOS

All drivers in drivers/ are compiled directly into the kernel binary (hamster-dos.bin). There is no dynamic driver loading. External .APP files cannot install new kernel drivers — they access hardware exclusively through the HamsterHostAPI function table passed to app_entry().

text

Kernel binary (hamster-dos.bin)
  └─ drivers/floppy.c   ──┐
  └─ drivers/ata.c       ├─ Storage Registry (drivers/storage.*)
  └─ drivers/atapi.c     │    storage_device_count() / storage_get_device_info()
  └─ drivers/cdrom.c    ──┘    storage_read_sector512() / storage_write_sector512()
  └─ drivers/sb16.c
  └─ drivers/vga.c            ↑ exposed to external apps via HamsterHostAPI
  └─ ...                      ↓
External .APP file
  └─ myapp_stubs.c  ──── calls g_host->sb16_card_present(), g_host->floppy_a_ready(), etc.

If your app needs hardware that isn't exposed yet, the path is: add the kernel function → append a new field to HamsterHostAPI (always at the end) → add a stub in your app's stubs file. Never reorder existing fields.


Driver registry

The driver registry (drivers/driver_model.h) provides a read-only view of every driver detected at boot.

c

/* Enumerate all drivers */
uint8_t count = driver_registry_count();  /* via g_host->... not exposed yet — use tool-app APIs directly */
DriverInfo info;
for (uint8_t i = 0; i < count; i++) {
    driver_registry_get(i, &info);
    /* info.id, info.name, info.driver_class, info.present, info.instance_count */
}

DriverClass values: DRIVER_CLASS_STORAGE, DRIVER_CLASS_INPUT, DRIVER_CLASS_AUDIO, DRIVER_CLASS_VIDEO, DRIVER_CLASS_TIMER, DRIVER_CLASS_SYSTEM.

Capability flags (bitfield in info.capability_flags): DRIVER_CAP_BLOCK_IO, DRIVER_CAP_REMOVABLE, DRIVER_CAP_READ_ONLY, DRIVER_CAP_POINTER, DRIVER_CAP_KEYBOARD, DRIVER_CAP_PCM_OUTPUT, DRIVER_CAP_FRAMEBUFFER, DRIVER_CAP_RTC_CLOCK, DRIVER_CAP_TIMER_TICKS, DRIVER_CAP_DIAGNOSTIC.

> The driver registry is informational only. It does not route I/O — use the storage registry or direct hardware APIs for actual reads/writes.


Storage registry

The storage registry (drivers/storage.h) enumerates every block device and abstracts reads/writes behind a single index-based API. The FAT layer uses this internally; apps can use it directly for raw sector access.

c

/* StorageDeviceInfo fields */
typedef struct {
    bool present;           /* device detected */
    bool media_present;     /* removable media loaded (floppy/CD-ROM) */
    bool read_only;         /* write-protected */
    bool removable;         /* can eject/swap media */
    StorageDeviceClass device_class;   /* FLOPPY / HARDDISK / CDROM */
    StorageBackend backend;            /* FLOPPY / ATA / ATAPI / MKE_CDROM */
    uint8_t backend_index;  /* drive ID within backend (0–3) */
    uint8_t instance_number;
    uint16_t block_size;    /* bytes per sector: 512 for floppy/HDD, 2048 for CD */
    const char *controller_name;  /* "IDE Primary", "Floppy Controller", … */
    const char *device_name;      /* "Primary Master", "Floppy Drive A", … */
} StorageDeviceInfo;

Enumerate and read:

c

uint8_t n = storage_device_count();
for (uint8_t i = 0; i < n; i++) {
    StorageDeviceInfo info;
    if (!storage_get_device_info(i, &info)) continue;
    if (!info.present || !info.media_present) continue;
    /* read sector 0 */
    uint8_t buf[512];
    if (storage_read_sector512(i, 0u, buf)) {
        /* buf contains first 512 bytes of the device */
    }
}

> storage_read/write_sector512 is not yet exposed via HamsterHostAPI. Use the specific driver APIs below for now.


Hardware APIs available to apps

All hardware access goes through HamsterHostAPI. Use these via your stubs file:

Floppy:

c

g_host->floppy_a_ready()                    /* true if drive A has media */
g_host->floppy_b_ready()                    /* true if drive B has media */
g_host->floppy_a_read(lba, buf)             /* read 512-byte sector */
g_host->floppy_a_write(lba, buf)
g_host->floppy_b_read(lba, buf)
g_host->floppy_b_write(lba, buf)
g_host->floppy_batch_begin(drive)           /* hold motor on for multi-sector ops */
g_host->floppy_batch_end()
g_host->floppy_cmos_types()                 /* CMOS nibbles for A/B drive types */
g_host->floppy_get_timing() / set_timing()  /* FloppyTimingMode */
g_host->floppy_last_failure(out)            /* FloppyFailureInfo */

CD-ROM / ATAPI audio:

c

g_host->cdrom_present()                     /* any CD-ROM controller found */
g_host->cdrom_mke()                         /* true = Matsushita/MKE interface */
g_host->cdrom_audio_ok()                    /* true = ATAPI audio transport available */
g_host->cdrom_read_toc(out)                 /* AtapiToc — track list and lengths */
g_host->cdrom_play(sm,ss,sf, em,es,ef)      /* play MSF range */
g_host->cdrom_pause(resume)
g_host->cdrom_stop()
g_host->cdrom_status(out)                   /* AtapiAudioStatus */
g_host->cdrom_eject()
g_host->cdrom_probe_port(base)              /* probe one MKE port; returns true if device responds */

ATA raw sector access (primary master only):

c

g_host->ata_drive_present()
g_host->ata_read_raw(lba, buf)              /* 512-byte sector, no partition translation */
g_host->ata_write_raw(lba, buf)
g_host->ata_sector_count_raw()              /* total sectors on device */

SoundBlaster 16:

c

g_host->sb16_card_present()                 /* SB16 detected at boot */
g_host->sb16_get_base_port()                /* I/O base (0x220, 0x240, …) */
g_host->sb16_get_irq_num()                  /* IRQ number */
g_host->sb16_play_pcm(buf, len, hz)         /* 8-bit unsigned mono PCM */
g_host->sb16_play_wav_buf(wav, sz)          /* play a WAV buffer from memory */
g_host->sb16_audio_stop()
g_host->sb16_opl3_note_on(midi_note)        /* FM synthesis via YMF262 */
g_host->sb16_opl3_note_off()

PC Speaker:

c

g_host->speaker_note_on(hz)                 /* start tone at frequency */
g_host->speaker_note_off()

Writing a hardware tool app — checklist

1. Add stubs for every g_host-> field you use in myapp_stubs.c (see apps/piano_stubs.c). 2. Check g_host->sb16_card_present() / g_host->floppy_a_ready() / g_host->cdrom_present() before using a device — never assume hardware is present. 3. Call g_host->kernel_service() inside long I/O loops to keep the mouse cursor moving. 4. For CD-ROM reads through FAT, use g_host->fat_load with FAT_DRIVE_CDROM — the FAT layer handles the MKE/ATAPI routing. Only use cdrom_probe_port or the direct ATAPI APIs if you are writing a diagnostic. 5. To add a new hardware API: add the kernel function, append one field to HamsterHostAPI in kernel/app_abi.h, wire it in kernel/app_loader.c, add a stub. Never insert between existing fields.


Writing kernel drivers (.DRV)

HamsterOS supports external loadable kernel drivers via the HAMD v1 format. Unlike .APP files (user-space apps), .DRV files are loaded early in boot and run as trusted kernel-level code with direct port I/O access.

Drop a `.DRV` into `/DRIVERS/` on the floppy and reboot — the driver is live.

The complete driver writing guide is in `docs/HAMSTEROS_DRIVER_SDK.md`, covering:

  • How HAMD differs from HAMA (magic, entry point, no compression, tools/driver.ld)
  • DriverHostAPI — the host services table drivers receive (port I/O, heap, serial)
  • DriverDescriptor — what your driver returns (storage, audio, and input callbacks)
  • Two-file structure (mydrv_entry.c + mydrv.c) and build recipe
  • StorageDeviceInfo fields for storage-class drivers
  • Safety rules and the apps/example_driver/nulldisk reference implementation

Loader troubleshooting notes

  • If an external app soft-crashes immediately after app_loader: loaded NAME.APP,

inspect descriptor relocations first. AppDescriptor contains function pointers and must have R_386_32 relocations applied like any other static pointer table.

  • If an icon-bearing app crashes before app_entry(), verify the loader skips

the optional 256-byte icon block before copying code.

  • If a window appears at (0,0) with broken bounds or does not open, confirm

the owner slot/loader calls desc->init() before desc->open().

  • If an app works as built-in code but not as .APP, check the stubs file for

any direct OS calls that were not routed through HamsterHostAPI.

  • If the screen stays at Loading NAME, the loader is probably blocked inside

the disk read path. Check serial output for app_loader: header load failed, app_loader: load failed, the FAT trace, and the byte/truncation counters.

  • If a load fails cleanly, the expected user experience is an error dialog such

as "File not found.", "Invalid application file.", "Not enough memory to open application.", or "Application file could not be read."

The quickest runtime smoke test is to boot build/hamster.img, launch an app, and watch serial output for app_loader: loaded NAME.APP followed by the app-specific loader message, with no fault/panic output afterward.


*HamsterOS — 32-bit protected-mode OS for 386/486 hardware.* *App Developer Guide — June 2026.*

HamsterOS Developer SDK

Driver SDK

Synced Jul 6, 2026

HamsterOS Driver SDK

External drivers let you add hardware support to HamsterOS without recompiling the kernel. Drop a .DRV file into /DRIVERS/ on the boot floppy, reboot — the driver is loaded, its init() is called, and any devices it registers appear as new drive letters.


How drivers differ from apps

.APP (external app).DRV (external driver)
MagicHAMA (0x414D4148)HAMD (0x444D4148)
Extension.APP.DRV
Folder/APPS/, /TOOLS/, /USER/, /GAMES//DRIVERS/
LoadedOn user clickAt boot, before ATA/CDROM init
Entry pointAppDescriptor *app_entry(HamsterHostAPI *)DriverDescriptor *driver_entry(DriverHostAPI *)
Port I/ONo — use hardware via HostAPI onlyYesinb/outb/inw/outw directly
CompressionSupportedNot supported (keep drivers small)

DriverHostAPI — what drivers receive

c

typedef struct {
    /* Port I/O — exclusive to drivers, never exposed to apps */
    uint8_t  (*inb)(uint16_t port);
    void     (*outb)(uint16_t port, uint8_t val);
    uint16_t (*inw)(uint16_t port);
    void     (*outw)(uint16_t port, uint16_t val);
    void     (*io_wait)(void);
    /* Memory */
    void    *(*heap_alloc)(uint32_t size);
    void     (*heap_free)(void *ptr);
    /* Timing */
    uint32_t (*pit_millis)(void);
    /* Debug serial output */
    void     (*serial_write)(const char *s);
    void     (*serial_hex32)(uint32_t v);
} DriverHostAPI;

DriverDescriptor — what your driver returns

c

typedef struct {
    const char *id;            /* unique slug, e.g. "aha154x_scsi" */
    const char *name;          /* display name for Device Manager */
    DriverClass driver_class;  /* DRIVER_CLASS_STORAGE / AUDIO / INPUT */

    /* Called once; return false if hardware is absent */
    bool (*init)(void);

    /* Storage class only — NULL for other classes */
    uint8_t (*storage_device_count)(void);
    bool    (*storage_device_info)(uint8_t index, StorageDeviceInfo *out);
    bool    (*storage_read)(uint8_t dev_index, uint32_t lba, uint8_t *buf);
    bool    (*storage_write)(uint8_t dev_index, uint32_t lba, const uint8_t *buf);

    /* Audio class only — NULL for other classes */
    bool (*audio_init)(void);

    /* Input class only — NULL for other classes */
    bool (*input_poll)(int16_t *dx, int16_t *dy, uint8_t *buttons);
} DriverDescriptor;

Required files

Every driver needs exactly two C files:

mydrv_entry.c (entry point — always the same shape)

c

#include "app_abi.h"    /* DriverDescriptor, DriverHostAPI */
#include "storage.h"    /* StorageDeviceInfo if storage class */
#include <stddef.h>

/* Forward-declare functions implemented in mydrv.c */
void    mydrv_set_host(DriverHostAPI *h);
bool    mydrv_init(void);
uint8_t mydrv_device_count(void);
bool    mydrv_device_info(uint8_t index, StorageDeviceInfo *out);
bool    mydrv_read(uint8_t dev, uint32_t lba, uint8_t *buf);
bool    mydrv_write(uint8_t dev, uint32_t lba, const uint8_t *buf);

static DriverDescriptor g_desc = {
    "mydrv",           /* id   */
    "My Controller",   /* name */
    DRIVER_CLASS_STORAGE,
    mydrv_init,
    mydrv_device_count,
    mydrv_device_info,
    mydrv_read,
    mydrv_write,
    NULL,  /* audio_init */
    NULL   /* input_poll */
};

DriverDescriptor *driver_entry(DriverHostAPI *host)
{
    mydrv_set_host(host);
    return &g_desc;
}

mydrv.c (all hardware logic)

c

#include "app_abi.h"
#include "storage.h"
#include <stdbool.h>
#include <stdint.h>

static DriverHostAPI *g_host;
void mydrv_set_host(DriverHostAPI *h) { g_host = h; }

static bool mydrv_init(void)
{
    /* Probe hardware.  Use g_host->inb/outb for port I/O. */
    uint8_t status = g_host->inb(0x330);
    if (status == 0xFF) {
        g_host->serial_write("mydrv: no card at 0x330\r\n");
        return false;
    }
    g_host->serial_write("mydrv: card found\r\n");
    return true;
}

/* ... storage_device_count, device_info, read, write ... */

Build recipe

sh

CC_FLAGS="-m32 -march=i386 -std=c11 -ffreestanding -fno-builtin \
          -fno-pic -fno-pie -fno-stack-protector \
          -Ikernel -Idrivers -Wall -Wextra -O2"

gcc $CC_FLAGS -c mydrv.c         -o mydrv.o
gcc $CC_FLAGS -c mydrv_entry.c   -o mydrv_entry.o

# --emit-relocs is mandatory (same rule as .APP files)
ld -T tools/app.ld -melf_i386 --emit-relocs -nostdlib \
   mydrv_entry.o mydrv.o -o mydrv.elf

# --driver writes HAMD magic instead of HAMA
python3 tools/mkapp.py --driver --elf mydrv.elf \
    --name MYDRV --output MYDRV.DRV

Copy MYDRV.DRV to /DRIVERS/ on the floppy. Reboot. Serial output will show:

DRIVER: scanning /DRIVERS/
mydrv: card found
DRIVER: loaded MYDRV.DRV (My Controller)

Writing a storage driver

StorageDeviceInfo fields your device_info() callback must fill:

FieldTypeNotes
presentboolAlways true here (you wouldn't add the device otherwise)
media_presentbooltrue for fixed disks; check media for removables
read_onlybooltrue for optical drives
removablebooltrue for optical/floppy-like devices
device_classStorageDeviceClassSTORAGE_DEVICE_HARDDISK, _FLOPPY, or _CDROM
backendStorageBackendLeave as STORAGE_BACKEND_NONE — loader fills this
backend_indexuint8_tYour internal device index (0, 1, …)
block_sizeuint16_t512 for disks, 2048 for raw CD-ROM
controller_nameconst char*E.g. "Adaptec AHA-154x"
device_nameconst char*E.g. "SCSI Hard Disk 0"

read() and write() receive 512-byte sector requests regardless of the device's native block size — the FAT layer handles all mapping.


Safety rules

  • All driver state must be static. The driver code buffer is never

freed after a successful load.

  • Do not call heap_alloc from interrupt context (there are none

yet, but keep this in mind for future IRQ-driven audio).

  • A crash in init() that returns false is fine — the driver slot

is skipped. A crash that triple-faults will reboot; the CMOS crash counter will auto-disable the driver after 3 consecutive faults if safe-mode kicks in and you put the driver in /STARTUP/ — see HAMSTEROS_APP_SDK.md for the safe-mode mechanism.

  • Maximum driver file size: 64 KB. Maximum loaded drivers: 8.

Example: SCSI storage driver skeleton

See apps/example_driver/nulldisk.c and nulldisk_entry.c for a minimal working storage driver. For a real SCSI implementation, replace nulldisk_init() with Adaptec AHA-154x host adapter detection via g_host->inb(0x330) and implement the SCSI CDB read/write protocol in nulldisk_read/write().