Jump to content

Photo

P-Machinery: System Clock & Timed Events


1 reply to this topic

#1 DZ-Jay OFFLINE  

DZ-Jay

    Quadrunner

  • 12,117 posts
  • The P-Machinery AGE is almost here!
  • Location:NC, USA

Posted Wed Oct 22, 2014 5:20 AM

NOTE: This is the second in a series of articles I intend to write to discuss new features to be included in the upcoming P-Machinery v2.0, a.k.a. P-Machinery AGE(Advanced Game Engine).


Tick-Tock: Introducing The System Clock
P-Machinery 1.0 relied heavily on Joe Zbiciak's excellent SDK-1600 for its core libraries. Specifically, it used the TASKQ and TIMER modules, which abstracted a processing queue for game tasks and delayed or repeating events, as the basis for its operating system. Although very useful, these modules had severe limitations not only in their functionality, but on the low-level programming model they promoted.

P-Machinery AGE aims to improve on this by implementing a brand new operating system core that supports prioritized processing tasks and timed events, and expose it all with a high-level programming model. The programmer can then concentrate on game logic without having the burden of dealing with the internal representation of data structures or other arcane details.

Moreover, this model is intended to be intuitive and efficient, with very little compromise on processing speed.

Timed events go beyond just delayed or repeating tasks such as updating display or changing enemy states; they are at the center of many of the features that a game engine should handle. These include synchronizing music and sound effects, sprite animation, moving-object velocity, applied physics, screen transitions, and many more.

If something occurs over a period of time, its life-cycle needs to be measured accurately in order to maintain the pace of the game--and it all starts with a clock: the System Clock.

In P-Machinery AGE, the System Clock is a 16-bit counter that keeps track of time. It is updated automatically by the game engine by an appropriate amount, and the entire system looks to it as the standard temporal measure.



The sprite driver needs to update animations? It just needs to calculate how many time units need to pass from the current System Clock value, based on the animation frame-rate.

 

The entrance of an enemy into the play-field needs to be delayed by five seconds? Just add to it the current value of the System Clock, and trigger a task on expiration.


Keeping time becomes a matter of tracking counters, and since the entire system agrees on the counting units, all pieces can synchronize with each other.

Moreover, P-Machinery AGE goes a step further. The framework encapsulates the tracking of time and triggering of events, not only allowing all sub-systems to work in synchronicity, but exposing these facilities to the programmer with an intuitive programming interface.


Vertical Blanking Madness
The only stable and practical means that the Intellivision offers to track time is the Vertical Blank interrupt request, or VBLANK IRQ. This IRQ is controlled by the video chip (STIC) and occurs on a very regular frequency, in between drawing television frames. During this time, while the television realigns its electron gun to draw a new screen frame, the STIC interrupts the CPU to read the graphics data buffer.

During this VBLANK period, the CPU, the STIC and the Graphics RAM, are all connected to the data bus, giving the programmer access to read and write to Graphics RAM and the STIC registers. In fact, this is the only way to manipulate graphics on an Intellivision.

More importantly, the VBLANK IRQ provides a regular signal that synchronizes all game events and time-dependent aspects of a program.

There is only one problem: it is directly tied to the frame-rate of the television signal.

This provides two main challenges. First, if we use the VBLANK signal as our System Clock, we can only track timed events that occur on periods that are multiples of the TV signal's frame-rate. Second, there are two variations of the Intellivision that support the most common TV standards around the world, and they both have different frame-rates: NTSC with a frame-rate of 60Hz, and PAL/SECAM with a frame-rate of 50Hz.


Jiffy Pop Time!
In order to synchronize all time-dependent aspects of the frame-work, it is important to define a standard unit to measure time. Moreover, for this unit to be equally useful regardless of the TV standard employed, it needs to be a multiple of both 50 and 60 Hertz, lest we want to be bogged down with complex fractional arithmetic.

Finally, in order to provide a more granular scale for timed events, such 10th or 100th of seconds, and still work with integers, the unit should be divisible by 10 or 100.

All these things considered, I have chosen a unit of time for P-Machinery AGE of 300. Borrowing a term from the Linux Kernel community, I'm calling this unit the Jiffy.
 

A Jiffy is formally defined as 1/300th of a second.


This leads automatically to the following ratios:

   1 NTSC Frame = 1/60th  Second
   1 PAL Frame  = 1/50th  Second
   1 Jiffy      = 1/300th Second

   1 NTSC Frame = (300 / 60) = 5 Jiffies
   1 PAL Frame  = (300 / 50) = 6 Jiffies

 300 Jiffies    = 1       Second
  30 Jiffies    = 1/10th  Second
   3 Jiffies    = 1/100th Second

What's more, P-Machinery AGE calibrates the game engine during the cartridge boot-up sequence, automatically detecting whether the game is running on an NTSC or PAL/SECAM environment. This sets the number of Jiffies by which to increment the System Clock on each VBLANK IRQ. Since all time calculations are based on Jiffies and not on individual frame counts, this effectively makes all time measures the same irrespective of the TV-standard employed.

No longer do games need to be region-specific, or play slower on one version of the console versus another. Timed events are normalized across all platforms.


Timed Events: Delayed & Repeating Tasks
The new TIMER module in P-Machinery AGE groups timed events into two categories:

  • Delayed Events: One-off tasks that need to start after a given initial delay.
  • Repeating Events: Recurring tasks that repeat on a given frequency or period.

P-Machinery AGE offers a set number of timers, by default eight but this is configurable at assembly time. Setting a timed event is a matter of defining the event and assigning it to one of the available timers. An event can be configured to use any of the available timers; but once set, the timer will remain dedicated to it until reset or re-configured.

That's it. The game engine takes it from there, converting the units accordingly, keeping track of the System Clock, and automatically triggering the event when appropriate.

The Application Programming Interface (API) exposed by the framework, offers five routines to manage timed events:

  • TIMER.Reset - Reset the timer sub-system.
  • TIMER.StartRepeatEvent - Initiates a repeating event immediately.
  • TIMER.SetRepeatEvent - Initiates a repeating event after a given delay.
  • TIMER.SetDelayEvent - Initiates a non-recurring event after a given delay.
  • TIMER.CancelEvent - Cancels an active timer event.

By the default, all timers are reset upon starting the program and during game state transitions, so the programmer only needs to do this manually when he wants to clear all active timers at once. The rest of the routines allow the programmer to set repeating or one-shot events, or cancel any untriggered event previously set.

Since P-Machinery AGE follows an event-driven model, timed events work by providing a pointer to an event-handler routine that will be called when the timed event is triggered. This is a program-specific routine to perform some task when the event occurs.

For instance, suppose you want to update a particular part of the screen once every second, perhaps to highlight a cave exit by making the door blink intermittently. You can use the TIMER.StartRepeatEvent routine to engage a repeating timer that will call your blinking routine at the appropriate time.

The arguments to the TIMER.StartRepeatEvent routine are as follows

TIMER.StartRepeatEvent:
-----------------------
timer - The timer number to use (0..PM.TIMER_COUNT).
rate  - The repeating rate frequency or period.
units - The units of the repeating rate frequency or period.
      - Available values are:
          Jiffy   Internal clock units.
          Fps     Frames per second.
          kFps    Thousand frames per second.
          Hz      Hertz (same as Fps).
          kHz     Kilo-Hertz (same as kFps).
          Sec     Seconds.
          mSec    Milli-seconds.
task  - A pointer to a task to execute when the event triggers.
data  - An optional data argument to pass to the task.

The following code will engage timer #0 with a repeating event that will call procedure BLINK_DOOR every second with an NULL (unused) argument.

  TIMER.StartRepeatEvent(0, 1, Sec, BLINK_DOOR, PM.NULL)

However, blinking a door may involve a two step process: first painting it with a highlighting colour, then reverting it back to normal after a short period; repeating this over and over.

We could enhance our code so that the procedure BLINK_DOOR accepts an argument telling it whether to paint the door with a highlight (1) or normal colour (0). We then set two timers, the second of which would be slightly offset after the first one, say 1/2 a second or 500 milli-seconds. For this we need to use the TIMER.SetRepeatEvent routine, which is similar to TIMER.StartRepeatEvent except that it accepts an initial delay.

The arguments to the TIMER.SetRepeatEvent routine are:

TIMER.SetRepeatEvent:
---------------------
timer - The timer number to use (0..PM.TIMER_COUNT).
delay - An initial delay before the first event
rate  - The repeating rate frequency or period.
units - The units of the repeating rate frequency or period.
      - Available values are:
          Jiffy   Internal clock units.
          Fps     Frames per second.
          kFps    Thousand frames per second.
          Hz      Hertz (same as Fps).
          kHz     Kilo-Hertz (same as kFps).
          Sec     Seconds.
          mSec    Milli-seconds.
task  - A pointer to a task to execute when the event triggers.
data  - An optional data argument to pass to the task.

Our code would then look like this:

  TIMER.StartRepeatEvent(0, 1, Sec, BLINK_DOOR, 1)
  TIMER.SetRepeatEvent(1, 500, 1000, mSec, BLINK_DOOR, 0)

If at some point you want to stop highlighting the door, say, when the player finally notices and opens it; just call the TIMER.CancelEvent routine at the appropriate point in your program. The routine accepts the timer number to cancel as an argument:

  TIMER.CancelEvent(0)
  TIMER.CancelEvent(1)

Et voilá! The timed events are now cancelled. To restart them, you'll have to call the same routines again to define the timers as described above.

That's all there is to it. There is no need to wait, loop, or poll the System Clock. There is not even the need to convert the units and calculate timer deadlines. The framework and the game-engine conspire to abstract all the timing complexities from your program.

Managing time is an integral part of any game. With a general System Clock to drive all temporal events, flexible time measurement units that are independent of the hardware and frame-rate speeds, and a frame-work that abstracts the complexities of conversion and event handling; P-Machinery AGE encourages the game programmer to think at a higher cognitive level, and lets him concentrate on the fun part of making games. :)

-dZ.



NOTE: I would like to thank Joe Zbiciak (intvnut) and Arnauld Chevallier for their assistance and inspiration. Much of the innards of the time management sub-system in P-Machinery AGE are based on ideas provided by them in the INTV-Prog mailing list.

Also, thanks to Joe for suggesting Jiffy as the name for the unit of time. I probably would have gone with something equally cute but not as cool, like TICKlets. :P


Edited by DZ-Jay, Sun Aug 14, 2016 8:15 AM.


#2 DZ-Jay OFFLINE  

DZ-Jay

    Quadrunner

  • Topic Starter
  • 12,117 posts
  • The P-Machinery AGE is almost here!
  • Location:NC, USA

Posted Fri Oct 24, 2014 6:05 AM

I forgot to mention that, apart from the high-level interface routines mentioned, the programmer also has full access to the lower-level procedures that implement the functionality.

Of course, when using P-Machinery AGE, it is recommended that you use the high-level interface to ensure compatibility and consistency of all abstractions. However, the framework is written in a way that invites direct access and understanding of its inner-workings. Below are the descriptions of the low-level implementation of the TIMER library.

.TIMER          STRUCT PM.NULL
@@Disabled      QEQU  PM.INVALID
@@NonRecurring  QEQU  @@Disabled
                ENDS
;; ======================================================================== ;;
;;  PM.TIMER.RESET                                                          ;;
;;  Procedure to reset the timer sub-system.  It cancels all active timer   ;;
;;  events at once and resets the System Clock.                             ;;
;;                                                                          ;;
;;  NOTE:   This routine is expected to be called from within a critical    ;;
;;          section.  It does not disable interrupts on entry, nor          ;;
;;          re-enables them upon return, so make sure you do so on your own.;;
;;          You can achieve this by calling it with the CALLD directive.    ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.RESET                                                ;;
;;      R5      Pointer to return address.                                  ;;
;;                                                                          ;;
;;  OUTPUT                                                                  ;;
;;      R0      Trashed (Zero).                                             ;;
;;      R1      Trashed (Zero).                                             ;;
;;      R2      Trashed (.TIMER.Disabled).                                  ;;
;;      R4      Trashed.                                                    ;;
;; ======================================================================== ;;
;; ======================================================================== ;;
;;  PM.TIMER.START                                                          ;;
;;  Procedure to schedule a new timer event.  The routine accepts a delay   ;;
;;  time expressed in terms of the System Clock, an optional repeat period, ;;
;;  and an pointer to an event handler to be dispatched upon expiration.    ;;
;;                                                                          ;;
;;  NOTE:   Timer delay times are expressed in the number of Jiffies to     ;;
;;          wait before triggering the event.                               ;;
;;                                                                          ;;
;;          Likewise, the optional repeat period represents the number of   ;;
;;          Jiffies to wait after expiration, before the event is           ;;
;;          re-triggered.  If the timer is non-recurring, the repeat period ;;
;;          must be set to the value .TIMER.Disabled.                       ;;
;;                                                                          ;;
;;  There are three entry points to this procedure:                         ;;
;;      PM.TIMER.START              Receives arguments in input record.     ;;
;;                                                                          ;;
;;      PM.TIMER.START.1            Receives timer number in a register.    ;;
;;                                                                          ;;
;;      PM.TIMER.START.2            Receives pointer to timer record in     ;;
;;                                  register.                               ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.START                                                ;;
;;      R3      Optional argument to pass to event handler upon expiration. ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;              Timer number.                           1 DECLE             ;;
;;              Delay time, in Jiffies.                 1 DECLE             ;;
;;              Optional repeat period, in Jiffies.     1 DECLE             ;;
;;              Pointer to event handler.               1 DECLE             ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.START.1                                              ;;
;;      R0      Timer number.                                               ;;
;;      R3      Optional argument to pass to event handler upon expiration. ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;              Delay time, in Jiffies.                 1 DECLE             ;;
;;              Optional repeat period, in Jiffies.     1 DECLE             ;;
;;              Pointer to event handler.               1 DECLE             ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.START.2                                              ;;
;;      R4      Pointer to timer record in .TIMER.TBL table.                ;;
;;      R3      Optional argument to pass to event handler upon expiration. ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;              Delay time, in Jiffies.                 1 DECLE             ;;
;;              Optional repeat period, in Jiffies.     1 DECLE             ;;
;;              Pointer to event handler.               1 DECLE             ;;
;;                                                                          ;;
;;  OUTPUT                                                                  ;;
;;      R0      Trashed.                                                    ;;
;;      R3      Optional argument, untouched.                               ;;
;;      R4      Trashed.                                                    ;;
;; ======================================================================== ;;
;; ======================================================================== ;;
;;  PM.TIMER.CANCEL                                                         ;;
;;  Procedure to cancel an active timer event.                              ;;
;;                                                                          ;;
;;  There are three entry points to this procedure:                         ;;
;;      PM.TIMER.CANCEL             Receives argument in input record.      ;;
;;                                                                          ;;
;;      PM.TIMER.CANCEL.1           Receives argument in a register.        ;;
;;                                                                          ;;
;;      PM.TIMER.CANCEL.2           Receives pointer to timer record in     ;;
;;                                  register.                               ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.CANCEL                                               ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;              Timer number.                           1 DECLE             ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.CANCEL.1                                             ;;
;;      R0      Timer number.                                               ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;                                                                          ;;
;;  INPUT for PM.TIMER.CANCEL.2                                             ;;
;;      R4      Pointer to timer record in .TIMER.TBL table.                ;;
;;      R5      Pointer to invocation record, followed by return address.   ;;
;;                                                                          ;;
;;  OUTPUT                                                                  ;;
;;      R0      Trashed (Zero).                                             ;;
;;      R4      Trashed.                                                    ;;
;; ======================================================================== ;;

The high-level interface also takes care of automatically detecting immediate, register, or input record calling modes; and will do The Right Thing, as expected.
    ; Initialize timer #0 to repeat every 5 seconds
    ; and call BLINK_DOOR with argument -1.
    ;   R0 = 0
    ;   R3 = $FFFF
    TIMER.StartRepeatEvent(R0, 5, Sec, BLINK_DOOR, R3)

It will optimize for the registers accepted by the underlying procedures, and will copy as necessary. The following will result in functionally equivalent code, but will copy R4 and R1 to R0 and R3, respectively:
    ; Initialize timer #0 to repeat every 5 seconds
    ; and call BLINK_DOOR with argument -1.
    ;   R4 = 0
    ;   R1 = $FFFF
    TIMER.StartRepeatEvent(R4, 5, Sec, BLINK_DOOR, R1)

Moreover, the framework will take care to validate all inputs, detect anomalies or ambiguities, and provide adequate and useful error messages. For instance:
; PROGRAM SOURCE:
; ==========================================

    TIMER.CancelEvent(123)
    TIMER.StartRepeatEvent(0, 1, Farenheit, BLINK_DOOR,  PM.NULL)


; LISTING FILE:
; ==========================================

                        ;   TIMER.CancelEvent(123)
                        ;
./src/driver.asm:25: ERROR - Invalid timer '123'.  Must be a numeric value between 0 and 8.
554C 0004 0150 020D     CALL    PM.TIMER.CANCEL
554F 007B               DECLE   123

                        ;   TIMER.StartRepeatEvent(0, 1, Farenheit, BLINK_DOOR,  PM.NULL)
                        ;
./src/driver.asm:26: ERROR - Invalid or unsupported unit of time 'Farenheit'.

Edited by DZ-Jay, Fri Oct 24, 2014 6:17 AM.





0 user(s) are browsing this forum

0 members, 0 guests, 0 anonymous users