openclonk/src/graphics/C4Shader.cpp

589 lines
15 KiB
C++

/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2014-2015, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/
#include "C4Include.h"
#include "C4Shader.h"
#include "C4Application.h"
#include "graphics/C4DrawGL.h"
struct C4ShaderPosName {
int Position; const char *Name;
};
C4ShaderPosName C4SH_PosNames[] = {
{ C4Shader_PositionInit, "init" },
{ C4Shader_PositionCoordinate, "coordinate" },
{ C4Shader_PositionTexture, "texture" },
{ C4Shader_PositionMaterial, "material" },
{ C4Shader_PositionNormal, "normal" },
{ C4Shader_PositionLight, "light" },
{ C4Shader_PositionColor, "color" },
{ C4Shader_PositionFinish, "finish" },
{ C4Shader_Vertex_TexCoordPos, "texcoord" },
{ C4Shader_Vertex_NormalPos, "normal" },
{ C4Shader_Vertex_PositionPos, "position" }
};
C4Shader::C4Shader()
: iTexCoords(0)
#ifndef USE_CONSOLE
, hVert(0), hFrag(0), hProg(0)
, pUniforms(NULL)
#endif
{
}
C4Shader::~C4Shader()
{
Clear();
}
void C4Shader::AddVertexSlice(int iPos, const char *szText)
{
AddSlice(VertexSlices, iPos, szText, NULL, 0);
}
void C4Shader::AddFragmentSlice(int iPos, const char *szText, const char *szSource, int iSourceTime)
{
AddSlice(FragmentSlices, iPos, szText, szSource, iSourceTime);
}
void C4Shader::AddVertexSlices(const char *szWhat, const char *szText, const char *szSource, int iSourceTime)
{
AddSlices(VertexSlices, szWhat, szText, szSource, iSourceTime);
}
void C4Shader::AddFragmentSlices(const char *szWhat, const char *szText, const char *szSource, int iSourceTime)
{
AddSlices(FragmentSlices, szWhat, szText, szSource, iSourceTime);
}
void C4Shader::AddSlice(ShaderSliceList& slices, int iPos, const char *szText, const char *szSource, int iSourceTime)
{
ShaderSlice Slice;
Slice.Position = iPos;
Slice.Text.Copy(szText);
Slice.Source = szSource;
Slice.SourceTime = iSourceTime;
slices.push_back(Slice);
}
void C4Shader::AddSlices(ShaderSliceList& slices, const char *szWhat, const char *szText, const char *szSource, int iSourceTime)
{
const char *pStart = szText, *pPos = szText;
int iDepth = -1;
int iPosition = -1;
bool fGotContent = false; // Anything in the slice apart from comments and white-space?
// Find slices
while(*pPos) {
// Comment? Might seem silly, but we don't want to get confused by braces in comments...
if (*pPos == '/' && *(pPos + 1) == '/') {
pPos += 2;
while (*pPos && *pPos != '\n') pPos++;
continue;
}
if (*pPos == '/' && *(pPos + 1) == '*') {
pPos += 2;
while (*pPos && (*pPos != '*' || *(pPos+1) != '/')) pPos++;
if (*pPos) pPos += 2;
continue;
}
// Opening brace?
if (*pPos == '{') {
iDepth++; pPos++;
continue;
}
if (*pPos == '}') {
// End of slice?
if (iPosition != -1 && !iDepth) {
// Have a new slice!
if (fGotContent)
{
StdStrBuf Str; Str.Copy(pStart, pPos - pStart);
AddSlice(slices, iPosition, Str.getData(), szSource, iSourceTime);
}
iPosition = -1;
pStart = pPos+1;
fGotContent = false;
}
if (iDepth >= 0)
iDepth--;
pPos++;
continue;
}
// New slice? We need a newline followed by "slice". Don't do
// the depth check, so that we also recognize slices inside
// an ifdefed-out "void main() {" block.
//if (iDepth < 0 && *pPos == '\n') {
if (*pPos == '\n') {
if (SEqual2(pPos+1, "slice") && !isalnum(*(pPos+6))) {
const char *pSliceEnd = pPos; pPos += 6;
while(isspace(*pPos)) pPos++;
if(*pPos != '(') { pPos++; continue; }
pPos++;
// Now let's parse the position
iPosition = ParsePosition(szWhat, &pPos);
if (iPosition != -1) {
// Make sure a closing parenthesis
while(isspace(*pPos)) pPos++;
if(*pPos != ')') { pPos++; continue; }
pPos++;
// Make sure an opening brace follows
while(isspace(*pPos)) pPos++;
if (*pPos == '{') {
// Add code before "slice" as new slice
if (fGotContent)
{
StdStrBuf Str; Str.Copy(pStart, pSliceEnd - pStart);
AddSlice(slices, -1, Str.getData(), szSource, iSourceTime);
}
iDepth = 0;
pStart = pPos+1;
fGotContent = false;
} else {
ShaderLogF(" gl: Missing opening brace in %s!", szWhat);
}
pPos++;
continue;
}
}
}
// Otherwise: Continue
if (!isspace(*pPos)) fGotContent = true;
pPos++;
}
// Add final slice
if (fGotContent)
{
StdStrBuf Str; Str.Copy(pStart, pPos - pStart);
AddSlice(slices, iPosition, Str.getData(), szSource, iSourceTime);
}
}
int C4Shader::ParsePosition(const char *szWhat, const char **ppPos)
{
const char *pPos = *ppPos;
while (isspace(*pPos)) pPos++;
// Expect a name
const char *pStart = pPos;
while (isalnum(*pPos)) pPos++;
StdStrBuf Name; Name.Copy(pStart, pPos - pStart);
// Lookup name
int iPosition = -1;
for (int i = 0; i < sizeof(C4SH_PosNames) / sizeof(*C4SH_PosNames); i++) {
if (SEqual(Name.getData(), C4SH_PosNames[i].Name)) {
iPosition = C4SH_PosNames[i].Position;
break;
}
}
if (iPosition == -1) {
ShaderLogF(" gl: Unknown slice position in %s: %s", szWhat, Name.getData());
return -1;
}
// Add modifier
while (isspace(*pPos)) pPos++;
if (*pPos == '+') {
int iMod, iModLen;
if (!sscanf(pPos+1, "%d%n", &iMod, &iModLen)) {
ShaderLogF(" gl: Invalid slice modifier in %s", szWhat);
return -1;
}
iPosition += iMod;
pPos += 1+iModLen;
}
if (*pPos == '-') {
int iMod, iModLen;
if (!sscanf(pPos+1, "%d%n", &iMod, &iModLen)) {
ShaderLogF(" gl: Invalid slice modifier in %s", szWhat);
return -1;
}
iPosition -= iMod;
pPos += 1+iModLen;
}
// Everything okay!
*ppPos = pPos;
return iPosition;
}
bool C4Shader::LoadSlices(C4GroupSet *pGroups, const char *szFile)
{
// Search for our shaders
C4Group *pGroup = pGroups->FindEntry(szFile);
if(!pGroup) return false;
// Load it, save the path for later reloading
StdStrBuf Shader;
if(!pGroup->LoadEntryString(szFile, &Shader))
return false;
// If it physically exists, save back creation time so we
// can automatically reload it if it changes
StdStrBuf Source = FormatString("%s" DirSep "%s", pGroup->GetFullName().getData(), szFile);
int iSourceTime = 0;
if(FileExists(Source.getData()))
iSourceTime = FileTime(Source.getData());
// Load
StdStrBuf What = FormatString("file %s", Config.AtRelativePath(Source.getData()));
AddFragmentSlices(What.getData(), Shader.getData(), Source.getData(), iSourceTime);
return true;
}
void C4Shader::AddVertexDefaults()
{
AddVertexSlice(C4Shader_Vertex_PositionPos, "gl_Position = ftransform();\n");
}
#ifndef USE_CONSOLE
GLenum C4Shader::AddTexCoord(const char *szName)
{
// Make sure we have enough space
assert(iTexCoords < C4Shader_MaxTexCoords);
if(iTexCoords >= C4Shader_MaxTexCoords)
return -1;
// Add slices
StdStrBuf Code = FormatString("gl_TexCoord[%d] = gl_MultiTexCoord%d;\n", iTexCoords, iTexCoords);
AddVertexSlice(C4Shader_Vertex_TexCoordPos, Code.getData());
Code.Format("#define %s gl_TexCoord[%d]\n", szName, iTexCoords);
AddFragmentSlice(-1, Code.getData());
return GL_TEXTURE0 + iTexCoords++;
}
#endif
void C4Shader::ClearSlices()
{
VertexSlices.clear();
FragmentSlices.clear();
iTexCoords = 0;
}
void C4Shader::Clear()
{
#ifndef USE_CONSOLE
if (!hProg) return;
// Need to be detached, then deleted
glDetachObjectARB(hProg, hFrag);
glDetachObjectARB(hProg, hVert);
glDeleteObjectARB(hFrag);
glDeleteObjectARB(hVert);
glDeleteObjectARB(hProg);
hFrag = hVert = hProg = 0;
// Clear uniform data
delete[] pUniforms; pUniforms = NULL;
iUniformCount = 0;
#endif
}
bool C4Shader::Init(const char *szWhat, const char **szUniforms)
{
#ifndef USE_CONSOLE
// No support?
if(!GLEW_ARB_fragment_program)
{
Log(" gl: no shader support!");
return false;
}
// Clear old shader first
if (hProg) Clear();
#endif
// Dump
if (C4Shader::IsLogging())
{
ShaderLogF("******** Vertex shader for %s:", szWhat);
ShaderLog(Build(VertexSlices, true).getData());
ShaderLogF("******** Fragment shader for %s:", szWhat);
ShaderLog(Build(FragmentSlices, true).getData());
}
#ifndef USE_CONSOLE
// Attempt to create shaders
StdStrBuf VertexShader = Build(VertexSlices),
FragmentShader = Build(FragmentSlices);
hVert = Create(GL_VERTEX_SHADER_ARB,
FormatString("%s vertex shader", szWhat).getData(),
VertexShader.getData());
hFrag = Create(GL_FRAGMENT_SHADER_ARB,
FormatString("%s fragment shader", szWhat).getData(),
FragmentShader.getData());
if(!hFrag || !hVert)
return false;
// Link program
hProg = glCreateProgramObjectARB();
glAttachObjectARB(hProg, hVert);
glAttachObjectARB(hProg, hFrag);
// Bind all input variables
for (int i = 0; i <= VAI_BoneWeightsMax - VAI_BoneWeights; ++i)
{
glBindAttribLocation(hProg, VAI_BoneWeights + i, FormatString("oc_BoneWeights%d", i).getData());
glBindAttribLocation(hProg, VAI_BoneIndices + i, FormatString("oc_BoneIndices%d", i).getData());
}
glLinkProgramARB(hProg);
// Link successful?
DumpInfoLog(FormatString("%s shader program", szWhat).getData(), hProg);
if(GetObjectStatus(hProg, GL_OBJECT_LINK_STATUS_ARB) != 1) {
Clear();
ShaderLogF(" gl: Failed to link %s shader!", szWhat);
return false;
}
ShaderLogF(" gl: %s shader linked successfully", szWhat);
// Okay, allocate uniform array
iUniformCount = 0;
while (szUniforms[iUniformCount])
iUniformCount++;
pUniforms = new GLint[iUniformCount];
// Get uniform locations. Note this is expected to fail for a few of them
// because the respective uniforms got optimized out!
for (int i = 0; i < iUniformCount; i++)
pUniforms[i] = glGetUniformLocationARB(hProg, szUniforms[i]);
#endif
return true;
}
bool C4Shader::Refresh(const char *szWhat, const char **szUniforms)
{
// Find a slice where the source file has updated
ShaderSliceList::iterator pSlice;
for (pSlice = FragmentSlices.begin(); pSlice != FragmentSlices.end(); pSlice++)
if (pSlice->Source.getLength() &&
FileExists(pSlice->Source.getData()) &&
FileTime(pSlice->Source.getData()) > pSlice->SourceTime)
break;
if (pSlice == FragmentSlices.end()) return true;
StdCopyStrBuf Source = pSlice->Source;
// Okay, remove all slices that came from this file
ShaderSliceList::iterator pNext;
for (; pSlice != FragmentSlices.end(); pSlice = pNext)
{
pNext = pSlice; pNext++;
if (SEqual(pSlice->Source.getData(), Source.getData()))
FragmentSlices.erase(pSlice);
}
// Load new shader
char szParentPath[_MAX_PATH+1]; C4Group Group;
StdStrBuf Shader;
GetParentPath(Source.getData(),szParentPath);
if(!Group.Open(szParentPath) ||
!Group.LoadEntryString(GetFilename(Source.getData()),&Shader) ||
!Group.Close())
{
ShaderLogF(" gl: Failed to refresh %s shader from %s!", szWhat, Source.getData());
return Refresh(szWhat, szUniforms);
}
// Load slices
int iSourceTime = FileTime(Source.getData());
StdStrBuf WhatSrc = FormatString("file %s", Config.AtRelativePath(Source.getData()));
AddFragmentSlices(WhatSrc.getData(), Shader.getData(), Source.getData(), iSourceTime);
// Reinitialise
if (!Init(szWhat, szUniforms))
return false;
// Retry
return Refresh(szWhat, szUniforms);
}
StdStrBuf C4Shader::Build(const ShaderSliceList &Slices, bool fDebug)
{
// At the start of the shader set the #version and number of
// available uniforms
StdStrBuf Buf;
#ifndef USE_CONSOLE
GLint iMaxFrags = 0, iMaxVerts = 0;
glGetIntegerv(GL_MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB, &iMaxFrags);
glGetIntegerv(GL_MAX_VERTEX_UNIFORM_COMPONENTS_ARB, &iMaxVerts);
#else
int iMaxFrags = INT_MAX, iMaxVerts = INT_MAX;
#endif
Buf.Format("#version %d\n"
"#define MAX_FRAGMENT_UNIFORM_COMPONENTS %d\n"
"#define MAX_VERTEX_UNIFORM_COMPONENTS %d\n",
C4Shader_Version, iMaxFrags, iMaxVerts);
// Put slices
int iPos = -1, iNextPos = -1;
do
{
iPos = iNextPos; iNextPos = C4Shader_LastPosition+1;
// Add all slices at the current level
if (fDebug && iPos > 0)
Buf.AppendFormat("\t// Position %d:\n", iPos);
for (ShaderSliceList::const_iterator pSlice = Slices.begin(); pSlice != Slices.end(); pSlice++)
{
if (pSlice->Position < iPos) continue;
if (pSlice->Position > iPos)
{
iNextPos = Min(iNextPos, pSlice->Position);
continue;
}
// Same position - add slice!
if (fDebug)
{
if (pSlice->Source.getLength())
Buf.AppendFormat("\t// Slice from %s:\n", pSlice->Source.getData());
else
Buf.Append("\t// Built-in slice:\n");
}
Buf.Append(pSlice->Text);
if (Buf[Buf.getLength()-1] != '\n')
Buf.AppendChar('\n');
}
// Add seperator - only priority (-1) is top-level
if (iPos == -1) {
Buf.Append("void main() {\n");
}
}
while (iNextPos <= C4Shader_LastPosition);
// Terminate
Buf.Append("}\n");
return Buf;
}
#ifndef USE_CONSOLE
GLhandleARB C4Shader::Create(GLenum iShaderType, const char *szWhat, const char *szShader)
{
// Create shader
GLhandleARB hShader = glCreateShaderObjectARB(iShaderType);
// Compile
glShaderSourceARB(hShader, 1, &szShader, 0);
glCompileShaderARB(hShader);
// Dump any information to log
DumpInfoLog(szWhat, hShader);
// Success?
if(GetObjectStatus(hShader, GL_OBJECT_COMPILE_STATUS_ARB) == 1)
return hShader;
// Did not work :/
glDeleteObjectARB(hShader);
return 0;
}
void C4Shader::DumpInfoLog(const char *szWhat, GLhandleARB hShader)
{
// Get length of info line
int iLength = 0;
glGetObjectParameterivARB(hShader, GL_OBJECT_INFO_LOG_LENGTH_ARB, &iLength);
if(iLength <= 1) return;
// Allocate buffer, get data
char *pBuf = new char [iLength + 1];
int iActualLength = 0;
glGetInfoLogARB(hShader, iLength, &iActualLength, pBuf);
if(iActualLength > iLength || iActualLength <= 0) return;
// Terminate, log
pBuf[iActualLength] = '\0';
ShaderLogF(" gl: Compiling %s:", szWhat);
ShaderLog(pBuf);
delete[] pBuf;
}
int C4Shader::GetObjectStatus(GLhandleARB hObj, GLenum type)
{
int iStatus = 0;
glGetObjectParameterivARB(hObj, type, &iStatus);
return iStatus;
}
#endif
bool C4Shader::IsLogging() { return !!Application.isEditor; }
#ifndef USE_CONSOLE
GLint C4ShaderCall::AllocTexUnit(int iUniform, GLenum iType)
{
// Want to bind uniform automatically? If not, the caller will take
// care of it.
if (iUniform >= 0) {
// If uniform isn't used, we should skip this. Also check texunit range.
if (!pShader->HaveUniform(iUniform)) return 0;
assert(iUnits < C4ShaderCall_MaxUnits);
if (iUnits >= C4ShaderCall_MaxUnits) return 0;
// Set the uniform
SetUniform1i(iUniform, iUnits);
}
// Activate the texture
GLint hTex = GL_TEXTURE0 + iUnits;
glActiveTexture(hTex);
hUnit[iUnits] = iType;
glEnable(iType);
iUnits++;
return hTex;
}
void C4ShaderCall::Start()
{
assert(!fStarted);
assert(pShader->hProg != 0); // Shader must be initialized
// Activate shader
glUseProgramObjectARB(pShader->hProg);
fStarted = true;
}
void C4ShaderCall::Finish()
{
// Remove shader
if (fStarted) {
glUseProgramObjectARB(0);
}
// Deactivate all texture units
for (int i = iUnits; i > 0; i--)
{
glActiveTexture(GL_TEXTURE0 + i - 1);
glDisable(hUnit[i - 1]);
}
iUnits = 0;
fStarted = false;
}
#endif