This guide explains how to write, build, and run applications for UnoDOS 3.
UnoDOS apps are flat .BIN binaries assembled with NASM. Each app runs in its own 64KB memory segment and communicates with the kernel via INT 0x80 system calls. Apps can create windows, draw graphics, handle keyboard and mouse input, play sounds, and read/write files.
Here is the smallest possible windowed application:
[BITS 16]
[ORG 0x0000]
; --- Icon Header (80 bytes) ---
db 0xEB, 0x4E ; JMP short to offset 0x50
db 'UI' ; Magic identifier
db 'Hello', 0 ; App name (12 bytes, null-padded)
times (0x04 + 12) - ($ - $$) db 0
; 16x16 icon bitmap (64 bytes, 2bpp CGA)
; Each row is 4 bytes. Color 3 = white, 0 = black.
times 64 db 0xFF ; Solid white square placeholder
times 0x50 - ($ - $$) db 0 ; Pad to code entry
; --- Code Entry (offset 0x50) ---
entry:
pusha
push ds
push es
mov ax, cs
mov ds, ax ; DS = our segment (for local data)
; Create a window
mov bx, 60 ; X
mov cx, 60 ; Y
mov dx, 200 ; Width
mov si, 60 ; Height
mov ax, cs
mov es, ax
mov di, title_str ; ES:DI = title
mov al, 0x03 ; Flags: title + border
mov ah, 20 ; API: win_create
int 0x80
jc .fail ; CF=1 means no free window slots
mov [cs:handle], al
; Activate drawing context (window-relative coordinates)
mov ah, 31 ; API: win_begin_draw
int 0x80
; Draw text at (10, 10) inside the window
mov bx, 10
mov cx, 10
mov si, msg
mov ah, 4 ; API: gfx_draw_string
int 0x80
; Main event loop
.loop:
sti ; CRITICAL: re-enable interrupts
mov ah, 9 ; API: event_get
int 0x80
jc .loop ; No event, keep polling
cmp al, 1 ; EVENT_KEY_PRESS?
jne .check_redraw
cmp dl, 27 ; ESC key?
je .exit
jmp .loop
.check_redraw:
cmp al, 6 ; EVENT_WIN_REDRAW?
jne .loop
; Repaint content
mov bx, 10
mov cx, 10
mov si, msg
mov ah, 4
int 0x80
jmp .loop
.exit:
mov ah, 32 ; API: win_end_draw
int 0x80
mov al, [cs:handle]
mov ah, 21 ; API: win_destroy
int 0x80
.fail:
xor ax, ax ; Exit code 0
pop es
pop ds
popa
retf ; Return to kernel
; --- Data ---
handle: db 0
title_str: db 'Hello', 0
msg: db 'Hello, UnoDOS!', 0Every UnoDOS application is a flat binary with an optional 80-byte icon header at the start. The kernel loads the entire file into a segment at offset 0 and calls the entry point at offset 0x0050.
Offset Size Content
0x00 2 JMP short 0x50 (bytes: 0xEB, 0x4E)
0x02 2 Magic: "UI" (0x55, 0x49)
0x04 12 App display name (null-padded ASCII)
0x10 64 Icon bitmap (16x16 pixels, 2bpp CGA format)
0x50 ... Code entry point
The icon header is detected by checking bytes 0-3. If the header is missing (legacy apps), the kernel derives the name from the FAT filename and uses a default icon.
The 64-byte bitmap is a 16x16 image in CGA 2bpp format:
- 4 bytes per row, 16 rows, top-to-bottom
- Each byte holds 4 pixels: bits 7-6 = leftmost, bits 1-0 = rightmost
- Colors: 0 = black, 1 = green, 2 = red, 3 = white
Design tips:
- Use white (3) for outlines - most visible on the black desktop
- Use black (0) for transparent/empty areas
- Keep shapes simple at 16x16 resolution
- Kernel loads the .BIN file into a free segment (0x3000-0x8000)
- Far CALL to
segment:0x0050- your entry point runs - App initializes: save registers, set DS, create window, set drawing context
- Event loop: poll for events, draw content, handle input
- Cleanup: end drawing context, destroy window
- RETF returns to kernel, segment is freed
| Register | Value |
|---|---|
| CS | Your app's segment (0x3000-0x8000) |
| DS | Unknown - you must set it |
| ES | Unknown - set as needed |
| SS:SP | Kernel stack |
Always do this first:
entry:
pusha ; Save all GP registers
push ds
push es
mov ax, cs
mov ds, ax ; DS = CS = our segmentRestore everything and return with RETF:
xor ax, ax ; Exit code in AX (0 = success)
pop es
pop ds
popa
retfYour app's code and data share the same segment (CS = DS after setup). Reference local variables with [cs:variable] or just [variable] after setting DS = CS.
Important: When passing string pointers to kernel APIs, DS:SI is read from your segment automatically (the kernel saves your DS). No special handling needed.
For window titles, the kernel reads from ES:DI. Set ES to your segment:
mov ax, cs
mov es, ax
mov di, my_title ; ES:DI = title in our segmentEvery interactive app needs an event loop. The pattern is always the same:
.loop:
sti ; RE-ENABLE INTERRUPTS (mandatory!)
; Optional: yield to let other apps run
mov ah, 34 ; API: app_yield
int 0x80
; Check for events
mov ah, 9 ; API: event_get
int 0x80
jc .no_event ; CF=1 = no event queued
; Dispatch by event type (in AL)
cmp al, 1 ; EVENT_KEY_PRESS
je .handle_key
cmp al, 4 ; EVENT_MOUSE
je .handle_mouse
cmp al, 6 ; EVENT_WIN_REDRAW
je .handle_redraw
jmp .loop
.handle_key:
; DL = ASCII code, DH = scan code
cmp dl, 27 ; ESC?
je .exit
; ... handle other keys ...
jmp .loop
.handle_mouse:
; DL = button state (bit 0=left, 1=right, 2=middle)
; Use API 28 (mouse_get_state) to get position
jmp .loop
.handle_redraw:
; DL = window handle
; Repaint your entire window content
call draw_content
jmp .loop
.no_event:
; Nothing to do - loop back
jmp .loopWhy STI is mandatory: INT 0x80 (like all x86 interrupts) clears the interrupt flag. Without STI, no keyboard or mouse IRQs fire, and the event queue stays empty forever.
mov bx, 50 ; X position
mov cx, 30 ; Y position
mov dx, 220 ; Width in pixels
mov si, 100 ; Height in pixels
mov ax, cs
mov es, ax
mov di, my_title ; ES:DI = window title
mov al, 0x03 ; WIN_FLAG_TITLE | WIN_FLAG_BORDER
mov ah, 20 ; API: win_create
int 0x80
jc .error ; CF=1 = out of window slots (16 max)
mov [cs:win_handle], al ; Save handle for laterAfter creating a window, activate the drawing context so all coordinates are relative to the window's content area:
mov al, [cs:win_handle]
mov ah, 31 ; API: win_begin_draw
int 0x80Now (0, 0) refers to the top-left pixel of the content area (inside the border and below the title bar). The content area is:
- Width: window width - 2 pixels (1px border on each side)
- Height: window height - 11 pixels (10px title bar + 1px border)
When the user drags another window over yours and then moves it away, the kernel sends EVENT_WIN_REDRAW. Your app must repaint its content from scratch:
.handle_redraw:
call draw_content
jmp .loop
draw_content:
; The drawing context is still active from win_begin_draw
mov bx, 5
mov cx, 5
mov si, some_text
mov ah, 4 ; gfx_draw_string
int 0x80
retAlways destroy your window before exiting:
mov ah, 32 ; API: win_end_draw
int 0x80
mov al, [cs:win_handle]
mov ah, 21 ; API: win_destroy
int 0x80All drawing APIs (0-6, 50-52) respect the active drawing context. Coordinates are window-relative when a context is set.
; Draw white text
mov bx, 10 ; X
mov cx, 20 ; Y
mov si, my_string ; DS:SI = string
mov ah, 4 ; gfx_draw_string
int 0x80
; Draw inverted text (black on white)
mov ah, 6 ; gfx_draw_string_inverted
int 0x80
; Word-wrapped text
mov bx, 5 ; X
mov cx, 5 ; Y
mov dx, 180 ; Max width before wrap
mov si, long_text
mov ah, 50 ; gfx_draw_string_wrap
int 0x80
; CX now contains the Y position after the last line ; Draw a rectangle outline
mov bx, 10 ; X
mov cx, 10 ; Y
mov dx, 50 ; Width
mov si, 30 ; Height
mov ah, 1 ; gfx_draw_rect
int 0x80
; Draw a filled rectangle
mov ah, 2 ; gfx_draw_filled_rect
int 0x80
; Clear an area to black
mov ah, 5 ; gfx_clear_area
int 0x80
; Draw a single pixel
mov bx, 100 ; X
mov cx, 50 ; Y
mov al, 3 ; Color: white
mov ah, 0 ; gfx_draw_pixel
int 0x80Three fonts are available. The default is font 1 (8x8).
; Switch to small font
mov al, 0 ; Font 0: 4x6
mov ah, 48 ; gfx_set_font
int 0x80
; Measure text width
mov si, my_string
mov ah, 33 ; gfx_text_width
int 0x80
; DX = width in pixels| Font | Size | Advance | Chars per 320px line |
|---|---|---|---|
| 0 | 4x6 | 6px | 53 |
| 1 | 8x8 | 12px | 26 |
| 2 | 8x12 | 12px | 26 |
; Draw a button
mov bx, 10 ; X
mov cx, 40 ; Y
mov dx, 80 ; Width
mov si, 16 ; Height
mov ax, cs
mov es, ax
mov di, btn_label ; ES:DI = label text
mov al, 0 ; 0 = normal, 1 = pressed
mov ah, 51 ; widget_draw_button
int 0x80Check if the mouse cursor is inside a rectangle (useful for button clicks):
; Is mouse inside the button area?
mov bx, 10 ; Button X
mov cx, 40 ; Button Y
mov dx, 80 ; Button width
mov si, 16 ; Button height
mov ah, 53 ; widget_hit_test
int 0x80
cmp al, 1
je .button_clicked ; Check if mouse is available
mov ah, 30 ; mouse_is_enabled
int 0x80
cmp al, 0
je .no_mouse
; Get mouse state
mov ah, 28 ; mouse_get_state
int 0x80
; BX = X, CX = Y, AL = buttons
; Check left button
test al, 1 ; Bit 0 = left button
jnz .left_clicked ; Play middle C (262 Hz)
mov bx, 262
mov ah, 41 ; speaker_tone
int 0x80
; Wait...
; Silence
mov ah, 42 ; speaker_off
int 0x80The speaker is automatically silenced when your app exits.
; Mount the boot drive
mov ah, 43 ; get_boot_drive
int 0x80
; AL = drive number
mov ah, 13 ; fs_mount
int 0x80
jc .mount_error
mov [cs:mount_handle], bx
; Open a file
mov bx, [cs:mount_handle]
mov si, filename ; "DATA.TXT"
mov ah, 14 ; fs_open
int 0x80
jc .open_error
mov [cs:file_handle], ax
; Read 512 bytes
mov ax, [cs:file_handle]
mov bx, 512
mov cx, cs
mov es, cx
mov di, buffer
mov ah, 15 ; fs_read
int 0x80
; AX = bytes actually read
; Close
mov ax, [cs:file_handle]
mov ah, 16 ; fs_close
int 0x80
; Data
filename: db 'DATA.TXT', 0
mount_handle: dw 0
file_handle: dw 0
buffer: times 512 db 0Assemble with NASM as a flat binary:
nasm -f bin -o MYAPP.BIN apps/myapp.asmTo include on the OS floppy, place the .BIN file in the FAT12 filesystem of build/unodos-144.img. The Makefile handles this for apps in the apps/ directory.
| Metric | Value |
|---|---|
| Resolution | 320 x 200 pixels |
| Colors | 4 (black, green, red, white) |
| Bits per pixel | 2 |
| Max windows | 16 |
| Max concurrent apps | 6 (+ launcher) |
| App segment size | 64 KB |
| Default font | 8x8, 12px advance |
| Title bar height | 10 px |
| Window border | 1 px |
-
Forgetting STI - The most common bug. Without
STIafterINT 0x80, your event loop hangs because hardware interrupts are disabled. -
Not handling EVENT_WIN_REDRAW - If you don't repaint when you receive this event, your window content disappears after another window is dragged over it.
-
Not setting DS - DS is undefined on entry. Always set
DS = CSbefore accessing local variables. -
Stack corruption - Every
PUSHmust have a matchingPOP. Mismatched pushes/pops cause crashes onRETF. -
Using BP for data pointers -
[BP + offset]defaults to the SS segment, not DS. This is a known x86 quirk. If you must use BP for data, write[ds:bp + offset]. -
FAT filename format -
fs_readdirreturns space-padded 11-byte names (CLOCK BIN).fs_openexpects dot format (CLOCK.BIN). You must convert between them.
For the complete API register reference (105 functions, APIs 0-104), see API_REFERENCE.md.
v3.23.0 Build 397