1/*
2 * Copyright (C) 2000  Brian Paul   All Rights Reserved.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included
12 * in all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
17 * BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
18 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 *
21 * Ported to EGL by Chia-I Wu <olvaffe@gmail.com>
22 */
23
24
25/*
26 * This program tests EGL thread safety.
27 * Command line options:
28 *  -p                       Open a display connection for each thread
29 *  -l                       Enable application-side locking
30 *  -n <num threads>         Number of threads to create (default is 2)
31 *  -display <display name>  Specify X display (default is $DISPLAY)
32 *  -t                       Use texture mapping
33 *
34 * Brian Paul  20 July 2000
35 */
36
37
38/*
39 * Notes:
40 * - Each thread gets its own EGL context.
41 *
42 * - The EGL contexts share texture objects.
43 *
44 * - When 't' is pressed to update the texture image, the window/thread which
45 *   has input focus is signalled to change the texture.  The other threads
46 *   should see the updated texture the next time they call glBindTexture.
47 */
48
49
50#if defined(PTHREADS)   /* defined by Mesa on Linux and other platforms */
51
52#include <assert.h>
53#include <X11/Xlib.h>
54#include <X11/Xutil.h>
55#include "gl_wrap.h"
56#include <EGL/egl.h>
57#include <math.h>
58#include <stdio.h>
59#include <stdlib.h>
60#include <string.h>
61#include <unistd.h>
62#include <pthread.h>
63
64
65/*
66 * Each window/thread/context:
67 */
68struct winthread {
69   Display *Dpy;
70   int Index;
71   pthread_t Thread;
72   Window Win;
73   EGLDisplay Display;
74   EGLContext Context;
75   EGLSurface Surface;
76   float Angle;
77   int WinWidth, WinHeight;
78   GLboolean NewSize;
79   GLboolean Initialized;
80   GLboolean MakeNewTexture;
81};
82
83
84#define MAX_WINTHREADS 100
85static struct winthread WinThreads[MAX_WINTHREADS];
86static int NumWinThreads = 0;
87static volatile GLboolean ExitFlag = GL_FALSE;
88
89static GLboolean MultiDisplays = 0;
90static GLboolean Locking = 0;
91static GLboolean Texture = GL_FALSE;
92static GLuint TexObj = 12;
93static GLboolean Animate = GL_TRUE;
94
95static pthread_mutex_t Mutex;
96static pthread_cond_t CondVar;
97static pthread_mutex_t CondMutex;
98
99
100static void
101Error(const char *msg)
102{
103   fprintf(stderr, "Error: %s\n", msg);
104   exit(1);
105}
106
107
108static void
109signal_redraw(void)
110{
111   pthread_mutex_lock(&CondMutex);
112   pthread_cond_broadcast(&CondVar);
113   pthread_mutex_unlock(&CondMutex);
114}
115
116
117static void
118MakeNewTexture(struct winthread *wt)
119{
120#define TEX_SIZE 128
121   static float step = 0.0;
122   GLfloat image[TEX_SIZE][TEX_SIZE][4];
123   GLint width;
124   int i, j;
125
126   for (j = 0; j < TEX_SIZE; j++) {
127      for (i = 0; i < TEX_SIZE; i++) {
128         float dt = 5.0 * (j - 0.5 * TEX_SIZE) / TEX_SIZE;
129         float ds = 5.0 * (i - 0.5 * TEX_SIZE) / TEX_SIZE;
130         float r = dt * dt + ds * ds + step;
131         image[j][i][0] =
132         image[j][i][1] =
133         image[j][i][2] = 0.75 + 0.25 * cos(r);
134         image[j][i][3] = 1.0;
135      }
136   }
137
138   step += 0.5;
139
140   glBindTexture(GL_TEXTURE_2D, TexObj);
141
142   glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &width);
143   if (width) {
144      assert(width == TEX_SIZE);
145      /* sub-tex replace */
146      glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, TEX_SIZE, TEX_SIZE,
147                   GL_RGBA, GL_FLOAT, image);
148   }
149   else {
150      /* create new */
151      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
152      glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
153
154      glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEX_SIZE, TEX_SIZE, 0,
155                   GL_RGBA, GL_FLOAT, image);
156   }
157}
158
159
160
161/* draw a colored cube */
162static void
163draw_object(void)
164{
165   glPushMatrix();
166   glScalef(0.75, 0.75, 0.75);
167
168   glColor3f(1, 0, 0);
169
170   if (Texture) {
171      glBindTexture(GL_TEXTURE_2D, TexObj);
172      glEnable(GL_TEXTURE_2D);
173   }
174   else {
175      glDisable(GL_TEXTURE_2D);
176   }
177
178   glBegin(GL_QUADS);
179
180   /* -X */
181   glColor3f(0, 1, 1);
182   glTexCoord2f(0, 0);  glVertex3f(-1, -1, -1);
183   glTexCoord2f(1, 0);  glVertex3f(-1,  1, -1);
184   glTexCoord2f(1, 1);  glVertex3f(-1,  1,  1);
185   glTexCoord2f(0, 1);  glVertex3f(-1, -1,  1);
186
187   /* +X */
188   glColor3f(1, 0, 0);
189   glTexCoord2f(0, 0);  glVertex3f(1, -1, -1);
190   glTexCoord2f(1, 0);  glVertex3f(1,  1, -1);
191   glTexCoord2f(1, 1);  glVertex3f(1,  1,  1);
192   glTexCoord2f(0, 1);  glVertex3f(1, -1,  1);
193
194   /* -Y */
195   glColor3f(1, 0, 1);
196   glTexCoord2f(0, 0);  glVertex3f(-1, -1, -1);
197   glTexCoord2f(1, 0);  glVertex3f( 1, -1, -1);
198   glTexCoord2f(1, 1);  glVertex3f( 1, -1,  1);
199   glTexCoord2f(0, 1);  glVertex3f(-1, -1,  1);
200
201   /* +Y */
202   glColor3f(0, 1, 0);
203   glTexCoord2f(0, 0);  glVertex3f(-1, 1, -1);
204   glTexCoord2f(1, 0);  glVertex3f( 1, 1, -1);
205   glTexCoord2f(1, 1);  glVertex3f( 1, 1,  1);
206   glTexCoord2f(0, 1);  glVertex3f(-1, 1,  1);
207
208   /* -Z */
209   glColor3f(1, 1, 0);
210   glTexCoord2f(0, 0);  glVertex3f(-1, -1, -1);
211   glTexCoord2f(1, 0);  glVertex3f( 1, -1, -1);
212   glTexCoord2f(1, 1);  glVertex3f( 1,  1, -1);
213   glTexCoord2f(0, 1);  glVertex3f(-1,  1, -1);
214
215   /* +Y */
216   glColor3f(0, 0, 1);
217   glTexCoord2f(0, 0);  glVertex3f(-1, -1, 1);
218   glTexCoord2f(1, 0);  glVertex3f( 1, -1, 1);
219   glTexCoord2f(1, 1);  glVertex3f( 1,  1, 1);
220   glTexCoord2f(0, 1);  glVertex3f(-1,  1, 1);
221
222   glEnd();
223
224   glPopMatrix();
225}
226
227
228/* signal resize of given window */
229static void
230resize(struct winthread *wt, int w, int h)
231{
232   wt->NewSize = GL_TRUE;
233   wt->WinWidth = w;
234   wt->WinHeight = h;
235   if (!Animate)
236      signal_redraw();
237}
238
239
240/*
241 * We have an instance of this for each thread.
242 */
243static void
244draw_loop(struct winthread *wt)
245{
246   while (!ExitFlag) {
247
248      if (Locking)
249         pthread_mutex_lock(&Mutex);
250
251      if (!wt->Initialized) {
252         eglMakeCurrent(wt->Display, wt->Surface, wt->Surface, wt->Context);
253         printf("xeglthreads: %d: GL_RENDERER = %s\n", wt->Index,
254                (char *) glGetString(GL_RENDERER));
255         if (Texture /*&& wt->Index == 0*/) {
256            MakeNewTexture(wt);
257         }
258         wt->Initialized = GL_TRUE;
259      }
260
261      if (Locking)
262         pthread_mutex_unlock(&Mutex);
263
264      eglBindAPI(EGL_OPENGL_API);
265      if (eglGetCurrentContext() != wt->Context) {
266         printf("xeglthreads: current context %p != %p\n",
267               eglGetCurrentContext(), wt->Context);
268      }
269
270      glEnable(GL_DEPTH_TEST);
271
272      if (wt->NewSize) {
273         GLfloat w = (float) wt->WinWidth / (float) wt->WinHeight;
274         glViewport(0, 0, wt->WinWidth, wt->WinHeight);
275         glMatrixMode(GL_PROJECTION);
276         glLoadIdentity();
277         glFrustum(-w, w, -1.0, 1.0, 1.5, 10);
278         glMatrixMode(GL_MODELVIEW);
279         glLoadIdentity();
280         glTranslatef(0, 0, -2.5);
281         wt->NewSize = GL_FALSE;
282      }
283
284      if (wt->MakeNewTexture) {
285         MakeNewTexture(wt);
286         wt->MakeNewTexture = GL_FALSE;
287      }
288
289      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
290
291      glPushMatrix();
292         glRotatef(wt->Angle, 0, 1, 0);
293         glRotatef(wt->Angle, 1, 0, 0);
294         glScalef(0.7, 0.7, 0.7);
295         draw_object();
296      glPopMatrix();
297
298      if (Locking)
299         pthread_mutex_lock(&Mutex);
300
301      eglSwapBuffers(wt->Display, wt->Surface);
302
303      if (Locking)
304         pthread_mutex_unlock(&Mutex);
305
306      if (Animate) {
307         usleep(5000);
308      }
309      else {
310         /* wait for signal to draw */
311         pthread_mutex_lock(&CondMutex);
312         pthread_cond_wait(&CondVar, &CondMutex);
313         pthread_mutex_unlock(&CondMutex);
314      }
315      wt->Angle += 1.0;
316   }
317   eglMakeCurrent(wt->Display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
318}
319
320
321static void
322keypress(XEvent *event, struct winthread *wt)
323{
324   char buf[100];
325   KeySym keySym;
326   XComposeStatus stat;
327
328   XLookupString(&event->xkey, buf, sizeof(buf), &keySym, &stat);
329
330   switch (keySym) {
331   case XK_Escape:
332      /* tell all threads to exit */
333      if (!Animate) {
334         signal_redraw();
335      }
336      ExitFlag = GL_TRUE;
337      /*printf("exit draw_loop %d\n", wt->Index);*/
338      return;
339   case XK_t:
340   case XK_T:
341      if (Texture) {
342         wt->MakeNewTexture = GL_TRUE;
343         if (!Animate)
344            signal_redraw();
345      }
346      break;
347   case XK_a:
348   case XK_A:
349      Animate = !Animate;
350      if (Animate)  /* yes, prev Animate state! */
351         signal_redraw();
352      break;
353   case XK_s:
354   case XK_S:
355      if (!Animate)
356         signal_redraw();
357      break;
358   default:
359      ; /* nop */
360   }
361}
362
363
364/*
365 * The main process thread runs this loop.
366 * Single display connection for all threads.
367 */
368static void
369event_loop(Display *dpy)
370{
371   XEvent event;
372   int i;
373
374   assert(!MultiDisplays);
375
376   while (!ExitFlag) {
377
378      if (Locking) {
379         while (1) {
380            int k;
381            pthread_mutex_lock(&Mutex);
382            k = XPending(dpy);
383            if (k) {
384               XNextEvent(dpy, &event);
385               pthread_mutex_unlock(&Mutex);
386               break;
387            }
388            pthread_mutex_unlock(&Mutex);
389            usleep(5000);
390         }
391      }
392      else {
393         XNextEvent(dpy, &event);
394      }
395
396      switch (event.type) {
397         case ConfigureNotify:
398            /* Find winthread for this event's window */
399            for (i = 0; i < NumWinThreads; i++) {
400               struct winthread *wt = &WinThreads[i];
401               if (event.xconfigure.window == wt->Win) {
402                  resize(wt, event.xconfigure.width,
403                         event.xconfigure.height);
404                  break;
405               }
406            }
407            break;
408         case KeyPress:
409            for (i = 0; i < NumWinThreads; i++) {
410               struct winthread *wt = &WinThreads[i];
411               if (event.xkey.window == wt->Win) {
412                  keypress(&event, wt);
413                  break;
414               }
415            }
416            break;
417         default:
418            /*no-op*/ ;
419      }
420   }
421}
422
423
424/*
425 * Separate display connection for each thread.
426 */
427static void
428event_loop_multi(void)
429{
430   XEvent event;
431   int w = 0;
432
433   assert(MultiDisplays);
434
435   while (!ExitFlag) {
436      struct winthread *wt = &WinThreads[w];
437      if (XPending(wt->Dpy)) {
438         XNextEvent(wt->Dpy, &event);
439         switch (event.type) {
440         case ConfigureNotify:
441            resize(wt, event.xconfigure.width, event.xconfigure.height);
442            break;
443         case KeyPress:
444            keypress(&event, wt);
445            break;
446         default:
447            ; /* nop */
448         }
449      }
450      w = (w + 1) % NumWinThreads;
451      usleep(5000);
452   }
453}
454
455
456
457/*
458 * we'll call this once for each thread, before the threads are created.
459 */
460static void
461create_window(struct winthread *wt, EGLContext shareCtx)
462{
463   Window win;
464   EGLContext ctx;
465   EGLSurface surf;
466   EGLint attribs[] = { EGL_RED_SIZE, 1,
467                        EGL_GREEN_SIZE, 1,
468                        EGL_BLUE_SIZE, 1,
469                        EGL_DEPTH_SIZE, 1,
470                        EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
471                        EGL_NONE };
472   EGLConfig config;
473   EGLint num_configs;
474   EGLint vid;
475   int scrnum;
476   XSetWindowAttributes attr;
477   unsigned long mask;
478   Window root;
479   XVisualInfo *visinfo, visTemplate;
480   int num_visuals;
481   int width = 160, height = 160;
482   int xpos = (wt->Index % 8) * (width + 10);
483   int ypos = (wt->Index / 8) * (width + 20);
484
485   scrnum = DefaultScreen(wt->Dpy);
486   root = RootWindow(wt->Dpy, scrnum);
487
488   if (!eglChooseConfig(wt->Display, attribs, &config, 1, &num_configs) ||
489       !num_configs) {
490      Error("Unable to choose an EGL config");
491   }
492
493   assert(config);
494   assert(num_configs > 0);
495
496   if (!eglGetConfigAttrib(wt->Display, config, EGL_NATIVE_VISUAL_ID, &vid)) {
497      Error("Unable to get visual id of EGL config\n");
498   }
499
500   visTemplate.visualid = vid;
501   visinfo = XGetVisualInfo(wt->Dpy, VisualIDMask,
502                        &visTemplate, &num_visuals);
503   if (!visinfo) {
504      Error("Unable to find RGB, Z, double-buffered visual");
505   }
506
507   /* window attributes */
508   attr.background_pixel = 0;
509   attr.border_pixel = 0;
510   attr.colormap = XCreateColormap(wt->Dpy, root, visinfo->visual, AllocNone);
511   attr.event_mask = StructureNotifyMask | ExposureMask | KeyPressMask;
512   mask = CWBackPixel | CWBorderPixel | CWColormap | CWEventMask;
513
514   win = XCreateWindow(wt->Dpy, root, xpos, ypos, width, height,
515                        0, visinfo->depth, InputOutput,
516                        visinfo->visual, mask, &attr);
517   if (!win) {
518      Error("Couldn't create window");
519   }
520
521   XFree(visinfo);
522
523   {
524      XSizeHints sizehints;
525      sizehints.x = xpos;
526      sizehints.y = ypos;
527      sizehints.width  = width;
528      sizehints.height = height;
529      sizehints.flags = USSize | USPosition;
530      XSetNormalHints(wt->Dpy, win, &sizehints);
531      XSetStandardProperties(wt->Dpy, win, "xeglthreads", "xeglthreads",
532                              None, (char **)NULL, 0, &sizehints);
533   }
534
535   eglBindAPI(EGL_OPENGL_API);
536
537   ctx = eglCreateContext(wt->Display, config, shareCtx, NULL);
538   if (!ctx) {
539      Error("Couldn't create EGL context");
540   }
541   surf = eglCreateWindowSurface(wt->Display, config, win, NULL);
542   if (!surf) {
543      Error("Couldn't create EGL surface");
544   }
545
546   XMapWindow(wt->Dpy, win);
547   XSync(wt->Dpy, 0);
548
549   /* save the info for this window/context */
550   wt->Win = win;
551   wt->Context = ctx;
552   wt->Surface = surf;
553   wt->Angle = 0.0;
554   wt->WinWidth = width;
555   wt->WinHeight = height;
556   wt->NewSize = GL_TRUE;
557}
558
559
560/*
561 * Called by pthread_create()
562 */
563static void *
564thread_function(void *p)
565{
566   struct winthread *wt = (struct winthread *) p;
567   draw_loop(wt);
568   return NULL;
569}
570
571
572/*
573 * called before exit to wait for all threads to finish
574 */
575static void
576clean_up(void)
577{
578   int i;
579
580   /* wait for threads to finish */
581   for (i = 0; i < NumWinThreads; i++) {
582      pthread_join(WinThreads[i].Thread, NULL);
583   }
584
585   for (i = 0; i < NumWinThreads; i++) {
586      eglDestroyContext(WinThreads[i].Display, WinThreads[i].Context);
587      XDestroyWindow(WinThreads[i].Dpy, WinThreads[i].Win);
588   }
589}
590
591
592static void
593usage(void)
594{
595   printf("xeglthreads: test of EGL/GL thread safety (any key = exit)\n");
596   printf("Usage:\n");
597   printf("  xeglthreads [options]\n");
598   printf("Options:\n");
599   printf("   -display DISPLAYNAME  Specify display string\n");
600   printf("   -n NUMTHREADS  Number of threads to create\n");
601   printf("   -p  Use a separate display connection for each thread\n");
602   printf("   -l  Use application-side locking\n");
603   printf("   -t  Enable texturing\n");
604   printf("Keyboard:\n");
605   printf("   Esc  Exit\n");
606   printf("   t    Change texture image (requires -t option)\n");
607   printf("   a    Toggle animation\n");
608   printf("   s    Step rotation (when not animating)\n");
609}
610
611
612int
613main(int argc, char *argv[])
614{
615   char *displayName = NULL;
616   int numThreads = 2;
617   Display *dpy = NULL;
618   EGLDisplay *egl_dpy = NULL;
619   int i;
620   Status threadStat;
621
622   if (argc == 1) {
623      usage();
624   }
625   else {
626      int i;
627      for (i = 1; i < argc; i++) {
628         if (strcmp(argv[i], "-display") == 0 && i + 1 < argc) {
629            displayName = argv[i + 1];
630            i++;
631         }
632         else if (strcmp(argv[i], "-p") == 0) {
633            MultiDisplays = 1;
634         }
635         else if (strcmp(argv[i], "-l") == 0) {
636            Locking = 1;
637         }
638         else if (strcmp(argv[i], "-t") == 0) {
639            Texture = 1;
640         }
641         else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc) {
642            numThreads = atoi(argv[i + 1]);
643            if (numThreads < 1)
644               numThreads = 1;
645            else if (numThreads > MAX_WINTHREADS)
646               numThreads = MAX_WINTHREADS;
647            i++;
648         }
649         else {
650            usage();
651            exit(1);
652         }
653      }
654   }
655
656   if (Locking)
657      printf("xeglthreads: Using explicit locks around Xlib calls.\n");
658   else
659      printf("xeglthreads: No explict locking.\n");
660
661   if (MultiDisplays)
662      printf("xeglthreads: Per-thread display connections.\n");
663   else
664      printf("xeglthreads: Single display connection.\n");
665
666   /*
667    * VERY IMPORTANT: call XInitThreads() before any other Xlib functions.
668    */
669   if (!MultiDisplays) {
670       if (!Locking) {
671           threadStat = XInitThreads();
672           if (threadStat) {
673               printf("XInitThreads() returned %d (success)\n",
674                      (int) threadStat);
675           }
676           else {
677               printf("XInitThreads() returned 0 "
678                      "(failure- this program may fail)\n");
679           }
680       }
681
682      dpy = XOpenDisplay(displayName);
683      if (!dpy) {
684         fprintf(stderr, "Unable to open display %s\n",
685                 XDisplayName(displayName));
686         return -1;
687      }
688      egl_dpy = eglGetDisplay(dpy);
689      if (!egl_dpy) {
690         fprintf(stderr, "Unable to get EGL display\n");
691         XCloseDisplay(dpy);
692         return -1;
693      }
694      if (!eglInitialize(egl_dpy, NULL, NULL)) {
695          fprintf(stderr, "Unable to initialize EGL display\n");
696          return -1;
697      }
698   }
699
700   pthread_mutex_init(&Mutex, NULL);
701   pthread_mutex_init(&CondMutex, NULL);
702   pthread_cond_init(&CondVar, NULL);
703
704   printf("xeglthreads: creating windows\n");
705
706   NumWinThreads = numThreads;
707
708   /* Create the EGL windows and contexts */
709   for (i = 0; i < numThreads; i++) {
710      EGLContext share;
711
712      if (MultiDisplays) {
713         WinThreads[i].Dpy = XOpenDisplay(displayName);
714         assert(WinThreads[i].Dpy);
715         WinThreads[i].Display = eglGetDisplay(WinThreads[i].Dpy);
716         assert(eglInitialize(WinThreads[i].Display, NULL, NULL));
717      }
718      else {
719         WinThreads[i].Dpy = dpy;
720         WinThreads[i].Display = egl_dpy;
721      }
722      WinThreads[i].Index = i;
723      WinThreads[i].Initialized = GL_FALSE;
724
725      share = (Texture && i > 0) ? WinThreads[0].Context : 0;
726
727      create_window(&WinThreads[i], share);
728   }
729
730   printf("xeglthreads: creating threads\n");
731
732   /* Create the threads */
733   for (i = 0; i < numThreads; i++) {
734      pthread_create(&WinThreads[i].Thread, NULL, thread_function,
735                     (void*) &WinThreads[i]);
736      printf("xeglthreads: Created thread %p\n",
737              (void *) WinThreads[i].Thread);
738   }
739
740   if (MultiDisplays)
741      event_loop_multi();
742   else
743      event_loop(dpy);
744
745   clean_up();
746
747   if (MultiDisplays) {
748      for (i = 0; i < numThreads; i++) {
749          eglTerminate(WinThreads[i].Display);
750          XCloseDisplay(WinThreads[i].Dpy);
751      }
752   }
753   else {
754      eglTerminate(egl_dpy);
755      XCloseDisplay(dpy);
756   }
757
758   return 0;
759}
760
761
762#else /* PTHREADS */
763
764
765#include <stdio.h>
766
767int
768main(int argc, char *argv[])
769{
770   printf("Sorry, this program wasn't compiled with PTHREADS defined.\n");
771   return 0;
772}
773
774
775#endif /* PTHREADS */
776