Enhancement: resolution multiplier for TclpGetTime on Win95/98/NT/2K 
Author Message
 Enhancement: resolution multiplier for TclpGetTime on Win95/98/NT/2K

Tcl 8.3.2 Feature:  Generated by Ajuba's bug entry form at
        http://www.*-*-*.com/
Responses to this post are encouraged.
------

Submitted by:  Kevin B Kenny

OperatingSystem:  Windows NT
OperatingSystemVersion:  Tested on Win95, 98, NT 4,0
Machine:  Windows Pentium II/III, various
Extensions:  None relevant
CustomShell:  Just the attached patch.
Synopsis:  Enhancement: resolution multiplier for TclpGetTime on Win95/98/NT/2K

ReproducibleScript:
time { for { set i 0 } { $i < 100 } { incr i } {} }

ObservedBehavior:
0 microseconds per iteration

DesiredBehavior:
Finer granularity: the patch submitted gives the output:
403 microseconds per iteration

Patch:
*** /tcl8.3.2/src/tcl8.3.2/win/tclWinNotify.c Fri Jul  2 18:08:30 1999
--- ./tclWinNotify.c Thu Aug 24 23:29:12 2000
***************
*** 510,514 ****
  Tcl_Sleep(ms)
      int ms;                   /* Number of milliseconds to sleep. */
  {
!     Sleep(ms);
  }
--- 510,548 ----
  Tcl_Sleep(ms)
      int ms;                   /* Number of milliseconds to sleep. */
  {
!     /*
!      * Simply calling 'Sleep' for the requisite number of milliseconds
!      * can make the process appear to wake up early because it isn't
!      * synchronized with the CPU performance counter that is used in
!      * tclWinTime.c.  This behavior is probably benign, but messes
!      * up some of the corner cases in the test suite.  We get around
!      * this problem by repeating the 'Sleep' call as many times
!      * as necessary to make the clock advance by the requisite amount.
!      */
!
!     Tcl_Time now;             /* Current wall clock time */
!     Tcl_Time desired;         /* Desired wakeup time */
!     int sleepTime = ms;               /* Time to sleep */
!
!     TclpGetTime( &now );
!     desired.sec = now.sec + ( ms / 1000 );
!     desired.usec = now.usec + 1000 * ( ms % 1000 );
!     if ( desired.usec > 1000000 ) {
!       ++desired.sec;
!       desired.usec -= 1000000;
!     }
!      
!     for ( ; ; ) {
!       Sleep( sleepTime );
!       TclpGetTime( &now );
!       if ( now.sec > desired.sec ) {
!           break;
!       } else if ( ( now.sec == desired.sec )
!            && ( now.usec >= desired.usec ) ) {
!           break;
!       }
!       sleepTime = ( ( 1000 * ( desired.sec - now.sec ) )
!                     + ( ( desired.usec - now.usec ) / 1000 ) );
!     }
!
  }
*** /tcl8.3.2/src/tcl8.3.2/win/tclWinTest.c Thu Oct 28 23:05:14 1999
--- ./tclWinTest.c Mon Sep  4 22:45:56 2000
***************
*** 22,27 ****
--- 22,31 ----
  static int    TestvolumetypeCmd _ANSI_ARGS_((ClientData dummy,
        Tcl_Interp *interp, int objc,
        Tcl_Obj *CONST objv[]));
+ static int      TestwinclockCmd _ANSI_ARGS_(( ClientData dummy,
+                                             Tcl_Interp* interp,
+                                             int objc,
+                                             Tcl_Obj *CONST objv[] ));

  /*
   *----------------------------------------------------------------------
***************
*** 52,57 ****
--- 56,63 ----
              (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
      Tcl_CreateObjCommand(interp, "testvolumetype", TestvolumetypeCmd,
              (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
+     Tcl_CreateObjCommand(interp, "testwinclock", TestwinclockCmd,
+             (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
      return TCL_OK;
  }

***************
*** 187,190 ****
--- 193,267 ----
      Tcl_SetResult(interp, volType, TCL_VOLATILE);
      return TCL_OK;
  #undef VOL_BUF_SIZE
+ }
+
+ /*
+  *----------------------------------------------------------------------
+  *
+  * TestclockCmd --
+  *
+  *    Command that returns the seconds and microseconds portions of
+  *    the system clock and of the Tcl clock so that they can be
+  *    compared to validate that the Tcl clock is staying in sync.
+  *
+  * Usage:
+  *    testclock
+  *
+  * Parameters:
+  *    None.
+  *
+  * Results:
+  *    Returns a standard Tcl result comprising a four-element list:
+  *    the seconds and microseconds portions of the system clock,
+  *    and the seconds and microseconds portions of the Tcl clock.
+  *
+  * Side effects:
+  *    None.
+  *
+  *----------------------------------------------------------------------
+  */
+
+ static int
+ TestwinclockCmd( ClientData dummy,
+                               /* Unused */
+                Tcl_Interp* interp,
+                               /* Tcl interpreter */
+                int objc,
+                               /* Argument count */
+                Tcl_Obj *CONST objv[] )
+                               /* Argument vector */
+ {
+     CONST static FILETIME posixEpoch = { 0xD53E8000, 0x019DB1DE };
+                               /* The Posix epoch, expressed as a
+                                * Windows FILETIME */
+     Tcl_Time tclTime;         /* Tcl clock */
+     FILETIME sysTime;         /* System clock */
+     Tcl_Obj* result;          /* Result of the command */
+     LARGE_INTEGER t1, t2;
+
+     if ( objc != 1 ) {
+       Tcl_WrongNumArgs( interp, 1, objv, "" );
+       return TCL_ERROR;
+     }
+
+     TclpGetTime( &tclTime );
+     GetSystemTimeAsFileTime( &sysTime );
+     t1.LowPart = posixEpoch.dwLowDateTime;
+     t1.HighPart = posixEpoch.dwHighDateTime;
+     t2.LowPart = sysTime.dwLowDateTime;
+     t2.HighPart = sysTime.dwHighDateTime;
+     t2.QuadPart -= t1.QuadPart;
+
+     result = Tcl_NewObj();
+     Tcl_ListObjAppendElement
+       ( interp, result, Tcl_NewIntObj( (int) (t2.QuadPart / 10000000 ) ) );
+     Tcl_ListObjAppendElement
+       ( interp, result,
+         Tcl_NewIntObj( (int) ( (t2.QuadPart / 10 ) % 1000000 ) ) );
+     Tcl_ListObjAppendElement( interp, result, Tcl_NewIntObj( tclTime.sec ) );
+     Tcl_ListObjAppendElement( interp, result, Tcl_NewIntObj( tclTime.usec ) );
+
+     Tcl_SetObjResult( interp, result );
+
+     return TCL_OK;
  }
*** /tcl8.3.2/src/tcl8.3.2/win/tclWinTime.c Tue Nov 30 19:08:44 1999
--- ./tclWinTime.c Wed Sep  6 14:36:28 2000
***************
*** 38,47 ****
--- 38,114 ----
  static Tcl_ThreadDataKey dataKey;

  /*
+  * Calibration interval for the high-resolution timer, in msec
+  */
+
+ static CONST unsigned long clockCalibrateWakeupInterval = 10000;
+                               /* FIXME: 10 s -- should be about 10 min! */
+
+ /*
+  * Data for managing high-resolution timers.
+  */
+
+ typedef struct TimeInfo {
+
+     CRITICAL_SECTION cs;      /* Mutex guarding this structure */
+
+     int initialized;          /* Flag == 1 if this structure is
+                                * initialized. */
+
+     int perfCounterAvailable; /* Flag == 1 if the hardware has a
+                                * performance counter */
+
+     HANDLE calibrationThread; /* Handle to the thread that keeps the
+                                * virtual clock calibrated. */
+
+     HANDLE readyEvent;                /* System event used to
+                                * trigger the requesting thread
+                                * when the clock calibration procedure
+                                * is initialized for the first time */
+
+     /*
+      * The following values are used for calculating virtual time.
+      * Virtual time is always equal to:
+      *    lastFileTime + (current perf counter - lastCounter)
+      *                                * 10000000 / curCounterFreq
+      * and lastFileTime and lastCounter are updated any time that
+      * virtual time is returned to a caller.
+      */
+
+     ULARGE_INTEGER lastFileTime;
+     LARGE_INTEGER lastCounter;
+     LARGE_INTEGER curCounterFreq;
+
+     /*
+      * The next two values are used only in the calibration thread, to track
+      * the frequency of the performance counter.
+      */
+
+     LONGLONG lastPerfCounter; /* Performance counter the last time
+                                * that UpdateClockEachSecond was called */
+     LONGLONG lastSysTime;     /* System clock at the last time
+                                * that UpdateClockEachSecond was called */
+     LONGLONG estPerfCounterFreq;
+                               /* Current estimate of the counter frequency
+                                * using the system clock as the standard */
+
+ } TimeInfo;
+
+ static TimeInfo timeInfo = {
+     NULL, 0, 0, NULL, NULL, 0, 0, 0, 0, 0
+ };
+
+ CONST static FILETIME posixEpoch = { 0xD53E8000, 0x019DB1DE };
+    
+ /*
   * Declarations for functions defined later in this file.
   */

  static struct tm *    ComputeGMT _ANSI_ARGS_((const time_t *tp));
+
+ static DWORD WINAPI     CalibrationThread _ANSI_ARGS_(( LPVOID arg ));
+
+ static void           UpdateTimeEachSecond _ANSI_ARGS_(( void ));

  /*
   *----------------------------------------------------------------------
***************
*** 63,69 ****
  unsigned long
  TclpGetSeconds()
  {
!     return (unsigned long) time((time_t *) NULL);
  }

  /*
--- 130,138 ----
  unsigned long
  TclpGetSeconds()
  {
!     Tcl_Time t;
!     TclpGetTime( &t );
!     return t.sec;
  }

  /*
***************
*** 89,95 ****
  unsigned long
  TclpGetClicks()
  {
!     return GetTickCount();
  }

  /*
--- 158,176 ----
  unsigned long
  TclpGetClicks()
  {
!     /*
!      * If the hardware supports a performance counter, read it and
!      * return the least significant 32 bits.  Otherwise, return the
!      * system tick count as something that is at least available
!      * everywhere.
!      */
!
!     LARGE_INTEGER clock;
!     if ( QueryPerformanceCounter( &clock ) ) {
!       return clock.LowPart;
!     } else {
!       return GetTickCount();
!     }
  }

  /*
***************
*** 134,140 ****
   *    Returns the current time in timePtr.
   *
   * Side effects:
!  *    None.
   *
   *----------------------------------------------------------------------
   */
--- 215,227 ----
   *    Returns the current time in timePtr.
   *
   * Side effects:
!  *    On the first call, initializes a set of static variables to
!  *    keep track of the base value of the performance counter, the
!  *    corresponding wall clock (obtained through ftime) and the
!  *    frequency of the performance counter.  Also spins a thread
!  *    whose function is to wake up periodically and monitor these
!  *    values, adjusting them as necessary to correct for drift
!  *    in the performance counter's oscillator.
   *
...

read more »



Mon, 24 Feb 2003 03:53:24 GMT  
 Enhancement: resolution multiplier for TclpGetTime on Win95/98/NT/2K
Hello,

I tried to apply the patch to the Tcl8.3.2 source tree, but with no real
success since many errors appeared.
I used patch -p1 <patchfile, I also tried with -p0, but none works.

Perhaps I miss something in the command line, or perhaps I extracted the
patch from the message the wrong way.
Could it be possible to give the patch as an attached file and give some
instructions how to apply it ?

Many thanks.

F. LEUBA
Software Engineer
TGS France




Quote:

> Tcl 8.3.2 Feature:  Generated by Ajuba's bug entry form at
> http://www.ajubasolutions.com/support/bugForm.html
> Responses to this post are encouraged.
> ------

> Submitted by:  Kevin B Kenny

> OperatingSystem:  Windows NT
> OperatingSystemVersion:  Tested on Win95, 98, NT 4,0
> Machine:  Windows Pentium II/III, various
> Extensions:  None relevant
> CustomShell:  Just the attached patch.
> Synopsis:  Enhancement: resolution multiplier for TclpGetTime on
Win95/98/NT/2K

> ReproducibleScript:
> time { for { set i 0 } { $i < 100 } { incr i } {} }

> ObservedBehavior:
> 0 microseconds per iteration

> DesiredBehavior:
> Finer granularity: the patch submitted gives the output:
> 403 microseconds per iteration

> Patch:
> *** /tcl8.3.2/src/tcl8.3.2/win/tclWinNotify.c Fri Jul  2 18:08:30 1999
> --- ./tclWinNotify.c Thu Aug 24 23:29:12 2000
> ***************
> *** 510,514 ****
>   Tcl_Sleep(ms)
>       int ms; /* Number of milliseconds to sleep. */
>   {
> !     Sleep(ms);
>   }
> --- 510,548 ----
>   Tcl_Sleep(ms)
>       int ms; /* Number of milliseconds to sleep. */
>   {
> !     /*
> !      * Simply calling 'Sleep' for the requisite number of milliseconds
> !      * can make the process appear to wake up early because it isn't
> !      * synchronized with the CPU performance counter that is used in
> !      * tclWinTime.c.  This behavior is probably benign, but messes
> !      * up some of the corner cases in the test suite.  We get around
> !      * this problem by repeating the 'Sleep' call as many times
> !      * as necessary to make the clock advance by the requisite amount.
> !      */
> !
> !     Tcl_Time now; /* Current wall clock time */
> !     Tcl_Time desired; /* Desired wakeup time */
> !     int sleepTime = ms; /* Time to sleep */
> !
> !     TclpGetTime( &now );
> !     desired.sec = now.sec + ( ms / 1000 );
> !     desired.usec = now.usec + 1000 * ( ms % 1000 );
> !     if ( desired.usec > 1000000 ) {
> ! ++desired.sec;
> ! desired.usec -= 1000000;
> !     }
> !
> !     for ( ; ; ) {
> ! Sleep( sleepTime );
> ! TclpGetTime( &now );
> ! if ( now.sec > desired.sec ) {
> !     break;
> ! } else if ( ( now.sec == desired.sec )
> !      && ( now.usec >= desired.usec ) ) {
> !     break;
> ! }
> ! sleepTime = ( ( 1000 * ( desired.sec - now.sec ) )
> !       + ( ( desired.usec - now.usec ) / 1000 ) );
> !     }
> !
>   }
> *** /tcl8.3.2/src/tcl8.3.2/win/tclWinTest.c Thu Oct 28 23:05:14 1999
> --- ./tclWinTest.c Mon Sep  4 22:45:56 2000
> ***************
> *** 22,27 ****
> --- 22,31 ----
>   static int TestvolumetypeCmd _ANSI_ARGS_((ClientData dummy,
>   Tcl_Interp *interp, int objc,
>   Tcl_Obj *CONST objv[]));
> + static int      TestwinclockCmd _ANSI_ARGS_(( ClientData dummy,
> +       Tcl_Interp* interp,
> +       int objc,
> +       Tcl_Obj *CONST objv[] ));

>   /*
>    *----------------------------------------------------------------------
> ***************
> *** 52,57 ****
> --- 56,63 ----
>               (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
>       Tcl_CreateObjCommand(interp, "testvolumetype", TestvolumetypeCmd,
>               (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
> +     Tcl_CreateObjCommand(interp, "testwinclock", TestwinclockCmd,
> +             (ClientData) 0, (Tcl_CmdDeleteProc *) NULL);
>       return TCL_OK;
>   }

> ***************
> *** 187,190 ****
> --- 193,267 ----
>       Tcl_SetResult(interp, volType, TCL_VOLATILE);
>       return TCL_OK;
>   #undef VOL_BUF_SIZE
> + }
> +
> + /*
> +  *----------------------------------------------------------------------
> +  *
> +  * TestclockCmd --
> +  *
> +  * Command that returns the seconds and microseconds portions of
> +  * the system clock and of the Tcl clock so that they can be
> +  * compared to validate that the Tcl clock is staying in sync.
> +  *
> +  * Usage:
> +  * testclock
> +  *
> +  * Parameters:
> +  * None.
> +  *
> +  * Results:
> +  * Returns a standard Tcl result comprising a four-element list:
> +  * the seconds and microseconds portions of the system clock,
> +  * and the seconds and microseconds portions of the Tcl clock.
> +  *
> +  * Side effects:
> +  * None.
> +  *
> +  *----------------------------------------------------------------------
> +  */
> +
> + static int
> + TestwinclockCmd( ClientData dummy,
> + /* Unused */
> + Tcl_Interp* interp,
> + /* Tcl interpreter */
> + int objc,
> + /* Argument count */
> + Tcl_Obj *CONST objv[] )
> + /* Argument vector */
> + {
> +     CONST static FILETIME posixEpoch = { 0xD53E8000, 0x019DB1DE };
> + /* The Posix epoch, expressed as a
> + * Windows FILETIME */
> +     Tcl_Time tclTime; /* Tcl clock */
> +     FILETIME sysTime; /* System clock */
> +     Tcl_Obj* result; /* Result of the command */
> +     LARGE_INTEGER t1, t2;
> +
> +     if ( objc != 1 ) {
> + Tcl_WrongNumArgs( interp, 1, objv, "" );
> + return TCL_ERROR;
> +     }
> +
> +     TclpGetTime( &tclTime );
> +     GetSystemTimeAsFileTime( &sysTime );
> +     t1.LowPart = posixEpoch.dwLowDateTime;
> +     t1.HighPart = posixEpoch.dwHighDateTime;
> +     t2.LowPart = sysTime.dwLowDateTime;
> +     t2.HighPart = sysTime.dwHighDateTime;
> +     t2.QuadPart -= t1.QuadPart;
> +
> +     result = Tcl_NewObj();
> +     Tcl_ListObjAppendElement
> + ( interp, result, Tcl_NewIntObj( (int) (t2.QuadPart / 10000000 ) ) );
> +     Tcl_ListObjAppendElement
> + ( interp, result,
> +   Tcl_NewIntObj( (int) ( (t2.QuadPart / 10 ) % 1000000 ) ) );
> +     Tcl_ListObjAppendElement( interp, result, Tcl_NewIntObj(
tclTime.sec ) );
> +     Tcl_ListObjAppendElement( interp, result, Tcl_NewIntObj(
tclTime.usec ) );
> +
> +     Tcl_SetObjResult( interp, result );
> +
> +     return TCL_OK;
>   }
> *** /tcl8.3.2/src/tcl8.3.2/win/tclWinTime.c Tue Nov 30 19:08:44 1999
> --- ./tclWinTime.c Wed Sep  6 14:36:28 2000
> ***************
> *** 38,47 ****
> --- 38,114 ----
>   static Tcl_ThreadDataKey dataKey;

>   /*
> +  * Calibration interval for the high-resolution timer, in msec
> +  */
> +
> + static CONST unsigned long clockCalibrateWakeupInterval = 10000;
> + /* FIXME: 10 s -- should be about 10 min! */
> +
> + /*
> +  * Data for managing high-resolution timers.
> +  */
> +
> + typedef struct TimeInfo {
> +
> +     CRITICAL_SECTION cs; /* Mutex guarding this structure */
> +
> +     int initialized; /* Flag == 1 if this structure is
> + * initialized. */
> +
> +     int perfCounterAvailable; /* Flag == 1 if the hardware has a
> + * performance counter */
> +
> +     HANDLE calibrationThread; /* Handle to the thread that keeps the
> + * virtual clock calibrated. */
> +
> +     HANDLE readyEvent; /* System event used to
> + * trigger the requesting thread
> + * when the clock calibration procedure
> + * is initialized for the first time */
> +
> +     /*
> +      * The following values are used for calculating virtual time.
> +      * Virtual time is always equal to:
> +      *    lastFileTime + (current perf counter - lastCounter)
> +      * * 10000000 / curCounterFreq
> +      * and lastFileTime and lastCounter are updated any time that
> +      * virtual time is returned to a caller.
> +      */
> +
> +     ULARGE_INTEGER lastFileTime;
> +     LARGE_INTEGER lastCounter;
> +     LARGE_INTEGER curCounterFreq;
> +
> +     /*
> +      * The next two values are used only in the calibration thread, to
track
> +      * the frequency of the performance counter.
> +      */
> +
> +     LONGLONG lastPerfCounter; /* Performance counter the last time
> + * that UpdateClockEachSecond was called */
> +     LONGLONG lastSysTime; /* System clock at the last time
> + * that UpdateClockEachSecond was called */
> +     LONGLONG estPerfCounterFreq;
> + /* Current estimate of the counter frequency
> + * using the system clock as the standard */
> +
> + } TimeInfo;
> +
> + static TimeInfo timeInfo = {
> +     NULL, 0, 0, NULL, NULL, 0, 0, 0, 0, 0
> + };
> +
> + CONST static FILETIME posixEpoch = { 0xD53E8000, 0x019DB1DE };
> +
> + /*
>    * Declarations for functions defined later in this file.
>    */

>   static struct tm * ComputeGMT _ANSI_ARGS_((const time_t *tp));
> +
> + static DWORD WINAPI     CalibrationThread _ANSI_ARGS_(( LPVOID arg ));
> +
> + static void UpdateTimeEachSecond _ANSI_ARGS_(( void ));

>   /*
>    *----------------------------------------------------------------------
> ***************
> *** 63,69 ****
>   unsigned long
>   TclpGetSeconds()
>   {
> !     return (unsigned long) time((time_t *) NULL);
>   }

>   /*
> --- 130,138 ----
>   unsigned long
>   TclpGetSeconds()
>   {
> !     Tcl_Time t;
> !     TclpGetTime( &t );
> !     return t.sec;
>   }

>   /*
> ***************
> *** 89,95 ****
>   unsigned long
>   TclpGetClicks()
>   {
> !     return GetTickCount();
>   }

>   /*
> --- 158,176 ----
>   unsigned long
>   TclpGetClicks()
>   {
> !     /*
> !      * If the hardware supports a performance counter, read it and
> !      * return the least significant 32 bits.  Otherwise, return the
> !      * system tick count as something that is at least available
> !      * everywhere.
> !      */
> !
> !     LARGE_INTEGER clock;
> !     if ( QueryPerformanceCounter( &clock ) ) {
> ! return clock.LowPart;
> !     } else {
> ! return GetTickCount();
> !     }
>   }

>   /*
> ***************
> *** 134,140 ****
>    * Returns the current time in timePtr.
>    *
>    * Side effects:

...

read more »



Tue, 25 Feb 2003 02:53:31 GMT  
 Enhancement: resolution multiplier for TclpGetTime on Win95/98/NT/2K

Quote:

>Hello,

>I tried to apply the patch to the Tcl8.3.2 source tree, but with no real
>success since many errors appeared.
>I used patch -p1 <patchfile, I also tried with -p0, but none works.

>Perhaps I miss something in the command line, or perhaps I extracted the
>patch from the message the wrong way.
>Could it be possible to give the patch as an attached file and give some
>instructions how to apply it ?

>Many thanks.

>F. LEUBA
>Software Engineer
>TGS France

try: 'patch.exe -c --dry-run -p 3 -i <patchfile>' from the top of the Tcl source
tree (where ChangeLog is).  Try variations of the strip number.  Remove
--dry-run when there aren't any errors left.

If that fails, you might have to edit the filename headers from:

*** /tcl8.3.2/src/tcl8.3.2/win/tclWinNotify.c Fri Jul  2 18:08:30 1999
--- ./tclWinNotify.c Thu Aug 24 23:29:12 2000

to something more understandable to patch.  The first one is the most important.
Patch uses it to derive the path.  try this:

*** win/tclWinNotify.c Fri Jul  2 18:08:30 1999
--- ./tclWinNotify.c Thu Aug 24 23:29:12 2000

PS.  I'm trying an educated guess, but my education may be off :)
--

  Sustaining Engineer (Tech Support)                  Ajuba Solutions
  (650) 230-4079



Tue, 25 Feb 2003 03:41:52 GMT  
 
 [ 3 post ] 

 Relevant Pages 

1. TIP #7: Increased resolution for TclpGetTime on Windows

2. NT / win95/98 Date/time problem HELP!!

3. Adding icon to SystemTray in win95/98/nt

4. UltimADE4 : Rapid Application Development for Win95/98/NT

5. Q: PL1-Compiler for WIN95, 98 or NT 4.0

6. different in win95/98 and NT

7. Affordable Common Lisp implementations on Win95/98/NT?

8. DDE fails on Win95/98 but NOT on NT

9. Will DVF-98 have some 2k features?

10. Expect for TCL/TK 8.3 and/or Win95/98/NT/2K?

11. Win95/98 look updates

12. RAS in Win95/98

 

 
Powered by phpBB® Forum Software