Counter Strike : Global Offensive Source Code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

493 lines
17 KiB

  1. //====== Copyright 1996-2005, Valve Corporation, All rights reserved. =======//
  2. //
  3. // glentrypoints.cpp
  4. //
  5. //=============================================================================//
  6. // Immediately include gl.h, etc. here to avoid compilation warnings.
  7. #include "togl/rendermechanism.h"
  8. #include "appframework/AppFramework.h"
  9. #include "appframework/IAppSystemGroup.h"
  10. #include "tier0/dbg.h"
  11. #include "tier0/icommandline.h"
  12. #include "tier0/dynfunction.h"
  13. #include "interface.h"
  14. #include "filesystem.h"
  15. #include "filesystem_init.h"
  16. #include "tier1/convar.h"
  17. #include "vstdlib/cvar.h"
  18. #include "inputsystem/ButtonCode.h"
  19. #include "tier1.h"
  20. #include "tier2/tier2.h"
  21. #ifdef _LINUX
  22. #include <GL/glx.h>
  23. #endif
  24. // NOTE: This has to be the last file included!
  25. #include "tier0/memdbgon.h"
  26. #if !defined(DX_TO_GL_ABSTRACTION)
  27. #error
  28. #endif
  29. #if defined( USE_SDL ) || defined(OSX)
  30. #include "appframework/ilaunchermgr.h"
  31. ILauncherMgr *g_pLauncherMgr = NULL;
  32. #endif
  33. #define DEBUG_ALL_GLCALLS 0
  34. #if DEBUG_ALL_GLCALLS
  35. bool g_bDebugOpenGLCalls = true;
  36. bool g_bPrintOpenGLCalls = false;
  37. #define GL_EXT(x,glmajor,glminor)
  38. #define GL_FUNC(ext,req,ret,fn,arg,call) \
  39. static ret (*fn##_gldebugptr) arg = NULL; \
  40. static ret fn##_gldebug arg { \
  41. if (!g_bDebugOpenGLCalls) { return fn##_gldebugptr call; } \
  42. if (g_bPrintOpenGLCalls) { \
  43. printf("Calling %s ... ", #fn); \
  44. fflush(stdout); \
  45. } \
  46. ret retval = fn##_gldebugptr call; \
  47. if (g_bPrintOpenGLCalls) { \
  48. printf("%s returned!\n", #fn); \
  49. fflush(stdout); \
  50. } \
  51. const GLenum err = glGetError_gldebugptr(); \
  52. if ( err == GL_INVALID_FRAMEBUFFER_OPERATION_EXT ) { \
  53. const GLenum fberr = gGL->glCheckFramebufferStatus( GL_FRAMEBUFFER_EXT ); \
  54. printf("%s triggered error GL_INVALID_FRAMEBUFFER_OPERATION_EXT! (0x%X)\n\n\n", #fn, (int) fberr); \
  55. fflush(stdout); \
  56. __asm__ __volatile__ ( "int $3\n\t" ); \
  57. } else if (err != GL_NO_ERROR) { \
  58. printf("%s triggered error 0x%X!\n\n\n", #fn, (int) err); \
  59. fflush(stdout); \
  60. __asm__ __volatile__ ( "int $3\n\t" ); \
  61. } \
  62. return retval; \
  63. }
  64. #define GL_FUNC_VOID(ext,req,fn,arg,call) \
  65. static void (*fn##_gldebugptr) arg = NULL; \
  66. static void fn##_gldebug arg { \
  67. if (!g_bDebugOpenGLCalls) { fn##_gldebugptr call; return; } \
  68. if (g_bPrintOpenGLCalls) { \
  69. printf("Calling %s ... ", #fn); \
  70. fflush(stdout); \
  71. } \
  72. fn##_gldebugptr call; \
  73. if (g_bPrintOpenGLCalls) { \
  74. printf("%s returned!\n", #fn); \
  75. fflush(stdout); \
  76. } \
  77. const GLenum err = glGetError_gldebugptr(); \
  78. if ( err == GL_INVALID_FRAMEBUFFER_OPERATION_EXT ) { \
  79. const GLenum fberr = gGL->glCheckFramebufferStatus( GL_FRAMEBUFFER_EXT ); \
  80. printf("%s triggered error GL_INVALID_FRAMEBUFFER_OPERATION_EXT! (0x%X)\n\n\n", #fn, (int) fberr); \
  81. fflush(stdout); \
  82. __asm__ __volatile__ ( "int $3\n\t" ); \
  83. } else if (err != GL_NO_ERROR) { \
  84. printf("%s triggered error 0x%X!\n\n\n", #fn, (int) err); \
  85. fflush(stdout); \
  86. __asm__ __volatile__ ( "int $3\n\t" ); \
  87. } \
  88. }
  89. #include "togl/glfuncs.inl"
  90. #undef GL_FUNC_VOID
  91. #undef GL_FUNC
  92. #undef GL_EXT
  93. #endif
  94. COpenGLEntryPoints *gGL = NULL;
  95. GL_GetProcAddressCallbackFunc_t gGL_GetProcAddressCallback = NULL;
  96. void *VoidFnPtrLookup_GlMgr( const char *libname, const char *fn, bool &okay, const bool bRequired, void *fallback)
  97. {
  98. void *retval = NULL;
  99. if ((!okay) && (!bRequired)) // always look up if required (so we get a complete list of crucial missing symbols).
  100. return NULL;
  101. // The SDL path would work on all these platforms, if we were using SDL there, too...
  102. #if defined( LINUX ) || defined( WIN32 )
  103. // SDL does the right thing, so we never need to use tier0 in this case.
  104. retval = (*gGL_GetProcAddressCallback)( libname, fn, okay, bRequired, fallback); //SDL_GL_GetProcAddress(fn);
  105. //printf("CDynamicFunctionOpenGL: SDL_GL_GetProcAddress(\"%s\") returned %p\n", fn, retval);
  106. if ((retval == NULL) && (fallback != NULL))
  107. {
  108. //printf("CDynamicFunctionOpenGL: Using fallback %p for \"%s\"\n", fallback, fn);
  109. retval = fallback;
  110. }
  111. #elif defined OSX
  112. // there's no glXGetProcAddress() equivalent for Mac OS X...it's just dlopen(), basically. Let tier0 handle that.
  113. retval = VoidFnPtrLookup_Tier0( libname, fn, (void *) fallback);
  114. #endif
  115. // Note that a non-NULL response doesn't mean it's safe to call the function!
  116. // You always have to check that the extension is supported;
  117. // an implementation MAY return NULL in this case, but it doesn't have to (and doesn't, with the DRI drivers).
  118. okay = (okay && (retval != NULL));
  119. if (bRequired && !okay)
  120. fprintf(stderr, "Could not find required OpenGL entry point '%s'!\n", fn);
  121. return retval;
  122. }
  123. COpenGLEntryPoints *GetOpenGLEntryPoints(GL_GetProcAddressCallbackFunc_t callback)
  124. {
  125. if (gGL == NULL)
  126. {
  127. gGL_GetProcAddressCallback = callback;
  128. gGL = new COpenGLEntryPoints(LIBGL_SONAME);
  129. if (!gGL->m_bHave_OpenGL)
  130. Error( "Missing basic required OpenGL functionality. %s", LIBGL_SONAME );
  131. }
  132. return gGL;
  133. }
  134. void ClearOpenGLEntryPoints()
  135. {
  136. if ( gGL )
  137. {
  138. gGL->ClearEntryPoints();
  139. }
  140. }
  141. COpenGLEntryPoints *ToGLConnectLibraries( CreateInterfaceFn factory )
  142. {
  143. ConnectTier1Libraries( &factory, 1 );
  144. ConVar_Register();
  145. ConnectTier2Libraries( &factory, 1 );
  146. if ( !g_pFullFileSystem )
  147. {
  148. Warning( "ToGL was unable to access the required interfaces!\n" );
  149. }
  150. // NOTE! : Overbright is 1.0 so that Hammer will work properly with the white bumped and unbumped lightmaps.
  151. MathLib_Init( 2.2f, 2.2f, 0.0f, 2.0f );
  152. #if defined( USE_SDL )
  153. g_pLauncherMgr = (ILauncherMgr *)factory( SDLMGR_INTERFACE_VERSION, NULL );
  154. #elif defined( OSX )
  155. g_pLauncherMgr = (ILauncherMgr *)factory( COCOAMGR_INTERFACE_VERSION, NULL );
  156. #endif
  157. return gGL;
  158. }
  159. void ToGLDisconnectLibraries()
  160. {
  161. DisconnectTier2Libraries();
  162. ConVar_Unregister();
  163. DisconnectTier1Libraries();
  164. }
  165. #define GLVERNUM(Major, Minor, Patch) (((Major) * 100000) + ((Minor) * 1000) + (Patch))
  166. static void GetOpenGLVersion( const char *libname, int *major, int *minor, int *patch)
  167. {
  168. *major = *minor = *patch = 0;
  169. static CDynamicFunctionOpenGL< true, const GLubyte *( APIENTRY *)(GLenum name), const GLubyte * > glGetString( libname, "glGetString");
  170. if (glGetString)
  171. {
  172. const char *version = (const char *) glGetString(GL_VERSION);
  173. if (version)
  174. {
  175. sscanf( version, "%d.%d.%d", major, minor, patch );
  176. }
  177. }
  178. }
  179. static int GetOpenGLVersionMajor(const char *libname)
  180. {
  181. int major, minor, patch;
  182. GetOpenGLVersion(libname, &major, &minor, &patch);
  183. return major;
  184. }
  185. static int GetOpenGLVersionMinor(const char *libname)
  186. {
  187. int major, minor, patch;
  188. GetOpenGLVersion(libname, &major, &minor, &patch);
  189. return minor;
  190. }
  191. static int GetOpenGLVersionPatch(const char *libname)
  192. {
  193. int major, minor, patch;
  194. GetOpenGLVersion(libname, &major, &minor, &patch);
  195. return patch;
  196. }
  197. static bool CheckBaseOpenGLVersion(const char *libname)
  198. {
  199. const int NEED_MAJOR = 2;
  200. const int NEED_MINOR = 0;
  201. const int NEED_PATCH = 0;
  202. int major, minor, patch;
  203. GetOpenGLVersion(libname, &major, &minor, &patch);
  204. const int need = GLVERNUM(NEED_MAJOR, NEED_MINOR, NEED_PATCH);
  205. const int have = GLVERNUM(major, minor, patch);
  206. if (have < need)
  207. {
  208. fprintf(stderr, "PROBLEM: You appear to have OpenGL %d.%d.%d, but we need at least %d.%d.%d!\n",
  209. major, minor, patch, NEED_MAJOR, NEED_MINOR, NEED_PATCH);
  210. return false;
  211. }
  212. return true;
  213. }
  214. static bool CheckOpenGLExtension_internal(const char *libname, const char *ext, const int coremajor, const int coreminor)
  215. {
  216. if ((coremajor >= 0) && (coreminor >= 0)) // we know that this extension is part of the base spec as of GL_VERSION coremajor.coreminor.
  217. {
  218. int major, minor, patch;
  219. GetOpenGLVersion(libname, &major, &minor, &patch);
  220. const int need = GLVERNUM(coremajor, coreminor, 0);
  221. const int have = GLVERNUM(major, minor, patch);
  222. if (have >= need)
  223. return true; // we definitely have access to this "extension," as it is part of this version of the GL's core functionality.
  224. }
  225. // okay, see if the GL_EXTENSIONS string reports it.
  226. static CDynamicFunctionOpenGL< true, const GLubyte *( APIENTRY *)(GLenum name), const GLubyte * > glGetString(libname, "glGetString");
  227. if (!glGetString)
  228. return false;
  229. // hacky scanning of this string, because I don't want to spend time breaking it into a vector like I should have.
  230. const char *extensions = (const char *) glGetString(GL_EXTENSIONS);
  231. const size_t extlen = strlen(ext);
  232. while ((extensions) && (*extensions))
  233. {
  234. const char *ptr = strstr(extensions, ext);
  235. #if _WIN32
  236. if (!ptr)
  237. {
  238. static CDynamicFunctionOpenGL< true, const char *( APIENTRY *)( ), const char * > wglGetExtensionsStringEXT(NULL, "wglGetExtensionsStringEXT");
  239. if (wglGetExtensionsStringEXT)
  240. {
  241. extensions = wglGetExtensionsStringEXT();
  242. ptr = strstr(extensions, ext);
  243. }
  244. if (!ptr)
  245. {
  246. return false;
  247. }
  248. }
  249. #elif defined (OSX)
  250. if (!ptr)
  251. return false; // definitely not there.
  252. #else
  253. if (!ptr)
  254. {
  255. static CDynamicFunctionOpenGL< true, Display *( APIENTRY *)( ), Display* > glXGetCurrentDisplay( NULL, "glXGetCurrentDisplay");
  256. static CDynamicFunctionOpenGL< true, const char *( APIENTRY *)( Display*, int ), const char * > glXQueryExtensionsString( NULL, "glXQueryExtensionsString");
  257. if (glXQueryExtensionsString && glXGetCurrentDisplay)
  258. {
  259. extensions = glXQueryExtensionsString(glXGetCurrentDisplay(), 0);
  260. ptr = strstr(extensions, ext);
  261. }
  262. if (!ptr)
  263. {
  264. return false;
  265. }
  266. }
  267. #endif
  268. // make sure this matches the entire string, and isn't a substring match of some other extension.
  269. // if ( ( (string is at start of extension list) or (the char before the string is a space) ) and
  270. // (the next char after the string is a space or a null terminator) )
  271. if ( ((ptr == extensions) || (ptr[-1] == ' ')) &&
  272. ((ptr[extlen] == ' ') || (ptr[extlen] == '\0')) )
  273. return true; // found it!
  274. extensions = ptr + extlen; // skip ahead, search again.
  275. }
  276. return false;
  277. }
  278. static bool CheckOpenGLExtension(const char *libname, const char *ext, const int coremajor, const int coreminor)
  279. {
  280. const bool retval = CheckOpenGLExtension_internal(libname, ext, coremajor, coreminor);
  281. printf("This system %s the OpenGL extension %s.\n", retval ? "supports" : "DOES NOT support", ext);
  282. return retval;
  283. }
  284. // The GL context you want entry points for must be current when you hit this constructor!
  285. COpenGLEntryPoints::COpenGLEntryPoints(const char *libname)
  286. : m_nTotalGLCycles(0)
  287. , m_nTotalGLCalls(0)
  288. , m_strLibName(libname)
  289. , m_nOpenGLVersionMajor(GetOpenGLVersionMajor(m_strLibName))
  290. , m_nOpenGLVersionMinor(GetOpenGLVersionMinor(m_strLibName))
  291. , m_nOpenGLVersionPatch(GetOpenGLVersionPatch(m_strLibName))
  292. , m_bHave_OpenGL(CheckBaseOpenGLVersion(m_strLibName)) // may reset to false as these lookups happen.
  293. #define GL_EXT(x,glmajor,glminor) , m_bHave_##x(CheckOpenGLExtension(m_strLibName, #x, glmajor, glminor))
  294. #define GL_FUNC(ext,req,ret,fn,arg,call) , fn(m_strLibName, #fn, m_bHave_##ext)
  295. #define GL_FUNC_VOID(ext,req,fn,arg,call) , fn(m_strLibName, #fn, m_bHave_##ext)
  296. #include "togl/glfuncs.inl"
  297. #undef GL_FUNC_VOID
  298. #undef GL_FUNC
  299. #undef GL_EXT
  300. {
  301. // Locally cache the copy of the GL device strings, to avoid needing to call these glGet's (which can be extremely slow) more than once.
  302. const char *pszString = ( const char * )glGetString(GL_VENDOR);
  303. m_pGLDriverStrings[cGLVendorString] = strdup( pszString ? pszString : "" );
  304. m_nDriverProvider = cGLDriverProviderUnknown;
  305. if ( V_stristr( m_pGLDriverStrings[cGLVendorString], "nvidia" ) )
  306. m_nDriverProvider = cGLDriverProviderNVIDIA;
  307. else if ( V_stristr( m_pGLDriverStrings[cGLVendorString], "amd" ) || V_stristr( m_pGLDriverStrings[cGLVendorString], "ati" ) )
  308. m_nDriverProvider = cGLDriverProviderAMD;
  309. else if ( V_stristr( m_pGLDriverStrings[cGLVendorString], "intel" ) )
  310. m_nDriverProvider = cGLDriverProviderIntelOpenSource;
  311. else if ( V_stristr( m_pGLDriverStrings[cGLVendorString], "apple" ) )
  312. m_nDriverProvider = cGLDriverProviderApple;
  313. pszString = ( const char * )glGetString(GL_RENDERER);
  314. m_pGLDriverStrings[cGLRendererString] = strdup( pszString ? pszString : "" );
  315. pszString = ( const char * )glGetString(GL_VERSION);
  316. m_pGLDriverStrings[cGLVersionString] = strdup( pszString ? pszString : "" );
  317. pszString = ( const char * )glGetString(GL_EXTENSIONS);
  318. m_pGLDriverStrings[cGLExtensionsString] = strdup( pszString ? pszString : "" );
  319. // !!! FIXME: Alfred says the original GL_APPLE_fence code only exists to
  320. // !!! FIXME: hint Apple's drivers and not because we rely on the
  321. // !!! FIXME: functionality. If so, just remove this check (and the
  322. // !!! FIXME: GL_NV_fence code entirely).
  323. if ((m_bHave_OpenGL) && ((!m_bHave_GL_NV_fence) && (!m_bHave_GL_ARB_sync) && (!m_bHave_GL_APPLE_fence)))
  324. {
  325. Error( "Required OpenGL extension \"GL_NV_fence\", \"GL_ARB_sync\", or \"GL_APPLE_fence\" is not supported. Please upgrade your OpenGL driver." );
  326. }
  327. // same extension, different name.
  328. if (m_bHave_GL_EXT_vertex_array_bgra || m_bHave_GL_ARB_vertex_array_bgra)
  329. {
  330. m_bHave_GL_EXT_vertex_array_bgra = m_bHave_GL_ARB_vertex_array_bgra = true;
  331. }
  332. // GL_ARB_framebuffer_object is a superset of GL_EXT_framebuffer_object,
  333. // (etc) but if you don't call in through the ARB entry points, you won't
  334. // get the relaxed restrictions on mismatched attachment dimensions.
  335. if (m_bHave_GL_ARB_framebuffer_object)
  336. {
  337. m_bHave_GL_EXT_framebuffer_object = true;
  338. m_bHave_GL_EXT_framebuffer_blit = true;
  339. m_bHave_GL_EXT_framebuffer_multisample = true;
  340. glBindFramebufferEXT.Force(glBindFramebuffer.Pointer());
  341. glBindRenderbufferEXT.Force(glBindRenderbuffer.Pointer());
  342. glCheckFramebufferStatusEXT.Force(glCheckFramebufferStatus.Pointer());
  343. glDeleteRenderbuffersEXT.Force(glDeleteRenderbuffers.Pointer());
  344. glFramebufferRenderbufferEXT.Force(glFramebufferRenderbuffer.Pointer());
  345. glFramebufferTexture2DEXT.Force(glFramebufferTexture2D.Pointer());
  346. glFramebufferTexture3DEXT.Force(glFramebufferTexture3D.Pointer());
  347. glGenFramebuffersEXT.Force(glGenFramebuffers.Pointer());
  348. glGenRenderbuffersEXT.Force(glGenRenderbuffers.Pointer());
  349. glDeleteFramebuffersEXT.Force(glDeleteFramebuffers.Pointer());
  350. glBlitFramebufferEXT.Force(glBlitFramebuffer.Pointer());
  351. glRenderbufferStorageMultisampleEXT.Force(glRenderbufferStorageMultisample.Pointer());
  352. }
  353. #if DEBUG_ALL_GLCALLS
  354. // push all GL calls through the debug wrappers.
  355. #define GL_EXT(x,glmajor,glminor)
  356. #define GL_FUNC(ext,req,ret,fn,arg,call) \
  357. fn##_gldebugptr = this->fn; \
  358. this->fn.Force(fn##_gldebug);
  359. #define GL_FUNC_VOID(ext,req,fn,arg,call) \
  360. fn##_gldebugptr = this->fn; \
  361. this->fn.Force(fn##_gldebug);
  362. #include "togl/glfuncs.inl"
  363. #undef GL_FUNC_VOID
  364. #undef GL_FUNC
  365. #undef GL_EXT
  366. #endif
  367. #ifdef OSX
  368. m_bHave_GL_NV_bindless_texture = false;
  369. m_bHave_GL_AMD_pinned_memory = false;
  370. #else
  371. if ( ( m_bHave_GL_NV_bindless_texture ) && ( !CommandLine()->CheckParm( "-gl_nv_bindless_texturing" ) ) )
  372. {
  373. m_bHave_GL_NV_bindless_texture = false;
  374. glGetTextureHandleNV.Force( NULL );
  375. glGetTextureSamplerHandleNV.Force( NULL );
  376. glMakeTextureHandleResidentNV.Force( NULL );
  377. glMakeTextureHandleNonResidentNV.Force( NULL );
  378. glUniformHandleui64NV.Force( NULL );
  379. glUniformHandleui64vNV.Force( NULL );
  380. glProgramUniformHandleui64NV.Force( NULL );
  381. glProgramUniformHandleui64vNV.Force( NULL );
  382. glIsTextureHandleResidentNV.Force( NULL );
  383. }
  384. if ( ( m_bHave_GL_AMD_pinned_memory ) && ( !CommandLine()->CheckParm( "-gl_amd_pinned_memory" ) ) )
  385. {
  386. m_bHave_GL_AMD_pinned_memory = false;
  387. }
  388. #endif
  389. if ( ( m_bHave_GL_ARB_buffer_storage ) && ( CommandLine()->CheckParm( "-gl_disable_arb_buffer_storage" ) ) )
  390. {
  391. m_bHave_GL_ARB_buffer_storage = false;
  392. }
  393. char buf[256];
  394. V_snprintf(buf, sizeof( buf ), "GL_NV_bindless_texture: %s\n", m_bHave_GL_NV_bindless_texture ? "ENABLED" : "DISABLED" );
  395. Plat_DebugString( buf );
  396. V_snprintf(buf, sizeof( buf ), "GL_AMD_pinned_memory: %s\n", m_bHave_GL_AMD_pinned_memory ? "ENABLED" : "DISABLED" );
  397. Plat_DebugString( buf );
  398. V_snprintf( buf, sizeof(buf), "GL_ARB_buffer_storage: %s\n", m_bHave_GL_ARB_buffer_storage ? "AVAILABLE" : "NOT AVAILABLE" );
  399. Plat_DebugString( buf );
  400. V_snprintf(buf, sizeof( buf ), "GL_EXT_texture_sRGB_decode: %s\n", m_bHave_GL_EXT_texture_sRGB_decode ? "AVAILABLE" : "NOT AVAILABLE" );
  401. Plat_DebugString( buf );
  402. bool bGLCanDecodeS3TCTextures = m_bHave_GL_EXT_texture_compression_s3tc || ( m_bHave_GL_EXT_texture_compression_dxt1 && m_bHave_GL_ANGLE_texture_compression_dxt3 && m_bHave_GL_ANGLE_texture_compression_dxt5 );
  403. if ( !bGLCanDecodeS3TCTextures )
  404. {
  405. Error( "This application requires either the GL_EXT_texture_compression_s3tc or the GL_EXT_texture_compression_dxt1 + GL_ANGLE_texture_compression_dxt3 + GL_ANGLE_texture_compression_dxt5 OpenGL extensions. Please install S3TC texture support.\n" );
  406. }
  407. #ifndef OSX
  408. if ( !m_bHave_GL_EXT_texture_sRGB_decode )
  409. {
  410. Error( "Required OpenGL extension \"GL_EXT_texture_sRGB_decode\" is not supported. Please update your OpenGL driver.\n" );
  411. }
  412. #endif
  413. }
  414. COpenGLEntryPoints::~COpenGLEntryPoints()
  415. {
  416. for ( uint i = 0; i < cGLTotalDriverProviders; ++i )
  417. {
  418. free( m_pGLDriverStrings[i] );
  419. m_pGLDriverStrings[i] = NULL;
  420. }
  421. }
  422. void COpenGLEntryPoints::ClearEntryPoints()
  423. {
  424. #define GL_EXT(x,glmajor,glminor)
  425. #define GL_FUNC(ext,req,ret,fn,arg,call) fn.Force( NULL );
  426. #define GL_FUNC_VOID(ext,req,fn,arg,call) fn.Force( NULL );
  427. #include "togl/glfuncs.inl"
  428. #undef GL_FUNC_VOID
  429. #undef GL_FUNC
  430. #undef GL_EXT
  431. }
  432. // Turn off memdbg macros (turned on up top) since this is included like a header
  433. #include "tier0/memdbgoff.h"