Ensure that the WM_ENTERIDLE message is not sent if the wake-up event
[wine] / loader / dos / dosvm.c
1 /*
2  * DOS Virtual Machine
3  *
4  * Copyright 1998 Ove Kåven
5  *
6  * This code hasn't been completely cleaned up yet.
7  */
8
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <errno.h>
13 #include <fcntl.h>
14 #include <signal.h>
15 #include <unistd.h>
16 #include <sys/time.h>
17 #include <sys/types.h>
18 #include <sys/stat.h>
19
20 #include "wine/winbase16.h"
21 #include "winuser.h"
22 #include "winnt.h"
23 #include "msdos.h"
24 #include "file.h"
25 #include "miscemu.h"
26 #include "dosexe.h"
27 #include "dosmod.h"
28 #include "stackframe.h"
29 #include "debugtools.h"
30
31 DECLARE_DEBUG_CHANNEL(int)
32 DECLARE_DEBUG_CHANNEL(module)
33 DECLARE_DEBUG_CHANNEL(relay)
34
35 #ifdef MZ_SUPPORTED
36
37 #include <sys/vm86.h>
38 #include <sys/mman.h>
39
40 #define IF_CLR(ctx) EFL_reg(ctx) &= ~VIF_MASK
41 #define IF_ENABLED(ctx) (EFL_reg(ctx) & VIF_MASK)
42 #define SET_PEND(ctx) EFL_reg(ctx) |= VIP_MASK
43 #define CLR_PEND(ctx) EFL_reg(ctx) &= ~VIP_MASK
44 #define IS_PEND(ctx) (EFL_reg(ctx) & VIP_MASK)
45
46 #undef TRY_PICRETURN
47
48 extern void WINAPI REGS_FUNC(RtlRaiseException)( EXCEPTION_RECORD *rec,
49                                                  CONTEXT *context );
50
51 static void do_exception( int signal, CONTEXT86 *context )
52 {
53     EXCEPTION_RECORD rec;
54     if ((signal == SIGTRAP) || (signal == SIGHUP))
55     {
56         rec.ExceptionCode  = EXCEPTION_BREAKPOINT;
57         rec.ExceptionFlags = EXCEPTION_CONTINUABLE;
58     }
59     else
60     {
61         rec.ExceptionCode  = EXCEPTION_ILLEGAL_INSTRUCTION;  /* generic error */
62         rec.ExceptionFlags = EH_NONCONTINUABLE;
63     }
64     rec.ExceptionRecord  = NULL;
65     rec.ExceptionAddress = (LPVOID)EIP_reg(context);
66     rec.NumberParameters = 0;
67     REGS_FUNC(RtlRaiseException)( &rec, context );
68 }
69
70 static void DOSVM_Dump( LPDOSTASK lpDosTask, int fn, int sig,
71                         struct vm86plus_struct*VM86 )
72 {
73  unsigned iofs;
74  BYTE*inst;
75  int x;
76
77  switch (VM86_TYPE(fn)) {
78   case VM86_SIGNAL:
79    printf("Trapped signal %d\n",sig); break;
80   case VM86_UNKNOWN:
81    printf("Trapped unhandled GPF\n"); break;
82   case VM86_INTx:
83    printf("Trapped INT %02x\n",VM86_ARG(fn)); break;
84   case VM86_STI:
85    printf("Trapped STI\n"); break;
86   case VM86_PICRETURN:
87    printf("Trapped due to pending PIC request\n"); break;
88   case VM86_TRAP:
89    printf("Trapped debug request\n"); break;
90   default:
91    printf("Trapped unknown VM86 type %d arg %d\n",VM86_TYPE(fn),VM86_ARG(fn)); break;
92  }
93 #define REGS VM86->regs
94  fprintf(stderr,"AX=%04lX CX=%04lX DX=%04lX BX=%04lX\n",REGS.eax,REGS.ecx,REGS.edx,REGS.ebx);
95  fprintf(stderr,"SI=%04lX DI=%04lX SP=%04lX BP=%04lX\n",REGS.esi,REGS.edi,REGS.esp,REGS.ebp);
96  fprintf(stderr,"CS=%04X DS=%04X ES=%04X SS=%04X\n",REGS.cs,REGS.ds,REGS.es,REGS.ss);
97  fprintf(stderr,"IP=%04lX EFLAGS=%08lX\n",REGS.eip,REGS.eflags);
98
99  iofs=((DWORD)REGS.cs<<4)+REGS.eip;
100 #undef REGS
101  inst=(BYTE*)lpDosTask->img+iofs;
102  printf("Opcodes:");
103  for (x=0; x<8; x++) printf(" %02x",inst[x]);
104  printf("\n");
105 }
106
107 static int DOSVM_Int( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
108 {
109  extern UINT16 DPMI_wrap_seg;
110
111  if (vect==0x31) {
112   if (CS_reg(context)==DPMI_wrap_seg) {
113    /* exit from real-mode wrapper */
114    return -1;
115   }
116   /* we could probably move some other dodgy stuff here too from dpmi.c */
117  }
118  INT_RealModeInterrupt(vect,context);
119  return 0;
120 }
121
122 static void DOSVM_SimulateInt( int vect, CONTEXT86 *context, LPDOSTASK lpDosTask )
123 {
124   FARPROC16 handler=INT_GetRMHandler(vect);
125
126   if (SELECTOROF(handler)==0xf000) {
127     /* if internal interrupt, call it directly */
128     INT_RealModeInterrupt(vect,context);
129   } else {
130     WORD*stack=(WORD*)(V86BASE(context)+(((DWORD)SS_reg(context))<<4)+LOWORD(ESP_reg(context)));
131     WORD flag=LOWORD(EFL_reg(context));
132
133     if (IF_ENABLED(context)) flag|=IF_MASK;
134     else flag&=~IF_MASK;
135
136     *(--stack)=flag;
137     *(--stack)=CS_reg(context);
138     *(--stack)=LOWORD(EIP_reg(context));
139     ESP_reg(context)-=6;
140     CS_reg(context)=SELECTOROF(handler);
141     EIP_reg(context)=OFFSETOF(handler);
142     IF_CLR(context);
143   }
144 }
145
146 #define SHOULD_PEND(x) \
147   (x && ((!lpDosTask->current) || (x->priority < lpDosTask->current->priority)))
148
149 static void DOSVM_SendQueuedEvent(CONTEXT86 *context, LPDOSTASK lpDosTask)
150 {
151   LPDOSEVENT event = lpDosTask->pending;
152
153   if (SHOULD_PEND(event)) {
154     /* remove from "pending" list */
155     lpDosTask->pending = event->next;
156     /* process event */
157     if (event->irq>=0) {
158       /* it's an IRQ, move it to "current" list */
159       event->next = lpDosTask->current;
160       lpDosTask->current = event;
161       TRACE_(int)("dispatching IRQ %d\n",event->irq);
162       /* note that if DOSVM_SimulateInt calls an internal interrupt directly,
163        * lpDosTask->current might be cleared (and event freed) in this very call! */
164       DOSVM_SimulateInt((event->irq<8)?(event->irq+8):(event->irq-8+0x70),context,lpDosTask);
165     } else {
166       /* callback event */
167       TRACE_(int)("dispatching callback event\n");
168       (*event->relay)(lpDosTask,context,event->data);
169       free(event);
170     }
171   }
172   if (!SHOULD_PEND(lpDosTask->pending)) {
173     TRACE_(int)("clearing Pending flag\n");
174     CLR_PEND(context);
175   }
176 }
177
178 static void DOSVM_SendQueuedEvents(CONTEXT86 *context, LPDOSTASK lpDosTask)
179 {
180   /* we will send all queued events as long as interrupts are enabled,
181    * but IRQ events will disable interrupts again */
182   while (IS_PEND(context) && IF_ENABLED(context))
183     DOSVM_SendQueuedEvent(context,lpDosTask);
184 }
185
186 void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data)
187 {
188   LPDOSTASK lpDosTask = MZ_Current();
189   LPDOSEVENT event, cur, prev;
190
191   if (lpDosTask) {
192     event = malloc(sizeof(DOSEVENT));
193     if (!event) {
194       ERR_(int)("out of memory allocating event entry\n");
195       return;
196     }
197     event->irq = irq; event->priority = priority;
198     event->relay = relay; event->data = data;
199
200     /* insert event into linked list, in order *after*
201      * all earlier events of higher or equal priority */
202     cur = lpDosTask->pending; prev = NULL;
203     while (cur && cur->priority<=priority) {
204       prev = cur;
205       cur = cur->next;
206     }
207     event->next = cur;
208     if (prev) prev->next = event;
209     else lpDosTask->pending = event;
210     
211     /* get dosmod's attention to the new event, except for irq==0 where we already have it */
212     if (irq && !lpDosTask->sig_sent) {
213       TRACE_(int)("new event queued, signalling dosmod\n");
214       kill(lpDosTask->task,SIGUSR2);
215       lpDosTask->sig_sent++;
216     } else {
217       TRACE_(int)("new event queued\n");
218     }
219   }
220 }
221
222 #define CV CP(eax,EAX); CP(ecx,ECX); CP(edx,EDX); CP(ebx,EBX); \
223            CP(esi,ESI); CP(edi,EDI); CP(esp,ESP); CP(ebp,EBP); \
224            CP(cs,CS); CP(ds,DS); CP(es,ES); \
225            CP(ss,SS); CP(fs,FS); CP(gs,GS); \
226            CP(eip,EIP); CP(eflags,EFL)
227
228 static int DOSVM_Process( LPDOSTASK lpDosTask, int fn, int sig,
229                           struct vm86plus_struct*VM86 )
230 {
231  CONTEXT86 context;
232  int ret=0;
233
234 #define CP(x,y) y##_reg(&context) = VM86->regs.x
235   CV;
236 #undef CP
237  if (VM86_TYPE(fn)==VM86_UNKNOWN) {
238   ret=INSTR_EmulateInstruction(&context);
239 #define CP(x,y) VM86->regs.x = y##_reg(&context)
240   CV;
241 #undef CP
242   if (ret) return 0;
243   ret=0;
244  }
245  (void*)V86BASE(&context)=lpDosTask->img;
246 #ifdef TRY_PICRETURN
247  if (VM86->vm86plus.force_return_for_pic) {
248    SET_PEND(&context);
249  }
250 #else
251  /* linux doesn't preserve pending flag on return */
252  if (SHOULD_PEND(lpDosTask->pending)) {
253    SET_PEND(&context);
254  }
255 #endif
256
257  switch (VM86_TYPE(fn)) {
258   case VM86_SIGNAL:
259    TRACE_(int)("DOS module caught signal %d\n",sig);
260    if ((sig==SIGALRM) || (sig==SIGUSR2)) {
261      if (sig==SIGALRM) {
262        DOSVM_QueueEvent(0,DOS_PRIORITY_REALTIME,NULL,NULL);
263      }
264      if (lpDosTask->pending) {
265        TRACE_(int)("setting Pending flag, interrupts are currently %s\n",
266                  IF_ENABLED(&context) ? "enabled" : "disabled");
267        SET_PEND(&context);
268        DOSVM_SendQueuedEvents(&context,lpDosTask);
269      } else {
270        TRACE_(int)("no events are pending, clearing Pending flag\n");
271        CLR_PEND(&context);
272      }
273      if (sig==SIGUSR2) lpDosTask->sig_sent--;
274    }
275    else if ((sig==SIGHUP) || (sig==SIGILL) || (sig==SIGSEGV)) {
276        do_exception( sig, &context );
277    } else {
278     DOSVM_Dump(lpDosTask,fn,sig,VM86);
279     ret=-1;
280    }
281    break;
282   case VM86_UNKNOWN: /* unhandled GPF */
283    DOSVM_Dump(lpDosTask,fn,sig,VM86);
284    do_exception( SIGSEGV, &context );
285    break;
286   case VM86_INTx:
287    if (TRACE_ON(relay))
288     DPRINTF("Call DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
289    ret=DOSVM_Int(VM86_ARG(fn),&context,lpDosTask);
290    if (TRACE_ON(relay))
291     DPRINTF("Ret  DOS int 0x%02x (EAX=%08lx) ret=%04lx:%04lx\n",VM86_ARG(fn),context.Eax,context.SegCs,context.Eip);
292    break;
293   case VM86_STI:
294   case VM86_PICRETURN:
295     TRACE_(int)("DOS task enabled interrupts with events pending, sending events\n");
296     DOSVM_SendQueuedEvents(&context,lpDosTask);
297     break;
298   case VM86_TRAP:
299    do_exception( SIGTRAP, &context );
300    break;
301   default:
302    DOSVM_Dump(lpDosTask,fn,sig,VM86);
303    ret=-1;
304  }
305
306 #define CP(x,y) VM86->regs.x = y##_reg(&context)
307  CV;
308 #undef CP
309 #ifdef TRY_PICRETURN
310  VM86->vm86plus.force_return_for_pic = IS_PEND(&context) ? 1 : 0;
311  CLR_PEND(&context);
312 #endif
313  return ret;
314 }
315
316 void DOSVM_ProcessMessage(LPDOSTASK lpDosTask,MSG *msg)
317 {
318   BYTE scan = 0;
319
320   fprintf(stderr,"got message %04x, wparam=%08x, lparam=%08lx\n",msg->message,msg->wParam,msg->lParam);
321   if ((msg->message>=WM_MOUSEFIRST)&&
322       (msg->message<=WM_MOUSELAST)) {
323     INT_Int33Message(msg->message,msg->wParam,msg->lParam);
324   } else {
325     switch (msg->message) {
326     case WM_KEYUP:
327       scan = 0x80;
328     case WM_KEYDOWN:
329       scan |= (msg->lParam >> 16) & 0x7f;
330
331       /* check whether extended bit is set,
332        * and if so, queue the extension prefix */
333       if (msg->lParam & 0x1000000) {
334         /* FIXME: some keys (function keys) have
335          * extended bit set even when they shouldn't,
336          * should check for them */
337         INT_Int09SendScan(0xE0);
338       }
339       INT_Int09SendScan(scan);
340       break;
341     }
342   }
343 }
344
345 void DOSVM_Wait( int read_pipe, HANDLE hObject )
346 {
347   LPDOSTASK lpDosTask = MZ_Current();
348   MSG msg;
349   DWORD waitret;
350   BOOL got_msg = FALSE;
351
352   do {
353     /* check for messages (waste time before the response check below) */
354     while (PeekMessageA(&msg,0,0,0,PM_REMOVE|PM_NOYIELD)) {
355       /* got a message */
356       DOSVM_ProcessMessage(lpDosTask,&msg);
357       /* we don't need a TranslateMessage here */
358       DispatchMessageA(&msg);
359       got_msg = TRUE;
360     }
361     if (read_pipe == -1) {
362       if (got_msg) break;
363     } else {
364       fd_set readfds;
365       struct timeval timeout={0,0};
366       /* quick check for response from dosmod
367        * (faster than doing the full blocking wait, if data already available) */
368       FD_ZERO(&readfds); FD_SET(read_pipe,&readfds);
369       if (select(read_pipe+1,&readfds,NULL,NULL,&timeout)>0)
370         break;
371     }
372     /* check for data from win32 console device */
373
374     /* nothing yet, block while waiting for something to do */
375     waitret=MsgWaitForMultipleObjects(1,&hObject,FALSE,INFINITE,QS_ALLINPUT);
376     if (waitret==(DWORD)-1) {
377       ERR_(module)("dosvm wait error=%ld\n",GetLastError());
378     }
379     if (read_pipe != -1) {
380       if (waitret==WAIT_OBJECT_0) break;
381     }
382   } while (TRUE);
383 }
384
385 int DOSVM_Enter( CONTEXT86 *context )
386 {
387  LPDOSTASK lpDosTask = MZ_Current();
388  struct vm86plus_struct VM86;
389  int stat,len,sig;
390
391  if (!lpDosTask) {
392   /* MZ_CreateProcess or MZ_AllocDPMITask should have been called first */
393   ERR_(module)("dosmod has not been initialized!");
394   return -1;
395  }
396
397  if (context) {
398 #define CP(x,y) VM86.regs.x = y##_reg(context)
399   CV;
400 #undef CP
401   if (VM86.regs.eflags & IF_MASK)
402     VM86.regs.eflags |= VIF_MASK;
403  } else {
404 /* initial setup */
405   /* allocate standard DOS handles */
406   FILE_InitProcessDosHandles(); 
407   /* registers */
408   memset(&VM86,0,sizeof(VM86));
409   VM86.regs.cs=lpDosTask->init_cs;
410   VM86.regs.eip=lpDosTask->init_ip;
411   VM86.regs.ss=lpDosTask->init_ss;
412   VM86.regs.esp=lpDosTask->init_sp;
413   VM86.regs.ds=lpDosTask->psp_seg;
414   VM86.regs.es=lpDosTask->psp_seg;
415   VM86.regs.eflags=VIF_MASK;
416   /* hmm, what else do we need? */
417  }
418
419  /* main exchange loop */
420  do {
421   stat = VM86_ENTER;
422   errno = 0;
423   /* transmit VM86 structure to dosmod task */
424   if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
425    ERR_(module)("dosmod sync lost, errno=%d, fd=%d, pid=%d\n",errno,lpDosTask->write_pipe,getpid());
426    return -1;
427   }
428   if (write(lpDosTask->write_pipe,&VM86,sizeof(VM86))!=sizeof(VM86)) {
429    ERR_(module)("dosmod sync lost, errno=%d\n",errno);
430    return -1;
431   }
432   /* wait for response, doing other things in the meantime */
433   DOSVM_Wait(lpDosTask->read_pipe, lpDosTask->hReadPipe);
434   /* read response */
435   while (1) {
436     if ((len=read(lpDosTask->read_pipe,&stat,sizeof(stat)))==sizeof(stat)) break;
437     if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
438      WARN_(module)("rereading dosmod return code due to errno=%d, result=%d\n",errno,len);
439      continue;
440     }
441     ERR_(module)("dosmod sync lost reading return code, errno=%d, result=%d\n",errno,len);
442     return -1;
443   }
444   TRACE_(module)("dosmod return code=%d\n",stat);
445   while (1) {
446     if ((len=read(lpDosTask->read_pipe,&VM86,sizeof(VM86)))==sizeof(VM86)) break;
447     if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
448      WARN_(module)("rereading dosmod VM86 structure due to errno=%d, result=%d\n",errno,len);
449      continue;
450     }
451     ERR_(module)("dosmod sync lost reading VM86 structure, errno=%d, result=%d\n",errno,len);
452     return -1;
453   }
454   if ((stat&0xff)==DOSMOD_SIGNAL) {
455     while (1) {
456       if ((len=read(lpDosTask->read_pipe,&sig,sizeof(sig)))==sizeof(sig)) break;
457       if (((errno==EINTR)||(errno==EAGAIN))&&(len<=0)) {
458         WARN_(module)("rereading dosmod signal due to errno=%d, result=%d\n",errno,len);
459         continue;
460       }
461       ERR_(module)("dosmod sync lost reading signal, errno=%d, result=%d\n",errno,len);
462       return -1;
463     } while (0);
464   } else sig=0;
465   /* got response */
466  } while (DOSVM_Process(lpDosTask,stat,sig,&VM86)>=0);
467
468  if (context) {
469 #define CP(x,y) y##_reg(context) = VM86.regs.x
470   CV;
471 #undef CP
472  }
473  return 0;
474 }
475
476 void DOSVM_PIC_ioport_out( WORD port, BYTE val)
477 {
478   LPDOSTASK lpDosTask = MZ_Current();
479   LPDOSEVENT event;
480
481   if (lpDosTask) {
482     if ((port==0x20) && (val==0x20)) {
483       if (lpDosTask->current) {
484         /* EOI (End Of Interrupt) */
485         TRACE_(int)("received EOI for current IRQ, clearing\n");
486         event = lpDosTask->current;
487         lpDosTask->current = event->next;
488         if (event->relay)
489         (*event->relay)(lpDosTask,NULL,event->data);
490         free(event);
491
492         if (lpDosTask->pending &&
493             !lpDosTask->sig_sent) {
494           /* another event is pending, which we should probably
495            * be able to process now, so tell dosmod about it */
496           TRACE_(int)("another event pending, signalling dosmod\n");
497           kill(lpDosTask->task,SIGUSR2);
498           lpDosTask->sig_sent++;
499         }
500       } else {
501         WARN_(int)("EOI without active IRQ\n");
502       }
503     } else {
504       FIXME_(int)("unrecognized PIC command %02x\n",val);
505     }
506   }
507 }
508
509 void DOSVM_SetTimer( unsigned ticks )
510 {
511   LPDOSTASK lpDosTask = MZ_Current();
512   int stat=DOSMOD_SET_TIMER;
513   struct timeval tim;
514
515   if (lpDosTask) {
516     /* the PC clocks ticks at 1193180 Hz */
517     tim.tv_sec=0;
518     tim.tv_usec=((unsigned long long)ticks*1000000)/1193180;
519     /* sanity check */
520     if (!tim.tv_usec) tim.tv_usec=1;
521
522     if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
523       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
524       return;
525     }
526     if (write(lpDosTask->write_pipe,&tim,sizeof(tim))!=sizeof(tim)) {
527       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
528       return;
529     }
530     /* there's no return */
531   }
532 }
533
534 unsigned DOSVM_GetTimer( void )
535 {
536   LPDOSTASK lpDosTask = MZ_Current();
537   int stat=DOSMOD_GET_TIMER;
538   struct timeval tim;
539
540   if (lpDosTask) {
541     if (write(lpDosTask->write_pipe,&stat,sizeof(stat))!=sizeof(stat)) {
542       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
543       return 0;
544     }
545     /* read response */
546     while (1) {
547       if (read(lpDosTask->read_pipe,&tim,sizeof(tim))==sizeof(tim)) break;
548       if ((errno==EINTR)||(errno==EAGAIN)) continue;
549       ERR_(module)("dosmod sync lost, errno=%d\n",errno);
550       return 0;
551     }
552     return ((unsigned long long)tim.tv_usec*1193180)/1000000;
553   }
554   return 0;
555 }
556
557 void DOSVM_SetSystemData( int id, void *data )
558 {
559   LPDOSTASK lpDosTask = MZ_Current();
560   DOSSYSTEM *sys, *prev;
561
562   if (lpDosTask) {
563     sys = lpDosTask->sys;
564     prev = NULL;
565     while (sys && (sys->id != id)) {
566       prev = sys;
567       sys = sys->next;
568     }
569     if (sys) {
570       free(sys->data);
571       sys->data = data;
572     } else {
573       sys = malloc(sizeof(DOSSYSTEM));
574       sys->id = id;
575       sys->data = data;
576       sys->next = NULL;
577       if (prev) prev->next = sys;
578       else lpDosTask->sys = sys;
579     }
580   } else free(data);
581 }
582
583 void* DOSVM_GetSystemData( int id )
584 {
585   LPDOSTASK lpDosTask = MZ_Current();
586   DOSSYSTEM *sys;
587
588   if (lpDosTask) {
589     sys = lpDosTask->sys;
590     while (sys && (sys->id != id))
591       sys = sys->next;
592     if (sys)
593       return sys->data;
594   }
595   return NULL;
596 }
597
598 #else /* !MZ_SUPPORTED */
599
600 int DOSVM_Enter( CONTEXT86 *context )
601 {
602  ERR_(module)("DOS realmode not supported on this architecture!\n");
603  return -1;
604 }
605
606 void DOSVM_Wait( int read_pipe, HANDLE hObject) {}
607 void DOSVM_PIC_ioport_out( WORD port, BYTE val) {}
608 void DOSVM_SetTimer( unsigned ticks ) {}
609 unsigned DOSVM_GetTimer( void ) { return 0; }
610 void DOSVM_SetSystemData( int id, void *data ) { free(data); }
611 void* DOSVM_GetSystemData( int id ) { return NULL; }
612 void DOSVM_QueueEvent( int irq, int priority, void (*relay)(LPDOSTASK,CONTEXT86*,void*), void *data) {}
613
614 #endif