DVX_GUI/shell/README.md
2026-03-22 21:03:10 -05:00

387 lines
17 KiB
Markdown

# 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 |