home *** CD-ROM | disk | FTP | other *** search
/ Mac-Source 1994 July / Mac-Source_July_1994.iso / C and C++ / Science⁄Math / VideoToolbox / VideoToolboxSources / GDTime.c < prev    next >
Encoding:
Text File  |  1993-04-26  |  17.4 KB  |  461 lines  |  [TEXT/KAHL]

  1. /*
  2. GDTime.c
  3.  
  4. All these routines measure the timing of some aspect of the function of a video
  5. device. For background, read “Video synch” and run TimeVideo, which reports the
  6. results of running all these routines.
  7.  
  8. double GDFrameRate(GDHandle device) measures the frame rate of a video device in
  9. Hz. (A NULL “device” argument to this routine, or any of the routines below,
  10. requests use of the System VBL interrupt, which normally runs at 60.15 Hz.) It
  11. times by counting VBL interrupts (discarding any spurious ones, to deal with the
  12. problem described in the next paragraph).
  13.  
  14. double GDVBLRate(GDHandle device) measures the rate of VBL interrupts generated
  15. by a video device in Hz. According to Apple’s Designing Cards and Drivers book
  16. and other documentation, the video driver and card are supposed to generate one
  17. VBL interrupt per frame. However, many don’t. E.g. Apple’s 4•8 and 8•24 video
  18. cards issue several interrupts per frame. Read the “Video synch” file.
  19.  
  20. double GDMovieSize(GDHandle device,int quickly) measures what fraction of the
  21. screen you can fill with a real-time movie (a new image on each frame), using
  22. CopyBitsQuickly (if quickly!=0) or CopyBits (if quickly==0) to copy from memory
  23. to video card. At one time CopyBitsQuickly() was much faster than CopyBits(),
  24. but my latest measurements, using GDMovieRate, indicate that there is no longer
  25. any difference in speed. However, CopyBitsQuickly ignores the color tables and
  26. CopyBits uses them. Of course, when you’re showing movies you don’t want to
  27. waste time with color tables, so GDMovieRate() makes a PixMap that shares the
  28. device’s color table. For reasons that I don’t understand even in that case
  29. using CopyBits to copy from and then back to the screen doesn’t always preserve
  30. the original colors.
  31.  
  32. double GDMovieRate(GDHandle device,int quickly) measures the rate (images/s) at
  33. which you can show a full-screen movie.
  34.  
  35. error=GDTimeClut(device,GDSetEntries,clutEntries,&s,&frames,&missingFrames,&frameRate);
  36. measures how long it takes to load the clut. It measures in two kinds of units
  37. simultaneously, frames and seconds. You supply the function to be tested, e.g.
  38. SetEntriesQuickly or GDSetEntries. (If gdType==directType GDTimeClut will
  39. automatically substitute GDDirectSetEntries for GDSetEntries.) The second
  40. argument, “clutEntries”, specifies how many clut entries you want to update each
  41. time, or zero for all. GDTimeClut also measures the frame rate independently,
  42. which it returns, after using it to estimate how many frame interrupts were
  43. missed during each clut load. If there’s at least one frame missing or if the
  44. frame count is very small, less than 0.5 per call, then it estimates the frames
  45. directly from the time. You may substitute NULL for any of the pointer-to-double
  46. arguments.
  47.  
  48. GDFramesPerClutUpdate, GDClutUpdateRate, and GDTimeClutUpdate are gone. Use 
  49. GDTimeClut instead.
  50.  
  51. NOTES:
  52. It is of interest to time GDSetEntries (and its sibling GDDirectSetEntries) at
  53. both normal (zero) and high (7) processor interrupt priority, because some
  54. drivers are asynchronous when run at low priority, returning immediately and
  55. deferring the actual clut loading until the vbl interrupt occurs, but Apple
  56. specifies that all drivers must be synchronous when run at high priority. You do
  57. this by supplying the new routine GDSetEntriesByTypeHighPriority as an argument
  58. to GDTimeClut.
  59.  
  60. Similarly, while the problem of multiple interrupts per frame is dealt with
  61. satisfactorily by VBLInstall.c, using the scheme suggested by Raynald Comtois,
  62. it is of technical interest to follow up the report that there are no extra
  63. interrupts if the processor instruction cache is disabled. One theory to account
  64. for this is that perhaps disabling the cache causes the interrupt service
  65. routine to take long enough that the hardware interrupt pulse has terminated
  66. before the routine exits. Thus I would like to write a routine called
  67. GDSetEntriesNoCache, but I don’t know how to disable the cache.
  68.  
  69. HISTORY:
  70. 8/22/92 dgp    wrote ‘em.
  71. 8/26/92    dgp    added clutEntries argument
  72. 8/28/92    dgp    updated to use new reentrant Timer.c
  73. 9/11/92    dgp enhanced GDFrameRate() to discard bogus VBL interrupts and to return
  74.             true frame rate, as suggested by Raynald Comtois. Added GDVBLRate(), 
  75.             which corresponds to the old GDFrameRate().
  76. 9/15/92    dgp    GDMovieRate() now asks the video driver what mode we’re in, just in case
  77.             QuickDraw’s been fooled.
  78. 9/17/92    dgp    Added second argument to GDMovieRate() to select CopyBitsQuickly vs CopyBits.
  79. 10/5/92    dgp    fixed bug in GDMovieRate() that caused crashes or noop when testing other 
  80.             than the main device.
  81. 10/9/92    dgp    now actually initialize the linearTable for direct clut.
  82. 10/10/92 dgp Squeezed out extra space from rowBytes in GDMovieRate() so as not to show
  83.             garbage. Now use Temporary memory if there isn’t enough memory in the application 
  84.             heap to show a full-screen movie. Show movie for 3 seconds. 
  85. 10/13/92 dgp In response to a report from Tom Busey, that frames were going uncounted
  86.             during the clut timing, which seems to be a problem with some video drivers,
  87.             GDFramesPerClutUpdate() now double checks the timing in secs, and if
  88.             it finds a discrepancy, prints a warning to the screen and reports a
  89.             corrected values based on the timing in secs. This should be more reliable.
  90. 10/13/92 dgp Fixed error in printf in GDFrameRate().
  91. 11/23/92 dgp Set nominalBits equal to pixelSize instead of Log2L(ctsize).
  92. 12/30/92 dgp Commented out warning from GDFramesPerClutUpdate().
  93.              •Check for SetEntries error in the clut timing routines, and return
  94.              NAN in that case. •Use trial and error to determine clut size.
  95. 1/4/92    dgp    GDTimeClutUpdate now returns NAN on GDSetEntries error.
  96. 1/6/92    dgp    Fixed computation of linearTable, so CLUT is preserved by GDTimeClutUpdate.
  97. 1/11/93    dgp    Enhanced GDMovieRate() to work even when Color QuickDraw is absent, by 
  98.             calling the new routine GDMovieRateNoColorQuickDraw().
  99.             Check returned Ptr from NewTimer() to make sure it’s ok; will be NULL
  100.             if computer only has Standard Time Manager.
  101. 1/24/93    dgp    Reduced timing interval from 3 to 1 s for movies.
  102. 3/11/93    dgp created GDTimeClut, based on GDTimeClutUpdate and
  103.             GDFramesPerClutUpdate. The enhancements are 1. you
  104.             supply the function to be tested, e.g. SetEntriesQuickly
  105.             or GDSetEntries. 2. It doesn’t print or exit, always
  106.             returning with an informative OSErr. 3. It measures frame
  107.             rate independently, which it returns, and also uses it to
  108.             estimate how many frame interrupts were missed during
  109.             each clut load. 4. If there’s at least one frame missing
  110.             or if the frame count is very small, less than 0.5 per
  111.             call, then it estimates the frames directly from the
  112.             time.
  113. 3/15/93    dgp    Fixed portRect clipping error in GDMovieRateNoColorQuickDraw.
  114. 4/17/93    dgp    Merged GDFrameRate.c and GDTimeClut.c to produce GDTime.c
  115. 4/19/93    dgp    Fixed bug in GDTimeClut that used garbage color table in place of
  116.             linear color table. Now uses GDNewLinearColorTable.
  117. 4/25/93    dgp    Changed struct from static to automatic.
  118. */
  119. #include "VideoToolbox.h"
  120. #include <assert.h>
  121. #include <math.h>
  122. #if THINK_C
  123.     #include <LoMem.h>
  124. #else
  125.     short MBarHeight : 0xBAA;
  126. #endif
  127. double GDMovieRateNoColorQuickDraw(int quickly);
  128. // Original typedef is in VideoToolbox.h
  129. //typedef OSErr (*SetEntriesFunction)(GDHandle device,short start,short count
  130. //    ,ColorSpec *aTable);
  131. #define CALLS 30            // fewer for speed, more for accuracy
  132. #define FRAMES 10            // fewer for speed, more for accuracy
  133. #define SHOW_MOVIE_WINDOW 0    // A matter of taste, but I prefer not to show it.
  134. #define SECONDS 0.5            // Movie duration.
  135.  
  136. OSErr GDTimeClut(GDHandle device,SetEntriesFunction function,short clutEntries
  137.     ,double *sPtr,double *framesPtr,double *missingFramesPtr,double *frameRatePtr)
  138. {
  139.     OSErr error;
  140.     short clutSize,i;
  141.     ColorSpec *table,*linearTable=NULL;
  142.     VBLTaskAndA5 vblData;
  143.     long frames;
  144.     Timer *timer;
  145.     double s,missingFrames,frameRate;
  146.  
  147.     if(sPtr!=NULL)*sPtr=NAN;
  148.     if(framesPtr!=NULL)*framesPtr=NAN;
  149.     if(missingFramesPtr!=NULL)*missingFramesPtr=NAN;
  150.     if(frameRatePtr!=NULL)*frameRatePtr=NAN;
  151.     if(device==NULL || (*device)->gdType==fixedType){
  152.         if(frameRatePtr!=NULL)*frameRatePtr=GDFrameRate(device);
  153.         return 0;
  154.     }
  155.     clutSize=GDClutSize(device);
  156.     if(clutEntries<0 || clutEntries>clutSize)return 1;
  157.     if(clutEntries==0)clutEntries=clutSize;
  158.     if((*device)->gdType==directType){
  159.         if(function==GDSetEntries)function=GDDirectSetEntries;
  160.         table=linearTable=GDNewLinearColorTable(device);
  161.         if(linearTable==NULL)return MemError();
  162.     }else table=((**(**(**device).gdPMap).pmTable)).ctTable;
  163.     vblData.subroutine=NULL;                        // setup frame counter
  164.     error=VBLInstall(&vblData,device,CALLS*20);        // setup frame counter
  165.     if(error)return error;
  166.     timer=NewTimer();                                // setup timer
  167.     if(timer==NULL)return 1;                        // lacks Revised Time Manager.
  168.     vblData.vbl.vblCount=1;                            // Enable interrupt service routine
  169.     for(i=-1;i<CALLS;i++) {
  170.         error=(function)(device,0,clutEntries-1,table);
  171.         if(i==-1){
  172.             StartTimer(timer);
  173.             frames=vblData.framesLeft;
  174.         }
  175.         if(error)break;
  176.     }
  177.     frames-=vblData.framesLeft;
  178.     s=StopTimerSecs(timer);
  179.     VBLRemove(&vblData);
  180.     DisposeTimer(timer);
  181.     if(linearTable!=NULL)DisposePtr((Ptr)linearTable);
  182.     if(error)return error;
  183.     
  184.     // Estimate number of missing frames by discrepancy between frames and secs.
  185.     frameRate=GDFrameRate(device);
  186.     missingFrames=s*frameRate-frames;
  187.     
  188.     // Return results
  189.     if(sPtr!=NULL)*sPtr=s/CALLS;
  190.     if(framesPtr!=NULL){
  191.         if(fabs(missingFrames)>1. || frames<CALLS/2) *framesPtr=frameRate*s/CALLS;
  192.         else *framesPtr=frames/(double)CALLS;
  193.     }
  194.     if(missingFramesPtr!=NULL)*missingFramesPtr=missingFrames/CALLS;
  195.     if(frameRatePtr!=NULL)*frameRatePtr=frameRate;
  196.     return 0;
  197. }
  198.  
  199. double GDFrameRate(GDHandle device)
  200. {
  201.     VBLTaskAndA5 vblData;
  202.     Timer *timer;
  203.     register long frames;
  204.     int error;
  205.     double s;
  206.     
  207.     timer=NewTimer();
  208.     if(timer==NULL)return NAN;            // lacks Revised Time Manager.
  209.     vblData.subroutine=NULL;
  210.     error=VBLInstall(&vblData,device,FRAMES);
  211.     if(error)PrintfExit("GDFrameRate: VBLInstall: error %d\n",error);
  212.     vblData.vbl.vblCount=1;                // Enable interrupt service routine
  213.     frames=vblData.framesDesired-2;
  214.     while(vblData.framesLeft>frames);    // wait for second frame
  215.     StartTimer(timer);
  216.     while(vblData.framesLeft);            // wait for last frame
  217.     s=StopTimerSecs(timer);
  218.     VBLRemove(&vblData);
  219.     DisposeTimer(timer);
  220.     return frames/s;
  221. }
  222.  
  223. double GDVBLRate(GDHandle device)
  224. {
  225.     VBLTaskAndA5 vblData;
  226.     Timer *timer;
  227.     register long frames;
  228.     int error;
  229.     double s;
  230.     
  231.     timer=NewTimer();
  232.     if(timer==NULL)return NAN;            // lacks Revised Time Manager.
  233.     vblData.subroutine=SimpleVBLSubroutine;
  234.     error=VBLInstall(&vblData,device,FRAMES);
  235.     if(error)PrintfExit("GDVBLRate: VBLInstall: error %d\n",error);
  236.     vblData.vbl.vblCount=1;                // Enable interrupt service routine
  237.     frames=vblData.framesDesired-2;
  238.     while(vblData.framesLeft>frames);    // wait for second frame
  239.     StartTimer(timer);
  240.     while(vblData.framesLeft);            // wait for last frame
  241.     s=StopTimerSecs(timer);
  242.     VBLRemove(&vblData);
  243.     DisposeTimer(timer);
  244.     return frames/s;
  245. }
  246.  
  247. double GDMovieSize(GDHandle device,int quickly)
  248. {
  249.     return GDMovieRate(device,quickly)/GDFrameRate(device);
  250. }
  251.  
  252. double GDMovieRate(GDHandle device,int quickly)
  253. {
  254.     Timer *timer;
  255.     register long image;
  256.     long images;
  257.     OSErr error;
  258.     double s=NAN,fractionOfFrame=NAN;
  259.     PixMap **pm;
  260.     unsigned long bytes;
  261.     GDHandle oldDevice;
  262.     WindowPtr window,oldPort;
  263.     Rect r,rLocal;
  264.     Ptr oldBaseAddr;
  265.     short nominalBits,trueBits;
  266.     Handle saveSpace,bufferHandle;
  267.     long osAttr;
  268.     int tempMem;
  269.     
  270.     if(!QD8Exists())return GDMovieRateNoColorQuickDraw(quickly);
  271.     oldDevice=GetGDevice();
  272.     SetGDevice(device);
  273.     pm=NewPixMap();
  274.     SetGDevice(oldDevice);
  275.     if(pm==NULL)goto done0;
  276.     HLock((Handle)pm);
  277.     // The color table is needed for CopyBits(); CopyBitsQuickly doesn't care.
  278.     (**pm).pmTable=(**(**device).gdPMap).pmTable;    // share device's color table
  279.     if(device==GetMainDevice())(**pm).bounds.top+=MBarHeight;
  280.     if(SHOW_MOVIE_WINDOW){
  281.         (**pm).bounds.top+=19;    // Allow room for window title
  282.         InsetRect(&(**pm).bounds,32,32);
  283.     }
  284.     bufferHandle=NULL;
  285.     bytes=(**pm).bounds.right-(**pm).bounds.left;
  286.     bytes*=(**pm).pixelSize;
  287.     bytes=((bytes+31)/32)*4;    // convert bits to bytes, rounding up to multiple of 4
  288.     (**pm).rowBytes &= ~0x3fff;
  289.     (**pm).rowBytes |= bytes;
  290.     Gestalt(gestaltOSAttr,&osAttr);
  291.     tempMem=osAttr & 1L<<gestaltTempMemSupport;
  292.     while(1){
  293.         bytes=(**pm).rowBytes & 0x3fff;
  294.         bytes*=(**pm).bounds.bottom-(**pm).bounds.top;
  295.         if(bytes==0)goto done1;
  296.         saveSpace=NewHandle(2000);    // save some space
  297.         bufferHandle=NewHandle(bytes+1200);    // extra is for drifting
  298.         if(saveSpace!=NULL)DisposeHandle(saveSpace);
  299.         if(bufferHandle==NULL && tempMem)bufferHandle=TempNewHandle(bytes+1200,&error);
  300.         if(bufferHandle!=NULL)break;
  301.         // Halve the window's height before trying again
  302.         (**pm).bounds.bottom=(**pm).bounds.top+((**pm).bounds.bottom-(**pm).bounds.top)/2;
  303.     }
  304.     HLock(bufferHandle);
  305.     (**pm).baseAddr=*bufferHandle;
  306.     GetPort(&oldPort);
  307.     r=(**pm).bounds;
  308.     window=NewCWindow(NULL,&r,"\pmovie",0,0,(WindowPtr)-1,0,0);    // don't show it yet
  309.     if(window==NULL)goto done2;
  310.     timer=NewTimer();
  311.     if(timer==NULL)goto done2;        // lacks Revised Time Manager.
  312.     SetPort(window);
  313.     HLock((Handle)((CWindowPtr)window)->portPixMap);
  314.     SetGDevice(device);
  315.     rLocal=r;
  316.     GlobalToLocalRect(&rLocal);
  317.     StartTimer(timer);
  318.     if(quickly)CopyBitsQuickly((BitMap *)*((CWindowPtr)window)->portPixMap,(BitMap *)*pm
  319.         ,&rLocal,&r,srcCopy,NULL);    // copy screen to memory
  320.     else{
  321.         CopyBits((BitMap *)*((CWindowPtr)window)->portPixMap,(BitMap *)*pm
  322.         ,&rLocal,&r,srcCopy,NULL);    // copy screen to memory
  323.         if(error=QDError()){
  324.             printf("GDMovieRate: CopyBits generated QuickDraw error %d\n",error);
  325.             goto done3;
  326.         }
  327.     }
  328.     s=StopTimerSecs(timer);    // rough estimate of time for one image
  329.     if(SHOW_MOVIE_WINDOW || !quickly)ShowWindow(window);// CopyBits won't copy to a hidden window
  330.     StartTimer(timer);
  331.     oldBaseAddr=(**pm).baseAddr;
  332.     images=ceil(SECONDS/s);
  333.     for(image=images;image>0;image--){
  334.         if(image==1)(**pm).baseAddr=oldBaseAddr;
  335.         else (**pm).baseAddr+=4;    // Drift image to prove it's a movie
  336.         // We drift by multiples of 4 bytes because long-aligned copying is faster.
  337.         if(quickly)CopyBitsQuickly((BitMap *)*pm,(BitMap *)*((CWindowPtr)window)->portPixMap
  338.             ,&r,&rLocal,srcCopy,NULL);    // copy memory to screen
  339.         else CopyBits((BitMap *)*pm,(BitMap *)*((CWindowPtr)window)->portPixMap
  340.             ,&r,&rLocal,srcCopy,NULL);    // copy memory to screen
  341.     }
  342.     s=StopTimerSecs(timer);
  343.     fractionOfFrame=(long)(r.bottom-r.top)*(long)(r.right-r.left);
  344.     r=(**(**device).gdPMap).bounds;
  345.     fractionOfFrame/=(long)(r.bottom-r.top)*(long)(r.right-r.left);
  346.     DisposeTimer(timer);
  347. done3:
  348.     SetPort(oldPort);
  349.     DisposeWindow(window);
  350. done2:
  351.     DisposHandle(bufferHandle);
  352. done1:
  353.     (**pm).pmTable=NULL;
  354.     DisposePixMap(pm);
  355. done0:
  356.     SetGDevice(oldDevice);
  357.     return images*fractionOfFrame/s;
  358. }
  359.  
  360. double GDMovieRateNoColorQuickDraw(int quickly)
  361. {
  362.     Timer *timer;
  363.     register long image;
  364.     long images;
  365.     OSErr error=0;
  366.     double s=NAN,fractionOfFrame=NAN;
  367.     BitMap bitmap;
  368.     unsigned long bytes;
  369.     Rect r;
  370.     Ptr oldBaseAddr;
  371.     Handle saveSpace,bufferHandle=NULL;
  372.     long osAttr;
  373.     int tempMem;
  374.     char string[100];
  375.     GrafPort portRec,*port=&portRec,*oldPort;
  376.     
  377.     GetPort(&oldPort);
  378.     OpenPort(port);
  379.     SetPort(port);
  380.     bitmap=port->portBits;
  381.     bytes=bitmap.bounds.right-bitmap.bounds.left;
  382.     bitmap.rowBytes=((bytes+31)/32)*4;    // convert bits to bytes, round up to mult of 4
  383.     Gestalt(gestaltOSAttr,&osAttr);
  384.     tempMem=osAttr & 1L<<gestaltTempMemSupport;
  385.     while(1){
  386.         bytes=bitmap.rowBytes & 0x3fff;
  387.         bytes*=bitmap.bounds.bottom-bitmap.bounds.top;
  388.         if(bytes==0)goto done;
  389.         saveSpace=NewHandle(2000);    // save some space
  390.         bufferHandle=NewHandle(bytes+600);    // extra is for drifting
  391.         if(saveSpace!=NULL)DisposeHandle(saveSpace);
  392.         if(bufferHandle==NULL && tempMem)bufferHandle=TempNewHandle(bytes+600,&error);
  393.         if(bufferHandle!=NULL)break;
  394.         // Halve the window's height before trying again
  395.         bitmap.bounds.bottom=bitmap.bounds.top+(bitmap.bounds.bottom-bitmap.bounds.top)/2;
  396.     }
  397.     HLock(bufferHandle);
  398.     bitmap.baseAddr=*bufferHandle;
  399.     timer=NewTimer();
  400.     if(timer==NULL)goto done;    // lacks Revised Time Manager.
  401.     r=bitmap.bounds;
  402.     StartTimer(timer);
  403.     if(quickly)CopyBitsQuickly(&port->portBits,&bitmap
  404.         ,&r,&r,srcCopy,NULL);    // copy screen to memory
  405.     else CopyBits(&port->portBits,&bitmap
  406.         ,&r,&r,srcCopy,NULL);    // copy screen to memory
  407.     s=StopTimerSecs(timer);        // approximate time for one image
  408.     if(!quickly && (error=QDError())){
  409.         printf("GDMovieRate: CopyBits generated QuickDraw error %d\n",error);
  410.         DisposeTimer(timer);
  411.         goto done;
  412.     }
  413.     oldBaseAddr=bitmap.baseAddr;
  414.     images=ceil(SECONDS/s);    // Let's time for this many seconds.
  415.     StartTimer(timer);
  416.     for(image=images;image>0;image--){
  417.         if(image==1)bitmap.baseAddr=oldBaseAddr;
  418.         else bitmap.baseAddr+=2;    // Drift the image, to prove it's a movie
  419.         if(quickly)CopyBitsQuickly(&bitmap,&port->portBits
  420.             ,&r,&r,srcCopy,NULL);    // copy memory to screen
  421.         else CopyBits(&bitmap,&port->portBits
  422.             ,&r,&r,srcCopy,NULL);    // copy memory to screen
  423.     }
  424.     s=StopTimerSecs(timer);
  425.     DisposeTimer(timer);
  426.     if(!quickly && (error=QDError())){
  427.         printf("GDMovieRate: CopyBits generated QuickDraw error %d\n",error);
  428.         DisposeTimer(timer);
  429.         goto done;
  430.     }
  431.     fractionOfFrame=(long)(r.bottom-r.top)*(long)(r.right-r.left);
  432.     r=port->portBits.bounds;
  433.     fractionOfFrame/=(long)(r.bottom-r.top)*(long)(r.right-r.left);
  434. done:
  435.     SetPort(oldPort);
  436.     ClosePort(port);
  437.     if(bufferHandle==NULL){
  438.         printf("GDMovieRate: Not enough memory!\n");
  439.         return NAN;
  440.     }
  441.     DisposeHandle(bufferHandle);
  442.     return images*fractionOfFrame/s;
  443. }
  444.  
  445. double TickRate(void)
  446. {
  447.     Timer *timer;
  448.     double s;
  449.     long t;
  450.     
  451.     timer=NewTimer();
  452.     if(timer==NULL)return NAN;            // lacks Revised Time Manager.
  453.     Delay(1,&t);
  454.     StartTimer(timer);
  455.     Delay(FRAMES,&t);
  456.     s=StopTimerSecs(timer);
  457.     DisposeTimer(timer);
  458.     return FRAMES/s;
  459. }
  460.  
  461.