430 lines
16 KiB
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;
|
|
}
|