Special Feature: ColorForth Commentary

COLOR.ASM: Tasks

Original file

NOTICE: This is a work in progress. Parts of my commentary are still very rough. However, I have it up on the web because even in this rough form it may be useful to ColorForth enthusiasts. Expect the contents of these files to change frequently.

Two tasks—two stacks each

ColorForth runs two tasks — the "god" task, which updates the graphic output display; and the "main" task, which waits for a keystroke. Each task has two stacks — a 750-cell return stack and a 1500-cell data stack. Each stack grows downward in memory from its starting address — each new item pushed "onto" the stack is stored in a cell four bytes lower in memory than the item pushed before.

gods equ 28000h*4 ; 0a0000h          ; 0xA0000: top of return stack for "god"
godd equ gods-750*4                  ; 0x9F448: top of data stack for "god"
mains equ godd-1500*4                ; 0x9DCD8: top of return stack for "main"
maind equ mains-750*4                ; 0x9D120: top of data stack for "main"

;.............................................................................
; Refs:
; -- gods: protected, godd, god, abort1
; -- godd: a20, cold, mains, c_, stack
; -- mains: maind, (main,) act
; -- maind: act

Switching tasks

The current task yields to the other task by calling pause, which preserves the state of the current task and jumps into round, which calls unpause to reload the state of the other task and to return to that task.

For each task, there are a number of things that must be preserved:

NOTE: Unpause takes its argument via the pointer on the return stack — that is, its data is expected to be stored inline immediately after the call, as it is in round. (Also note that unpause does not return to its caller.)

align 4
me:	dd	offset god
screen:	dd	0 ; logo

;.............................................................................
; Refs:
; -- me: pause (R), unpause (W)
; -- screen: (start1,) show

ROUND

Note the structure of round: For each of the two tasks, there is a call to unpause, followed by a variable. Clearly the system cannot simply return to the variable's address, and indeed it doesn't: Unpause takes the address on the return stack as an argument; it is the address of a variable from which information (in this case the return-stack pointer of the incoming task) is to be taken. Unpause also adds four (the size of the variable) to the return address, so that it returns to the instruction immediately following the variable.

round:	call	unpause            ; Pass god on return stack to unpause.
god:	dd	0 ; gods-2*4
	call	unpause            ; Pass main on return stack to unpause.
main:	dd	0 ; mains-2*4
	jmp	round

;.............................................................................
; Refs:
; -- round: main
; -- god: me, debug, stack
; -- main: act, debug

PAUSE

ColorForth offers cooperative multitasking, which means that the system does not interrupt one task to run another. The current task must stop itself and release control to the system so that the other task has a chance to run.

Pause stops the current task and saves its state. It ends up jumping to a call to unpause, which reloads the state of the other task and causes the other task to run.

Pause takes no arguments.

pause:
	dup_	                   ; Push TOS onto data stack..
	push	esi        ; [RST] ; Push NOS (data-stack pointer) onto return
		                   ;   stack (which already has the task's
		                   ;   return address).
	mov	eax, me            ; Get pointer (to either god or main --
		                   ;   me always points to one of the two).
	mov	[eax], esp         ; Save return-stack pointer (in god or
		                   ;   main).
	add	eax, 4             ; Get address AFTER god or main.
	jmp	eax                ; Jump there (into round, which immediately
		                   ;   calls unpause below).

;.............................................................................
; Refs -- pause: switch, forth2, key

UNPAUSE

Pause stops the current task and jumps into round, which then calls unpause, which causes the other task to run.

unpause:
	pop	eax        ; [RST] ; Return address is always god or main,
		                   ;   where return-stack pointer for OTHER
		                   ;   task is stored.
	mov	esp, [eax] ; [RST] ; Load other task's return-stack pointer
		                   ;   (this switches return stacks).
	mov	me, eax            ; EAX = pointer to other task's return-
		                   ;   stack pointer; save it to main or god.
	pop	esi        ; [RST] ; Restore other task's NOS from the return
		                   ;   stack.
	drop	                   ; Restore other task's TOS from the data
		                   ;   stack.
	ret	                   ; Return to other task, which is now the
		                   ;   current task.

;.............................................................................
; Refs -- unpause: round

Starting tasks

The words act and show tell the system what code to run when a specific task is active.

Popping the return stack

Certain routines in ColorForth take not only data (via the data stack) but also code (via the return stack). The item left on the return stack after a CALL is the address of the code immediately following the CALL. Therefore, a routine can get a code address simply by requiring that the code immediately follow the CALL.

ColorForth has two routines for setting up each of two possible tasks: act and show. Each of these pops the return address and saves it somewhere. Nothing is pushed back onto the return stack, so each of these will return, not to its caller, but to its caller's caller — the code following the CALL to the routine is code to be run later. Act saves the code address so the code can be run as the MAIN task; show has the code executed as the GOD (graphic output display) task.

ACT

Act changes the code that is run as the background or MAIN task (see this message).

Act removes an item from each stack. The top-of-stack item (TOS) is stored as the first item in the MAIN task's data stack; the top item on the current return stack (the address of the code following the call to act) is stored as the first return address in the MAIN task's return stack. (It should be the GOD task that is running when act is called.)

act:
	mov	edx, maind-4       ; Get pointer to bottommost slot in "main"
	mov	[edx], eax         ;   data stack; store TOS in that slot.
	mov	eax, mains-4       ; Get pointer to bottommost slot in "main"
	pop	[eax]      ; [RST] ;   return stack; store return address.
	sub	eax, 4             ; Get pointer to next slot up in "main"
	mov	[eax], edx         ;   return stack; store pointer to TOS.
	mov	main, eax          ; Store pointer inside round.
	drop
	ret	                   ; Return to caller's caller.

;.............................................................................
; Refs -- act: show, forth2

SHOW0

This sets up both tasks — show sets up show0's code (the RET instruction) as the GOD task and show's own code as the MAIN task.

Note that when show0 is run at startup, the current task is the GOD task.

show0:	call	show               ; Pass to show the pointer to "ret" on
	ret	                   ;   the return stack.

;.............................................................................
; Refs -- show0: start1

SHOW

Show changes the code that is run as the foreground or GOD (graphic output display) task. Note, however, that whenever show sets up the GOD task to run the caller's code, it also resets the MAIN task to run its own code.

(Note "call [screen]" -- if show is called from show0, this just returns to show -- if show is called from refresh, this runs almost all of refresh)

show:
	pop	screen     ; [RST] ;
	dup_
	xor	eax, eax
	call	act

The call to act is effectively the end of the show routine, because act forces a return to show's caller.

	; ret

The code that follows is the code to be run as the MAIN task. Note two things:

First, this code is a loop. Because the address of this code is stored as the bottommost return address in the MAIN return stack, it has to be a loop; once the system has returned to this code, the return stack is empty. A RET instruction here would cause a system crash.

Second, the code passed to either act or show must lead eventually to a call to pause so that the other task can be run. In this case, the call to switch causes pause to execute.

@@:	call	graphic
	call	[screen]
	call	switch             ; (Switches tasks via jump to pause)
	inc	eax
	jmp	@b

;.............................................................................
; Refs -- show: show0, forth2, refresh

Hint in difference between pause and unpause: Pause saves ESP where "me" points. Unpause does NOT retrieve ESP from where "me" points; instead it loads ESP from the address on the return stack. (Pause does not alter "me"; unpause does.) Note also that, at startup, "me" already contains a pointer to "god".

So what happens when a ColorForth block calls show?

C

Change NOS to point to highest cell in return stack for "god" ("godd" marks the address one cell HIGHER than the highest-addressed cell in the data stack, so "godd+4" is not in the data stack)

c_: ;"c"
;[Refs: forth2]
	mov	esi, godd+4
	ret

Check the index for other entries.