# The MIT License (MIT)
#
# Copyright (C) 2026 Scott Duensing
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

.section Libraries
.topic libtasks
.title libtasks -- Cooperative Task Switching
.toc 0 libtasks -- Cooperative Task Switching
.index libtasks
.index Task Switching
.index Cooperative Multitasking
.index taskswitch.h

.h1 libtasks -- Cooperative Task Switching

Credit-based cooperative (non-preemptive) multitasking library for DOS protected mode (DJGPP/DPMI). Each task receives (priority + 1) credits per scheduling round. Tasks run round-robin, consuming one credit per turn. When all credits are exhausted, every ready task is refilled. Higher-priority tasks run proportionally more often but never starve lower ones.

Header: libtasks/taskSwch.h

Loaded as: bin/libs/libtasks.lib

.h2 Why Cooperative

DOS is single-threaded with no kernel scheduler. DPMI provides no preemption primitives. The DVX GUI event model is inherently single-threaded: one compositor, one input queue, one window stack. Cooperative switching lets each task yield at safe points, avoiding synchronization primitives entirely.

.h2 Scheduling Semantics

Task storage is a stb_ds dynamic array. Terminated slots are recycled by tsCreate so the array does not grow unboundedly. The scheduler performs a linear scan for the next Ready task whose credit count is positive; when no task has credits left, every Ready task is refilled with (priority + 1) credits and the scan restarts.

.table
  Priority            Credits per round  Approximate share
  --------            -----------------  -----------------
  TS_PRIORITY_LOW     1                  ~4% (1 of 25 possible credits)
  TS_PRIORITY_NORMAL  6                  ~22%
  TS_PRIORITY_HIGH    11                 ~41%
.endtable

Actual share depends on the mix of priorities currently running. What the algorithm guarantees is that no ready task ever goes unscheduled -- even a priority-0 task always earns at least one turn per round.

.h3 Stack Allocation

Task 0 uses the caller's stack (no separate allocation). Every other task gets a heap-allocated stack of the size passed to tsCreate (or TS_DEFAULT_STACK_SIZE if 0 is passed). Stacks are freed at task termination, either via tsExit or tsKill.

.h2 Error Codes

.table
  Constant         Value   Description
  --------         -----   -----------
  TS_OK            0       Success
  TS_ERR_INIT      -1      Task system not initialized
  TS_ERR_PARAM     -2      Invalid parameter (bad task ID, etc.)
  TS_ERR_FULL      -3      Task array full (should not occur; array grows)
  TS_ERR_NOMEM     -4      Memory allocation failed
  TS_ERR_STATE     -5      Invalid state transition
.endtable

.h2 Constants

.table
  Constant                  Value   Description
  --------                  -----   -----------
  TS_DEFAULT_STACK_SIZE     32768   Default stack size per task (32 KB)
  TS_NAME_MAX               32      Maximum task name length including NUL
  TS_PRIORITY_LOW           0       Low priority (1 credit per round)
  TS_PRIORITY_NORMAL        5       Normal priority (6 credits per round)
  TS_PRIORITY_HIGH          10      High priority (11 credits per round)
.endtable

.h2 Types

.h3 TaskStateE

Task scheduling state. Only Ready tasks participate in scheduling.

.table
  Value                  Description
  -----                  -----------
  TaskStateReady         Eligible for scheduling
  TaskStateRunning       Currently executing (cosmetic; marks active task)
  TaskStatePaused        Skipped by scheduler until explicitly resumed
  TaskStateTerminated    Slot available for reuse
.endtable

.h3 TaskEntryT

.code
typedef void (*TaskEntryT)(void *arg);
.endcode

Task entry point function signature. The void* argument lets the caller pass arbitrary context (e.g. a shell app descriptor).

.h2 tsInit

.code
int32_t tsInit(void);
.endcode

Initialize the task system. The calling context becomes task 0 (the main task). Task 0 is special: it cannot be killed or paused, and tsRecoverToMain() always returns control here. No separate stack is allocated for task 0; it uses the process stack directly.

Returns: TS_OK on success, TS_ERR_PARAM if already initialized.

.h2 tsShutdown

.code
void tsShutdown(void);
.endcode

Shut down the task system and free all task stacks and internal storage. Safe to call even if tsInit() was never called.

.h2 tsCreate

.code
int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t stackSize, int32_t priority);
.endcode

Create a new task. Terminated task slots are recycled to avoid unbounded array growth.

.table
  Parameter    Description
  ---------    -----------
  name         Task name (truncated to TS_NAME_MAX - 1 characters)
  entry        Task entry point function
  arg          Opaque argument passed to entry
  stackSize    Stack size in bytes (pass 0 for TS_DEFAULT_STACK_SIZE)
  priority     Scheduling priority (0..10; use TS_PRIORITY_* constants)
.endtable

Returns: Task ID (>= 0) on success, or a negative error code (TS_ERR_PARAM if not initialized or entry is NULL, TS_ERR_NOMEM if stack allocation failed).

.h2 tsYield

.code
void tsYield(void);
.endcode

Yield CPU to the next eligible ready task using credit-based round-robin. This is the sole mechanism for task switching. Every task must call this (or a GUI function that calls it) periodically, or it will monopolize the CPU.

.h2 tsPause

.code
int32_t tsPause(uint32_t taskId);
.endcode

Pause a task, removing it from scheduling. Cannot pause the main task (ID 0). If a task pauses itself, an implicit yield occurs.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to pause
.endtable

Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE if the task is the main task or not in a pausable state (not Running or Ready).

.h2 tsResume

.code
int32_t tsResume(uint32_t taskId);
.endcode

Resume a paused task. Credits are refilled so the task gets a fair share of CPU time immediately rather than waiting for the next scheduling round.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to resume
.endtable

Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, TS_ERR_STATE if the task is not paused.

.h2 tsSetPriority

.code
int32_t tsSetPriority(uint32_t taskId, int32_t priority);
.endcode

Set a task's scheduling priority. Also refills credits so the change takes effect immediately.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to modify
  priority    New priority level (0..10)
.endtable

Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE if the task is terminated.

.h2 tsGetPriority

.code
int32_t tsGetPriority(uint32_t taskId);
.endcode

Get a task's current scheduling priority.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to query
.endtable

Returns: Priority value (0..10) on success, TS_ERR_PARAM on invalid ID.

.h2 tsGetState

.code
TaskStateE tsGetState(uint32_t taskId);
.endcode

Get a task's current scheduling state.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to query
.endtable

Returns: TaskStateE value. Returns TaskStateTerminated for invalid IDs.

.h2 tsCurrentId

.code
uint32_t tsCurrentId(void);
.endcode

Get the ID of the currently executing task. Always valid while the task system is initialized.

Returns: Current task ID.

.h2 tsGetName

.code
const char *tsGetName(uint32_t taskId);
.endcode

Get a task's name string.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to query
.endtable

Returns: Pointer to the task's name, or NULL on invalid ID. The pointer remains valid until the task slot is reused.

.h2 tsExit

.code
void tsExit(void);
.endcode

Terminate the calling task. Must not be called from the main task (ID 0). The stack is freed immediately and the slot is marked for reuse. This function never returns; it performs an internal context switch to the next ready task.

.h2 tsKill

.code
int32_t tsKill(uint32_t taskId);
.endcode

Forcibly terminate another task. Cannot kill the main task (ID 0) or the currently running task (use tsExit() for self-termination). Safe because cooperative scheduling guarantees the target is suspended at a yield point.

.table
  Parameter   Description
  ---------   -----------
  taskId      ID of the task to terminate
.endtable

Returns: TS_OK on success, TS_ERR_PARAM on invalid ID, or TS_ERR_STATE on illegal target (main task, self, or already terminated).

.h2 tsRecoverToMain

.code
void tsRecoverToMain(void);
.endcode

Crash recovery: force the scheduler back to the main task (ID 0). Call after longjmp from a signal handler that fired in a non-main task. The crashed task is NOT cleaned up by this call; call tsKill() afterward to free its resources. This exists because longjmp unwinds the crashed task's stack but the scheduler state still points to it.

.h2 tsActiveCount

.code
uint32_t tsActiveCount(void);
.endcode

Get the number of non-terminated tasks currently in the system.

Returns: Count of tasks in Ready, Running, or Paused state.
