[OPENGL32]
[reactos.git] / reactos / dll / win32 / opengl32 / opengl32.c
1 /*
2 * COPYRIGHT: See COPYING in the top level directory
3 * PROJECT: ReactOS kernel
4 * FILE: lib/opengl32/opengl32.c
5 * PURPOSE: OpenGL32 lib
6 * PROGRAMMER: Anich Gregor (blight), Royce Mitchell III
7 * UPDATE HISTORY:
8 * Feb 1, 2004: Created
9 */
10
11 #include "opengl32.h"
12
13 /* function prototypes */
14 static void OPENGL32_AppendICD( GLDRIVERDATA *icd );
15 static void OPENGL32_RemoveICD( GLDRIVERDATA *icd );
16 static GLDRIVERDATA *OPENGL32_LoadDriver( LPCWSTR regKey );
17 static DWORD OPENGL32_InitializeDriver( GLDRIVERDATA *icd );
18 static BOOL OPENGL32_UnloadDriver( GLDRIVERDATA *icd );
19 static DWORD OPENGL32_RegGetDriverInfo( LPCWSTR driver, GLDRIVERDATA *icd );
20
21
22 /* global vars */
23 /* Do not assume it have the free value MAXDWORD set, any value can be in here */
24 DWORD OPENGL32_tls = MAXDWORD;
25 GLPROCESSDATA OPENGL32_processdata;
26
27
28 static BOOL
29 OPENGL32_ThreadAttach( void )
30 {
31 GLTHREADDATA* lpData = NULL;
32 PROC *dispatchTable = NULL;
33 TEB *teb = NULL;
34
35 dispatchTable = (PROC*)HeapAlloc( GetProcessHeap(),
36 HEAP_GENERATE_EXCEPTIONS | HEAP_ZERO_MEMORY,
37 sizeof (((ICDTable *)(0))->dispatch_table) );
38 if (dispatchTable == NULL)
39 {
40 DBGPRINT( "Error: Couldn't allocate GL dispatch table" );
41 return FALSE;
42 }
43
44 lpData = (GLTHREADDATA*)HeapAlloc( GetProcessHeap(),
45 HEAP_GENERATE_EXCEPTIONS | HEAP_ZERO_MEMORY,
46 sizeof (GLTHREADDATA) );
47 if (lpData == NULL)
48 {
49 DBGPRINT( "Error: Couldn't allocate GLTHREADDATA" );
50 HeapFree( GetProcessHeap(), 0, dispatchTable );
51 return FALSE;
52 }
53
54 teb = NtCurrentTeb();
55
56 /* initialize dispatch table with empty functions */
57 #define X(func, ret, typeargs, args, icdidx, tebidx, stack) \
58 dispatchTable[icdidx] = (PROC)glEmptyFunc##stack; \
59 if (tebidx >= 0) \
60 teb->glDispatchTable[tebidx] = (PVOID)glEmptyFunc##stack;
61 GLFUNCS_MACRO
62 #undef X
63
64 teb->glTable = dispatchTable;
65 TlsSetValue( OPENGL32_tls, lpData );
66
67 return TRUE;
68 }
69
70
71 static void
72 OPENGL32_ThreadDetach( void )
73 {
74 GLTHREADDATA* lpData = NULL;
75 PROC *dispatchTable = NULL;
76
77 rosglMakeCurrent( NULL, NULL );
78
79 lpData = (GLTHREADDATA*)TlsGetValue( OPENGL32_tls );
80 if (lpData != NULL)
81 {
82 if (!HeapFree( GetProcessHeap(), 0, lpData ))
83 DBGPRINT( "Warning: HeapFree() on GLTHREADDATA failed (%d)",
84 GetLastError() );
85 lpData = NULL;
86 }
87
88 dispatchTable = NtCurrentTeb()->glTable;
89 if (dispatchTable != NULL)
90 {
91 if (!HeapFree( GetProcessHeap(), 0, dispatchTable ))
92 DBGPRINT( "Warning: HeapFree() on dispatch table failed (%d)",
93 GetLastError() );
94 }
95 }
96
97
98 static BOOL
99 OPENGL32_ProcessAttach( void )
100 {
101 SECURITY_ATTRIBUTES attrib = { sizeof (SECURITY_ATTRIBUTES), /* nLength */
102 NULL, /* lpSecurityDescriptor */
103 TRUE /* bInheritHandle */ };
104
105 OPENGL32_tls = TlsAlloc();
106 if (OPENGL32_tls == MAXDWORD)
107 return FALSE;
108
109 memset( &OPENGL32_processdata, 0, sizeof (OPENGL32_processdata) );
110
111 /* create driver, glrc & dcdata list mutex */
112 OPENGL32_processdata.driver_mutex = CreateMutex( &attrib, FALSE, NULL );
113 if (OPENGL32_processdata.driver_mutex == NULL)
114 {
115 DBGPRINT( "Error: Couldn't create driver_list mutex (%d)",
116 GetLastError() );
117 return FALSE;
118 }
119 OPENGL32_processdata.glrc_mutex = CreateMutex( &attrib, FALSE, NULL );
120 if (OPENGL32_processdata.glrc_mutex == NULL)
121 {
122 DBGPRINT( "Error: Couldn't create glrc_list mutex (%d)",
123 GetLastError() );
124 return FALSE;
125 }
126 OPENGL32_processdata.dcdata_mutex = CreateMutex( &attrib, FALSE, NULL );
127 if (OPENGL32_processdata.dcdata_mutex == NULL)
128 {
129 DBGPRINT( "Error: Couldn't create dcdata_list mutex (%d)",
130 GetLastError() );
131 return FALSE;
132 }
133
134 return TRUE;
135 }
136
137
138 static void
139 OPENGL32_ProcessDetach( void )
140 {
141 GLDRIVERDATA *icd, *icd2;
142 GLDCDATA *dcdata, *dcdata2;
143 GLRC *glrc, *glrc2;
144
145 /* free lists */
146 for (dcdata = OPENGL32_processdata.dcdata_list; dcdata != NULL;)
147 {
148 dcdata2 = dcdata;
149 dcdata = dcdata->next;
150 if (!HeapFree( GetProcessHeap(), 0, dcdata2 ))
151 DBGPRINT( "Warning: HeapFree() on DCDATA 0x%08x failed (%d)",
152 dcdata2, GetLastError() );
153 }
154
155 for (glrc = OPENGL32_processdata.glrc_list; glrc != NULL;)
156 {
157 glrc2 = glrc;
158 glrc = glrc->next;
159 if (!HeapFree( GetProcessHeap(), 0, glrc2 ))
160 DBGPRINT( "Warning: HeapFree() on GLRC 0x%08x failed (%d)",
161 glrc2, GetLastError() );
162 }
163
164 for (icd = OPENGL32_processdata.driver_list; icd != NULL;)
165 {
166 icd2 = icd;
167 icd = icd->next;
168 if (!HeapFree( GetProcessHeap(), 0, icd2 ))
169 DBGPRINT( "Warning: HeapFree() on DRIVERDATA 0x%08x failed (%d)",
170 icd2, GetLastError() );
171 }
172
173 /* free mutexes */
174 if (OPENGL32_processdata.driver_mutex != NULL)
175 CloseHandle( OPENGL32_processdata.driver_mutex );
176 if (OPENGL32_processdata.glrc_mutex != NULL)
177 CloseHandle( OPENGL32_processdata.glrc_mutex );
178 if (OPENGL32_processdata.dcdata_mutex != NULL)
179 CloseHandle( OPENGL32_processdata.dcdata_mutex );
180
181 /* free TLS */
182 if (OPENGL32_tls != MAXDWORD)
183 TlsFree(OPENGL32_tls);
184 }
185
186
187 BOOL WINAPI
188 DllMain(HINSTANCE hInstance, DWORD Reason, LPVOID Reserved)
189 {
190 DBGPRINT( "Info: Called!" );
191 switch ( Reason )
192 {
193 /* The DLL is loading due to process
194 * initialization or a call to LoadLibrary.
195 */
196 case DLL_PROCESS_ATTACH:
197 DBGTRACE( "Process attach" );
198 if (!OPENGL32_ProcessAttach())
199 return FALSE;
200 /* No break: Initialize the index for first thread. */
201
202 /* The attached process creates a new thread. */
203 case DLL_THREAD_ATTACH:
204 DBGTRACE( "Thread attach" );
205 if (!OPENGL32_ThreadAttach())
206 return FALSE;
207 break;
208
209 /* The thread of the attached process terminates. */
210 case DLL_THREAD_DETACH:
211 DBGTRACE( "Thread detach" );
212 /* Release the allocated memory for this thread.*/
213 OPENGL32_ThreadDetach();
214 break;
215
216 /* DLL unload due to process termination or FreeLibrary. */
217 case DLL_PROCESS_DETACH:
218 DBGTRACE( "Process detach" );
219 OPENGL32_ThreadDetach();
220 OPENGL32_ProcessDetach();
221 break;
222 }
223
224 return TRUE;
225 }
226
227
228 /*! \brief Append ICD to linked list.
229 *
230 * \param icd GLDRIVERDATA to append to list
231 *
232 * \note Only call this when you hold the driver_mutex.
233 */
234 static void
235 OPENGL32_AppendICD( GLDRIVERDATA *icd )
236 {
237 if (OPENGL32_processdata.driver_list == NULL)
238 OPENGL32_processdata.driver_list = icd;
239 else
240 {
241 GLDRIVERDATA *p = OPENGL32_processdata.driver_list;
242 while (p->next != NULL)
243 p = p->next;
244 p->next = icd;
245 }
246 }
247
248
249 /*! \brief Remove ICD from linked list.
250 *
251 * \param icd GLDRIVERDATA to remove from list
252 *
253 * \note Only call this when you hold the driver_mutex.
254 */
255 static void
256 OPENGL32_RemoveICD( GLDRIVERDATA *icd )
257 {
258 if (icd == OPENGL32_processdata.driver_list)
259 OPENGL32_processdata.driver_list = icd->next;
260 else
261 {
262 GLDRIVERDATA *p = OPENGL32_processdata.driver_list;
263 while (p != NULL)
264 {
265 if (p->next == icd)
266 {
267 p->next = icd->next;
268 return;
269 }
270 p = p->next;
271 }
272 DBGPRINT( "Error: ICD 0x%08x not found in list!", icd );
273 }
274 }
275
276
277 /*! \brief Load an ICD.
278 *
279 * \param driver Name of installable client driver.
280 *
281 * \return Pointer to an allocated GLDRIVERDATA struct.
282 * \retval NULL Failure.
283 *
284 * \todo Call SetLastError() where appropriate.
285 */
286 static GLDRIVERDATA *
287 OPENGL32_LoadDriver( LPCWSTR driver )
288 {
289 LONG ret;
290 GLDRIVERDATA *icd;
291
292 DBGPRINT( "Info: Loading driver %ws...", driver );
293
294 /* allocate driver data */
295 icd = (GLDRIVERDATA*)HeapAlloc( GetProcessHeap(),
296 HEAP_GENERATE_EXCEPTIONS | HEAP_ZERO_MEMORY,
297 sizeof (GLDRIVERDATA) );
298 if (icd == NULL)
299 {
300 DBGPRINT( "Error: Couldn't allocate GLDRIVERDATA! (%d)", GetLastError() );
301 return NULL;
302 }
303
304 ret = OPENGL32_RegGetDriverInfo( driver, icd );
305 if (ret != ERROR_SUCCESS)
306 {
307 DBGPRINT( "Error: Couldn't query driver information (%d)", ret );
308 if (!HeapFree( GetProcessHeap(), 0, icd ))
309 DBGPRINT( "Error: HeapFree() returned false, error code = %d",
310 GetLastError() );
311 return NULL;
312 }
313
314 DBGPRINT( "Info: Dll = %ws", icd->dll );
315 DBGPRINT( "Info: Version = 0x%08x", icd->version );
316 DBGPRINT( "Info: DriverVersion = 0x%08x", icd->driver_version );
317 DBGPRINT( "Info: Flags = 0x%08x", icd->flags );
318
319 /* load/initialize ICD */
320 ret = OPENGL32_InitializeDriver( icd );
321 if (ret != ERROR_SUCCESS)
322 {
323 DBGPRINT( "Error: Couldnt initialize ICD!" );
324 if (!HeapFree( GetProcessHeap(), 0, icd ))
325 DBGPRINT( "Error: HeapFree() returned false, error code = %d",
326 GetLastError() );
327 return NULL;
328 }
329
330 /* append ICD to list */
331 OPENGL32_AppendICD( icd );
332 DBGPRINT( "Info: ICD loaded." );
333
334 return icd;
335 }
336
337
338 /*! \brief Initialize a driver (Load DLL and DrvXxx procs)
339 *
340 * \param icd ICD to initialize with the dll, version, driverVersion
341 * and flags already filled.
342 * \return Error code.
343 * \retval ERROR_SUCCESS Success
344 */
345 #define LOAD_DRV_PROC( icd, proc, required ) \
346 *(char**)&icd->proc = (char*)GetProcAddress( icd->handle, #proc ); \
347 if (required && icd->proc == NULL) { \
348 DBGPRINT( "Error: GetProcAddress(\"%s\") failed!", #proc ); \
349 FreeLibrary( icd->handle ); \
350 return GetLastError(); \
351 }
352
353 static DWORD
354 OPENGL32_InitializeDriver( GLDRIVERDATA *icd )
355 {
356 /* check version */
357 if (icd->version > 2)
358 DBGPRINT( "Warning: ICD version > 2 (%d)", icd->version );
359
360 /* load dll */
361 icd->handle = LoadLibraryW( icd->dll );
362 if (icd->handle == NULL)
363 {
364 DWORD err = GetLastError();
365 DBGPRINT( "Error: Couldn't load DLL! (%d)", err );
366 return err;
367 }
368
369 /* validate version */
370 if (icd->driver_version > 1)
371 {
372 LOAD_DRV_PROC(icd, DrvValidateVersion, FALSE);
373 if (icd->DrvValidateVersion != NULL)
374 {
375 if (!icd->DrvValidateVersion( icd->driver_version ))
376 {
377 DBGPRINT( "Error: DrvValidateVersion failed!" );
378 DBGBREAK();
379 FreeLibrary( icd->handle );
380 return ERROR_INVALID_FUNCTION; /* FIXME: use better error code */
381 }
382 }
383 else
384 DBGPRINT( "Info: DrvValidateVersion not exported by ICD" );
385 }
386
387 /* load DrvXXX procs */
388 LOAD_DRV_PROC(icd, DrvCopyContext, TRUE);
389 LOAD_DRV_PROC(icd, DrvCreateContext, FALSE);
390 LOAD_DRV_PROC(icd, DrvCreateLayerContext, FALSE);
391 LOAD_DRV_PROC(icd, DrvDeleteContext, TRUE);
392 LOAD_DRV_PROC(icd, DrvDescribeLayerPlane, TRUE);
393 LOAD_DRV_PROC(icd, DrvDescribePixelFormat, TRUE);
394 LOAD_DRV_PROC(icd, DrvGetLayerPaletteEntries, TRUE);
395 LOAD_DRV_PROC(icd, DrvGetProcAddress, TRUE);
396 LOAD_DRV_PROC(icd, DrvReleaseContext, TRUE);
397 LOAD_DRV_PROC(icd, DrvRealizeLayerPalette, TRUE);
398 LOAD_DRV_PROC(icd, DrvSetContext, TRUE);
399 LOAD_DRV_PROC(icd, DrvSetLayerPaletteEntries, TRUE);
400 LOAD_DRV_PROC(icd, DrvSetPixelFormat, TRUE);
401 LOAD_DRV_PROC(icd, DrvShareLists, TRUE);
402 LOAD_DRV_PROC(icd, DrvSwapBuffers, TRUE);
403 LOAD_DRV_PROC(icd, DrvSwapLayerBuffers, TRUE);
404
405 /* we require at least one of DrvCreateContext and DrvCreateLayerContext */
406 if (icd->DrvCreateContext == NULL && icd->DrvCreateLayerContext == NULL)
407 {
408 DBGPRINT( "Error: One of DrvCreateContext/DrvCreateLayerContext is required!" );
409 FreeLibrary( icd->handle );
410 return ERROR_INVALID_FUNCTION; /* FIXME: use better error code... */
411 }
412
413 return ERROR_SUCCESS;
414 }
415
416
417 /*! \brief Unload ICD.
418 *
419 * \retval TRUE Success.
420 * \retval FALSE Failure.
421 */
422 static BOOL
423 OPENGL32_UnloadDriver( GLDRIVERDATA *icd )
424 {
425 BOOL allOk = TRUE;
426
427 DBGPRINT( "Info: Unloading driver %ws...", icd->driver_name );
428 if (icd->refcount != 0)
429 DBGPRINT( "Warning: ICD refcount = %d (should be 0)", icd->refcount );
430
431 /* unload dll */
432 if (!FreeLibrary( icd->handle ))
433 {
434 allOk = FALSE;
435 DBGPRINT( "Warning: FreeLibrary on ICD %ws failed! (%d)", icd->dll,
436 GetLastError() );
437 }
438
439 /* free resources */
440 OPENGL32_RemoveICD( icd );
441 if (!HeapFree( GetProcessHeap(), 0, icd ))
442 {
443 allOk = FALSE;
444 DBGPRINT( "Warning: HeapFree() returned FALSE, error code = %d",
445 GetLastError() );
446 }
447
448 return allOk;
449 }
450
451
452 /*! \brief Load ICD (shared ICD data)
453 *
454 * \return Pointer to an allocated GLDRIVERDATA on success.
455 * \retval NULL Failure.
456 */
457 GLDRIVERDATA *
458 OPENGL32_LoadICD( LPCWSTR driver )
459 {
460 GLDRIVERDATA *icd;
461
462 /* synchronize */
463 if (WaitForSingleObject( OPENGL32_processdata.driver_mutex, INFINITE ) ==
464 WAIT_FAILED)
465 {
466 DBGPRINT( "Error: WaitForSingleObject() failed (%d)", GetLastError() );
467 return NULL; /* FIXME: do we have to expect such an error and handle it? */
468 }
469
470 /* look if ICD is already loaded */
471 for (icd = OPENGL32_processdata.driver_list; icd; icd = icd->next)
472 {
473 if (!_wcsicmp( driver, icd->driver_name )) /* found */
474 {
475 /* release mutex */
476 if (!ReleaseMutex( OPENGL32_processdata.driver_mutex ))
477 DBGPRINT( "Error: ReleaseMutex() failed (%d)", GetLastError() );
478
479 return icd;
480 }
481 }
482
483 /* not found - try to load */
484 icd = OPENGL32_LoadDriver( driver );
485
486 /* release mutex */
487 if (!ReleaseMutex( OPENGL32_processdata.driver_mutex ))
488 DBGPRINT( "Error: ReleaseMutex() failed (%d)", GetLastError() );
489
490 return icd;
491 }
492
493
494 /*! \brief Unload ICD (shared ICD data)
495 *
496 * \retval TRUE Success.
497 * \retval FALSE Failure.
498 */
499 BOOL
500 OPENGL32_UnloadICD( GLDRIVERDATA *icd )
501 {
502 BOOL ret = TRUE;
503
504 /* synchronize */
505 if (WaitForSingleObject( OPENGL32_processdata.driver_mutex, INFINITE ) ==
506 WAIT_FAILED)
507 {
508 DBGPRINT( "Error: WaitForSingleObject() failed (%d)", GetLastError() );
509 return FALSE; /* FIXME: do we have to expect such an error and handle it? */
510 }
511
512 if (icd->refcount == 0)
513 ret = OPENGL32_UnloadDriver( icd );
514
515 /* release mutex */
516 if (!ReleaseMutex( OPENGL32_processdata.driver_mutex ))
517 DBGPRINT( "Error: ReleaseMutex() failed (%d)", GetLastError() );
518
519 return ret;
520 }
521
522
523 /*! \brief Enumerate OpenGLDrivers (from registry)
524 *
525 * \param idx Index of the driver to get information about.
526 * \param name Pointer to an array of WCHARs (can be NULL)
527 * \param cName Pointer to a DWORD. Input is len of name array.
528 * Output is length of the drivername.
529 * Can be NULL if name is NULL.
530 *
531 * \return Error code
532 * \retval ERROR_NO_MORE_ITEMS End of driver list.
533 * \retval ERROR_SUCCESS Success.
534 */
535 #if 0 /* unused */
536 DWORD
537 OPENGL32_RegEnumDrivers( DWORD idx, LPWSTR name, LPDWORD cName )
538 {
539 HKEY hKey;
540 LPCWSTR subKey = OPENGL_DRIVERS_SUBKEY;
541 LONG ret;
542 DWORD size;
543 WCHAR driver[256];
544
545 if (name == NULL)
546 return ERROR_SUCCESS; /* nothing to do */
547
548 if (cName == NULL)
549 return ERROR_INVALID_FUNCTION; /* we need cName when name is given */
550
551 /* open OpenGLDrivers registry key */
552 ret = RegOpenKeyExW( HKEY_LOCAL_MACHINE, subKey, 0, KEY_READ, &hKey );
553 if (ret != ERROR_SUCCESS)
554 {
555 DBGPRINT( "Error: Couldn't open registry key '%ws'", subKey );
556 return ret;
557 }
558
559 /* get subkey name */
560 size = sizeof (driver) / sizeof (driver[0]);
561 ret = RegEnumKeyW( hKey, idx, name, *cName );
562 if (ret != ERROR_SUCCESS)
563 {
564 DBGPRINT( "Error: Couldn't get OpenGLDrivers subkey name (%d)", ret );
565 RegCloseKey( hKey );
566 return ret;
567 }
568 *cName = wcslen( name );
569
570 /* close key */
571 RegCloseKey( hKey );
572 return ERROR_SUCCESS;
573 }
574 #endif /* 0 -- unused */
575
576
577 /*! \brief Get registry values for a driver given a name.
578 *
579 * \param driver Name of the driver to get information about.
580 * \param icd Pointer to GLDRIVERDATA.
581 *
582 * \return Error code.
583 * \retval ERROR_SUCCESS Success.
584 *
585 * \note On success the following fields of \a icd are filled: \a driver_name,
586 * \a dll, \a version, \a driver_version and \a flags.
587 */
588 static DWORD
589 OPENGL32_RegGetDriverInfo( LPCWSTR driver, GLDRIVERDATA *icd )
590 {
591 HKEY hKey;
592 WCHAR subKey[1024] = OPENGL_DRIVERS_SUBKEY2;
593 LONG ret;
594 DWORD type, size;
595
596 /* drivers registry values */
597 DWORD version = 1, driverVersion = 0, flags = 0;
598 WCHAR dll[256];
599
600 /* open driver registry key */
601 wcsncat( subKey, driver, 1024 );
602 ret = RegOpenKeyExW( HKEY_LOCAL_MACHINE, subKey, 0, KEY_READ, &hKey );
603 if (ret != ERROR_SUCCESS)
604 {
605 DBGPRINT( "Error: Couldn't open registry key '%ws'", subKey );
606 return ret;
607 }
608
609 /* query values */
610 size = sizeof (dll);
611 ret = RegQueryValueExW( hKey, L"Dll", 0, &type, (LPBYTE)dll, &size );
612 if (ret != ERROR_SUCCESS || type != REG_SZ)
613 {
614 DBGPRINT( "Error: Couldn't query Dll value or not a string" );
615 RegCloseKey( hKey );
616 return ret;
617 }
618
619 size = sizeof (DWORD);
620 ret = RegQueryValueExW( hKey, L"Version", 0, &type, (LPBYTE)&version, &size );
621 if (ret != ERROR_SUCCESS || type != REG_DWORD)
622 DBGPRINT( "Warning: Couldn't query Version value or not a DWORD" );
623
624 size = sizeof (DWORD);
625 ret = RegQueryValueExW( hKey, L"DriverVersion", 0, &type,
626 (LPBYTE)&driverVersion, &size );
627 if (ret != ERROR_SUCCESS || type != REG_DWORD)
628 DBGPRINT( "Warning: Couldn't query DriverVersion value or not a DWORD" );
629
630 size = sizeof (DWORD);
631 ret = RegQueryValueExW( hKey, L"Flags", 0, &type, (LPBYTE)&flags, &size );
632 if (ret != ERROR_SUCCESS || type != REG_DWORD)
633 DBGPRINT( "Warning: Couldn't query Flags value or not a DWORD" );
634
635 /* close key */
636 RegCloseKey( hKey );
637
638 /* output data */
639 /* FIXME: NUL-terminate strings? */
640 wcsncpy( icd->driver_name, driver,
641 sizeof (icd->driver_name) / sizeof (icd->driver_name[0]) - 1 );
642 wcsncpy( icd->dll, dll,
643 sizeof (icd->dll) / sizeof (icd->dll[0]) );
644 icd->version = version;
645 icd->driver_version = driverVersion;
646 icd->flags = flags;
647
648 return ERROR_SUCCESS;
649 }
650
651 /* EOF */
652