[cosmetics] update date in GPL header
[vuplus_xbmc] / xbmc / cores / dvdplayer / DVDDemuxSPU.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "DVDDemuxSPU.h"
22 #include "DVDClock.h"
23 #include "utils/log.h"
24
25 #undef ALIGN
26 #define ALIGN(value, alignment) (((value)+((alignment)-1))&~((alignment)-1))
27
28 // #define SPU_DEBUG
29
30 void DebugLog(const char *format, ...)
31 {
32 #ifdef SPU_DEBUG
33   static char temp_spubuffer[1024];
34   va_list va;
35
36   va_start(va, format);
37   _vsnprintf(temp_spubuffer, 1024, format, va);
38   va_end(va);
39
40   CLog::Log(LOGDEBUG,temp_spubuffer);
41 #endif
42 }
43
44 CDVDDemuxSPU::CDVDDemuxSPU()
45 {
46   memset(&m_spuData, 0, sizeof(m_spuData));
47   memset(m_clut, 0, sizeof(m_clut));
48   m_bHasClut = false;
49 }
50
51 CDVDDemuxSPU::~CDVDDemuxSPU()
52 {
53   if (m_spuData.data) free(m_spuData.data);
54 }
55
56 void CDVDDemuxSPU::Reset()
57 {
58   FlushCurrentPacket();
59
60   // We can't reset this during playback, cause we don't always
61   // get a new clut from libdvdnav leading to invalid colors
62   // so let's just never reset it. It will only be reset
63   // when dvdplayer is destructed and constructed
64   // m_bHasClut = false;
65   // memset(m_clut, 0, sizeof(m_clut));
66 }
67
68 void CDVDDemuxSPU::FlushCurrentPacket()
69 {
70   if (m_spuData.data) free(m_spuData.data);
71   memset(&m_spuData, 0, sizeof(m_spuData));
72 }
73
74 CDVDOverlaySpu* CDVDDemuxSPU::AddData(BYTE* data, int iSize, double pts)
75 {
76   SPUData* pSPUData = &m_spuData;
77
78   if (pSPUData->iNeededSize > 0 &&
79       (pSPUData->iSize != pSPUData->iNeededSize) &&
80       ((pSPUData->iSize + iSize) > pSPUData->iNeededSize))
81   {
82     DebugLog("corrupt spu data: packet does not fit");
83     m_spuData.iNeededSize = 0;
84     m_spuData.iSize = 0;
85     return NULL;
86   }
87
88   // check if we are about to start a new packet
89   if (pSPUData->iSize == pSPUData->iNeededSize)
90   {
91     // for now we don't delete the memory assosiated with m_spuData.data
92     pSPUData->iSize = 0;
93
94     // check spu data lenght, only needed / possible in the first spu pakcet
95     unsigned __int16 length = data[0] << 8 | data[1];
96     if (length == 0)
97     {
98       DebugLog("corrupt spu data: zero packet");
99       m_spuData.iNeededSize = 0;
100       m_spuData.iSize = 0;
101       return NULL;
102     }
103     if (length > iSize) pSPUData->iNeededSize = length;
104     else pSPUData->iNeededSize = iSize;
105
106     // set presentation time stamp
107     if (pts > 0) pSPUData->pts = pts;
108   }
109
110   // allocate data if not already done ( done in blocks off 16384 bytes )
111   // or allocate some more if 16384 bytes is not enough
112   if((pSPUData->iSize + iSize) > pSPUData->iAllocatedSize)
113     pSPUData->data = (BYTE*)realloc(pSPUData->data, ALIGN(pSPUData->iSize + iSize, 0x4000));
114
115   if(!pSPUData->data)
116     return NULL; // crap realloc failed, this will have leaked some memory due to odd realloc
117
118   // add new data
119   memcpy(pSPUData->data + pSPUData->iSize, data, iSize);
120   pSPUData->iSize += iSize;
121
122   if (pSPUData->iNeededSize - pSPUData->iSize == 1) // to make it even
123   {
124     DebugLog("missing 1 byte to complete packet, adding 0xff");
125
126     pSPUData->data[pSPUData->iSize] = 0xff;
127     pSPUData->iSize++;
128   }
129
130   if (pSPUData->iSize == pSPUData->iNeededSize)
131   {
132     DebugLog("got complete spu packet\n  length: %i bytes\n  stream: %i\n", pSPUData->iSize);
133
134     return ParsePacket(pSPUData);
135   }
136
137   return NULL;
138 }
139
140 #define CMD_END     0xFF
141 #define FSTA_DSP    0x00
142 #define STA_DSP     0x01
143 #define STP_DSP     0x02
144 #define SET_COLOR   0x03
145 #define SET_CONTR   0x04
146 #define SET_DAREA   0x05
147 #define SET_DSPXA   0x06
148 #define CHG_COLCON  0x07
149
150 CDVDOverlaySpu* CDVDDemuxSPU::ParsePacket(SPUData* pSPUData)
151 {
152   unsigned int alpha[4];
153   BYTE* pUnparsedData = NULL;
154
155   if (pSPUData->iNeededSize != pSPUData->iSize)
156   {
157     DebugLog("GetPacket, packet is incomplete, missing: %i bytes", (pSPUData->iNeededSize - pSPUData->iSize));
158   }
159
160   if (pSPUData->data[pSPUData->iSize - 1] != 0xff)
161   {
162     DebugLog("GetPacket, missing end of data 0xff");
163   }
164
165   CDVDOverlaySpu* pSPUInfo = new CDVDOverlaySpu();
166   BYTE* p = pSPUData->data; // pointer to walk through all data
167
168   // get data length
169   unsigned __int16 datalength = p[2] << 8 | p[3]; // datalength + 4 control bytes
170
171   pUnparsedData = pSPUData->data + 4;
172
173   // if it is set to 0 it means it's a menu overlay by defualt
174   // this is not what we want too, cause you get strange results on a parse error
175   pSPUInfo->iPTSStartTime = -1;
176
177   //skip data packet and goto control sequence
178   p += datalength;
179
180   bool bHasNewDCSQ = true;
181   while (bHasNewDCSQ)
182   {
183     DebugLog("  starting new SP_DCSQT");
184     // p is beginning of first SP_DCSQT now
185     unsigned __int16 delay = p[0] << 8 | p[1];
186     unsigned __int16 next_DCSQ = p[2] << 8 | p[3];
187
188     //offset within the Sub-Picture Unit to the next SP_DCSQ. If this is the last SP_DCSQ, it points to itself.
189     bHasNewDCSQ = ((pSPUData->data + next_DCSQ) != p);
190     // skip 4 bytes
191     p += 4;
192
193     while (*p != CMD_END && (unsigned int)(p - pSPUData->data) <= pSPUData->iSize)
194     {
195       switch (*p)
196       {
197       case FSTA_DSP:
198         p++;
199         DebugLog("    GetPacket, FSTA_DSP: Forced Start Display, no arguments");
200         pSPUInfo->iPTSStartTime = pSPUData->pts;
201         pSPUInfo->iPTSStopTime = 0x9000000000000LL;
202         pSPUInfo->bForced = true;
203         // delay is always 0, the dvdplayer should decide when to display the packet (menu highlight)
204         break;
205       case STA_DSP:
206         {
207           p++;
208           pSPUInfo->iPTSStartTime = pSPUData->pts;
209           pSPUInfo->iPTSStartTime += (double)delay * 1024 * DVD_TIME_BASE / 90000;
210           DebugLog("    GetPacket, STA_DSP: Start Display, delay: %i", ((delay * 1024) / 90000));
211         }
212         break;
213       case STP_DSP:
214         {
215           p++;
216           pSPUInfo->iPTSStopTime = pSPUData->pts;
217           pSPUInfo->iPTSStopTime += (double)delay * 1024 * DVD_TIME_BASE / 90000;
218           DebugLog("    GetPacket, STP_DSP: Stop Display, delay: %i", ((delay * 1024) / 90000));
219         }
220         break;
221       case SET_COLOR:
222         {
223           p++;
224
225           if (m_bHasClut)
226           {
227             pSPUInfo->bHasColor = true;
228
229             unsigned int idx[4];
230             // 0, 1, 2, 3
231             idx[0] = (p[0] >> 4) & 0x0f;
232             idx[1] = (p[0]) & 0x0f;
233             idx[2] = (p[1] >> 4) & 0x0f;
234             idx[3] = (p[1]) & 0x0f;
235
236             for (int i = 0; i < 4 ; i++) // emphasis 1, emphasis 2, pattern, back ground
237             {
238               BYTE* iColor = m_clut[idx[i]];
239
240               pSPUInfo->color[3 - i][0] = iColor[0]; // Y
241               pSPUInfo->color[3 - i][1] = iColor[1]; // Cr
242               pSPUInfo->color[3 - i][2] = iColor[2]; // Cb
243             }
244           }
245
246           DebugLog("    GetPacket, SET_COLOR:");
247           p += 2;
248         }
249         break;
250       case SET_CONTR:  // alpha
251         {
252           p++;
253           // 3, 2, 1, 0
254           alpha[0] = (p[0] >> 4) & 0x0f;
255           alpha[1] = (p[0]) & 0x0f;
256           alpha[2] = (p[1] >> 4) & 0x0f;
257           alpha[3] = (p[1]) & 0x0f;
258
259           // Ignore blank alpha palette.
260           if (alpha[0] | alpha[1] | alpha[2] | alpha[3])
261           {
262             pSPUInfo->bHasAlpha = true;
263
264             // 0, 1, 2, 3
265             pSPUInfo->alpha[0] = alpha[3]; //0 // background, should be hidden
266             pSPUInfo->alpha[1] = alpha[2]; //1
267             pSPUInfo->alpha[2] = alpha[1]; //2 // wm button overlay
268             pSPUInfo->alpha[3] = alpha[0]; //3
269           }
270
271           DebugLog("    GetPacket, SET_CONTR:");
272           p += 2;
273         }
274         break;
275       case SET_DAREA:
276         {
277           p++;
278           pSPUInfo->x = (p[0] << 4) | (p[1] >> 4);
279           pSPUInfo->y = (p[3] << 4) | (p[4] >> 4);
280           pSPUInfo->width = (((p[1] & 0x0f) << 8) | p[2]) - pSPUInfo->x + 1;
281           pSPUInfo->height = (((p[4] & 0x0f) << 8) | p[5]) - pSPUInfo->y + 1;
282           DebugLog("    GetPacket, SET_DAREA: x,y:%i,%i width,height:%i,%i",
283                    pSPUInfo->x, pSPUInfo->y, pSPUInfo->width, pSPUInfo->height);
284           p += 6;
285         }
286         break;
287       case SET_DSPXA:
288         {
289           p++;
290           unsigned __int16 tfaddr = (p[0] << 8 | p[1]); // offset in packet
291           unsigned __int16 bfaddr = (p[2] << 8 | p[3]); // offset in packet
292           pSPUInfo->pTFData = (tfaddr - 4); //pSPUInfo->pData + (tfaddr - 4); // pSPUData->data = packet startaddr - 4
293           pSPUInfo->pBFData = (bfaddr - 4); //pSPUInfo->pData + (bfaddr - 4); // pSPUData->data = packet startaddr - 4
294           p += 4;
295           DebugLog("    GetPacket, SET_DSPXA: tf: %i bf: %i ", tfaddr, bfaddr);
296         }
297         break;
298       case CHG_COLCON:
299         {
300           p++;
301           unsigned __int16 paramlength = p[0] << 8 | p[1];
302           DebugLog("GetPacket, CHG_COLCON, skippin %i bytes", paramlength);
303           p += paramlength;
304         }
305         break;
306
307       default:
308         DebugLog("GetPacket, error parsing control sequence");
309         delete pSPUInfo;
310         return NULL;
311         break;
312       }
313     }
314     DebugLog("  end off SP_DCSQT");
315     if (*p == CMD_END) p++;
316     else
317     {
318       DebugLog("GetPacket, end off SP_DCSQT, but did not found 0xff (CMD_END)");
319     }
320   }
321
322   // parse the rle.
323   // this should be chnaged so it get's converted to a yuv overlay
324   return ParseRLE(pSPUInfo, pUnparsedData);
325 }
326
327 /*****************************************************************************
328  * AddNibble: read a nibble from a source packet and add it to our integer.
329  *****************************************************************************/
330 inline unsigned int AddNibble( unsigned int i_code, BYTE* p_src, unsigned int* pi_index )
331 {
332   if ( *pi_index & 0x1 )
333   {
334     return ( i_code << 4 | ( p_src[(*pi_index)++ >> 1] & 0xf ) );
335   }
336   else
337   {
338     return ( i_code << 4 | p_src[(*pi_index)++ >> 1] >> 4 );
339   }
340 }
341
342 /*****************************************************************************
343  * ParseRLE: parse the RLE part of the subtitle
344  *****************************************************************************
345  * This part parses the subtitle graphical data and stores it in a more
346  * convenient structure for later decoding. For more information on the
347  * subtitles format, see http://sam.zoy.org/doc/dvd/subtitles/index.html
348  *****************************************************************************/
349 CDVDOverlaySpu* CDVDDemuxSPU::ParseRLE(CDVDOverlaySpu* pSPU, BYTE* pUnparsedData)
350 {
351   BYTE* p_src = pUnparsedData;
352
353   unsigned int i_code = 0;
354
355   unsigned int i_width = pSPU->width;
356   unsigned int i_height = pSPU->height;
357   unsigned int i_x, i_y;
358
359   // allocate a buffer for the result
360   unsigned __int16* p_dest = (unsigned __int16*)pSPU->result;
361
362   /* The subtitles are interlaced, we need two offsets */
363   unsigned int i_id = 0;                   /* Start on the even SPU layer */
364   unsigned int pi_table[2];
365   unsigned int *pi_offset;
366
367   /* Colormap statistics */
368   int i_border = -1;
369   int stats[4]; stats[0] = stats[1] = stats[2] = stats[3] = 0;
370
371   pi_table[ 0 ] = pSPU->pTFData << 1;
372   pi_table[ 1 ] = pSPU->pBFData << 1;
373
374   for ( i_y = 0 ; i_y < i_height ; i_y++ )
375   {
376     pi_offset = pi_table + i_id;
377
378     for ( i_x = 0 ; i_x < i_width ; i_x += i_code >> 2 )
379     {
380       i_code = AddNibble( 0, p_src, pi_offset );
381
382       if ( i_code < 0x04 )
383       {
384         i_code = AddNibble( i_code, p_src, pi_offset );
385
386         if ( i_code < 0x10 )
387         {
388           i_code = AddNibble( i_code, p_src, pi_offset );
389
390           if ( i_code < 0x040 )
391           {
392             i_code = AddNibble( i_code, p_src, pi_offset );
393
394             if ( i_code < 0x0100 )
395             {
396               /* If the 14 first bits are set to 0, then it's a
397                * new line. We emulate it. */
398               if ( i_code < 0x0004 )
399               {
400                 i_code |= ( i_width - i_x ) << 2;
401               }
402               else
403               {
404                 /* We have a boo boo ! */
405                 CLog::Log(LOGERROR, "ParseRLE: unknown RLE code 0x%.4x", i_code);
406                 return NULL;
407               }
408             }
409           }
410         }
411       }
412
413       if ( ( (i_code >> 2) + i_x + i_y * i_width ) > i_height * i_width )
414       {
415         CLog::Log(LOGERROR, "ParseRLE: out of bounds, %i at (%i,%i) is out of %ix%i",
416                  i_code >> 2, i_x, i_y, i_width, i_height );
417         return NULL;
418       }
419
420       // keep trace of all occouring pixels, even keeping the background in mind
421       stats[i_code & 0x3] += i_code >> 2;
422
423       // count the number of pixels for every occouring parts, without background
424       if (pSPU->alpha[i_code & 0x3] != 0x00)
425       {
426         // the last non background pixel is probably the border color
427         i_border = i_code & 0x3;
428         stats[i_border] += i_code >> 2;
429       }
430
431       /* Check we aren't overwriting our data range
432          This occurs on "The Triplets of BelleVille" region 4 disk (NTSC)"
433          where we use around 96k rather than 64k + 20bytes */
434       if ((BYTE *)p_dest >= pSPU->result + sizeof(pSPU->result))
435       {
436         CLog::Log(LOGERROR, "ParseRLE: Overrunning our data range.  Need %li bytes", (long)((BYTE *)p_dest - pSPU->result));
437         return NULL;
438       }
439       *p_dest++ = i_code;
440     }
441
442     /* Check that we didn't go too far */
443     if ( i_x > i_width )
444     {
445       CLog::Log(LOGERROR, "ParseRLE: i_x overflowed, %i > %i", i_x, i_width );
446       return NULL;
447     }
448
449     /* Byte-align the stream */
450     if ( *pi_offset & 0x1 )
451     {
452       (*pi_offset)++;
453     }
454
455     /* Swap fields */
456     i_id = ~i_id & 0x1;
457   }
458
459   /* We shouldn't get any padding bytes */
460   if ( i_y < i_height )
461   {
462     DebugLog("ParseRLE: padding bytes found in RLE sequence" );
463     DebugLog("ParseRLE: send mail to <sam@zoy.org> if you want to help debugging this" );
464
465     /* Skip them just in case */
466     while ( i_y < i_height )
467     {
468       /* Check we aren't overwriting our data range
469          This occurs on "The Triplets of BelleVille" region 4 disk (NTSC)"
470          where we use around 96k rather than 64k + 20bytes */
471       if ((BYTE *)p_dest >= pSPU->result + sizeof(pSPU->result))
472       {
473         CLog::Log(LOGERROR, "ParseRLE: Overrunning our data range.  Need %li bytes", (long)((BYTE *)p_dest - pSPU->result));
474         return NULL;
475       }
476       *p_dest++ = i_width << 2;
477       i_y++;
478     }
479
480     return NULL;
481   }
482
483   DebugLog("ParseRLE: valid subtitle, size: %ix%i, position: %i,%i",
484            pSPU->width, pSPU->height, pSPU->x, pSPU->y );
485
486   // forced spu's (menu overlays) retrieve their alpha/color information from InputStreamNavigator::GetCurrentButtonInfo
487   // also they may contain completely covering data wich is supposed to be hidden normally
488   // since whole spu is drawn, if this is done for forced, that may be displayed
489   // so we must trust what is given
490   if( !pSPU->bForced )
491   {
492     // Handle color if no palette was found.
493     // we only set it if there is a valid i_border color
494     if (!pSPU->bHasColor)
495     {
496       CLog::Log(LOGINFO, "%s - no color palette found, using default", __FUNCTION__);
497       FindSubtitleColor(i_border, stats, pSPU);
498     }
499
500     // check alpha values, for non forced spu's we use a default value
501     if (pSPU->bHasAlpha)
502     {
503       // check alpha values
504       // the array stats represents the nr of pixels for each color channel
505       // thus if there are no pixels to display, we assume the alphas are incorrect.
506       if (!CanDisplayWithAlphas(pSPU->alpha, stats))
507       {
508         CLog::Log(LOGINFO, "%s - no  matching color and alpha found, resetting alpha", __FUNCTION__);
509
510         pSPU->alpha[0] = 0x00; // back ground
511         pSPU->alpha[1] = 0x0f;
512         pSPU->alpha[2] = 0x0f;
513         pSPU->alpha[3] = 0x0f;
514       }
515     }
516     else
517     {
518       CLog::Log(LOGINFO, "%s - ignoring blank alpha palette, using default", __FUNCTION__);
519
520       pSPU->alpha[0] = 0x00; // back ground
521       pSPU->alpha[1] = 0x0f;
522       pSPU->alpha[2] = 0x0f;
523       pSPU->alpha[3] = 0x0f;
524     }
525
526   }
527
528   return pSPU;
529 }
530
531 void CDVDDemuxSPU::FindSubtitleColor(int last_color, int stats[4], CDVDOverlaySpu* pSPU)
532 {
533   const int COLOR_INNER = 0;
534   const int COLOR_SHADE = 1;
535   const int COLOR_BORDER = 2;
536
537   //BYTE custom_subtitle_color[4][3] = { // blue, yellow and something else (xine)
538   //  { 0x80, 0x90, 0x80 }, // inner color
539   //  { 0x00, 0x90, 0x00 }, // shade color
540   //  { 0x00, 0x90, 0xff }  // border color
541   //};
542
543   BYTE custom_subtitle_color[4][3] = { // inner color white, gray shading and a black border
544     { 0xff, 0x80, 0x80 }, // inner color, white
545     { 0x80, 0x80, 0x80 }, // shade color, gray
546     { 0x00, 0x80, 0x80 }  // border color, black
547   };
548
549   //BYTE custom_subtitle_color[4][3] = { // completely white and a black border
550   //  { 0xff, 0x80, 0x80 }, // inner color, white
551   //  { 0xff, 0x80, 0x80 }, // shade color, white
552   //  { 0x00, 0x80, 0x80 }  // border color, black
553   //};
554
555
556   int nrOfUsedColors = 0;
557   for (int i = 0; i < 4; i++)
558   {
559     if (pSPU->alpha[i] > 0) nrOfUsedColors++;
560   }
561
562   if (nrOfUsedColors == 0)
563   {
564     // nothing todo
565     DebugLog("FindSubtitleColor: all 4 alpha channels are 0, nothing todo");
566   }
567   else if (nrOfUsedColors == 1)
568   {
569     // only one color is used, probably the inner color
570     for (int i = 0; i < 4; i++) // find the position that is used
571     {
572       if (pSPU->alpha[i] > 0)
573       {
574         pSPU->color[i][0] = custom_subtitle_color[COLOR_INNER][0]; // Y
575         pSPU->color[i][1] = custom_subtitle_color[COLOR_INNER][1]; // Cr ?
576         pSPU->color[i][2] = custom_subtitle_color[COLOR_INNER][2]; // Cb ?
577         return;
578       }
579     }
580
581   }
582   else
583   {
584     // old code
585     int i, i_inner = -1, i_shade = -1;
586
587     if (last_color >= 0 && last_color < 4)
588     {
589       // Set the border color, the last color is probably the border color
590       pSPU->color[last_color][0] = custom_subtitle_color[COLOR_BORDER][0];
591       pSPU->color[last_color][1] = custom_subtitle_color[COLOR_BORDER][1];
592       pSPU->color[last_color][2] = custom_subtitle_color[COLOR_BORDER][2];
593       stats[last_color] = 0;
594
595     // find the inner colors
596     for ( i = 0 ; i < 4 && i_inner == -1 ; i++ )
597     {
598       if ( stats[i] )
599       {
600         i_inner = i;
601       }
602     }
603
604     // try to find the shade color
605     for ( ; i < 4 && i_shade == -1 ; i++)
606     {
607       if ( stats[i] )
608       {
609         if ( stats[i] > stats[i_inner] )
610         {
611           i_shade = i_inner;
612           i_inner = i;
613         }
614         else
615         {
616           i_shade = i;
617         }
618       }
619     }
620
621     /* Set the inner color */
622     if ( i_inner != -1 )
623     {
624       // white color
625         pSPU->color[i_inner][0] = custom_subtitle_color[COLOR_INNER][0]; // Y
626         pSPU->color[i_inner][1] = custom_subtitle_color[COLOR_INNER][1]; // Cr ?
627         pSPU->color[i_inner][2] = custom_subtitle_color[COLOR_INNER][2]; // Cb ?
628     }
629
630     /* Set the anti-aliasing color */
631     if ( i_shade != -1 )
632     {
633       // gray
634         pSPU->color[i_shade][0] = custom_subtitle_color[COLOR_SHADE][0];
635         pSPU->color[i_shade][1] = custom_subtitle_color[COLOR_SHADE][1];
636         pSPU->color[i_shade][2] = custom_subtitle_color[COLOR_SHADE][2];
637     }
638
639       DebugLog("ParseRLE: using custom palette (border %i, inner %i, shade %i)", last_color, i_inner, i_shade);
640     }
641   }
642 }
643
644 bool CDVDDemuxSPU::CanDisplayWithAlphas(int a[4], int stats[4])
645 {
646   return(
647     a[0] * stats[0] > 0 ||
648     a[1] * stats[1] > 0 ||
649     a[2] * stats[2] > 0 ||
650     a[3] * stats[3] > 0);
651 }