[Mono-winforms-list] Master's Thesis

Jonathan Gilbert 2a5gjx302 at sneakemail.com
Wed Aug 17 14:25:17 EDT 2005


At 11:10 AM 10/08/2005 +0300, Alvi Happonen wrote:
[snip]
>This is easy to do with .NET by overriding main forms "OnPaint"-method and 
>calling "this.Invalidate()" at the end of it. This is very simple and 
>effective but only seems to work with .NET. Mono doesn't start looping this 
>way. It seems that Mono always needs somekind of event (from Timer for 
>example) to call "OnPaint"-method.

While the GUI widgets and such all require marshalling calls to a specific
UI thread, System.Drawing is completely threadsafe and can be used from
multiple threads at the same time with only a few minor locking issues (a
Bitmap can only have its bits locked once, though those locked bits can
then be accessed from multiple threads).

What you want to do is draw directly to the screen each time you want to
put a frame onto the screen. Since you want to run at the maximum possible
framerate, you don't want to introduce anything like a timer or a delay, so
what you want is a control with a background thread that does basically this:

class GameViewport : Control
{
  public GameViewport()
  {
    SetStyle(ControlStyles.AllPaintingInWmPaint, true);
    SetStyle(ControlStyles.UserPaint, true);
    SetStyle(ControlStyles.Opaque, true);
    // could also set UserMouse, but DoubleBuffer is not necessary
    // here since we're not painting through the OnPaint event
  }

  public void StartGameThread()
  {
    Thread game_thread = new Thread(new ThreadStart(game_thread));

    // optional, but makes the runtime kill the game thread when 
    // the UI thread dies (means you don't have to manually kill
    // it when the user exits abruptly)
    game_thread.IsBackground = true;

    game_thread.Start();
  }

  void game_thread()
  {
    while (true)
    {
      update_game_world();
      if (is_time_to_quit())
        break;
      draw_next_frame();
    }
  }

  void draw_next_frame()
  {
    using (Graphics g = base.CreateGraphics())
    {
      // insert the code to draw a frame here
    }
  }
}

If your drawing code doesn't use the System.Drawing.Graphics primitives
much and you would prefer to draw directly to a framebuffer of sorts, then
you can do this:

class GameViewport : Control
{
  [...]
  Bitmap frame_bitmap = new Bitmap(640, 480, PixelFormat.Format32bppRgb);

  unsafe void draw_next_frame()
  {
    BitmapData data = null;

    try
    {
      data = frame_bitmap.LockBits(new Rectangle(0, 0, 640, 480),
ImageLockMode.WriteOnly, frame_bitmap.PixelFormat);

      byte *data_ptr = (byte *)data.Scan0.ToPointer();

      // insert the code to draw the frame to the raw bytes at
      // data_ptr here. each scan consists of 640 * 4 = 2560 bytes,
      // but the offset of the next scan should be determined using
      // the data.Stride property. when using pointers, .NET does
      // *not* do bounds-checking for you, so you need to make
      // absolutely sure you don't write outside of the bitmap's
      // bits.
    }
    finally
    {
      if (data != null)
        frame_bitmap.UnlockBits(data);
    }

    using (Graphics g = base.CreateGraphics())
      g.DrawImageUnscaled(frame_bitmap, 0, 0);
  }
}

Because of the current design of libgdiplus, using a 32-bit pixel format is
necessary if you want your code to run quickly; 24-bit pixel formats get
stored as 32-bit behind the scenes, and when you lock the bits, it has to
blit from 32-bit to 24-bit and back (with a dynamic memory allocation to
boot!). This probably doesn't apply under Windows, but using a 32-bit pixel
format won't make your code run more slowly with GDI+.

Using Invalidate() the way you described seems easy, but it is actually a
very bad design choice. At least in Windows, calling Invalidate() appends a
WM_PAINT message to the end of the message queue. This same queue contains,
for instance, keyboard & mouse messages, so if the user is flinging the
mouse about wildly, the WM_PAINT message will be delayed. There is some
issue of the thread running essentially in a busy loop. On processors with
hyperthreading, and on multiprocessor systems, this is essentially a
non-issue, since the UI thread can actually run concurrently to the game
loop thread, but on older uniprocessor systems, it might be a good idea to
have one of these somewhere in the game thread's loop:

  Thread.Sleep(0);

This call indicates that the thread should simply yield to other waiting
threads, but not actually delay its next scheduled timeslice.

To the best of my knowledge, this approach should work just fine under
Mono, but I haven't actually tried precisely this combination :-) I'd be
interested to hear if it works for you.

Good luck with your game :-)

Jonathan Gilbert


More information about the Mono-winforms-list mailing list