What Happens to RISC OS Applications if We Introduce Preemptive Multitasking?
In this article we discuss and analyse what can happen to traditional RISC OS software if a preemptive scheduler is introduced, as discussed in a previous post.
Author: Paolo Fabio Zaino.
“RISC OS applications rely on cooperative multitasking. If the operating system becomes preemptive, do all applications need to be rewritten?”
This seems to be a common and certainly a very reasonable question raised whenever RISC OS modernisation is discussed.
The short answer is simple and reassuring:
No, existing RISC OS applications do not need to be rewritten.
The long answer explains why this is true, how it works on real hardware, and what must never change if compatibility is to be preserved. That long answer is what this article provides.
Examples of this already working today
This is not a theoretical claim. There are already concrete examples where legacy RISC OS software runs correctly on top of a preemptive environment, without being rewritten.
RISC OS on Linux
One important example is the RISC OS on Linux project.
In this setup, RISC OS runs on top of a Linux kernel, which is fully preemptive and multi-tasking. Linux provides hardware abstraction, scheduling, interrupt handling, and memory management, while RISC OS runs as a guest environment on top of it.
Despite this, the RISC OS desktop, the Wimp, and traditional applications continue to behave exactly as they always have:
- Applications still rely on cooperative multitasking
- Events are still delivered exclusively via
Wimp_Poll - No asynchronous callbacks are introduced
- No application re-entry occurs
- Legacy software runs unmodified
This works because Linux preemption is not exposed to RISC OS applications. The cooperative execution model defined by the Wimp and relied upon by applications is preserved, even though the underlying system is fully preemptive.
This demonstrates an important principle that will recur throughout this article:
A system can be preemptive internally while remaining cooperative at the application level.
Ultima VM on classic RISC OS
Another practical example comes from Ultima VM, which takes a fundamentally different approach from RISC OS on Linux.
Ultima VM runs inside classic RISC OS, but it acts as an abstraction layer between legacy RISC OS behaviour and modern development paradigms. Rather than bypassing the Wimp, Ultima integrates with it.
From the Wimp’s point of view:
- Ultima applications are Wimp applications
- They register with the Wimp like any other task
- They participate in the cooperative desktop model
- They receive events originating from the Wimp
Internally, however, Ultima introduces a preemptive execution model of its own.
Ultima applications do not call Wimp_Poll. Instead:
- They register event handlers with Ultima
- Ultima performs the
Wimp_Pollon their behalf (or callbacks, etc.) - Ultima collects Wimp events
- Ultima dispatches those events to the appropriate application handlers
- Ultima preempts and schedules its applications independently
Crucially, when Ultima preempts an application, it yields control back to the Wimp. This ensures that:
- The Wimp remains in control of cooperative scheduling
- No re-entry into Wimp code occurs
- No asynchronous Wimp event delivery is exposed
- The PRM-defined execution contract is preserved
In effect, Ultima successfully combines two layers of scheduling:
- A higher-level preemptive scheduler inside Ultima VM
- A lower-level cooperative WIMP
This is the inverse layering of RISC OS on Linux:
- In RISC OS on Linux, a preemptive kernel sits below a cooperative RISC OS environment
- In Ultima VM, a preemptive runtime sits above a cooperative Wimp
In both cases, the same principle holds:
Preemption exists, but it is never exposed in a way that violates the Wimp’s cooperative execution model.
This is why Ultima applications can be fully preemptive while still appearing to the desktop as normal Wimp applications, and why legacy RISC OS software continues to behave exactly as the PRMs describe.
Why these examples matter
These two examples are important because they prove a key point in practice, not just in theory:
- Preemptive scheduling does not automatically break RISC OS
- Compatibility depends on what is exposed, not on what exists underneath
- The Wimp execution contract defined in the PRMs can be preserved even on top of a preemptive system
A counter argument however, could be that both these two examples have complete control over what it being executed on top of them. While this is true, that would also be the case in the situation in which preemptive scheduling is added to RISC OS itself.
With these real-world examples in mind, we can now examine the execution model that makes this possible, starting with how cooperative multitasking works in classic RISC OS.
Cooperative multitasking in classic RISC OS
Classic RISC OS uses cooperative multitasking at the application level.
In practical terms this means:
- Only one application runs at a time
- An application retains control until it explicitly gives it up
- Control is normally yielded by calling the Wimp, most commonly via
Wimp_Poll or Wimp_PollIdle - Applications do not expect asynchronous callbacks. Do not confuse this with ‘OS_CallBack’, which is not an asynchronous callback in the modern sense, but a deferred callback that is always delivered at a safe, controlled time (see the RISC OS PRMs for details).
- Applications assume they will not be re-entered while they are running
This model is defined and relied upon throughout the RISC OS Programmer’s Reference Manuals (PRMs), especially in the Wimp documentation.
It is important to understand that this is an application-level model, not a statement about how the CPU behaves internally.
Preemptive multitasking, in contrast
In a preemptive system:
- The operating system decides when code runs
- Code can be stopped and resumed automatically
- Multiple tasks may appear to run at once
- The programmer must assume interruption at almost any point, although this is usually made transparent to the application developer
This is powerful, but it is also incompatible with software written under cooperative assumptions unless those assumptions are carefully preserved.
This is why the question arises in the first place.
A crucial clarification: RISC OS already preempts applications
Here is the point that often causes confusion, and it is essential to be precise.
On a single-core system, even classic RISC OS already preempts applications.
When a hardware interrupt occurs:
- The CPU stops executing the current application
- It switches to a privileged interrupt mode
- It runs an interrupt handler
- It then resumes the application exactly where it left off
This is real CPU-level preemption, and it has always existed on RISC OS.
So why does this not break cooperative applications?
Because RISC OS is extremely strict about what interrupt handlers are allowed to do.
Why RISC OS is so strict about IRQ Off
Note: IRQ Off means Interrupt ReQuests Off, in other words system interrupts disabled.
The PRMs allow code to disable IRQs around critical sections, and they repeatedly stress that interrupt handlers must be carefully written.
This is because:
- RISC OS has a single shared address space
- There is no true memory protection. What is sometimes described as “memory protection” is actually Wimp task paging, which is a side effect of tasks being swapped out when not running. This is not a protection mechanism, as those pages remain accessible via the kernel and any application can enter the kernel via mechanisms such as OS_EnterOS, if kept available, or use Wimp_TransferBlock to snoop other WIMP tasks memory.
- An interrupt at the wrong moment could corrupt shared state
As a result, interrupt handlers are expected to:
- Be very short
- Touch only kernel-owned data
- Never call application code
- Never deliver Wimp events
- Never cause re-entry into application logic
The key distinction is this:
Applications are physically interrupted, but they are never logically re-entered.
The PRMs rely on this distinction everywhere.
What the Wimp guarantees, according to the PRMs
The Wimp defines a very strict and deliberate execution contract:
- Applications never receive hardware interrupts
- Applications never receive asynchronous callbacks
- All input arrives via
Wimp_Poll/Wimp_PollIdle - Exactly one event is returned per call
- Wimp event delivery is non-reentrant
- Event ordering is strictly defined
An application only observes the outside world when it explicitly calls into the Wimp.
This is the foundation of cooperative multitasking on RISC OS.
A practical example: clicking the mouse
A mouse click is a good example because applications depend directly on it.
What happens on classic RISC OS
- An application is running
- The user presses a mouse button
- The mouse hardware generates an interrupt
- The CPU switches to IRQ mode
- The OS records the button state
- The interrupt handler exits
- The application resumes exactly where it was
At this point, the application is completely unaware of the click.
Only later:
- The application calls
Wimp_Poll - The Wimp fills in the event block
- The Wimp returns a reason code
6 (Mouse Click)and the event block - The application handles the click
From the application’s point of view, the click did not exist until Wimp_Poll returned it.
This behaviour is explicitly defined in the PRMs and relied upon by almost all desktop software.
Introducing a preemptive kernel: what really changes
A modern kernel introduces scheduler preemption in addition to interrupt preemption.
This means that after an interrupt:
- Other kernel work may run
- Background services may be scheduled
- Drivers may execute independently
This is the point where concern is justified.
The rule that must be preserved is simple:
Legacy RISC OS code must never be able to observe concurrency.
The same mouse click on a preemptive system
Let us now follow the same mouse click on a system with a preemptive kernel, step by step.
- An application is running
- The user presses a mouse button
- A hardware interrupt occurs
- The CPU switches to IRQ mode
- The interrupt handler records the click
- The interrupt handler exits
- The application resumes exactly where it was
Up to this point, nothing differs from classic RISC OS.
Internally, the kernel may then:
- Queue the input internally
- Schedule kernel work
- Update internal Wimp state in a controlled, serialized context (the Wimp remains single-threaded and non-reentrant; the change is in how it is entered, not in its behaviour).
Crucially:
- No application code runs
- No Wimp event is delivered
- No re-entry occurs
Finally:
- The application calls
Wimp_Poll - The Wimp returns
Mouse_Click - The application handles the event
From the application’s point of view, nothing has changed.
Physical preemption vs logical preemption
This is the most important concept for understanding compatibility.
- Physical preemption means the CPU was interrupted
- Logical preemption means the application observed overlapping execution or re-entry
Classic RISC OS allows physical preemption and strictly forbids logical preemption.
Any modern system that wishes to remain compatible must preserve exactly the same rule.
What happens as the modernised RISC OS system evolves
As the system evolves, classic ROM modules may be rewritten in a modern style.
This does not break compatibility, but it shifts responsibility.
Before:
- ROM modules implicitly preserved cooperative behaviour
After:
- The kernel explicitly enforces it
- Native services expose legacy-compatible entry points
- Blocking, ordering, and error behaviour remain unchanged
Internally, services may be concurrent and asynchronous.
Externally, legacy code sees exactly what the PRMs describe.
User modules and legacy code
Legacy user modules behave somewhat like applications:
- They assume cooperative execution if they are TaskModules, or uninterrupted execution if they are single-task modules
- They are often not re-entrant
- They may rely on undocumented behaviour
They must therefore be treated as legacy code:
- Same ABI
- Same serialization
- Same execution guarantees
They can continue to run unchanged, however the new kernel must enforce serialized execution for non-reentrant modules, and may allow controlled concurrency only for modules that explicitly support reentrancy.
What must never happen
To preserve compatibility, the system must never:
- Run two legacy applications at the same time (this is expected to remain true even with ongoing RISC OS 5 multi-core support work, unless the application execution model itself is changed)
- Call legacy code from interrupt context
- Deliver Wimp events asynchronously to legacy applications
- Allow re-entry into legacy code
- Expose internal concurrency to legacy callers
If legacy software can observe the difference, the design is wrong.
So if applications needs no rewriting, why don’t we already have preemptive multi-tasking in RISC OS?
That is the part that is really hard to add in the current code base (RISC OS 5).
This requires:
- Careful planning and preparation
- Conversion of the source tree to C (as a more manageable language than Assembly)
- Redesign of the entire kernel (this is why, for example I started with Merlin instead of the usual attempt to patch the current source-tree)
- While redesigning the Kernel, it’s important to keep “the WIMP contract” in place to avoid breaking old applications.
