DVX_GUI/tasks/README.md
2026-03-20 20:00:05 -05:00

15 KiB

taskswitch -- Cooperative Task Switching Library

Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS protected mode). Part of the DVX GUI project.

Tasks voluntarily yield the CPU by calling tsYield(). A credit-based weighted round-robin scheduler gives higher-priority tasks proportionally more CPU time while guaranteeing that low-priority tasks are never starved. A priority-10 task gets 11 turns per scheduling round; a priority-0 task gets 1 -- but it always runs eventually.

The task array is backed by stb_ds and grows dynamically. Terminated task slots are recycled, so there is no fixed upper limit on the number of tasks created over the lifetime of the application.

Why Cooperative?

DOS is single-threaded. DPMI provides no timer-based preemption. The DVX GUI event model is inherently single-threaded: one compositor, one input queue, one window stack. Preemptive switching would require locking around every GUI call for no benefit. Cooperative switching lets each task yield at safe points, avoiding synchronization entirely.

Files

File Description
taskswitch.h Public API -- types, constants, function prototypes
taskswitch.c Implementation (scheduler, context switch, slots)
demo.c Standalone test harness exercising all features
thirdparty/stb_ds.h stb dynamic array/hashmap library (third-party)
Makefile DJGPP cross-compilation build rules

Building

Cross-compile from Linux:

make            # builds ../lib/libtasks.a
make demo       # also builds ../bin/tsdemo.exe
make clean      # removes objects, library, and demo binary

Output:

Path Description
../lib/libtasks.a Static library
../obj/tasks/ Object files
../bin/tsdemo.exe Demo executable

Quick Start

#include <stdio.h>
#include "taskswitch.h"

void myTask(void *arg) {
    const char *name = (const char *)arg;
    for (int i = 0; i < 3; i++) {
        printf("[%s] working...\n", name);
        tsYield();
    }
}

int main(void) {
    tsInit();
    tsCreate("alpha", myTask, "alpha", 0, TS_PRIORITY_NORMAL);
    tsCreate("beta",  myTask, "beta",  0, TS_PRIORITY_HIGH);

    while (tsActiveCount() > 1) {
        tsYield();
    }

    tsShutdown();
    return 0;
}

Lifecycle

  1. tsInit() -- Initialize the task system. The calling context (typically main) becomes task 0 with TS_PRIORITY_NORMAL. No separate stack is allocated for task 0 -- it uses the process stack.

  2. tsCreate(...) -- Create tasks. Each gets a name, entry function, argument pointer, stack size (0 for the default), and a priority. Returns the task ID (>= 0) or a negative error code. Terminated task slots are reused automatically.

  3. tsYield() -- Call from any task (including main) to hand the CPU to the next eligible task. This is the sole mechanism for task switching.

  4. tsShutdown() -- Free all task stacks and the task array.

Tasks terminate by returning from their entry function or by calling tsExit(). The main task (id 0) must never call tsExit(). When a task terminates, its stack is freed immediately and its slot becomes available for reuse by the next tsCreate() call.

API Reference

Initialization and Teardown

Function Signature Description
tsInit int32_t tsInit(void) Initialize the library. Returns TS_OK or a negative error code.
tsShutdown void tsShutdown(void) Free all resources. Safe to call even if tsInit was never called.

Task Creation and Termination

Function Signature Description
tsCreate int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri) Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for ss to use TS_DEFAULT_STACK_SIZE. Reuses terminated slots.
tsExit void tsExit(void) Terminate the calling task. Must not be called from the main task. Never returns.
tsKill int32_t tsKill(uint32_t taskId) Forcibly terminate another task. Cannot kill main (id 0) or self (use tsExit instead).

Scheduling

Function Signature Description
tsYield void tsYield(void) Voluntarily relinquish the CPU to the next eligible ready task.

Pausing and Resuming

Function Signature Description
tsPause int32_t tsPause(uint32_t id) Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield.
tsResume int32_t tsResume(uint32_t id) Resume a paused task. Credits are refilled so it is not penalized for having been paused.

Priority

Function Signature Description
tsSetPriority int32_t tsSetPriority(uint32_t id, int32_t pri) Change a task's priority. Credits are reset so the change takes effect immediately.
tsGetPriority int32_t tsGetPriority(uint32_t id) Return the task's priority, or TS_ERR_PARAM on an invalid ID.

Crash Recovery

Function Signature Description
tsRecoverToMain void tsRecoverToMain(void) Reset scheduler state to task 0 after a longjmp from a signal handler. Call before tsKill on the crashed task. The crashed task's slot is NOT freed -- call tsKill afterward.

Query

Function Signature Description
tsGetState TaskStateE tsGetState(uint32_t id) Return the task's state enum value.
tsCurrentId uint32_t tsCurrentId(void) Return the ID of the currently running task.
tsGetName const char *tsGetName(uint32_t id) Return the task's name string, or NULL on invalid ID.
tsActiveCount uint32_t tsActiveCount(void) Return the number of non-terminated tasks.

Constants

Error Codes

Name Value Meaning
TS_OK 0 Success
TS_ERR_INIT -1 Library not initialized
TS_ERR_PARAM -2 Invalid parameter
TS_ERR_FULL -3 Task table full (unused, kept for compatibility)
TS_ERR_NOMEM -4 Memory allocation failed
TS_ERR_STATE -5 Invalid state transition

Priority Presets

Name Value Credits per Round
TS_PRIORITY_LOW 0 1
TS_PRIORITY_NORMAL 5 6
TS_PRIORITY_HIGH 10 11

Any non-negative int32_t may be used as a priority. The presets are provided for convenience. In the DVX Shell, the main task runs at TS_PRIORITY_HIGH to keep the UI responsive; app tasks default to TS_PRIORITY_NORMAL.

Defaults

Name Value Description
TS_DEFAULT_STACK_SIZE 32768 Default stack per task
TS_NAME_MAX 32 Max task name length

Types

TaskStateE

typedef enum {
    TaskStateReady      = 0,  // Eligible for scheduling
    TaskStateRunning    = 1,  // Currently executing
    TaskStatePaused     = 2,  // Suspended until tsResume()
    TaskStateTerminated = 3   // Finished; slot will be recycled
} TaskStateE;

Only Ready tasks participate in scheduling. Running is cosmetic (marks the currently executing task). Paused tasks are skipped until explicitly resumed. Terminated slots are recycled by tsCreate.

TaskEntryT

typedef void (*TaskEntryT)(void *arg);

The signature every task entry function must follow. The arg parameter is the pointer passed to tsCreate.

Scheduler Details

The scheduler is a credit-based weighted round-robin, a variant of the Linux 2.4 goodness() scheduler.

  1. Every ready task holds a credit counter initialized to priority + 1.
  2. When tsYield() is called, the scheduler scans tasks starting one past the current task (wrapping around) looking for a ready task with credits > 0. When found, that task's credits are decremented and it becomes the running task.
  3. When no ready task has credits remaining, every ready task is refilled to priority + 1 (one "epoch") and the scan repeats.

This means a priority-10 task receives 11 turns for every 1 turn a priority-0 task receives, but the low-priority task still runs -- it is never starved.

Credits are also refilled when:

  • A task is created (tsCreate) -- starts with priority + 1.
  • A task is resumed (tsResume) -- refilled so it runs promptly.
  • A task's priority changes (tsSetPriority) -- reset to new + 1.

Task Slot Management

The task array is a stb_ds dynamic array that grows automatically. Each slot has an allocated flag:

  • tsCreate() scans for the first unallocated slot (starting at index 1, since slot 0 is always the main task). If no free slot exists, the array is extended with arrput().
  • tsExit() and tsKill() free the terminated task's stack immediately and mark the slot as unallocated, making it available for the next tsCreate() call.
  • Task IDs are stable array indices. Slots are never removed or reordered, so a task ID remains valid for queries until the slot is recycled.

This supports long-running applications (like the DVX Shell) that create and destroy many tasks over their lifetime without unbounded memory growth.

Context Switch Internals

Context switching uses inline assembly with both i386 and x86_64 code paths. The contextSwitch function is marked noinline to preserve callee-saved register assumptions.

Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save callee-saved registers and do not give control over the stack pointer in a portable way. New tasks need a fresh stack with the instruction pointer set to a trampoline -- setjmp cannot bootstrap that. The asm approach also avoids ABI differences in jmp_buf layout across DJGPP versions.

i386 (DJGPP target)

Six callee-saved values are saved and restored per switch:

Register Offset Purpose
EBX 0 Callee-saved general purpose
ESI 4 Callee-saved general purpose
EDI 8 Callee-saved general purpose
EBP 12 Frame pointer
ESP 16 Stack pointer
EIP 20 Resume address (captured as local label)

x86_64 (for native Linux testing)

Eight callee-saved values are saved and restored per switch:

Register Offset Purpose
RBX 0 Callee-saved general purpose
R12 8 Callee-saved general purpose
R13 16 Callee-saved general purpose
R14 24 Callee-saved general purpose
R15 32 Callee-saved general purpose
RBP 40 Frame pointer
RSP 48 Stack pointer
RIP 56 Resume address (RIP-relative lea)

Segment registers are not saved because DJGPP runs in a flat protected-mode environment where CS, DS, ES, and SS share the same base.

New tasks have their initial stack pointer set to a 16-byte-aligned region at the top of a malloc'd stack, with the instruction pointer set to an internal trampoline that calls the user's entry function and then tsExit().

Limitations

  • Cooperative only -- tasks must call tsYield() (or tsPause/tsExit) to allow other tasks to run. A task that never yields blocks everything.
  • Not interrupt-safe -- no locking or volatile module state. Do not call library functions from interrupt handlers.
  • Single-threaded -- designed for one CPU under DOS protected mode.
  • Stack overflow is not detected -- size the stack appropriately for each task's needs.

Demo

demo.c exercises five phases:

  1. Priority scheduling -- creates tasks at low, normal, and high priority. All tasks run, but the high-priority task gets significantly more turns.
  2. Pause -- pauses one task mid-run and shows it stops being scheduled.
  3. Resume -- resumes the paused task and shows it picks up where it left off.
  4. Priority boost -- raises the low-priority task above all others and shows it immediately gets more turns.
  5. Slot reuse -- creates three waves of short-lived tasks that terminate and shows subsequent waves reuse the same task IDs.

Build and run:

make demo
tsdemo

Third-Party Dependencies

  • stb_ds.h (Sean Barrett) -- dynamic array and hashmap library. Located in thirdparty/stb_ds.h. Used for the task control block array. Public domain / MIT licensed.