DVX_GUI/shell/shellMain.c

430 lines
16 KiB
C

// shellMain.c -- DVX Shell entry point and main loop
//
// Initializes the GUI, task system, DXE export table, and loads
// the desktop app. Runs the cooperative main loop, yielding to
// app tasks and reaping terminated apps each frame.
//
// The main loop design (dvxUpdate + tsYield + reap + notify):
// Each iteration does four things:
// 1. dvxUpdate: processes input events, dispatches callbacks, composites
// dirty rects, flushes to the LFB. This is the shell's primary job.
// 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps
// would never run because the shell task would monopolize the CPU.
// 3. shellReapApps: cleans up any apps that terminated during this frame
// (either their task returned or their last window was closed).
// 4. desktopUpdate: notifies the desktop app (Program Manager) if any
// apps were reaped, so it can refresh its task list.
//
// Crash recovery uses setjmp/longjmp:
// The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL
// via platformInstallCrashHandler(). If a crash occurs in an app task, the
// handler logs platform-specific diagnostics (register dump on DJGPP),
// then longjmps back to the setjmp point in main(). This works because
// longjmp restores the main task's stack frame regardless of which task
// was running. tsRecoverToMain() then fixes the scheduler's bookkeeping,
// the shell logs app-specific info, and the crashed app is killed.
// This gives the shell Windows 3.1-style fault tolerance -- one bad app
// doesn't take down the whole system.
#include "shellApp.h"
#include "shellInf.h"
#include "dvxDlg.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxComp.h"
#include "dvxPrefs.h"
#include "dvxPlat.h"
#include "stb_ds_wrap.h"
#include <stdarg.h>
#include <setjmp.h>
#include <stdio.h>
#include <string.h>
#include "dvxMem.h"
// ============================================================
// Module state
// ============================================================
static AppContextT sCtx;
static PrefsHandleT *sPrefs = NULL;
// setjmp buffer for crash recovery. The crash handler longjmps here to
// return control to the shell's main loop after an app crashes.
static jmp_buf sCrashJmp;
// Volatile because it's written from a signal handler context. Tells
// the recovery code which signal fired (for logging/diagnostics).
static volatile int sCrashSignal = 0;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel)
typedef void (*DesktopUpdateFnT)(void);
static DesktopUpdateFnT *sDesktopUpdateFns = NULL;
// ============================================================
// Prototypes
// ============================================================
static void idleYield(void *ctx);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
// ============================================================
// shellDesktopUpdate -- notify desktop app of state change
// ============================================================
void shellDesktopUpdate(void) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
sDesktopUpdateFns[i]();
}
}
// ============================================================
// ctrlEscHandler -- system-wide Ctrl+Esc callback
// ============================================================
static void ctrlEscHandler(void *ctx) {
if (shellCtrlEscFn) {
shellCtrlEscFn((AppContextT *)ctx);
}
}
// ============================================================
// f1HelpHandler -- system-wide F1 launches context-sensitive help
// ============================================================
static void f1HelpHandler(void *ctx) {
AppContextT *ac = (AppContextT *)ctx;
// Find the focused window's owning app
char args[1024] = {0};
int32_t focusedAppId = 0;
if (ac->stack.focusedIdx >= 0) {
WindowT *win = ac->stack.windows[ac->stack.focusedIdx];
focusedAppId = win->appId;
}
if (focusedAppId > 0) {
ShellAppT *app = shellGetApp(focusedAppId);
if (app && app->dxeCtx) {
// Let the app refresh helpTopic based on current context
if (app->dxeCtx->onHelpQuery) {
app->dxeCtx->onHelpQuery(app->dxeCtx->helpQueryCtx);
}
if (app->dxeCtx->helpFile[0]) {
if (app->dxeCtx->helpTopic[0]) {
snprintf(args, sizeof(args), "%s %s",
app->dxeCtx->helpFile, app->dxeCtx->helpTopic);
} else {
snprintf(args, sizeof(args), "%s", app->dxeCtx->helpFile);
}
}
}
}
// Fall back to system help if no app-specific help
char viewerPath[DVX_MAX_PATH];
snprintf(viewerPath, sizeof(viewerPath), "APPS%cKPUNCH%cDVXHELP%cDVXHELP.APP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP);
if (!args[0]) {
snprintf(args, sizeof(args), "APPS%cKPUNCH%cPROGMAN%cDVXHELP.HLP", DVX_PATH_SEP, DVX_PATH_SEP, DVX_PATH_SEP);
}
shellLoadAppWithArgs(ac, viewerPath, args);
}
// ============================================================
// titleChangeHandler -- refresh listeners when a window title changes
// ============================================================
static void titleChangeHandler(void *ctx) {
(void)ctx;
shellDesktopUpdate();
}
// ============================================================
// idleYield -- called when no dirty rects need compositing
// ============================================================
// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
// processed all pending events and there are no dirty rects to composite.
// Instead of busy-spinning, we yield to app tasks -- this is where most
// of the CPU time for main-loop apps comes from when the UI is idle.
// The tsActiveCount > 1 check avoids the overhead of a tsYield call
// (which would do a scheduler scan) when the shell is the only task.
static void idleYield(void *ctx) {
(void)ctx;
if (tsActiveCount() > 1) {
tsYield();
}
}
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
(void)userData;
dvxLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp);
}
// ============================================================
// ============================================================
// shellRegisterDesktopUpdate
// ============================================================
void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
arrput(sDesktopUpdateFns, updateFn);
}
// ============================================================
// shellUnregisterDesktopUpdate
// ============================================================
void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
if (sDesktopUpdateFns[i] == updateFn) {
arrdel(sDesktopUpdateFns, i);
return;
}
}
}
// ============================================================
// shellMain -- entry point called by the DVX loader
// ============================================================
int shellMain(int argc, char *argv[]) {
(void)argc;
(void)argv;
dvxLog("DVX Shell starting...");
// Load preferences (missing file or keys silently use defaults)
sPrefs = prefsLoad("CONFIG/DVX.INI");
// Initialize task system (no GUI needed)
if (tsInit() != TS_OK) {
dvxLog("Failed to initialize task system");
dvxShutdown(&sCtx);
return 1;
}
// Shell task (task 0) gets high priority so the UI remains responsive
// even when app tasks are CPU-hungry. With HIGH priority (11 credits
// per epoch) vs app tasks at NORMAL (6 credits), the shell gets
// roughly twice as many scheduling turns as any single app.
tsSetPriority(0, TS_PRIORITY_HIGH);
// Gather system information (CPU, memory, drives, etc.)
shellInfoInit(&sCtx);
// Initialize app slot table
shellAppInit();
// Point the memory tracker at currentAppId so allocations are
// attributed to whichever app is currently executing.
dvxMemAppIdPtr = &sCtx.currentAppId;
// Set up idle callback for cooperative yielding. When dvxUpdate has
// no work to do (no input events, no dirty rects), it calls this
// instead of busy-looping. This is the main mechanism for giving
// app tasks CPU time during quiet periods.
// Install crash handler before loading apps so faults during app
// initialization are caught and recovered from gracefully.
platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog);
// Initialize GUI — switch from VGA splash to VESA mode as late as possible
// so the VGA loading splash stays visible through all the init above.
{
int32_t videoW = prefsGetInt(sPrefs, "video", "width", 640);
int32_t videoH = prefsGetInt(sPrefs, "video", "height", 480);
int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp", 16);
dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);
int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp);
if (result != 0) {
dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result);
tsShutdown();
return 1;
}
// Set callbacks AFTER dvxInit (which memsets the context to zero)
sCtx.idleCallback = idleYield;
sCtx.idleCtx = &sCtx;
sCtx.onCtrlEsc = ctrlEscHandler;
sCtx.ctrlEscCtx = &sCtx;
sCtx.onF1 = f1HelpHandler;
sCtx.f1Ctx = &sCtx;
sCtx.onTitleChange = titleChangeHandler;
sCtx.titleChangeCtx = &sCtx;
dvxLog("Available video modes:");
platformVideoEnumModes(logVideoMode, NULL);
dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
// Apply mouse preferences
const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", "normal");
int32_t wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;
int32_t dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", 500);
const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium");
int32_t accelVal = 0;
if (strcmp(accelStr, "off") == 0) { accelVal = 10000; }
else if (strcmp(accelStr, "low") == 0) { accelVal = 100; }
else if (strcmp(accelStr, "medium") == 0) { accelVal = 64; }
else if (strcmp(accelStr, "high") == 0) { accelVal = 32; }
dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal);
dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr);
// Apply saved color scheme
bool colorsLoaded = false;
for (int32_t i = 0; i < ColorCountE; i++) {
const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL);
if (val) {
int r, g, b;
if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) {
sCtx.colorRgb[i][0] = (uint8_t)r;
sCtx.colorRgb[i][1] = (uint8_t)g;
sCtx.colorRgb[i][2] = (uint8_t)b;
colorsLoaded = true;
}
}
}
if (colorsLoaded) {
dvxApplyColorScheme(&sCtx);
dvxLog("Preferences: loaded custom color scheme");
}
// Apply saved wallpaper
const char *wpMode = prefsGetString(sPrefs, "desktop", "mode", "stretch");
if (strcmp(wpMode, "tile") == 0) { sCtx.wallpaperMode = WallpaperTileE; }
else if (strcmp(wpMode, "center") == 0) { sCtx.wallpaperMode = WallpaperCenterE; }
else { sCtx.wallpaperMode = WallpaperStretchE; }
const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL);
if (wpPath) {
if (dvxSetWallpaper(&sCtx, wpPath)) {
dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode);
} else {
dvxLog("Preferences: failed to load wallpaper %s", wpPath);
}
}
}
// Load the desktop app (configurable via [shell] desktop= in dvx.ini)
const char *desktopApp = prefsGetString(sPrefs, "shell", "desktop", SHELL_DESKTOP_APP);
int32_t desktopId = shellLoadApp(&sCtx, desktopApp);
if (desktopId < 0) {
dvxLog("Failed to load desktop app '%s'", desktopApp);
tsShutdown();
dvxShutdown(&sCtx);
return 1;
}
dvxLog("DVX Shell ready.");
// Set recovery point for crash handler. setjmp returns 0 on initial
// call (falls through to the main loop). On a crash, longjmp makes
// setjmp return non-zero, entering this recovery block. The recovery
// code runs on the main task's stack (restored by longjmp) so it's
// safe to call any shell function.
if (setjmp(sCrashJmp) != 0) {
// Returned here from crash handler via longjmp.
// The task switcher's currentIdx still points to the crashed task.
// Fix it before doing anything else so the scheduler is consistent.
tsRecoverToMain();
// Platform handler already logged signal name and register dump.
// Log app-specific info here.
dvxLog("Current app ID: %ld", (long)sCtx.currentAppId);
if (sCtx.currentAppId > 0) {
ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId);
if (crashedApp) {
dvxLog("App name: %s", crashedApp->name);
dvxLog("App path: %s", crashedApp->path);
dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no");
dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId);
}
} else {
dvxLog("Crashed in shell (task 0)");
}
dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId);
// Clear busy cursor so the fault dialog is interactive
dvxSetBusy(&sCtx, false);
if (sCtx.currentAppId > 0) {
ShellAppT *app = shellGetApp(sCtx.currentAppId);
if (app) {
char msg[256];
snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
shellForceKillApp(&sCtx, app);
sCtx.currentAppId = 0;
dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR);
}
}
sCtx.currentAppId = 0;
sCrashSignal = 0;
shellDesktopUpdate();
}
// Main loop -- runs until dvxQuit() sets sCtx.running = false.
// Two yield points per iteration: one explicit (below) and one via
// the idle callback inside dvxUpdate. The explicit yield here ensures
// app tasks get CPU time even during busy frames (lots of repaints).
// Without it, a flurry of mouse-move events could starve app tasks
// because dvxUpdate would keep finding work to do and never call idle.
while (sCtx.running) {
dvxUpdate(&sCtx);
// Give app tasks CPU time even during active frames
if (tsActiveCount() > 1) {
tsYield();
}
// Reap terminated apps and notify desktop if anything changed.
// This is the safe point for cleanup -- we're at the top of the
// main loop, not inside any callback or compositor operation.
if (shellReapApps(&sCtx)) {
shellDesktopUpdate();
}
}
dvxLog("DVX Shell shutting down...");
// Clean shutdown: terminate all apps first (destroys windows, kills
// tasks, closes DXE handles), then tear down the task system and GUI
// in reverse initialization order.
shellTerminateAllApps(&sCtx);
tsShutdown();
dvxShutdown(&sCtx);
prefsClose(sPrefs);
sPrefs = NULL;
dvxLog("DVX Shell exited.");
return 0;
}