1
0
forked from 0ad/0ad

Stop using WMI for detecting the sound card, and use OpenAL instead. Delete WMI code altogether now it is unused.

This fixes #4561 and makes sound card detection work on non-Windows
platforms.

Reviewed By: echotangoecho
Tested By: Imarok, elexis
Differential Revision: https://code.wildfiregames.com/D636
This was SVN commit r19877.
This commit is contained in:
Nicolas Auvray 2017-07-06 17:29:49 +00:00
parent 3cc2eed989
commit 9aad0137ba
13 changed files with 49 additions and 446 deletions

View File

@ -803,6 +803,7 @@ function setup_all_libs ()
extern_libs = {
"boost",
"sdl",
"openal",
"opengl",
"libpng",
"zlib",

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2010 Wildfire Games
/* Copyright (c) 2017 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -25,27 +25,42 @@
*/
#include "precompiled.h"
#include "lib/sysdep/snd.h"
#include "lib/snd.h"
#if OS_WIN
# include "lib/sysdep/os/win/wsnd.h"
#endif
#include <stdio.h>
#include <stdlib.h>
#include "lib/external_libraries/openal.h"
wchar_t snd_card[SND_CARD_LEN];
wchar_t snd_drv_ver[SND_DRV_VER_LEN];
std::string snd_card;
std::string snd_drv_ver;
void snd_detect()
{
// note: OpenAL alGetString is worthless: it only returns
// OpenAL API version and renderer (e.g. "Software").
// OpenAL alGetString might not return anything interesting on certain platforms
// (see https://stackoverflow.com/questions/28960638 for an example).
// However our previous code supported only Windows, and alGetString does work on
// Windows, so this is an improvement.
#if OS_WIN
win_get_snd_info();
#else
// At least reset the values for unhandled platforms.
ENSURE(SND_CARD_LEN >= 8 && SND_DRV_VER_LEN >= 8); // protect strcpy
wcscpy_s(snd_card, ARRAY_SIZE(snd_card), L"Unknown");
wcscpy_s(snd_drv_ver, ARRAY_SIZE(snd_drv_ver), L"Unknown");
#endif
// Sound cards
const ALCchar* devices = nullptr;
if (alcIsExtensionPresent(nullptr, "ALC_enumeration_EXT") == AL_TRUE)
{
if (alcIsExtensionPresent(nullptr, "ALC_enumerate_all_EXT") == AL_TRUE)
devices = alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER);
else
devices = alcGetString(nullptr, ALC_DEVICE_SPECIFIER);
}
WARN_IF_FALSE(devices);
snd_card.clear();
do {
snd_card += devices;
devices += strlen(devices) + 1;
snd_card += "; ";
} while (*devices);
// Driver version
snd_drv_ver = alGetString(AL_VERSION);
}

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2015 Wildfire Games
/* Copyright (c) 2017 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -27,17 +27,17 @@
#ifndef INCLUDED_SND
#define INCLUDED_SND
const size_t SND_CARD_LEN = 512;
#include <string>
/**
* description of sound card.
**/
extern wchar_t snd_card[SND_CARD_LEN];
extern std::string snd_card;
const size_t SND_DRV_VER_LEN = 512;
/**
* sound driver identification and version.
**/
extern wchar_t snd_drv_ver[SND_DRV_VER_LEN];
extern std::string snd_drv_ver;
/**
* detect sound card and set the above information.

View File

@ -39,8 +39,8 @@ namespace gfx {
std::wstring CardName()
{
// GL_VENDOR+GL_RENDERER are good enough here, so we don't use wgfx_CardName,
// plus that can cause crashes with Nvidia Optimus and some netbooks
// GL_VENDOR+GL_RENDERER are good enough here, so we don't use WMI to detect the cards.
// On top of that WMI can cause crashes with Nvidia Optimus and some netbooks
// see http://trac.wildfiregames.com/ticket/1952
// http://trac.wildfiregames.com/ticket/1575
wchar_t cardName[128];

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2010 Wildfire Games
/* Copyright (c) 2017 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -30,7 +30,6 @@
#include "lib/sysdep/gfx.h"
#include "lib/sysdep/os/win/wdll_ver.h"
#include "lib/sysdep/os/win/wutil.h"
#include "lib/sysdep/os/win/wmi.h"
#if MSC_VERSION
#pragma comment(lib, "advapi32.lib") // registry
@ -137,25 +136,6 @@ static void AppendDriverVersionsFromKnownFiles(VersionList& versionList)
}
Status wgfx_CardName(wchar_t* cardName, size_t numChars)
{
WmiInstances instances;
RETURN_STATUS_IF_ERR(wmi_GetClassInstances(L"Win32_VideoController", instances));
wchar_t* pos = cardName;
for(WmiInstances::iterator it = instances.begin(); it != instances.end(); ++it)
{
if((*it)[L"Availability"].intVal == 8) // offline
continue;
const int ret = swprintf_s(pos, numChars-(pos-cardName), L"%ls; ", (*it)[L"Caption"].bstrVal);
if(ret > 0)
pos += ret;
return INFO::OK;
}
return ERR::FAIL; // no active card found
}
std::wstring wgfx_DriverInfo()
{
VersionList versionList;

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2010 Wildfire Games
/* Copyright (c) 2017 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -23,7 +23,6 @@
#ifndef INCLUDED_WGFX
#define INCLUDED_WGFX
extern Status wgfx_CardName(wchar_t* cardName, size_t numChars);
extern std::wstring wgfx_DriverInfo();
#endif // #ifndef INCLUDED_WGFX

View File

@ -1,148 +0,0 @@
/* Copyright (c) 2011 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* wrapper for Windows Management Instrumentation
*/
#include "precompiled.h"
#include "lib/sysdep/os/win/wmi.h"
#include <wbemidl.h>
#include "lib/module_init.h"
#pragma comment(lib, "wbemuuid.lib")
static IWbemServices* pSvc;
_COM_SMARTPTR_TYPEDEF(IWbemLocator, __uuidof(IWbemLocator));
_COM_SMARTPTR_TYPEDEF(IWbemClassObject, __uuidof(IWbemClassObject));
_COM_SMARTPTR_TYPEDEF(IEnumWbemClassObject, __uuidof(IEnumWbemClassObject));
static ModuleInitState initState;
static bool didInitCOM = false;
static Status Init()
{
HRESULT hr;
hr = CoInitialize(0);
ENSURE(hr == S_OK || hr == S_FALSE); // S_FALSE => already initialized
// balance calls to CoInitialize and CoUninitialize
if (hr == S_FALSE)
CoUninitialize();
else if (hr == S_OK)
didInitCOM = true;
hr = CoInitializeSecurity(0, -1, 0, 0, RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE, 0);
if(FAILED(hr))
WARN_RETURN(ERR::_2);
{
IWbemLocatorPtr pLoc = 0;
hr = CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (void**)&pLoc);
if(FAILED(hr))
WARN_RETURN(ERR::_3);
hr = pLoc->ConnectServer(_bstr_t(L"ROOT\\CIMV2"), 0, 0, 0, 0, 0, 0, &pSvc);
if(FAILED(hr))
return ERR::_4; // NOWARN (happens if WMI service is disabled)
}
hr = CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, 0, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, 0, EOAC_NONE);
if(FAILED(hr))
WARN_RETURN(ERR::_5);
return INFO::OK;
}
static void Shutdown()
{
pSvc->Release();
if (didInitCOM)
{
/* From MSDN documentation: A thread must call CoUninitialize once for each successful call
* it has made to the CoInitialize or CoInitializeEx function, including any call that returns
* S_FALSE. Only the CoUninitialize call corresponding to the CoInitialize or CoInitializeEx
* call that initialized the library can close it.
*
* So it should be perfectly safe to call this, since it balances out the CoInitialize in Init
*/
CoUninitialize();
didInitCOM = false;
}
}
void wmi_Shutdown()
{
ModuleShutdown(&initState, Shutdown);
}
Status wmi_GetClassInstances(const wchar_t* className, WmiInstances& instances)
{
RETURN_STATUS_IF_ERR(ModuleInit(&initState, Init));
IEnumWbemClassObjectPtr pEnum = 0;
wchar_t query[200];
swprintf_s(query, ARRAY_SIZE(query), L"SELECT * FROM %ls", className);
HRESULT hr = pSvc->ExecQuery(L"WQL", _bstr_t(query), WBEM_FLAG_FORWARD_ONLY|WBEM_FLAG_RETURN_IMMEDIATELY, 0, &pEnum);
if(FAILED(hr))
WARN_RETURN(ERR::FAIL);
ENSURE(pEnum);
for(;;)
{
IWbemClassObjectPtr pObj = 0;
ULONG numReturned = 0;
hr = pEnum->Next((LONG)WBEM_INFINITE, 1, &pObj, &numReturned);
if(FAILED(hr))
WARN_RETURN(ERR::FAIL);
if(numReturned == 0)
break;
ENSURE(pEnum);
WmiInstance instance;
pObj->BeginEnumeration(WBEM_FLAG_NONSYSTEM_ONLY);
for(;;)
{
BSTR name = NULL;
VARIANT value;
VariantInit(&value);
if(pObj->Next(0, &name, &value, 0, 0) != WBEM_S_NO_ERROR)
{
SysFreeString(name);
break;
}
instance[name] = value;
SysFreeString(name);
}
instances.push_back(instance);
}
return INFO::OK;
}

View File

@ -1,51 +0,0 @@
/* Copyright (c) 2010 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* wrapper for Windows Management Instrumentation
*/
#ifndef INCLUDED_WMI
#define INCLUDED_WMI
#include <map>
// note: we expose the VARIANT value as returned by WMI. this allows other
// modules to use the values they want directly, rather than forcing
// everything to be converted to/parsed from strings. it does drag in
// OLE headers, but this module is entirely Windows-specific anyway.
#define _WIN32_DCOM
#include "lib/sysdep/os/win/win.h"
#include <comdef.h> // VARIANT
// contains name and value of all instance properties
typedef std::map<std::wstring, VARIANT> WmiInstance;
typedef std::vector<WmiInstance> WmiInstances;
/**
* get all instances of the requested class.
**/
extern Status wmi_GetClassInstances(const wchar_t* className, WmiInstances& instances);
extern void wmi_Shutdown();
#endif // #ifndef INCLUDED_WMI

View File

@ -1,155 +0,0 @@
/* Copyright (c) 2010 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* sound card detection on Windows.
*/
#include "precompiled.h"
#include "lib/sysdep/snd.h"
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <set>
#include "lib/file/file_system.h"
#include "lib/sysdep/os/win/wdll_ver.h"
#include "lib/sysdep/os/win/wversion.h"
#include "lib/sysdep/os/win/wutil.h"
#include "lib/sysdep/os/win/wmi.h"
static bool IsOpenAlDllName(const Path::String& name)
{
// (matches "*oal.dll" and "*OpenAL*", as with OpenAL router's search)
return name.find(L"oal.dll") != Path::String::npos || name.find(L"OpenAL") != Path::String::npos;
}
// ensures each OpenAL DLL is only listed once (even if present in several
// directories on our search path).
typedef std::set<Path::String> StringSet;
// find all OpenAL DLLs in a dir.
// call in library search order (exe dir, then win sys dir); otherwise,
// DLLs in the executable's starting directory hide those of the
// same name in the system directory.
static void add_oal_dlls_in_dir(const OsPath& path, StringSet& dlls, VersionList& versionList)
{
CFileInfos files;
(void)GetDirectoryEntries(path, &files, 0);
for(size_t i = 0; i < files.size(); i++)
{
const Path::String name = files[i].Name().string();
if(!IsOpenAlDllName(name))
continue;
// already in StringSet (i.e. has already been wdll_ver_list_add-ed)
std::pair<StringSet::iterator, bool> ret = dlls.insert(name);
if(!ret.second) // insert failed - element already there
continue;
wdll_ver_Append(path / name, versionList);
}
}
//-----------------------------------------------------------------------------
// DirectSound driver version
// we've seen audio problems caused by buggy DirectSound drivers (because
// OpenAL can use them in its implementation), so their version should be
// retrieved as well. the only way I know of is to enumerate all DS devices.
//
// unfortunately this fails with Vista's DS emulation - it returns some
// GUID crap as the module name. to avoidc crashing when attempting to get
// the version info for that bogus driver path, we'll skip this code there.
// (delay-loading dsound.dll eliminates any overhead)
static OsPath directSoundDriverPath;
// store sound card name and path to DirectSound driver.
// called for each DirectSound driver, but aborts after first valid driver.
static BOOL CALLBACK DirectSoundCallback(void* guid, const wchar_t* UNUSED(description),
const wchar_t* module, void* UNUSED(cbData))
{
// skip first dummy entry (description == "Primary Sound Driver")
if(guid == NULL)
return TRUE; // continue calling
// note: $system\\drivers is not in LoadLibrary's search list,
// so we have to give the full pathname.
directSoundDriverPath = wutil_SystemPath()/"drivers" / module;
// we assume the first "driver name" (sound card) is the one we want;
// stick with that and stop calling.
return FALSE;
}
static const OsPath& GetDirectSoundDriverPath()
{
#define DS_OK 0
typedef BOOL (CALLBACK* LPDSENUMCALLBACKW)(void*, const wchar_t*, const wchar_t*, void*);
HMODULE hDsoundDll = LoadLibraryW(L"dsound.dll");
WUTIL_FUNC(pDirectSoundEnumerateW, HRESULT, (LPDSENUMCALLBACKW, void*));
WUTIL_IMPORT(hDsoundDll, DirectSoundEnumerateW, pDirectSoundEnumerateW);
if(pDirectSoundEnumerateW)
{
HRESULT ret = pDirectSoundEnumerateW(DirectSoundCallback, (void*)0);
ENSURE(ret == DS_OK);
}
FreeLibrary(hDsoundDll);
return directSoundDriverPath;
}
//-----------------------------------------------------------------------------
Status win_get_snd_info()
{
WmiInstances instances;
RETURN_STATUS_IF_ERR(wmi_GetClassInstances(L"Win32_SoundDevice", instances));
std::set<std::wstring> names; // get rid of duplicate "High Definition Audio Device" entries
for(WmiInstances::iterator it = instances.begin(); it != instances.end(); ++it)
{
if((*it)[L"Availability"].intVal != 8) // not offline
names.insert(std::wstring((*it)[L"ProductName"].bstrVal));
}
wchar_t* pos = snd_card;
for(std::set<std::wstring>::const_iterator it = names.begin(); it != names.end(); ++it)
{
const int ret = swprintf_s(pos, SND_CARD_LEN-(pos-snd_card), L"%ls; ", it->c_str());
if(ret > 0)
pos += ret;
}
// find all DLLs related to OpenAL and retrieve their versions.
VersionList versionList;
if(wversion_Number() < WVERSION_VISTA)
wdll_ver_Append(GetDirectSoundDriverPath(), versionList);
StringSet dlls; // ensures uniqueness
(void)add_oal_dlls_in_dir(wutil_ExecutablePath(), dlls, versionList);
(void)add_oal_dlls_in_dir(wutil_SystemPath(), dlls, versionList);
wcscpy_s(snd_drv_ver, SND_DRV_VER_LEN, versionList.c_str());
return INFO::OK;
}

View File

@ -1,28 +0,0 @@
/* Copyright (c) 2010 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef INCLUDED_WSND
#define INCLUDED_WSND
extern Status win_get_snd_info();
#endif // #ifndef INCLUDED_WSND

View File

@ -100,10 +100,6 @@
#define MUST_INIT_X11 0
#endif
#if OS_WIN
extern void wmi_Shutdown();
#endif
extern void restart_engine();
#include <iostream>
@ -757,12 +753,6 @@ void Shutdown(int flags)
g_Profiler2.ShutdownGPU();
#if OS_WIN
TIMER_BEGIN(L"shutdown wmi");
wmi_Shutdown();
TIMER_END(L"shutdown wmi");
#endif
// Free cursors before shutting down SDL, as they may depend on SDL.
cursor_shutdown();

View File

@ -20,6 +20,7 @@
#include "scriptinterface/ScriptInterface.h"
#include "lib/ogl.h"
#include "lib/snd.h"
#include "lib/svn_revision.h"
#include "lib/timer.h"
#include "lib/utf8.h"
@ -30,7 +31,6 @@
#include "lib/sysdep/gfx.h"
#include "lib/sysdep/numa.h"
#include "lib/sysdep/os_cpu.h"
#include "lib/sysdep/snd.h"
#if ARCH_X86_X64
# include "lib/sysdep/arch/x86_x64/cache.h"
# include "lib/sysdep/arch/x86_x64/topology.h"
@ -266,8 +266,8 @@ void RunHardwareDetection()
scriptInterface.SetProperty(settings, "gfx_card", gfx::CardName());
scriptInterface.SetProperty(settings, "gfx_drv_ver", gfx::DriverInfo());
scriptInterface.SetProperty(settings, "snd_card", std::wstring(snd_card));
scriptInterface.SetProperty(settings, "snd_drv_ver", std::wstring(snd_drv_ver));
scriptInterface.SetProperty(settings, "snd_card", snd_card);
scriptInterface.SetProperty(settings, "snd_drv_ver", snd_drv_ver);
ReportGLLimits(scriptInterface, settings);

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016 Wildfire Games.
/* Copyright (C) 2017 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -21,12 +21,12 @@
#include "lib/posix/posix_utsname.h"
#include "lib/ogl.h"
#include "lib/snd.h"
#include "lib/timer.h"
#include "lib/bits.h" // round_up
#include "lib/allocators/shared_ptr.h"
#include "lib/sysdep/sysdep.h" // sys_OpenFile
#include "lib/sysdep/gfx.h"
#include "lib/sysdep/snd.h"
#include "lib/sysdep/cpu.h"
#include "lib/sysdep/os_cpu.h"
#if ARCH_X86_X64
@ -137,8 +137,8 @@ void WriteSystemInfo()
fprintf(f, "Video Mode : %dx%d:%d\n", g_VideoMode.GetXRes(), g_VideoMode.GetYRes(), g_VideoMode.GetBPP());
// sound
fprintf(f, "Sound Card : %ls\n", snd_card);
fprintf(f, "Sound Drivers : %ls\n", snd_drv_ver);
fprintf(f, "Sound Card : %s\n", snd_card.c_str());
fprintf(f, "Sound Drivers : %s\n", snd_drv_ver.c_str());
// OpenGL extensions (write them last, since it's a lot of text)
const char* exts = ogl_ExtensionString();