17 KiB
DVX Shell
The DVX Shell (dvx.exe) is the host process for the DVX desktop environment.
It initializes the GUI subsystem, loads DXE3 application modules at runtime,
runs the cooperative main loop, and provides crash recovery so a faulting app
does not take down the entire system. Think of it as the Windows 3.1 Program
Manager and kernel combined into one executable.
Files
| File | Purpose |
|---|---|
shellMain.c |
Entry point, main loop, crash recovery, logging, desktop update callbacks |
shellApp.c |
App loading (dlopen/dlsym), lifecycle state machine, reaping, resource tracking, config dirs |
shellApp.h |
ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, public shell API |
shellExport.c |
DXE export table (400+ symbols), wrapper functions for resource tracking |
shellInfo.c |
System information gathering (delegates to platform layer), caches result |
shellInfo.h |
shellInfoInit(), shellGetSystemInfo() |
shellTaskMgr.c |
Task Manager window -- list view, Switch To / End Task / Run buttons |
shellTaskMgr.h |
shellTaskMgrOpen(), shellTaskMgrRefresh() |
Makefile |
Cross-compile rules, links -ldvx -ltasks -lm |
Building
make # builds ../bin/dvx.exe (also builds libdvx.a, libtasks.a)
make clean # removes objects and binary
Requires the DJGPP cross-compiler toolchain and the DXE3 tools (dxe3gen).
Startup Sequence
main() in shellMain.c performs initialization in this order:
- Change to exe directory -- resolve the directory containing
dvx.exeviaargv[0]andplatformChdir()so that relative paths (CONFIG/,APPS/, etc.) work regardless of where the user launched from. - Truncate log -- open
dvx.logfor write to clear it, then close. All subsequent writes use append-per-write (the file is never held open). - Load preferences --
prefsLoad("CONFIG/DVX.INI"). Missing file or keys silently fall back to compiled-in defaults. - dvxInit -- initialize VESA video (LFB), backbuffer, compositor, window manager, font, cursor, input subsystems. Reads video width/height/bpp from preferences (default 640x480x16).
- Mouse config -- read wheel direction, double-click speed, acceleration
from
[mouse]section and calldvxSetMouseConfig(). - Color scheme -- read
[colors]section (20 RGB triplets), apply viadvxApplyColorScheme(). - Wallpaper -- read
[desktop]section for wallpaper path and mode (stretch/tile/center), load viadvxSetWallpaper(). - Video mode log -- enumerate all available VESA modes to
dvx.log. - Task system --
tsInit(), set shell task (task 0) toTS_PRIORITY_HIGHso the UI stays responsive under load. - System info --
shellInfoInit()gathers CPU, memory, drive info via the platform layer and logs it. - DXE exports --
shellExportInit()callsdlregsym()to register the export table. Must happen before anydlopen(). - App slot table --
shellAppInit()zeroes the 32-slot fixed array. - Idle/hotkey callbacks -- wire up
idleYield,ctrlEscHandler,titleChangeHandleron theAppContextT. - Desktop app --
shellLoadApp(ctx, "apps/progman/progman.app"). If this fails, the shell exits. - Crash handlers --
installCrashHandler()registers signal handlers for SIGSEGV, SIGFPE, SIGILL. Installed last so initialization crashes get the default DJGPP abort-with-register-dump instead of our recovery path.
Main Loop
The main loop runs until dvxQuit() sets ctx->running = false:
while (ctx->running) {
dvxUpdate(ctx); // (1)
tsYield(); // (2)
shellReapApps(ctx); // (3)
shellDesktopUpdate(); // (4)
}
- dvxUpdate -- processes mouse/keyboard input, dispatches widget and
window callbacks, composites dirty rects, flushes to the LFB. This is the
shell's primary job. When idle (no events, no dirty rects), dvxUpdate
calls the registered
idleCallbackwhich yields to app tasks. - tsYield -- explicit yield to give app tasks CPU time even during busy frames with many repaints. Without this, a flurry of mouse-move events could starve app tasks because dvxUpdate would keep finding work.
- shellReapApps -- scans the 32-slot app table for apps in
AppStateTerminatingE. Calls the shutdown hook (if present), destroys all windows owned by the app, kills the task, closes the DXE handle, and cleans up temp files. Returns true if anything was reaped. - shellDesktopUpdate -- if apps were reaped, iterates the registered desktop update callback list so listeners (Program Manager, Task Manager) can refresh their UI.
Crash Recovery
The shell provides Windows 3.1-style fault tolerance using setjmp/longjmp:
installCrashHandler()registerscrashHandlerfor SIGSEGV, SIGFPE, SIGILL.setjmp(sCrashJmp)inmain()establishes the recovery point.- If a signal fires (in any task),
crashHandlerlogs the crash details (signal, app name, full register dump from__djgpp_exception_state_ptr), re-installs the handler (DJGPP uses SysV semantics -- handler resets toSIG_DFLafter each delivery), thenlongjmp(sCrashJmp, 1). longjmprestores the main task's stack frame to thesetjmppoint. This is safe because cooperative switching means the main task's stack is always intact -- it was cleanly suspended at a yield point.tsRecoverToMain()fixes the task scheduler'scurrentIdxso it points back to task 0 instead of the crashed task.- The crashed app is force-killed via
shellForceKillApp(), and a message box is displayed: "'AppName' has caused a fault and will be terminated." - The desktop update callbacks are notified so the UI refreshes.
The register dump logged includes EIP, CS, all GPRs (EAX-EDI), segment registers, and EFLAGS -- invaluable for post-mortem debugging.
App Lifecycle
DXE3 Loading
DXE3 is DJGPP's dynamic linking mechanism. Each .app file is a DXE3 shared
object. The shell resolves symbols from the export table registered via
dlregsym(). The load sequence in shellLoadApp():
- Allocate a slot from the 32-entry fixed array (slot 0 is the shell).
- Check if this DXE path is already loaded (
findLoadedPath).- If the existing app's descriptor says
multiInstance = true, copy the.appto a unique temp file (usingTEMP/TMPenv var) sodlopengets an independent code+data image. - If
multiInstance = false, block with an error message.
- If the existing app's descriptor says
dlopen(loadPath, RTLD_GLOBAL)-- load the DXE.dlsym(handle, "_appDescriptor")-- resolve the metadata struct.dlsym(handle, "_appMain")-- resolve the entry point.dlsym(handle, "_appShutdown")-- resolve the optional shutdown hook.- Fill in the
ShellAppTslot (name, path, handle, state, etc.). - Derive
appDir(directory containing the.appfile) andconfigDir(CONFIG/<apppath>/-- mirrors the app's directory structure underCONFIG/). - Set
sCurrentAppIdto the slot index. - Launch:
- Callback-only (
hasMainLoop = false): callentryFn()directly in task 0. The app creates windows, registers callbacks, and returns. - Main-loop (
hasMainLoop = true): calltsCreate()to make a cooperative task with the descriptor's stack size and priority. The task wrapper setssCurrentAppId, callsentryFn(), and marks the appAppStateTerminatingEwhen it returns.
- Callback-only (
- Reset
sCurrentAppIdto 0. Set state toAppStateRunningE. Notify desktop update callbacks.
AppDescriptorT
Every DXE app exports a global AppDescriptorT:
| Field | Type | Description |
|---|---|---|
name |
char[64] |
Display name (Task Manager, title bars) |
hasMainLoop |
bool |
true = dedicated cooperative task; false = callback-only |
multiInstance |
bool |
true = allow multiple simultaneous instances via temp file copy |
stackSize |
int32_t |
Task stack in bytes (SHELL_STACK_DEFAULT = use default) |
priority |
int32_t |
TS_PRIORITY_LOW / TS_PRIORITY_NORMAL / TS_PRIORITY_HIGH |
Callback vs Main-Loop Apps
Callback-only (hasMainLoop = false):
appMain()runs in the shell's task 0, creates windows, registers event callbacks, and returns immediately.- All subsequent work happens through callbacks dispatched by
dvxUpdate(). - Lifecycle ends when the last window is closed (detected by the
shellWrapDestroyWindowwrapper). - No task stack allocated -- simpler and cheaper.
- Examples: Program Manager, Notepad, Control Panel, DVX Demo, Image Viewer.
Main-loop (hasMainLoop = true):
- A cooperative task is created via
tsCreate(). appMain()runs in that task with its own loop callingtsYield().- Needed when the app has ongoing work that cannot be expressed purely as event callbacks (polling, computation, animation).
- Lifecycle ends when
appMain()returns. - Example: Clock (polls
time()each second).
Multi-Instance Support
DXE3's dlopen is reference-counted per path -- loading the same .app
twice returns the same handle, sharing all globals and statics. For apps
that set multiInstance = true, the shell copies the .app to a temp file
(e.g., C:\TEMP\_dvx02.app) before dlopen, giving each instance its own
code and data segment. Temp files are cleaned up on app termination.
App State Machine
Free -> Loaded -> Running -> Terminating -> Free
- Free: slot available.
- Loaded: DXE loaded, entry point not yet called (transient).
- Running: entry point called, app is active.
- Terminating: app's task returned or last window closed; awaiting reap.
Resource Tracking
sCurrentAppId is a global set before calling any app code (entry, shutdown,
callbacks). The shell's dvxCreateWindow wrapper stamps every new window with
win->appId = sCurrentAppId. This is how the shell knows which windows belong
to which app, enabling:
- Per-app window cleanup on crash or termination (walk the window stack,
destroy all windows with matching
appId). - The last-window-closes-app rule for callback-only apps: when
shellWrapDestroyWindowdetects that a callback-only app has no remaining windows, it marks the appAppStateTerminatingE.
sCurrentAppId is a simple global (not thread-local) because cooperative
multitasking means only one task runs at a time.
Task Manager
shellTaskMgr.c implements a shell-level Task Manager accessible via
Ctrl+Esc regardless of which app is focused or whether the desktop app
is running. It is owned by the shell (appId = 0), not by any DXE app.
Features:
- ListView with 5 columns: Name, Title, File, Type (Task/Callback), Status.
- Switch To -- find the app's topmost window, restore if minimized, raise and focus it.
- End Task -- force-kill the selected app via
shellForceKillApp(). - Run... -- open a file dialog filtered to
*.app, load the selected file. - Status bar -- shows running app count and memory usage (total/free MB).
- Registers with
shellRegisterDesktopUpdateso the list auto-refreshes when apps load, terminate, or crash.
Desktop Update Callbacks
shellRegisterDesktopUpdate(fn) adds a function pointer to a dynamic array
(managed via stb_ds arrput/arrdel). shellDesktopUpdate() iterates the
array and calls each registered function.
This is the mechanism by which the shell notifies interested parties (Program
Manager status bar, Task Manager list) when app state changes -- without
polling. Multiple listeners are supported. Listeners should call
shellUnregisterDesktopUpdate() before they are destroyed.
App Config Storage
Each app gets a dedicated writable config directory under CONFIG/. The path
mirrors the app's location under APPS/:
App path: APPS/PROGMAN/progman.app
Config dir: CONFIG/PROGMAN/
API:
shellEnsureConfigDir(ctx)-- create the directory tree viaplatformMkdirRecursive(). Returns 0 on success.shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))-- build a full path to a file in the config directory.
Apps use the standard preferences system (prefsLoad/prefsSave) pointed at
their config directory for persistent settings.
DXE Export Table
shellExport.c contains the ABI contract between the shell and apps. Three
categories of exports:
-
Wrapped functions (3):
dvxCreateWindow,dvxCreateWindowCentered,dvxDestroyWindow. These are intercepted to stampwin->appIdfor resource ownership tracking. Apps see them under their original names -- the wrapping is transparent. -
Direct exports (200+): all other
dvx*,wgt*,wm*,ts*, drawing, preferences, platform, and shell API functions. Safe to call without interception. -
libc / libm / runtime exports (200+): DXE3 modules are relocatable objects, not fully linked executables. Every C library function a DXE calls must be explicitly listed so the loader can resolve it at dlopen time. This includes:
- Memory (malloc, calloc, realloc, free)
- String operations (str*, mem*)
- Formatted I/O (printf, snprintf, fprintf, sscanf, etc.)
- File I/O (fopen, fread, fwrite, fclose, etc.)
- Directory operations (opendir, readdir, closedir, mkdir)
- Time (time, localtime, clock, strftime)
- Math (sin, cos, sqrt, pow, floor, ceil, etc.)
- stb_ds internals (arrput/arrfree/hm* macro backends)
- stb_image / stb_image_write
- libgcc 64-bit integer helpers (__divdi3, __moddi3, etc.)
- DJGPP stdio internals (__dj_stdin, __dj_stdout, __dj_stderr)
The table is registered once via dlregsym() before any dlopen().
System Hotkeys
These are always active regardless of which app is focused:
| Hotkey | Action |
|---|---|
| Alt+Tab | Cycle windows forward (rotate top to bottom of stack) |
| Shift+Alt+Tab | Cycle windows backward (pull bottom to top) |
| Alt+F4 | Close the focused window (calls its onClose callback) |
| Ctrl+F12 | Full screen screenshot -- prompts for save path (PNG/BMP/JPG) |
| Ctrl+Shift+F12 | Focused window screenshot -- prompts for save path |
| Ctrl+Esc | Open/raise the Task Manager (shell-level, always available) |
| F10 | Activate/toggle the focused window's menu bar |
| Alt+Space | Open/close the system menu on the focused window |
Screenshot System
Two screenshot functions are available, both accessible from the system menu (Alt+Space) or via hotkeys:
interactiveScreenshot(ctx)-- captures the full screen (composited backbuffer), then opens a Save As file dialog filtered to PNG/BMP/JPG/TGA. Called by Ctrl+F12 or the "Screenshot..." system menu item.interactiveWindowScreenshot(ctx, win)-- captures just the focused window's content buffer. Called by Ctrl+Shift+F12 or the "Window Shot..." system menu item.
The system menu also includes standard window operations: Restore, Move, Size, Minimize, Maximize, and Close.
Logging
shellLog(fmt, ...) appends a printf-formatted line to dvx.log. The file
is opened, written, and closed on each call (append-per-write) so:
- The file is never held open, allowing Notepad to read it while the shell runs.
- Writes are flushed immediately, important for crash diagnostics.
- The file is truncated once at startup.
Log output includes: startup sequence, preferences applied, video modes enumerated, system information, app load/reap events, crash details with full register dumps.
Shell API Summary
| Function | Description |
|---|---|
shellAppInit() |
Zero the 32-slot app table |
shellLoadApp(ctx, path) |
Load and start a DXE app. Returns app ID (>= 1) or -1 |
shellReapApps(ctx) |
Clean up terminated apps (call each frame). Returns true if any reaped |
shellReapApp(ctx, app) |
Gracefully shut down one app (calls shutdown hook) |
shellForceKillApp(ctx, app) |
Forcibly kill an app -- skips shutdown hook |
shellTerminateAllApps(ctx) |
Kill all running apps (shell shutdown) |
shellGetApp(appId) |
Get app slot by ID (NULL if invalid/free) |
shellRunningAppCount() |
Count running apps (excluding the shell) |
shellLog(fmt, ...) |
Append to dvx.log |
shellEnsureConfigDir(ctx) |
Create an app's config directory tree |
shellConfigPath(ctx, name, buf, size) |
Build path to file in app's config dir |
shellExportInit() |
Register DXE symbol export table via dlregsym() |
shellRegisterDesktopUpdate(fn) |
Register callback for app state changes |
shellUnregisterDesktopUpdate(fn) |
Remove a previously registered callback |
shellDesktopUpdate() |
Notify all registered listeners |
shellTaskMgrOpen(ctx) |
Open or raise the Task Manager window |
shellTaskMgrRefresh() |
Refresh the Task Manager list and status |
shellInfoInit(ctx) |
Gather and log system hardware information |
shellGetSystemInfo() |
Return cached system info text |