Skip to content

The Plan

Simon Willcocks edited this page Mar 2, 2022 · 3 revisions

The plan to make RISC OS multi-tasking and -processing friendly

Each core has essentially its own kernel (with local storage).

The aim is to eliminate anything that synchonously takes a "long" time from the kernel (meaning anything where interrupts would be enabled, like font drawing or memory management) and delegate that to user mode tasks.

The context for a given process will allow it to be blocked until awoken in response to an event (usually an interrupt), so device drivers, file systems, etc. will start a process running (e.g. a block fetch from a storage device) and block the current thread until that process completes or fails, allowing another thread to execute.

The VDU command interface will be just one possible output stream from a program, each process maintaining its own version of "current working directory", "text foreground colour", etc. Sub-processes inheriting that context should mean that processes shouldn't interfere with each other unintentionally.

Streams will allow senders to block until there's space for them to write the amount of data they need and receivers to block until there's a minimum amount of data for them to read (e.g. the size of a disc block).

First steps

There will be no need to re-write filesystems immediately, having them work asynchronously is an aspiration, and even so, multiple cores may well show an improvement in speed since other cores can continue their tasks while the file is read or written.

As I understand it, the Wimp is currently in charge of managing tasking; if a Wimp task starts another program, it's mapped out of memory while the program runs and mapped back in when it completes (or calls Wimp_Initialise to indicate that the two programs can co-exist).

If the program running the new command is not a Wimp task it will be replaced by the new command (unless it takes steps to protect itself, I suppose).

OS_Task SWI (proposed)

On entry
  R0 = code
  Others depending on code

On exit
  Registers preserved depending on code
  May not return

OS_Task 0

MyTask

Store the current thread's context and resume the thread given, if not already running. Makes the current thread not runnable (blocked). If the given thread is not blocked (runnable, or even running on a different core), increments its wakeup count

On entry
  R0 = 0

On exit
  R0 preserved
  R1 = task handle

OS_Task 1

WaitForTask

Store the current thread's context and resume the thread given, if not already running. Makes the current thread not runnable (blocked). If the given thread is not blocked (runnable, or even running on a different core), increments its wakeup count

On entry
  R0 = 1
  R1 = task handle

OS_Task 2

WakeTask

If the given task is not blocked (runnable, or even running on a different core), increments its wakeup count The current task will continue running

On entry
  R0 = 2
  R1 = task handle

OS_Task 3

BumpTask

Store the current thread's context and resume the thread given, if not already running. The current task will remain runnable (so will resume when scheduled, or even continue running if the given task is running on another core). If the given thread is not blocked (runnable, or even running on a different core), increments its wakeup count

On entry
  R0 = 1
  R1 = task handle

OS_Task 4

Sleep

On entry
  R0 = 4
  R1 = delay in microseconds; 0 => until woken

On exist
  R0,R1 preserved
  R2 = time remaining (0 if timeout occurred)
  R3 = task that interrupted this sleep (0 if timeout occurred)

OS_Task 5

AccessRegisters

On entry
  R0 = 5
  R1 = task handle
  R2 -> Register change block

On exist
  R0-R2 preserved, change block updated

Register change block

  24 words (96 bytes), 12 BIC words, and 12 EOR words
  task r0 = (task r0 AND NOT [R2, #0x00]) EOR [R2, #0x30] 
  [R2, #0x00] = old task r0
  task r1 = (task r1 AND NOT [R2, #0x04]) EOR [R2, #0x34] 
  [R2, #0x08] = old task r1
  task r2 = (task r2 AND NOT [R2, #0x08]) EOR [R2, #0x38] 
  [R2, #0x10] = old task r2
  ...
  task r9 = (task r9 AND NOT [R2, #0x24]) EOR [R2, #0x44] 
  [R2, #0x16] = old task r9
  task pc = (task pc AND NOT [R2, #0x28]) EOR [R2, #0x58] 
  [R2, #0x16] = old task pc
  task spsr = (task spsr AND NOT [R2, #0x2c]) EOR [R2, #0x5c] 
  [R2, #0x16] = old task spsr

Set the block to all zeros to read values into the first 12 words.

The call MUST be CERTAIN that the task it is accessing is blocked. That is to say that the task must be known to be in the sleeping until woken state (not just waiting for a timeout).

Will fail if the task is not blocked without timeout. (It's up to the driver to implement timeouts for its clients, if desired.)

A device driver module's task should follow this pattern:

  Initialise pending requests container
  Initialise active requests container
  REPEAT
    Sleep for a while (possibly forever)
    IF timed out THEN
      Retry the hardware or inform the client of failure
    ELSIF woken by interrupt THEN
      Check the hardware 
      IF a client's active request has completed THEN
        Update the client's registers and wake it
      ENDIF

      Start the next request, if needed
    ELSE ; Woken by task making a request
      Claim a lock on the pending container
      Find the pending request associated with the task that woke this task
      Release the lock
      Put the client into active requests container
      Start the hardware acting on the request
    ENDIF
  UNTIL FALSE

The module code called by the client will:

  Examine the request
  IF the request can be handled instantly
    Do so
  ELSE
    Set a callback to:
    Claim a lock on the pending container
    Add itself to the pending requests container
    Release the lock
    Wait for the driver task (both blocks the current task and wakes the other)
  ENDIF

Since it's a callback, the SVC mode stack will be at its "ready" position, practically empty.

Clone this wiki locally