/* 
 * This demo implements two GL canvas
 * drawing a rotating cube and a thread.
 *  
 *  The first canvas is drawn by the main process
 *    through an idle callback.
 *  The second canvas is drawn by the thread.
 *
 *  N Castagne, june 2002
 *  nicolas.castagne@imag.fr
 */


/* 
 * Compiled on SGI octane syst. 6.4 with Xforms release 0.89 and glX release 1.3
 *with :
cc ThreadGL.c -o ThreadGL  -O2 -g -xansi -n32 -L/usr/lib32 -L$HOME/XFORMS_089_N32/FORMS  -lforms -L/usr/X11/lib -lpthread -lGL -lGLU -lXext  -lX11 -lm -I$HOME/XFORMS_089_N32/FORMS -I. 
 *
 */




#include <stdio.h>

#include "forms.h"		   
#include <GL/gl.h>
#include <GL/glu.h> 
#include <GL/glx.h>

#include <pthread.h>


/* CONVENTIONS :
   - the name of the procedures called by the main process begin with            Main
   - the name of the procedures called by the thread begin with                  Thread
   - the name of the procedures called by the two processes begin with           Both


   - the name of the global variables relative to the main process begin with     Main
   - the name of the global variables relative to thread begin with               Thread
   - the name of the global variables relative to the two processes begin with    Both
*/


#define DEBUG 0 /* 1 if outputs is needed */

#define DIRECT 1 /* 1 if direct rendering is wished for the canvas 
		    0 if rendering is to be performed through the X server */
                 /* NOTE : in case of a distant execution, 
		    rendering is always not direct */
                 /* NOTE : with this implementation, non-direct rendering 
		    always generates a GLXBadContextTag error 
		    Why ? That is the question ! */


typedef struct { 
  FL_FORM *Form;

  FL_OBJECT *GoBtn;   /* PUSH button that runs the rotation */

  FL_OBJECT *MainCanvas;

} FORM_T;

FORM_T *Form; 
/* the form */



FL_OBJECT *ThreadCanvas;
/* the canvas (created and added by the thread) */




pthread_mutex_t DrawOnMutex = PTHREAD_MUTEX_INITIALIZER;
/* DrawOnMutex is owned by the thread while it draws.
 It is owned by the main process when roation of the cube
 is off*/

int DrawOnMutexLocked = 0 ;
/* 1 if the main process locked the mutex DrawOnMutex
   Needed to avoid double lock of the mutex */




int ExposeEvent = 0 ;
/* ExposeEvent is used to comunicate Expose events to the thread */
/* ExposeEvent = 0 => no expose event currently handled 
             (value 0 is set by the main process when expose  is complet
   ExposeEvent = 1 => expose requested  
             (value 1 is set by the main process)
   ExposeEvent = 2 => the threads has performed the expose 
             (value 2 is set by the thread)
*/
/* NOTE : with this implementation,
   expose events will stop the main process until it is performed by the thread */
/* NOTE : The use of such a flag is quite a bad choice, 
   since both processes spin awaiting complection 
   of the expose event... Should be replaced with 
   an array of mutexes or semaphores.
   The communication process for expose events should be improved */
   



long MainComptRot = 0;
/* the number of rotation perfomed on the main process' cube*/

long ThreadComptRot = 0;
/* the number of rotation perfomed on the thread's cube*/

pthread_mutex_t ThreadComptRotMutex  = PTHREAD_MUTEX_INITIALIZER;
/* Mutex to avoid concurrent accesses to the variable ThreadComptRot */


/*******************************************************************************/
/*    Properties procedures for the canvas                                     */
/*******************************************************************************/

/* redefinition of the Xforms procedure
   for canvas properties */
static int
Thread_glx_init(FL_OBJECT * ob);
static int
Thread_glx_activate(FL_OBJECT * ob);
static int
Thread_glx_cleanup(FL_OBJECT * ob);



static int
Main_glx_init(FL_OBJECT * ob);
static int
Main_glx_activate(FL_OBJECT * ob);
static int
Main_glx_cleanup(FL_OBJECT * ob);




/***************************************************************/
/*       procedures called with both processes                 */
/***************************************************************/
void BothDrawCube(float t);
void BothDrawCanvas(FL_OBJECT *TheCanvas, int NbRot, GLfloat r, GLfloat g, GLfloat b);
int BothExposeCanvas(FL_OBJECT *ob, int w, int h);

void BothWaitABit(int);

/***************************************************************/
/*       procedures executed only within the thread            */
/***************************************************************/
void ThreadAddCanvas();

void * TheThread(void *data); /* the thread itself */


/***************************************************************/
/*     procedures executed only within the main process        */
/***************************************************************/

void MainExitBtn_cb(FL_OBJECT *ob, long data);
void MainGoBtn_cb(FL_OBJECT *ob, long data);
void MainInitBtn_cb(FL_OBJECT *ob, long data);

FORM_T *MainCreateForm(void);

int BothExposeHandler(FL_OBJECT *ob, Window win, 
                                    int win_width, int win_height,
                                    XEvent *xev, void *user_data);


void MainRqstExposeOnThreadCanvas(void);

int MainDrawOn_idle(XEvent *xev, void *userData);



/**********************************************************************************************/
/**********************************************************************************************/


/*******************************************************************************/
/*    Properties for the canvas                                                */
/*******************************************************************************/


/* Because the main process must not activate the canvas,
   we need to redefine 
   the glx_activate procedure (glcanvas.c)
   with fl_modify_canvas_prop( FL_OBJECT, init, activate, cleanup) .
   To that aim, we need the three procedures glx_init, glx_activate and glx_cleanup */

#define MAXATTRIB  34
typedef struct
{
    XVisualInfo *xvinfo;
    GLXContext context;
    int direct;
    int glconfig[MAXATTRIB];
} CSPEC;
#define GLPROP(ob)   ((CSPEC *)(ob->c_vdata))



/* NOTE : The GLXContext of the ThreadCanvas should be created within the thread.
   Thread_glx_init should then be called by the thread, not by XForms...

   As far as I know, in this implementation,  Thread_glx_init
   is called whenthe ThreadCanvas is added to the form, 
   i.e. in the fl_add_object(Form, ThreadCanvas) procedure
   called within ThreadAddCanvas() by the thread.
   It should be correct, then...
*/
static int
Thread_glx_init(FL_OBJECT * ob)
{
    XVisualInfo *vi;
    GLXContext context;

    /* query for openGL capabilities */
    if (!glXQueryExtension(fl_display, 0, 0))
    {
	fprintf(stderr, "GLCanvas: OpenGL not supported\n");
	return -1;
    }

    if (!(vi = glXChooseVisual(fl_display, fl_screen, GLPROP(ob)->glconfig)))
    {
	/* if can't get a visual, quit */
	fprintf(stderr, "GLCanvas: Can't get visual\n");
	return -1;
    }

    /* change canvas defaults */
    fl_set_canvas_visual(ob, vi->visual);
    fl_set_canvas_depth(ob, vi->depth);
    fl_set_canvas_colormap(ob, fl_create_colormap(vi, 1));

    GLPROP(ob)->direct = DIRECT ; /* DIRECT : see top of the file*/

    context = glXCreateContext(fl_display, vi, None, GLPROP(ob)->direct);

    if (!context)
    {
	fprintf(stderr, "Can't create GLX context!\n");
	return -1; 
    }

    /* under some conditions, the parent of the gl canvas might go away,
       leaving the old context and vi hanging. */
    Thread_glx_cleanup(ob);

    GLPROP(ob)->context = context;
    GLPROP(ob)->xvinfo = vi;
    return 0;
}


/* this procedure was not changed */
/*  This routine is called after a valid window is created. It simply
 *  binds the OpenGL context to the canvas window
 */
static int
Thread_glx_activate(FL_OBJECT * ob)
{
  /*     original XFORMS call :  */
  /*     glXMakeCurrent(fl_display, FL_ObjWin(ob), GLPROP(ob)->context); */

  /* no activation is perfomed at all... 
     to avoid an activation of the thread's canvas by the main process
     (glx_activate2 is automatically called by the XFORMS main loop
     when the canvas  window is created ) */
    return 0;
}


/* this procedure was not changed */
/*
 * cleanup is called before destroying the window.  Might be called
 * more than once
 */
static int
Thread_glx_cleanup(FL_OBJECT * ob)
{
    if (GLPROP(ob)->context)
    {
	glXDestroyContext(fl_display, GLPROP(ob)->context);
	XFree(GLPROP(ob)->xvinfo);
	GLPROP(ob)->context = 0;
	GLPROP(ob)->xvinfo = 0;
    }
    return 0;
}







static int
Main_glx_init(FL_OBJECT * ob)
{
    XVisualInfo *vi;
    GLXContext context;

    /* query for openGL capabilities */
    if (!glXQueryExtension(fl_display, 0, 0))
    {
	fprintf(stderr, "GLCanvas: OpenGL not supported\n");
	return -1;
    }

    if (!(vi = glXChooseVisual(fl_display, fl_screen, GLPROP(ob)->glconfig)))
    {
	/* if can't get a visual, quit */
	fprintf(stderr, "GLCanvas: Can't get visual\n");
	return -1;
    }

    /* change canvas defaults */
    fl_set_canvas_visual(ob, vi->visual);
    fl_set_canvas_depth(ob, vi->depth);
    fl_set_canvas_colormap(ob, fl_create_colormap(vi, 1));
    GLPROP(ob)->direct = DIRECT ;

    context = glXCreateContext(fl_display, vi, None, GLPROP(ob)->direct);

    if (!context)
    {
	fprintf(stderr, "Can't create GLX context!\n");
	return -1; 
    }

    /* under some conditions, the parent of the gl canvas might go away,
       leaving the old context and vi hanging. */
    Thread_glx_cleanup(ob);

    GLPROP(ob)->context = context;
    GLPROP(ob)->xvinfo = vi;
/*     printf("******************************INIT\n"); */
    return 0;
}



/* this procedure was not changed */
/*  This routine is called after a valid window is created. It simply
 *  binds the OpenGL context to the canvas window
 */
static int
Main_glx_activate(FL_OBJECT * ob)
{
  glXMakeCurrent(fl_display, FL_ObjWin(ob), GLPROP(ob)->context);

  return 0;
}





/* this procedure was not changed */
/*
 * cleanup is called before destroying the window.  Might be called
 * more than once
 */
static int
Main_glx_cleanup(FL_OBJECT * ob)
{
    if (GLPROP(ob)->context)
    {
	glXDestroyContext(fl_display, GLPROP(ob)->context);
	XFree(GLPROP(ob)->xvinfo);
	GLPROP(ob)->context = 0;
	GLPROP(ob)->xvinfo = 0;
    }
    return 0;
}







/**********************************************************************************************/
/**********************************************************************************************/




/***************************************************************/
/*       procedures executed by the two processes              */
/*       for drawing the two canvas                            */
/***************************************************************/


/* draw a cube of size t 
 A  GL context must be valid */
void BothDrawCube(float t){
  /* Front */
  glBegin(GL_QUADS);
    glNormal3f(   0,   0,   1);
    glVertex3f(-t/2, t/2, t/2); glVertex3f( t/2, t/2, t/2);
    glVertex3f( t/2,-t/2, t/2); glVertex3f(-t/2,-t/2, t/2);
  glEnd();
  /* Top */
  glBegin(GL_QUADS);
    glNormal3f(   0,   1,   0);
    glVertex3f(-t/2, t/2, t/2); glVertex3f( t/2, t/2, t/2);
    glVertex3f( t/2, t/2,-t/2); glVertex3f(-t/2, t/2,-t/2);
  glEnd();
  /* Right */
  glBegin(GL_QUADS);
    glNormal3f(   1,   0,   0);
    glVertex3f( t/2, t/2, t/2); glVertex3f( t/2, t/2,-t/2);
    glVertex3f( t/2,-t/2,-t/2); glVertex3f( t/2,-t/2, t/2);
  glEnd();
  /* Bottom */
  glBegin(GL_QUADS);
    glNormal3f(   0,  -1,   0);
    glVertex3f( t/2,-t/2, t/2); glVertex3f( t/2,-t/2,-t/2);
    glVertex3f(-t/2,-t/2,-t/2); glVertex3f(-t/2,-t/2, t/2);
  glEnd();
  /* Left */
  glBegin(GL_QUADS);
    glNormal3f(  -1,   0,   0);
    glVertex3f(-t/2,-t/2,-t/2); glVertex3f(-t/2,-t/2, t/2);
    glVertex3f(-t/2, t/2, t/2); glVertex3f(-t/2, t/2,-t/2);
  glEnd();
  /* Rear */
  glBegin(GL_QUADS);
    glNormal3f(   0,   0,  -1);
    glVertex3f(-t/2, t/2,-t/2); glVertex3f( t/2, t/2,-t/2);
    glVertex3f( t/2,-t/2,-t/2); glVertex3f(-t/2,-t/2,-t/2);
  glEnd();
  
}



/* draw a cube on TheCanvas*/
void BothDrawCanvas(FL_OBJECT *TheCanvas, int NbRot, GLfloat r, GLfloat g, GLfloat b){

#if DEBUG
  printf("BothDrawCanvas avant make c %x win %d context %d\n",  
	 TheCanvas, 
	 FL_ObjWin(TheCanvas), 
	 GLPROP(TheCanvas)->context);
  fflush(stdout);
#endif

  /* note :
     since each thread has a single GLcanvas
     if is not necessary to call glXMakeCurrent
     each time.
     However, it is much safer in case of a future more complex implementation */
  glXMakeCurrent(fl_display, FL_ObjWin(TheCanvas), GLPROP(TheCanvas)->context);

  glClearColor(r,g,b, 0.0); 
  glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT); 

  glLoadIdentity();
  glColor3f(1.0,0.0,0.0);

  glRotatef(1.3*NbRot, 1,0,0);
  glRotatef(1.3*NbRot, 0,1,0);
  glRotatef(2.0*NbRot, 0,0,1);

  BothDrawCube(2);
  
  glXSwapBuffers(fl_display, fl_get_canvas_id(TheCanvas)); 
}  



/* performs exposure, given the size of the specified canvas */
int BothExposeCanvas(FL_OBJECT *ob, int w, int h){
#if DEBUG
  printf("BothExposeCanvas avant make c %x win %d context %d\n", ob , FL_ObjWin(ob), GLPROP(ob)->context);
  fflush(stdout);
#endif

  /* note :
     since each thread has a single GLcanvas
     if is not necessary to call glXMakeCurrent
     each time.
     However, it is much safer in case of a future more complex implementation */
  glXMakeCurrent(fl_display, FL_ObjWin(ob), GLPROP(ob)->context);
  
  glViewport(0,0,w,h);
  glEnable(GL_LIGHTING);
  glEnable(GL_LIGHT0);
  glEnable(GL_DEPTH_TEST);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(60.0, ((GLfloat)w)/(GLfloat)h, 1.0, 100.0);
  gluLookAt(0,0,10,0,0,0,0,1,0);


  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  

  return(1);
}



/**********************************************************************************************/
/**********************************************************************************************/



/***************************************************************/
/*     "tool"procedures called by the two processes             */
/***************************************************************/

/* blocks the calling process HowMuch ms */ 
void BothWaitABit(int HowMuch){
    struct timeval sleepTime;
    sleepTime.tv_sec = HowMuch /1000;
    sleepTime.tv_usec = (HowMuch%1000) * 1000;
    
    select(0, NULL, NULL, NULL, &sleepTime);
}




/**********************************************************************************************/
/**********************************************************************************************/




/***************************************************************/
/*       procedures executed only by the Thread                */
/***************************************************************/

/* called by the thread */
/* adds a glcanvas to the form Form->Form*/
void ThreadAddCanvas(){
  FL_OBJECT *obj ;
 
  ThreadCanvas = obj = fl_create_glcanvas(FL_NORMAL_CANVAS, 100, 50, 395, 395 ,"");
    fl_modify_canvas_prop(obj, Thread_glx_init, Thread_glx_activate, Thread_glx_cleanup);
    fl_set_object_gravity(obj, FL_NorthWest, FL_SouthEast);
    fl_add_canvas_handler(obj, Expose, BothExposeHandler , NULL);

  fl_add_object(Form->Form, ThreadCanvas);

  }



/* the thread itself*/
void * TheThread(void *data)
{
  /* The canvas must be created by the thread 
   since some GL variables (such as contexts)
   are not shared between threads*/

    ThreadAddCanvas();

  
  while(1) {

    /* should the thread draw  ?*/
    /* Check the mutex !*/
    pthread_mutex_lock(&DrawOnMutex) ; 

    /* in case of an Expose Event, ExposeEvent is non zero*/
    if(ExposeEvent){
      if(ExposeEvent == 1){
	/* indicate that the ExposeEvent is gonna be performed */
	ExposeEvent = 2 ;
	/* perform the expose */

  	BothExposeCanvas(ThreadCanvas, ThreadCanvas->w, ThreadCanvas->h); 

  
	/* draw without moving the cube*/
	{
	  long Rot;

	  pthread_mutex_lock(&ThreadComptRotMutex);
	  Rot = ThreadComptRot;
	  pthread_mutex_unlock(&ThreadComptRotMutex);

  	  BothDrawCanvas(ThreadCanvas, Rot, 0.5, 0.0, 0.2);

  	}
      }
      /* if ExposeEvent == 2,
	 the thread loops doing nothing.
	 It waits the completion of the MainRqstExposeOnThreadCanvas
	 procedure by the Main process */
    }
    else {
      /* move the cube and draw */
      long Rot;
      
      pthread_mutex_lock(&ThreadComptRotMutex);
      Rot = ThreadComptRot++;
      pthread_mutex_unlock(&ThreadComptRotMutex);

      BothDrawCanvas(ThreadCanvas, Rot, 0.5, 0.0, 0.2);

      }
    pthread_mutex_unlock(&DrawOnMutex);
  }
} 



/**********************************************************************************************/
/**********************************************************************************************/

/***************************************************************/
/*       Communication Main -> Thread                          */
/***************************************************************/



/* procedure called in case of an expose event */
/* asks the threads to call ThreadExposeCanvas 
 and waits till completion */
/* The communication process between the main process and the thread
   should be improved */
void MainRqstExposeOnThreadCanvas(void) {
  ExposeEvent = 1 ;

  /* allow drawing (in case the thread was stoped )*/
  if(DrawOnMutexLocked){
    pthread_mutex_unlock(&DrawOnMutex);
    DrawOnMutexLocked = 0 ;
  }

  /* wait the completion of exposure by the thread */
  while(ExposeEvent != 2) {
    /* wait a bit to avoid wasting CPU time */
    BothWaitABit(10);
  }

  /* should we stop the thread ? */
  if(! fl_get_button(Form->GoBtn)){
    pthread_mutex_lock(&DrawOnMutex);
    DrawOnMutexLocked = 1 ;
  }
  /* end of the expose event */
  ExposeEvent = 0;
}





/**********************************************************************************************/
/**********************************************************************************************/

/***************************************************************/
/*     procedures executed only within the main process        */
/***************************************************************/

/* Expose handler
   Always called by the main process
   Concerns for the two canvas */
int BothExposeHandler(FL_OBJECT *ob, Window win, 
                                    int win_width, int win_height,
                                    XEvent *xev, void *user_data){

#if DEBUG
  printf("BothExposeHandler %x\n", ob);
  fflush(stdout);
#endif

    if(ob == Form->MainCanvas){
    BothExposeCanvas(ob, win_width, win_height);
    BothDrawCanvas(ob, MainComptRot, 0.1, 0.1, 0.5);
  }
  else if(ob == ThreadCanvas){
    MainRqstExposeOnThreadCanvas();
  }
  
  return 0 ;
}




/* creation of the form */
FORM_T *MainCreateForm(void){
 
  FL_OBJECT *obj;
  FORM_T *fdui = (FORM_T *) fl_calloc(1, sizeof(*fdui));
  fdui->Form = fl_bgn_form(FL_FLAT_BOX, 900, 480);

  fl_add_box(FL_FLAT_BOX, 0, 0, 900, 480, "");

  fdui->GoBtn = obj = fl_add_button(FL_PUSH_BUTTON,5,50,90,50,"Go !");
    fl_set_object_lalign(obj, FL_ALIGN_CENTER);
    fl_set_object_callback(obj,MainGoBtn_cb,0);
    fl_set_object_gravity(obj,FL_NorthWest, FL_NorthWest);


  obj = fl_add_button(FL_NORMAL_BUTTON,5,120,90,50,"Init !");
    fl_set_object_lalign(obj, FL_ALIGN_CENTER);
    fl_set_object_callback(obj,MainInitBtn_cb,0);
    fl_set_object_gravity(obj,FL_NorthWest, FL_NorthWest);


  obj = fl_add_button(FL_NORMAL_BUTTON,5,400,90,50,"Exit !");
    fl_set_object_lalign(obj, FL_ALIGN_CENTER);
    fl_set_object_callback(obj,MainExitBtn_cb,0);
    fl_set_object_gravity(obj,FL_SouthWest, FL_SouthWest);

  fl_end_form();

  fdui->MainCanvas = obj = fl_create_glcanvas(FL_NORMAL_CANVAS,500,50,395,395,"");
    fl_add_canvas_handler(obj, Expose, BothExposeHandler , NULL); 
    fl_set_object_gravity(obj,FL_NorthWest, FL_SouthWest);
    fl_modify_canvas_prop(obj, Main_glx_init, Main_glx_activate, Main_glx_cleanup);

  fl_add_object(fdui->Form, obj);
  return fdui;
}






/***************************************************************/
/*       Callback procedures                                   */
/***************************************************************/


/* idle callback called when rotation is on */
/* draws the canvas of the Main process */
int MainDrawOn_idle(XEvent *xev, void *userData){
  char chaine[256];
  MainComptRot ++;

  BothDrawCanvas(Form->MainCanvas, MainComptRot, 0.1, 0.1, 0.5);
  
  return 1;
}


/* start rotating the cubes on the two canvas*/
void MainGoBtn_cb(FL_OBJECT *ob, long data){
  if(fl_get_button(ob)){
    fl_set_object_label(ob, "Stop !");
    if(DrawOnMutexLocked){
      pthread_mutex_unlock(&DrawOnMutex); 
      DrawOnMutexLocked = 0 ;
    fl_set_idle_callback(MainDrawOn_idle, NULL);
    }
  }
  else{
    fl_set_object_label(ob, "Go !");
    pthread_mutex_lock(&DrawOnMutex);
    DrawOnMutexLocked = 1 ;
    fl_set_idle_callback(NULL, NULL);
  }
}


/* start rotating the cubes on the two canvas*/
void MainInitBtn_cb(FL_OBJECT *ob, long data){
  MainComptRot = 0 ;
  pthread_mutex_lock(&ThreadComptRotMutex);
  ThreadComptRot = 0 ;
  pthread_mutex_unlock(&ThreadComptRotMutex);
  
  MainRqstExposeOnThreadCanvas();

  BothDrawCanvas(Form->MainCanvas, MainComptRot, 0.1, 0.1, 0.5);
}



void MainExitBtn_cb(FL_OBJECT *ob, long data){
  exit(0);
}






/***************************************************************/
/*       Main procedure                                        */
/***************************************************************/

int main(int argc, char *argv[]){

  /* INIT XForms */
  fl_initialize(&argc, argv, "Thread", 0, 0);


  /*as far as I tested,
    XInitThreads is not really necessary here... */
  if(XInitThreads()  == 0){
    if(fl_show_question("XInitThreads() is not supported on this machine\n\
The program may not work !\n\
Continue?",
			FL_PLACE_CENTER)  == 0)
      exit(-1);
    
  }
  
  /* create the form */
  Form = MainCreateForm();

  fl_set_form_minsize(Form->Form, 900, 450);
  fl_set_form_maxsize(Form->Form, 900, 900);
  
  /* show the form */
  fl_show_form(Form->Form,FL_PLACE_CENTER|FL_FREE_SIZE,FL_FULLBORDER,argv[0]);

  
  /* block the rotation of the canvas */
  fl_set_button(Form->GoBtn, 0);

  pthread_mutex_lock(&DrawOnMutex);
  DrawOnMutexLocked =1;

  {
    pthread_attr_t t_Attr;
    pthread_t TheThreadId;

    pthread_attr_init(&t_Attr);
    /* create the thread.
       The thread will 
       1) create the canvas, 
       2) declare a handler for Expose events, 
           which will be executed by the main process
       3) add it to the form
       4) wait for drawing */
    pthread_create(&TheThreadId, &t_Attr, TheThread, NULL);
    
    pthread_attr_destroy(&t_Attr);
  }

  /* make sure the thread is created and added the canvas : wait a bit */
    BothWaitABit(200);

  while(fl_check_forms());
  

#if DEBUG
    printf("     ********* MainCanvas adress %x\n", Form->MainCanvas);
    printf("     ********* ThreadCanvas adress %x\n", ThreadCanvas);
#endif


  /* main loop */
  while(1) {
    fl_check_forms();
  }
}
