NET:Concepts:Frame Locking

From Lavish Software Wiki
Jump to navigation Jump to search

Overview

Frame locking is a term used to describe the act of synchronizing with the host application's main thread. Generally, this means a game's rendering loop.

Behavior

Frame locking is implemented in Inner Space as a reader-writer lock. Any number of .NET application threads may obtain any number of locks overall or per thread. There is no harm in locking multiple times in a single thread, and no harm in multiple threads locking at once.

The host's thread will always attempt to continue as long as the reader count is zero. If a reader is waiting, the host will release its writer lock and wait for the readers to leave. As soon as all readers have left, the host will re-acquire the writer lock and repeat the process.

Acquiring frame lock

Acquiring frame lock may require the host application to complete its processing of the current frame, because the host will not always be ready when you are. That is, unless the frame is already locked, and the host thread is waiting for the number of readers (your locks) to reach zero. Frame lock is acquired after, and only after, that state is reached. Each time you acquire a lock, the reader count is incremented.

Unlocking

Each time you unlock, the reader count for the current thread is decremented. As soon as the count for all threads is zero, the host application is signaled to continue, and may perform any of its necessary duties until the following frame.

Implicit frame locks

All LavishScript and most Inner Space .NET API functions implicitly lock and unlock the frame.

Frame lock is also implicit in code that is being executed by the writer thread. Examples of this are LavishScript events generated directly or indirectly by the host -- such as "OnFrame".

Using Non-persistent LavishScript Objects

Object persistence, in terms of LavishScript Objects, is the ability to transcend the bounds of frame lock. Persistent LavishScript objects may be referenced and used from one frame to the next. However, non-persistent LavishScript objects are garbage collected (by LavishScript) when the frame ends. This results in disposal of the reference .NET is holding, which means that checking the IsValid property of a LavishScriptObject will return false -- the object no longer has any relevance to LavishScript, and therefore no members or methods of that LavishScriptObject will yield useful results.

To reuse an object reference safely, the object should either be persistent, or only used within a frame locked segment. This includes where the reference is only used on the same line. For example, here is code using ISXEQ2:

/* ext is an arbitrary .NET object with a method called Me(), which returns a derived LavishScriptObject. The derived class has a property X. */
double X = ext.Me().X;

ext.Me() returns a non-persistent LavishScript object. Without explicitly locking the frame around the code segment, the host may advance the frame, and the reference to the return value of ext.Me() to retrieve the property X may fall on deaf ears -- the object may have already been disposed. The call to Me() must implicitly lock and unlock the frame to guarantee its function, and upon unlocking, the host application may continue, causing the LavishScript API to invalidate the reference.

Wrapper sample, for the examples following it
using LavishScriptAPI;
using LavishVMAPI;

/* Ideally, the following class would derive from LavishScriptPersistentObject. However, the LavishScript type must support object persistence. In this case, persistence is not supported. This means that we must instantiate this class each time we need to use it after unlocking the frame. */
public class Me : LavishScriptObject
{
   public Me()
      :base(LavishScript.Objects.GetObject("Me"))
   {
   }

   public double X
   {
      get { return GetMember<double>("X");
   }
   public double Y
   {
      get { return GetMember<double>("Y");
   }
   public double Z
   {
      get { return GetMember<double>("Z");
   }
   public string Name
   {
      get { return GetMember<string>("Name"); }
   }

   public Location Location
   {
      get { return new Location(this); }
   }
}

// A .NET-facing object representing a point in 3d space. Has constructors to retrieve the values from LavishScript objects.
public class Location
{
   // Since "Me" is NOT a persistent object, this constructor should ONLY be called from within frame lock!
   public Location(Me me)
   {
      x = me.X;
      y = me.Y;
      z = me.Z;
   }

   double x,y,z;
   public X
   {
     get { return x; }
     set { x = value; }
   }

   public Z
   {
     get { return y; }
     set { y = value; }
   }

   public Z
   {
     get { return z; }
     set { z = value; }
   }

   // standard euclidian distance function in 3 dimensions
   public double Distance(Location other)
   {
      double dX = other.X-x;
      double dY = other.Y-y;
      double dZ = other.Z-z;
      return Math.Sqrt((dX*dX)+(dY*dY)+(dZ*dZ));
   }
}
Example
track how far Me moved
 Location LastLocation; // for the purpose of this example, assume that LastLocation has already been populated.
 double GetDistanceMoved()
 {
    using (new FrameLock(true))
    {
      // we want a reference to Me. Since it's not persistent, we can't just keep a static reference somewhere, we have to call into LavishScript to get it.
      Me me = new Me();
   
      // get our current location. Location is residing in .NET instead of LavishScript, so we could keep it around if we wanted.
      Location loc = me.Location;

      // get the distance to the previous location        
      double distance = loc.Distance(LastLocation);

      // set the previous location for the next call
      LastLocation = loc;

      return distance;
    }
 }

Exclusive Locks

Exclusive locking blocks any other thread attempting to acquire frame lock, until the exclusive lock is released -- it does not suspend execution of the other threads, only blocks frame lock attempts. This generally should not be used, as it may deadlock the application if the exclusive locking thread must wait on a resource in use in another thread that attempts a frame lock. Standard thread synchronization should be used instead.

Frame wait

A frame wait will cause a thread to yield execution until the following frame. This can be used instead of System.Threading.Thread.Sleep(0) (or Sleep(1) as many tend to use) to indicate the shortest yield possible, only continuing when the current frame has ended and the next begun. Frame lock can be implicitly acquired during frame wait.

Bad practice
while(true)
{
   using (new FrameLock(true))
   {
     // check some condition that can only happen after the game processes a frame
   }
   Thread.Sleep(1);
}
Good practice
while(true)
{
   Frame.Wait(true);
     // check some condition that can only happen after the game processes a frame
   Frame.Unlock();
}
Or, to not explicitly wait for the next frame on the first check
Frame.Lock();
while(true)
{
     // check some condition that can only happen after the game processes a frame
   Frame.Unlock();
   Frame.Wait(true);
}

OnFrame LavishScript event

The OnFrame event is executed each frame before any frame lock can be acquired. This can be advantageous for updating persistent, and even non-persistent, object references each frame, before any frame locked code will attempt to access the reference. Using the ISXEQ2 sample wrapper, here is another way for the movement tracking example to work:

Example
track how far Me moved
using LavishScriptAPI;
using LavishVMAPI;
using InnerSpaceAPI;
public class MyObject
{
 Location LastLocation; // for the purpose of this example, assume that LastLocation has already been populated.

 /* let's keep a reference, this way if we call a bunch of different methods, and each of them acquires its own frame lock and needs to access this object, we've already guaranteed it. */
 Me me;

 void MyObject()
 {
   // attach our event handler
   LavishScript.Events.AttachEventTarget(LavishScript.Events.RegisterEvent("OnFrame"),OnFrame);
 }

 void OnFrame(object sender, LSEventArgs e)
 {
    // update the reference each frame
    me = new Me();
 }

 double GetDistanceMoved()
 {
    using (new FrameLock(true))
    {   
      // get our current location. Location is residing in .NET instead of LavishScript, so we could keep it around if we wanted.
      Location loc = me.Location;

      // get the distance to the previous location        
      double distance = loc.Distance(LastLocation);

      // set the previous location for the next call
      LastLocation = loc;

      return distance;
    }
 }

 // just another random function to demonstrate our reference
 void DoSomethingElse()
 {
    using (new FrameLock(true))
    {
        InnerSpace.Echo("My name is " + me.Name); 
    }
 }
}

Frame Locking API

Frame locking API in .NET is implemented in LavishVMAPI.Frame. Equivalent functions with the same names as those in LavishVMAPI.Frame are found in ISInterface for Inner Space extensions.

Commentary

Source: Lax (http://www.isxwow.net/forums/viewtopic.php?f=15&t=791&p=5247&hilit=Frame.Lock#p5247])

The reason it goes slower without explicit frame locking in your code, is that without it, you're relying 100% on the 
API to perform the frame locking for you.

It has to be done in order to ensure synchronization with the game, so the API does it internally on a per-call basis. The 
problem with relying on the API to  do it for you, is that consecutive function calls may take place on the following frame, 
instead of the current one. Without frame locking, 60 individual function calls may end up taking 60 individual frames, with 
an elapsed time of 1 second at 60 fps, instead of all being done in one frame, with an elapsed time counted in milliseconds 
or better (even if the function calls are consecutive). To improve efficiency, you want to strategically lock when doing sections 
of calls that should be in the same frame.

A lot of people seem to want to avoid frame locking, with the belief that it's going to hurt their game performance. To avoid 
it, they cache a lot of data in a frame lock, and make use of it outside of frame lock, entering back into frame lock when the 
game must again be manipulated (e.g. to perform an action in the game). To some degree, this is a great idea. But, probably not 
for the right reason. Locking and unlocking is not going to alter performance very much, and doesn't slow down the game. 

Locking just means that your thread is going to wait for the game to be ready for you, not blocking the game wherever it happens 
to be (thusly why n individual calls may end up taking n frames). The real performance hurt comes from reusing the same LavishScript 
API calls to obtain data, regardless of frame locking. In other words, if you have to keep using Me.X, it is helpful to cache it in 
.NET because this removes LavishScript parsing as well as the managed to unmanaged to managed marshaling and context changes. But it 
doesn't help so much to cache rarely used but constantly changing data (because then you're still doing the hard work, but perhaps 
for nothing). In other words, optimizing out additional LavishScript calls is much more beneficial than optimizing out an additional 
frame lock, any way you look at it.

See Also