362 lines
17 KiB
Markdown
362 lines
17 KiB
Markdown
# 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:
|
|
|
|
1. **Change to exe directory** -- resolve the directory containing `dvx.exe`
|
|
via `argv[0]` and `platformChdir()` so that relative paths (`CONFIG/`,
|
|
`APPS/`, etc.) work regardless of where the user launched from.
|
|
2. **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).
|
|
3. **Load preferences** -- `prefsLoad("CONFIG/DVX.INI")`. Missing file or
|
|
keys silently fall back to compiled-in defaults.
|
|
4. **dvxInit** -- initialize VESA video (LFB), backbuffer, compositor, window
|
|
manager, font, cursor, input subsystems. Reads video width/height/bpp from
|
|
preferences (default 640x480x16).
|
|
5. **Mouse config** -- read wheel direction, double-click speed, acceleration
|
|
from `[mouse]` section and call `dvxSetMouseConfig()`.
|
|
6. **Color scheme** -- read `[colors]` section (20 RGB triplets), apply via
|
|
`dvxApplyColorScheme()`.
|
|
7. **Wallpaper** -- read `[desktop]` section for wallpaper path and mode
|
|
(stretch/tile/center), load via `dvxSetWallpaper()`.
|
|
8. **Video mode log** -- enumerate all available VESA modes to `dvx.log`.
|
|
9. **Task system** -- `tsInit()`, set shell task (task 0) to
|
|
`TS_PRIORITY_HIGH` so the UI stays responsive under load.
|
|
10. **System info** -- `shellInfoInit()` gathers CPU, memory, drive info via
|
|
the platform layer and logs it.
|
|
11. **DXE exports** -- `shellExportInit()` calls `dlregsym()` to register the
|
|
export table. Must happen before any `dlopen()`.
|
|
12. **App slot table** -- `shellAppInit()` zeroes the 32-slot fixed array.
|
|
13. **Idle/hotkey callbacks** -- wire up `idleYield`, `ctrlEscHandler`,
|
|
`titleChangeHandler` on the `AppContextT`.
|
|
14. **Desktop app** -- `shellLoadApp(ctx, "apps/progman/progman.app")`. If
|
|
this fails, the shell exits.
|
|
15. **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 `main()` 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. The shell resolves symbols from the export table 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.
|
|
|
|
## 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.
|
|
|
|
## DXE Export Table
|
|
|
|
`shellExport.c` contains the ABI contract between the shell and apps. Three
|
|
categories of exports:
|
|
|
|
1. **Wrapped functions** (3): `dvxCreateWindow`, `dvxCreateWindowCentered`,
|
|
`dvxDestroyWindow`. These are intercepted to stamp `win->appId` for
|
|
resource ownership tracking. Apps see them under their original names --
|
|
the wrapping is transparent.
|
|
|
|
2. **Direct exports** (200+): all other `dvx*`, `wgt*`, `wm*`, `ts*`,
|
|
drawing, preferences, platform, and shell API functions. Safe to call
|
|
without interception.
|
|
|
|
3. **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 |
|