VBL with Mach1
Volume Number: 2
Issue Number: 6
Column Tag: Threaded Code
Install a VBL Task with Mach1
By Jörg Langowski, EMBL, c/o I.L.L., Grenoble, Cedex, France,
Editorial Board
Installing a Forth VBL Task - another CRT saver
Bob Denny's Article on vertical blanking tasks (MT V1#9) has been a constant
challenge to me ever since it appeared. It seemed that there are certain things that one
just could not do in Forth - the installation of an independently running task in the
system heap being one of them. After all, where do you put the Forth runtime support
when you leave Forth and want the task to run?
Another thing that would be difficult to do in Forth, for example, would be a desk
accessory. In general, any tasks that will run independently and con currently in the
Mac operating system are almost impossible to handle with a language that needs a
runtime interpreter to work.
After Mach1 came in, one could sort of see the light at the end of the tunnel. Here
was a Forth that created real 68000 machine language output, and in principle its code
could run anywhere without ever needing any runtime package. Maybe this makes some
of the 'impossible' things that I mentioned less impossible.
Still, Mach1 does contain runtime support. Multitasking is the most important
one, but by no means all, even the LOOP of a DO...LOOP structure is compiled as a JSR to
a kernel routine.
The first rule for creating programs that are not only 'stand-alone applications',
but self-contained code, is to avoid all references to kernel routines from within the
program. This general statement will hold for any Forth system; given a Forth
assembler, enough patience and a good set of macro definitions one will always be able
to write code that is self-contained and won't need the runtime interpreter (e.g. using
inline macros similar to the ones defined in MT V1#9, Forth column). Mach1 code
doesn't contain too many references to its kernel anyway, so it should be much easier to
accomplish what we try to do.
Our problem is to create a piece of code that does exactly what Bob Denny's CRT
saver does: intercepts the GetNextEvent trap and updates a counter to keep track of
when the last non-null event happened, and a VBL task that looks at that counter and
blanks the screen whenever a certain time has elapsed after the last non-null event.
Furthermore, the GetNextEvent intercept routine will repaint the screen after it has
been blanked when a new non-null event occurs.
For the purpose of illustration, Listing 1 shows some Mach1 code that installs a
very similar screen blanker as a background task under Mach1. This is the simple way
to do it and will of course run only within the Mach1 system.
Short explanation of the program: the blankout routine opens a new GrafPort and
paints its portRect (default size: whole screen) with the default pattern (black).
This routine is installed into one background task, CRTsaver, that checks
continuously whether the time after the last 'relevant' event is larger than a preset
value and blanks the screen if it is; thereafter, it unblanks the screen when a new event
is received. The task's infinite loop contains a PAUSE to give control back to the
scheduler.
The second background task, eventmonitor, checks for events by calling
EventAvail. Whenever a 'relevant' event occurs, it is intercepted by this routine and the
event tracking counter last.action updated.
Try and install these routines on your Mach1 system, and you'll have a fine CRT
saver. Note that the multitasking system is not running while the screen is dark; I did
this to keep it completely blank. You can insert a PAUSE in the event waiting loop of
do.blank; in that case, all tasks will keep running, but you'll have a small white
rectangle on the black screen (the cursor which is still active).
Installing the CRT saver as an independent VBL task
The example in Listing 1 contains many things that will just not work
independently of the Mach1 runtime package:
- background tasks;
- variable definitions, which are kept separate from the main code block;
- JSR references to the kernel are not contained here, but could be easily in other
tasks (as mentioned, a simple DO...LOOP);
- there are certain trap calls that are incompatible with the VBL task mechanism;
as mentioned in IM, trap routines that move, purge or reallocate memory may not
be called from a VBL task, the reason being that if the task happens to interrupt
the memory manager and then calls the memory manager itself, very strange
effects may result.
The list of 'forbidden' routines (IM, Addison-Wesley Vol. III in the appendix) is
impressive, and EventAvail is one of them. The Quickdraw calls in blankout are also
forbidden.
-The Mach1 data stack is maintained through the A6 register. In self-contained
code, this register will have to be set up to point to a local stack area when the code is
first entered. Also, for safety reasons the complete register file should be saved on
entering and restored on exit.
An impressive list of restrictions; however, the example in Listing 2 shows that
it is not that bad after all.
First, we may not use variables anymore. Any variable storage space should be
defined within our piece of code. This can be done using create or header. References to
variables thus defined must be through ['] (in colon definitions) or ' (in direct
execution). If a create variable is referenced by name in a colon definition, a JSR to its
code is compiled. create's own code again references the kernel and therefore cannot be
used for self-contained programs.
If you use variables in this way, you can't call them directly by name after
storing something there, because the create execution code will be overwritten. You'll
always have to 'tick' their addresses on the stack.
Second, the screen blanker has to be rewritten; we store the black pattern into
the screen area directly instead of calling Quickdraw routines, which may not be used.
This is a little slower (Quickdraw really deserves its name), making the blanking less
'instantaneous'. Also, we may not use a DO...LOOP (reference to the kernel), so the
blanking loop is implemented using BEGIN...UNTIL, a little more cumbersome, but still
readable; and self-contained. See the definition of blankout in Listing 2. Note that
HideCursor is not in the list of 'forbidden' traps.
Third, we cannot intercept events from within the VBL task using EventAvail.
This is the reason why in Bob Denny's example from V1#9 a GetNextEvent filter
procedure was used. We'll have to do the same thing.
The GNEFilter procedure
A short review of the method to install a GetNextEvent hook:
There is an (undocumented) system global at $29A which contains a location that
GetNextEvent jumps to right after removing an Event from the queue. Through this
hook, one can install a routine that will be called whenever GetNextEvent receives an
event. A pointer to the event record is contained in A1. At the end of this routine, of
course, one will have to jump to the location that was contained in $29A.
The GNE hook routine that we are going to install will, each time a non-null
event is received, update the last.action counter with the current number of system
ticks and repaint the screen if it was blank. In order to keep the definition short, we
call traps directly instead of going through the Mach1 'glue' mechanism.
The GNEintfc routine saves most of the registers and restores them after exiting.
A local stack (100 bytes) is set up for the Mach1 A6 stack pointer. Some inline
assembly code is used to move arguments between the A7 and A6 stacks, and to setup a
jump vector at the end of the routine.
The vertical blanking task
one.run is the heart of the vertical blanking task that will be the other part of
the CRT saver. It checks whether one minute has passed since the last non-null event
and blanks the screen in that case. Furthermore, it then sets a global flag, dark, to
indicate to GNEintfc that the screen is dark. At the end of each run, screen blanked or
not, the task reschedules itself by resetting the counter in its VBL queue element.
Registers are saved and restored, and the routine also uses the local stack. (Note:
writing this I realize that in the case that the GetNextEvent filter is interrupted by the
vertical blanking and one.run is run during that same interrupt, it will use the same
local stack. This might create a problem; so far the CRT saver has not crashed on me.
You might think of duplicating the stack area so that the two routines use independent
stacks.)
Installation of the CRT saver in the system heap
Of course, the code that we defined so far is local to the Mach1 system and will
disappear as soon as Mach1 is exited. If we happened to install the CRT saver before, too
bad! Most certainly the system won't survive this kind of abuse. Therefore it remains to
copy the routines to a safe place in memory; we'll move them to the system heap before
installing. The code that we have to copy is marked by the two headers START and END.
The word install.blanker gets a pointer to a chunk of system heap (END - START) bytes
long and copies the code to it. The offset is placed into the variable (yes, here we may
use a good old variable) blockoffset. All references to the copied routines during
installation (for initialization of flags and counters, and for the address passed to the
VBL queue element) are made through the original addresses offset by this value.
After the installation (install.blanker and install.GNEfilter), you may leave
Mach1, wait one minute and see your screen go dark. Any keypress or mouseclick will
make reappear whatever was there.
Turnkeying the CRT saver installer
The word CRTsaver installs the tasks and quits Mach1. This word is used for
turnkeying the program:
turnkey CRTsaver CRT
will create an application file CRT, 19K long (Mach1 runtime overhead of 16K) which
installs the CRT saver.
Listing 1: CRT saver background task to run in the Mach1 system
( CRT saver task, © 1986 JL for MacTutor)
only forth definitions
also assembler also mac
hex
904 constant currentA5
9EE constant grayRgn
16A constant Ticks
74 constant screenbits
10 constant portrect
decimal
( first define port structure )
header screenport
2 allot ( device )
14 allot ( bitmap )
8 allot ( portrect )
84 allot ( remaining bytes )
( *** now define background task that does the blanking *** )
variable last.action