#!/usr/bin/lua -- BindingGenerator.lua (c) hecks 2021 -- This script is a part of IrrlichtMT, released under the same license. -- By default we assume you're running this from /scripts/ -- and that you have the necessary headers there (gitignored for your convenience) local sourceTreePath = os.getenv( "IRRMTREPO" ) or ".."; -- Otherwise run this from wherever you want and set the above env variable. local glHeaderPath = os.getenv( "GLHEADERPATH" ) or "."; -- GL headers will be looked for in the current directory or GLHEADERPATH. -- At the moment we require: -- "glcorearb.h" -- "gl2ext.h" -- Files other than glcorearb.h are only parsed for vendor specific defines -- and aliases. Otherwise we only use what exists in glcorearb.h, further -- restricted to procedures that are either core or ARB. -- Emulate a portion of the libraries that this was written against. getmetatable( "" ).__index = string; getmetatable( "" ).__len = string.len; getmetatable( "" ).__call = string.format; function string:Split( pat ) local r = {}; local pos = 1; local from, to; while pos and pos <= #self do from, to = self:find( pat, pos ); if not from then break; end r[#r+1] = self:sub( pos, from - 1 ); pos = to + 1; end r[#r+1] = self:sub( pos, #self ); return r; end function string:TrimBothEnds() return self:gsub("^%s+",""):gsub("%s+$",""); end local List; List = function( t ) return setmetatable( t or {}, { __index = { Add = function( t, v ) t[#t+1] = v; end; AddFormat = function( t, str, ... ) t:Add( str( ... ) ); end; Where = function( t, f ) local r = {}; for i=1, #t do if f(t[i]) then r[#r+1] = t[i]; end end return List( r ); end; Select = function( t, f ) local r = {}; for i=1, #t do r[#r+1] = f( t[i] ); end return List( r ); end; Join = function( t, n ) local r = {}; for i=1, #t do r[i] = t[i]; end for i=1, #n do r[#r+1] = n[i]; end return List( r ); end; Concat = table.concat; }; } ); end ------------ Header parsing ------------ -- GL and GLES alike local driverVendors = { "NV", "AMD", "INTEL", "OVR", "QCOM", "IMG", "ANGLE", "APPLE", "MESA" } local vendorSuffixes = { "ARB", "EXT", "KHR", "OES", unpack( driverVendors ) }; local vendorSuffixPattern = {}; local constSuffixPattern = {}; for i=1, #vendorSuffixes do vendorSuffixPattern[i] = vendorSuffixes[i] .. "$"; constSuffixPattern[i] = ("_%s$")( vendorSuffixes[i] ); end local constBanned = {}; for i=1, #driverVendors do constBanned[driverVendors[i]] = true; end -- Strip the uppercase extension vendor suffix from a name. local function StripVendorSuffix( str, const ) local patterns = const and constSuffixPattern or vendorSuffixPattern; local n; for i=1, #patterns do str, n = str:gsub( patterns[i], "" ); if n > 0 then return str, vendorSuffixes[i]; end end return str; end -- Normalize the type of an argument or return, also stripping any suffix -- and normalizing all whitespace regions to single spaces. local function NormalizeType( str ) local chunks = str:Split( "%s+" ); for j=1, #chunks do chunks[j] = StripVendorSuffix( chunks[j] ); end local T = table.concat(chunks, " " ); return T:TrimBothEnds(); end -- Normalize an argument, returning the normalized type and the name separately, -- always sticking the * of a pointer to the type rather than the name. -- We need this to generate a normalized arg list and function signature (below) local function NormalizeArgument( str ) local chunks = str:Split( "%s+" ); for j=1, #chunks do chunks[j] = StripVendorSuffix( chunks[j] ); end local last = chunks[#chunks]; local name = last:match( "[%w_]+$" ); chunks[#chunks] = #name ~= #last and last:sub( 1, #last-#name) or nil; local T = table.concat(chunks, " " ):TrimBothEnds(); return T, name end -- Normalize an argument list so that two matching prototypes -- will produce the same table if fed to this function. local function NormalizeArgList( str ) local args = str:Split( ",%s*" ); local r = {}; for i=1, #args do local T, name = NormalizeArgument( args[i] ); r[i] = { T, name }; end return r; end -- Normalize a function signature into a unique string for keying -- in such a way that if two different GL procedures may be assigned -- to the same function pointer, this will produce an identical string for both. -- This makes it possible to detect function aliases that may work as a fallback. -- You still have to check for the appropriate extension. local function NormalizeFunctionSignature( T, str ) local args = str:Split( ",%s*" ); local r = {}; for i=1, #args do r[i] = NormalizeArgument( args[i] ); end return ("%s(%s)")( T, table.concat( r, ", " ) ); end -- Mangle the PFN name so that we don't collide with a -- typedef from any of the GL headers. local pfnFormat = "PFNGL%sPROC_MT"; --( T, name, args ) local typedefFormat = "\ttypedef %s (APIENTRYP %s) (%s);" -- Generate a PFN...GL style typedef for a procedure -- local function GetProcedureTypedef( proc ) local args = {}; for i=1, #proc.args do args[i] = ("%s %s")( unpack( proc.args[i] ) ) end return typedefFormat( proc.retType, pfnFormat( proc.name:upper() ), table.concat( args, ", " ) ); end local procedures = List(); local nameset = {}; local definitions = List(); local consts = List(); --[[ Structured procedure representation: ProcSpec = { string name; -- Normalized name as it appears in the GL spec string? vendor; -- Uppercase vendor string (ARB, EXT, AMD, NV etc) string signature; string retType; args = { { type, name } }; }; ]] -- Parse a whole header, extracting the data. local function ParseHeader( path, into, apiRegex, defs, consts, nameSet, noNewNames ) defs:AddFormat( "\t// %s", path ); local f = assert( io.open( path, "r" ), "Could not open " .. path ); for line in f:lines() do -- Do not parse PFN typedefs; they're easily reconstructible. local T, rawName, args = line:match( apiRegex ); if T then T = NormalizeType( T ); -- Strip the 'gl' namespace prefix. local procName = rawName:sub(3,-1); local name, vendor = StripVendorSuffix( procName ); if not (noNewNames and nameSet[name]) then nameSet[name] = true; into:Add{ name = name; vendor = vendor; -- pfnType = pfnFormat( procName:upper() ); signature = NormalizeFunctionSignature( T, args ); retType = T; args = NormalizeArgList( args ); }; end elseif ( line:find( "#" ) and not line:find( "#include" ) ) then local rawName, value = line:match( "#define%s+GL_([_%w]+)%s+0x(%w+)" ); if rawName and value then local name, vendor = StripVendorSuffix( rawName, true ); if not constBanned[vendor] then consts:Add{ name = name, vendor = vendor, value = "0x"..value }; end end ::skip:: elseif( line:find( "typedef" ) and not line:find( "%(" ) ) then -- Passthrough non-PFN typedefs defs:Add( "\t" .. line ); end end defs:Add ""; f:close(); end ------------ Parse the headers ------------ -- ES/gl2.h is a subset of glcorearb.h and does not need parsing. local funcRegex = "GLAPI%s+(.+)APIENTRY%s+(%w+)%s*%((.*)%)"; local funcRegexES = "GL_APICALL%s+(.+)GL_APIENTRY%s+(%w+)%s*%((.*)%)"; ParseHeader( glHeaderPath .. "/glcorearb.h", procedures, funcRegex, definitions, consts, nameset ); ParseHeader( glHeaderPath .. "/gl2ext.h", procedures, funcRegexES, List(), consts, nameset, true ); -- Typedefs are redirected to a dummy list here on purpose. -- The only unique typedef from gl2ext is this: definitions:Add "\ttypedef void *GLeglClientBufferEXT;"; ------------ Sort out constants ------------ local cppConsts = List(); do local constBuckets = {}; for i=1, #consts do local vendor = consts[i].vendor or "core"; constBuckets[consts[i].name] = constBuckets[consts[i].name] or {}; constBuckets[consts[i].name][vendor] = consts[i].value; end local names = {}; for i=1, #consts do local k = consts[i].name; local b = constBuckets[k]; if k == "WAIT_FAILED" or k == "DIFFERENCE" then -- This is why using #define as const is evil. k = "_" .. k; end if b and not names[k] then names[k] = true; -- I have empirically tested that constants in GL with the same name do not differ, -- at least for these suffixes. local v = b.core or b.KHR or b.ARB or b.OES or b.EXT; if v then local T = v:find( "ull" ) and "GLuint64" or "GLenum"; cppConsts:AddFormat( "\tstatic constexpr const %s %s = %s;", T, k, v ); end end end end ------------ Sort out procedures ------------ local procTable = {}; local coreProcedures = procedures:Where( function(x) return not x.vendor; end ); local arbProcedures = procedures:Where( function(x) return x.vendor == "ARB"; end ); -- Only consider core and ARB functions. local nameList = coreProcedures:Join( arbProcedures ):Select( function(p) return p.name; end ); local nameSet = {}; local uniqueNames = List(); for s, k in ipairs( nameList ) do if not nameSet[k] then nameSet[k] = true; uniqueNames:Add( k ); end end for i=1, #procedures do local p = procedures[i]; procTable[p.name] = procTable[p.name] or {}; local key = p.vendor or "core"; procTable[p.name][key] = p; end local priorityList = List{ "core", "ARB", "OES", "KHR" }; local typedefs = List(); local pointers = List(); local loader = List(); for s, str in ipairs( uniqueNames ) do pointers:Add( ("\t%s %s = NULL;")( pfnFormat( str:upper() ), str ) ); local typeDefGenerated = false; for i=1, #priorityList do local k = priorityList[i]; local proc = procTable[str][k] if proc then if not typeDefGenerated then typedefs:Add( GetProcedureTypedef( proc ) ); typeDefGenerated = true; end local vendor = k == "core" and "" or k; loader:AddFormat( '\tif (!%s) %s = (%s)cmgr->getProcAddress("%s");\n', str, str, pfnFormat( proc.name:upper() ), ("gl%s%s")(str,vendor) ); end end end ------------ Write files ------------ -- Write loader header local f = io.open( sourceTreePath .. "/include/mt_opengl.h", "wb" ); f:write[[ // This code was generated by scripts/BindingGenerator.lua // Do not modify it, modify and run the generator instead. #pragma once #include #include #include "IrrCompileConfig.h" // for IRRLICHT_API #include "irrTypes.h" #include "IContextManager.h" #include #ifndef APIENTRY #define APIENTRY KHRONOS_APIENTRY #endif #ifndef APIENTRYP #define APIENTRYP APIENTRY * #endif #ifndef GLAPI #define GLAPI extern #endif ]]; f:write[[ class OpenGLProcedures { private: ]]; f:write( definitions:Concat( "\n" ) ); f:write( "\n" ); f:write[[ // The script will miss this particular typedef thinking it's a PFN, // so we have to paste it in manually. It's the only such type in OpenGL. typedef void (APIENTRY *GLDEBUGPROC) (GLenum source,GLenum type,GLuint id,GLenum severity,GLsizei length,const GLchar *message,const void *userParam); ]] f:write( typedefs:Concat( "\n" ) ); f:write( "\n\n" ); f:write [[ std::unordered_set extensions; public: // Call this once after creating the context. void LoadAllProcedures(irr::video::IContextManager *cmgr); // Check if an extension is supported. inline bool IsExtensionPresent(const std::string &ext) const { return extensions.count(ext) > 0; } ]]; f:write( pointers:Concat( "\n" ) ); f:write( "\n\n" ); f:write( cppConsts:Concat( "\n" ) ); f:write( "\n\n" ); f:write[[ static constexpr const GLenum ZERO = 0; static constexpr const GLenum ONE = 1; static constexpr const GLenum NONE = 0; ]]; f:write( "};\n" ); f:write( "\n// Global GL procedures object.\n" ); f:write( "IRRLICHT_API extern OpenGLProcedures GL;\n" ); f:close(); -- Write loader implementation f = io.open( sourceTreePath .. "/src/mt_opengl_loader.cpp", "wb" ); f:write[[ // This code was generated by scripts/BindingGenerator.lua // Do not modify it, modify and run the generator instead. #include "mt_opengl.h" #include #include OpenGLProcedures GL = OpenGLProcedures(); void OpenGLProcedures::LoadAllProcedures(irr::video::IContextManager *cmgr) { ]]; f:write( loader:Concat() ); f:write[[ // OpenGL 3 way to enumerate extensions GLint ext_count = 0; GetIntegerv(NUM_EXTENSIONS, &ext_count); extensions.reserve(ext_count); for (GLint k = 0; k < ext_count; k++) { auto tmp = GetStringi(EXTENSIONS, k); if (tmp) extensions.emplace((char*)tmp); } if (!extensions.empty()) return; // OpenGL 2 / ES 2 way to enumerate extensions auto ext_str = GetString(EXTENSIONS); if (!ext_str) return; // get the extension string, chop it up std::stringstream ext_ss((char*)ext_str); std::string tmp; while (std::getline(ext_ss, tmp, ' ')) extensions.emplace(tmp); } ]]; f:close();