DVX_GUI/shell
2026-03-25 21:43:41 -05:00
..
Makefile First pass of major debugging. At a glance, everything is working again. 2026-03-25 21:43:41 -05:00
README.md Docs. 2026-03-22 21:03:10 -05:00
shellApp.c First pass of major debugging. At a glance, everything is working again. 2026-03-25 21:43:41 -05:00
shellApp.h First pass of major debugging. At a glance, everything is working again. 2026-03-25 21:43:41 -05:00
shellInfo.c First pass of major debugging. At a glance, everything is working again. 2026-03-25 21:43:41 -05:00
shellInfo.h Start of major refactor for dynamic library and widget loading. 2026-03-22 20:50:25 -05:00
shellMain.c First pass of major debugging. At a glance, everything is working again. 2026-03-25 21:43:41 -05:00
shellTaskMgr.c MAJOR refactoring. Everything is dynamically loaded now. All new bugs to squash! 2026-03-24 23:03:05 -05:00
shellTaskMgr.h Start of major refactor for dynamic library and widget loading. 2026-03-22 20:50:25 -05:00

DVX Shell

The DVX Shell (dvxshell.lib) is a DXE module loaded by the DVX loader at startup. 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.

The shell is not a standalone executable. The loader maps it into memory via dlopen, resolves its entry point (shellMain), and calls it. All symbols the shell needs -- libdvx, libtasks, widgets, libc -- are resolved at load time from DXE modules already loaded by the loader and the loader's own dlregsym table.

Files

File Purpose
shellMain.c Entry point (shellMain), main loop, crash recovery, logging
shellApp.c App loading via dlopen/dlsym, lifecycle, reaping
shellApp.h ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, API
shellExport.c 3 DXE wrapper overrides for window resource tracking
shellInfo.c System info gathering
shellInfo.h shellInfoInit(), shellGetSystemInfo()
shellTaskMgr.c Task Manager window
shellTaskMgr.h shellTaskMgrOpen(), shellTaskMgrRefresh()
Makefile Cross-compile rules, produces dvxshell.lib and dvxshell.dep

Building

make        # builds ../bin/libs/dvxshell.lib and ../bin/libs/dvxshell.dep
make clean  # removes objects, binary, and deployed config files

CFLAGS: -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../tasks/thirdparty

The shell is compiled to object files and packaged into a DXE via dxe3gen. It exports _shell so the loader can resolve shellMain. All other symbols are unresolved imports (-U) satisfied at load time.

Requires the DJGPP cross-compiler toolchain and the DXE3 tools (dxe3gen).

Dependencies

dvxshell.dep lists the modules the loader must load before the shell:

libtasks
libdvx
box
button
checkbox
dropdown
label
listbox
listview
radio
separator
spacer
statbar
textinpt

The loader reads this file, loads each module via dlopen with RTLD_GLOBAL, then loads the shell itself. This is how all symbols (dvx*, wgt*, ts*, etc.) become available without the shell statically linking anything.

Startup Sequence

shellMain() in shellMain.c is called by the loader after all dependencies are loaded. It performs initialization in this order:

  1. Truncate log -- open dvx.log for write to clear it, then close. All subsequent writes use append-per-write (the file is never held open).
  2. Load preferences -- prefsLoad("CONFIG/DVX.INI"). Missing file or keys silently fall back to compiled-in defaults.
  3. dvxInit -- initialize VESA video (LFB), backbuffer, compositor, window manager, font, cursor, input subsystems. Reads video width/height/bpp from preferences (default 640x480x16).
  4. Mouse config -- read wheel direction, double-click speed, acceleration from [mouse] section and call dvxSetMouseConfig().
  5. Color scheme -- read [colors] section (20 RGB triplets), apply via dvxApplyColorScheme().
  6. Wallpaper -- read [desktop] section for wallpaper path and mode (stretch/tile/center), load via dvxSetWallpaper().
  7. Video mode log -- enumerate all available VESA modes to dvx.log.
  8. Task system -- tsInit(), set shell task (task 0) to TS_PRIORITY_HIGH so the UI stays responsive under load.
  9. System info -- shellInfoInit() gathers CPU, memory, drive info via the platform layer and logs it.
  10. DXE exports -- shellExportInit() calls dlregsym() to register the 3 wrapper overrides. Must happen before any dlopen() of app DXEs.
  11. App slot table -- shellAppInit() zeroes the 32-slot fixed array.
  12. Idle/hotkey callbacks -- wire up idleYield, ctrlEscHandler, titleChangeHandler on the AppContextT.
  13. Desktop app -- shellLoadApp(ctx, "apps/progman/progman.app"). If this fails, the shell exits.
  14. 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)
}
  1. 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 idleCallback which yields to app tasks.
  2. 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.
  3. 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.
  4. 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:

  1. installCrashHandler() registers crashHandler for SIGSEGV, SIGFPE, SIGILL.
  2. setjmp(sCrashJmp) in shellMain() establishes the recovery point.
  3. If a signal fires (in any task), crashHandler logs the crash details (signal, app name, full register dump from __djgpp_exception_state_ptr), re-installs the handler (DJGPP uses SysV semantics -- handler resets to SIG_DFL after each delivery), then longjmp(sCrashJmp, 1).
  4. longjmp restores the main task's stack frame to the setjmp point. This is safe because cooperative switching means the main task's stack is always intact -- it was cleanly suspended at a yield point.
  5. tsRecoverToMain() fixes the task scheduler's currentIdx so it points back to task 0 instead of the crashed task.
  6. The crashed app is force-killed via shellForceKillApp(), and a message box is displayed: "'AppName' has caused a fault and will be terminated."
  7. 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. Symbols are resolved from the loaded RTLD_GLOBAL modules (libdvx, libtasks, widgets) and the shell's 3 wrapper overrides registered via dlregsym(). The load sequence in shellLoadApp():

  1. Allocate a slot from the 32-entry fixed array (slot 0 is the shell).
  2. Check if this DXE path is already loaded (findLoadedPath).
    • If the existing app's descriptor says multiInstance = true, copy the .app to a unique temp file (using TEMP/TMP env var) so dlopen gets an independent code+data image.
    • If multiInstance = false, block with an error message.
  3. dlopen(loadPath, RTLD_GLOBAL) -- load the DXE.
  4. dlsym(handle, "_appDescriptor") -- resolve the metadata struct.
  5. dlsym(handle, "_appMain") -- resolve the entry point.
  6. dlsym(handle, "_appShutdown") -- resolve the optional shutdown hook.
  7. Fill in the ShellAppT slot (name, path, handle, state, etc.).
  8. Derive appDir (directory containing the .app file) and configDir (CONFIG/<apppath>/ -- mirrors the app's directory structure under CONFIG/).
  9. Set sCurrentAppId to the slot index.
  10. Launch:
    • Callback-only (hasMainLoop = false): call entryFn() directly in task 0. The app creates windows, registers callbacks, and returns.
    • Main-loop (hasMainLoop = true): call tsCreate() to make a cooperative task with the descriptor's stack size and priority. The task wrapper sets sCurrentAppId, calls entryFn(), and marks the app AppStateTerminatingE when it returns.
  11. Reset sCurrentAppId to 0. Set state to AppStateRunningE. 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 shellWrapDestroyWindow wrapper).
  • 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 calling tsYield().
  • 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 shellWrapDestroyWindow detects that a callback-only app has no remaining windows, it marks the app AppStateTerminatingE.

sCurrentAppId is a simple global (not thread-local) because cooperative multitasking means only one task runs at a time.

DXE Export Table

shellExport.c registers 3 wrapper functions via dlregsym() that override the real implementations for subsequently loaded app DXEs:

  1. dvxCreateWindow -- stamps win->appId for resource ownership tracking.
  2. dvxCreateWindowCentered -- stamps win->appId for resource ownership tracking.
  3. dvxDestroyWindow -- checks if the destroyed window was a callback-only app's last window and marks it for reaping.

The key mechanic: dlregsym takes precedence over RTLD_GLOBAL exports. Since libdvx (which has the real functions) was loaded before shellExportInit() registers these wrappers, libdvx keeps the real implementations. But any app DXE loaded afterward gets the wrappers, which add resource tracking transparently.

All other symbol exports -- dvx*, wgt*, ts*, platform*, libc, libm -- come from the DXE modules loaded with RTLD_GLOBAL by the loader. They no longer need to be listed in the shell's export table.

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 shellRegisterDesktopUpdate so 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 via platformMkdirRecursive(). 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.

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
shellMain(argc, argv) Entry point called by the DVX loader
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 3 DXE wrapper overrides 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