SlackProcess.mesa
Copyright © 1986 by Xerox Corporation. All rights reserved.
Last edited by Pier on November 5, 1986 7:05:11 pm PST
Bier, May 4, 1987 5:08:06 pm PDT
DIRECTORY
IO, Rope, ViewerClasses;
SlackProcess: CEDAR DEFINITIONS = BEGIN
Overview
SlackProcess solves a problem in user interfaces that employ smooth motion. For instance, in Gargoyle and Solidviews, the user can move objects smoothly by moving the mouse. Problem: how often should the frames be redrawn? The easiest solution to implement is to redraw the scene once for each mouse motion action from the TIP table. Unfortunately, for complex frames, this results in the redrawing getting behind the user. A simple improvement, suggested years ago by Scott McGregor, is to queue up the TIP table actions, and provide a second process, the slack process, to dequeue and execute them. If the slack process gets behind, it drops some actions on the floor in order to catch up. Thus the motion is "as smooth as possible" but doesn't get behind. Scott points out that, as more processing power becomes available, this algorithm allows motion to get smoother without recoding.
The name SlackProcess refers to the user's perception, with the naive algorithm, that the objects he is dragging are lagging farther and farther from the cursor, like a dog with too long a leash.
SlackProcess also serves to synchronize user actions. There is no need to use MBQueues and similar packages if SlackProcess is used.
Multiple slack processes and their associated queues may be used by many clients; the slack process issues a handle containing MONITORED data for use by the client. Actions may be queued by calls to QueueInputAction or QueueInputActionWithPoint.
Each SlackProcess provides a logging feature for clients. A client can register a Logging procedure and enable/disable Session logging. With logging enabled, the registered Logging procedure will be called every time an action is dequeued. The client may then save a log or script of the session in progress for later playback to a future SlackProcess.
Each SlackProcess provides an abort feature for clients. A client can register an Abort procedure and some abort data and enable/disable aborting. With aborting enabled, the slack queue will be flushed and the registered abort procedure will be called (with the abort data as argument) every time an abort occurs. The current trigger for the abort action is holding down SHIFT-SWAT (SHIFT-Spare3) on a Dorado with the cursor in the viewer to be aborted. If the viewer specified when aborting is registered is NIL, then the cursor position will be ignored when checking for abort.
BEGIN DEFS
BitVector: TYPE = REF BitVectorObj;
BitVectorObj: TYPE = RECORD [bits: PACKED SEQUENCE len: NAT OF BOOL];
Callback PROC TYPEs
EventProc: TYPE = PROC [clientData: REF ANY, event: LIST OF REF ANY];
LoggingProc: TYPE = PROC [clientData: REF ANY, event: LIST OF REF ANY];
AbortProc: TYPE = PROC [data: REF ANY];
PUBLIC TYPES
ClientDatas: TYPE = REF ClientDatasData;
ClientDatasData: TYPE = RECORD[SEQUENCE len: NAT OF REF ANY];
OptimizeHints: TYPE = REF OptimizeHintsData;
OptimizeHintsData: TYPE = RECORD[SEQUENCE len: NAT OF REF ANY];
Actions: TYPE = REF ActionsData;
ActionsData: TYPE = RECORD[SEQUENCE len: NAT OF LIST OF REF ANY];
Problem: ERROR [msg: Rope.ROPE];
SlackHandle: TYPE = REF SlackData;
The SlackData record. Should be considered PRIVATE by clients of SlackProcess.
BEGIN PRIVATE STUFF ...
SlackData: TYPE = MONITORED RECORD [
slackProcess: PROCESS,
queue: Queue,
qeGen: QueueEntryGenerator, -- so we don't have to reallocate the storage each time
optimizeProc: OptimizeProc,
log: Log,
abort: Abort
]; -- eventually should be opaque
Queue: TYPE = REF QueueData;
The QueueData record and its fields should be considered PRIVATE by clients of SlackProcess
QueueData: TYPE = RECORD [
notFull: CONDITION,
head: NAT ← 0,
tail: NAT ← 0,
size: NAT ← 0,
clientDatas: ClientDatas ← NIL,
optimizeHints: OptimizeHints ← NIL,
actions: Actions ← NIL,
eventProcs: EventProcs ← NIL
];
EventProcs: TYPE = REF EventProcsData;
EventProcsData: TYPE = RECORD[SEQUENCE len: NAT OF EventProc];
Log: TYPE = REF LogData;
The LogData record and its fields should be considered PRIVATE by clients of SlackProcess
LogData: TYPE = RECORD[
head: NAT ← 0,
size: NAT ← 0,
loggerEnabled: BOOLFALSE,
logger: LoggingProc ← NIL,
received: Received, -- received an action versus performed an action.
bashed: Bashed, -- bashed an action versus performed an action.
events: Events
];
Received: TYPE = REF ReceivedData;
ReceivedData: TYPE = RECORD[SEQUENCE len: NAT OF BOOL];
Bashed: TYPE = REF BashedData;
BashedData: TYPE = RECORD[SEQUENCE len: NAT OF BOOL];
Events: TYPE = REF EventsData;
EventsData: TYPE = RECORD[SEQUENCE len: NAT OF REF ANY];
Abort: TYPE = REF AbortData;
AbortData: TYPE = RECORD [
enabled: BOOLFALSE,
proc: AbortProc,
viewer: ViewerClasses.Viewer,
data: REF
];
... END PRIVATE STUFF
Creating a SlackProcess
Create: PROC [queueSize: NAT ← 50, logSize: NAT ← 50, optimizeProc: OptimizeProc, loggingProc: LoggingProc ← NIL, abortProc: AbortProc ← NIL, abortData: REF ANYNIL, abortViewer: ViewerClasses.Viewer ← NIL] RETURNS[handle: SlackHandle];
Create a new queue and slack process. The default queueSize and logSize should be adequate for most applications. The optimizeProc is called whenever the slack process finishes an action, before performing the next action. The optimizeProc allows the client to look at the queue and decide how many (if any) of the actions on the front of the queue may be skipped. A loggingProc can be provided that will be called whenever a new action is about to be performed (See EnableSessionLogging below). SlackProcess will abort itself when SHIFT-SWAT are down and the mouse is in a specified viewer (see EnableAborts below).
General-Purpose Queuing Action
QueueInputAction: PROC [handle: SlackHandle, callBack: EventProc, inputAction: LIST OF REF ANY, clientData: REF ANY, optimizeHint: REF ANY];
Queues a menu or keyboard action and returns immediately. When this action reaches the front of the queue and is not skipped by the OptimizeProc, callBack[inputAction, clientData] will be called. optimizeHints are queued with their corresponding input action and passed to the OptimizeProc (see Create above) to help decide if this action should be skipped (if the slack process begins to get behind the user).
Optimizing the Queue
The optimizeProc registered with Create will be called whenever the slack process finishes an action. As argument, it receives a QueueEntryGenerator. Call GetQueueEntry to see what is on the queue. GetQueueEntry[qeGen, 0] is the entry first in line to be done, GetQueueEntry[qeGen, 1] is second in line and so on. Index should be in the range [0..actionsOnQueue-1].
OptimizeProc: TYPE = PROC [qeGen: QueueEntryGenerator, actionsOnQueue: NAT] RETURNS [skipActions: NAT];
The client looks at the queue summary (using the GetQueueEntry routine below) and decides how many of the actions should be skipped. If the next action on the queue should be performed, this routine should return 0.
Caution: the OptimizeProc will be called with the SlackProcess MONITOR locked. Because the Cedar InputNotifier calls this MONITOR, your Cedar world will lock up if any ERROR is raised by by your OptimizeProc. To ease the pain of debugging, SlackProcess will catch the ERROR SlackProcess.Problem and release the lock. If possible, catch all ERRORs that your OptimizeProc may generate and raise SlackProcess.Problem instead, putting a helpful ROPE in its message field. You will not be able to browse your stack, but you'll have some idea of what went wrong, and your world won't freeze up.
QueueEntryGenerator: TYPE = REF QueueEntryGeneratorObj;
QueueEntryGeneratorObj: TYPE = RECORD [
queue: Queue,
actionsInQueue: NAT
];
GetQueueEntry: PROC [qeGen: QueueEntryGenerator, index: NAT] RETURNS [clientData: REF ANY, optimizeHint: REF ANY, action: LIST OF REF ANY];
Restarting a SlackProcess that has been ABORTed (e.g. from an ERROR window).
Restart: PROC [handle: SlackHandle];
Creates a new slack process. Call this procedure if a bug in the slack process forces it to be aborted.
Cancelling all actions on the queue.
FlushQueue: PROC [handle: SlackHandle];
Removes all unprocessed actions from the slack queue. CAUTION: If the caller is counting on a certain set of actions to be done atomically, this call could destroy its invariants.
Creating Session Logs
SlackProcess supports reporting of all actions actually performed via slack process. The client provides a logging procedure to be called whenever an action is performed for the application (see Create above). Playback scripts for interactive systems may be created using Session logs.
EnableSessionLogging: PROC [handle: SlackHandle] RETURNS [alreadyEnabled: BOOLFALSE];
DisableSessionLogging: PROC [handle: SlackHandle] RETURNS [alreadyDisabled: BOOLFALSE];
RegisterLogger: PROC [handle: SlackHandle, loggingProc: LoggingProc] RETURNS [alreadyRegistered: BOOLFALSE];
If you didn't register a loggingProc when you called Create, this gives you another chance.
The registered abortProc will be called (if aborts are enabled) with abortData as argument when the slack process is aborted.
EnableAborts: PROC [handle: SlackHandle] RETURNS [alreadyEnabled: BOOLFALSE];
DisableAborts: PROC [handle: SlackHandle] RETURNS [alreadyDisabled: BOOLFALSE];
RegisterAbortProc: PROC [handle: SlackHandle, abortProc: AbortProc ← NIL, abortData: REF ANYNIL, abortViewer: ViewerClasses.Viewer ← NIL] RETURNS [alreadyRegistered: BOOLFALSE];
If you didn't register an abortProc when you called Create, this gives you another chance.
END.