openGL program object 研究
openGL program object是一个比较特殊的对象,因为他的老对手DX没有(DX的shader直接set在DeviceContext上),他的后辈vulkan和metal没有(pipeline object上)。今天写一篇文章,学习一下openGL program object的一些东西。
创建openGL program
openGL program的创建其实很灵活,可以从glsl源码创建,也可以从编译好的openGL binary program创建,还可以使用SPIR-V文件创建(openGL 4.5 +)
1. Use this to load and compile the shader program.
使用glsl创建program,就是compile shader文件,attach shader,link program。
GLuint programHandle;
void compileShaderProgram() {
std::cout << "Compiling shader program" << std::endl;
//////////////////////////////////////////////////////
/////////// Vertex shader //////////////////////////
//////////////////////////////////////////////////////
// Load contents of file
std::ifstream inFile("shader/basic.vert.glsl");
if (!inFile) {
fprintf(stderr, "Error opening file: shader/basic.vert.glsl\n");
exit(1);
}
std::stringstream code;
code << inFile.rdbuf();
inFile.close();
string codeStr(code.str());
// Create the shader object
GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
if (0 == vertShader) {
std::cerr << "Error creating vertex shader." << std::endl;
exit(EXIT_FAILURE);
}
// Load the source code into the shader object
const GLchar* codeArray[] = { codeStr.c_str() };
glShaderSource(vertShader, 1, codeArray, NULL);
// Compile the shader
glCompileShader(vertShader);
// Check compilation status
GLint result;
glGetShaderiv(vertShader, GL_COMPILE_STATUS, &result);
if (GL_FALSE == result) {
std::cerr << "Vertex shader compilation failed!" << std::endl;
std::cerr << getShaderInfoLog(vertShader) << std::endl;
exit(EXIT_FAILURE);
}
//////////////////////////////////////////////////////
/////////// Fragment shader //////////////////////////
//////////////////////////////////////////////////////
// Load contents of file into shaderCode here
std::ifstream fragFile("shader/basic.frag.glsl");
if (!fragFile) {
fprintf(stderr, "Error opening file: shader/basic.frag.glsl\n");
exit(1);
}
std::stringstream fragCode;
fragCode << fragFile.rdbuf();
fragFile.close();
codeStr = fragCode.str();
// Create the shader object
GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
if (0 == fragShader) {
fprintf(stderr, "Error creating fragment shader.\n");
exit(1);
}
// Load the source code into the shader object
codeArray[0] = codeStr.c_str();
glShaderSource(fragShader, 1, codeArray, NULL);
// Compile the shader
glCompileShader(fragShader);
// Check compilation status
glGetShaderiv(fragShader, GL_COMPILE_STATUS, &result);
if (GL_FALSE == result) {
std::cerr << "Fragment shader compilation failed!" << std::endl;
std::cerr << getShaderInfoLog(vertShader) << std::endl;
exit(EXIT_FAILURE);
}
linkMe(vertShader, fragShader);
}
void linkMe(GLint vertShader, GLint fragShader)
{
// Create the program object
programHandle = glCreateProgram();
if(0 == programHandle) {
std::cerr << "Error creating program object." << std::endl;
exit(EXIT_FAILURE);
}
// Bind index 0 to the shader input variable "VertexPosition"
//glBindAttribLocation(programHandle, 0, "VertexPosition");
// Bind index 1 to the shader input variable "VertexColor"
//glBindAttribLocation(programHandle, 1, "VertexColor");
// Attach the shaders to the program object
glAttachShader( programHandle, vertShader );
glAttachShader( programHandle, fragShader );
// Link the program
glLinkProgram( programHandle );
// Check for successful linking
GLint status;
glGetProgramiv( programHandle, GL_LINK_STATUS, &status );
if (GL_FALSE == status) {
std::cerr << "Failed to link shader program!" << std::endl;
std::cerr << getProgramInfoLog(programHandle) << std::endl;
exit(EXIT_FAILURE);
}
// Clean up shader objects
glDetachShader(programHandle, vertShader);
glDetachShader(programHandle, fragShader);
glDeleteShader(vertShader);
glDeleteShader(fragShader);
glUseProgram( programHandle );
}
在 glUseProgram
之前,我调用了 glDetachShader
。其实这样完全没有关系,对于openGL program,只有在调用 glLinkProgram
才会真正使用attach的shader生成新的program。换句话说,只要不调用 glLinkProgram
,program上的shader可以自由的attach和detach,不会对功能有任何影响。
对于某些硬件,支持单shader program(比如只有vertex shader没有fragment shader)。这其实非常有用,对于现代渲染管线(F+,CFR),early-Z pass至关重要,只需要顶点光栅化写depth buffer,完全不需要fragment shader以及之后所有操作。单shader program的作用就凸现出来了(当然和vulkan那种定制pipeline,还有使用compute shader定制pipeline相比还是很原始)。
在上面的程序中,我detach shader之后才执行 delete shader,但是其实shader attach之后就可以删除了。至于detach命令,一般情况下都不会用到。在delete program的时候,会自动detach并delete所有相关shader。而据 openGL 文档描述,在 render context 确认没有对 program 的使用后,也会自动 delete program。
2. Load a binary (pre-compiled) shader program. (file: “shader/program.bin”)
使用二进制文件创建 program 的过程非常简单。
GLuint programHandle;
void loadShaderBinary(GLint format) {
std::cout << "Loading shader binary: shader/program.bin (format = " << format << ")" << std::endl;
// Create the program object
programHandle = glCreateProgram();
if (0 == programHandle) {
std::cerr << "Error creating program object." << std::endl;
exit(EXIT_FAILURE);
}
std::ifstream inStream("shader/program.bin", std::ios::binary);
std::istreambuf_iterator<char> startIt(inStream), endIt;
std::vector<char> buffer(startIt, endIt);
inStream.close();
glProgramBinary(programHandle, format, buffer.data(), buffer.size());
// Check for successful linking
GLint status;
glGetProgramiv(programHandle, GL_LINK_STATUS, &status);
if (GL_FALSE == status) {
std::cerr << "Failed to load binary shader program!" << std::endl;
std::cerr << getProgramInfoLog(programHandle) << std::endl;
exit(EXIT_FAILURE);
}
glUseProgram(programHandle);
}
openGL program看似和DXBC差不多,其实差异甚大。对于openGL program,不同制造商甚至同一制造商的不同代产品,都有可能有差异,因此binary最好是在本机第一次运行时保存下来,在第二次运行以后再使用(这个技术在RCPS3,cemu等模拟器中都有使用)。
3. Load a SPIR-V shader program. (files: “shader/vert.spv” and “shader/frag.spv”)
在openGL 4.5版本中带来了使用SPIR-V shader的功能,个人猜测是给向vulkan迁移的程序一个过渡。
void loadSpirvShader() {
std::cout << "Loading SPIR-V shaders." << std::endl;
GLint status;
GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
{
std::cout << "Loading SPIR-V binary: shader/vert.spv" << std::endl;
std::ifstream inStream("shader/vert.spv", std::ios::binary);
std::istreambuf_iterator<char> startIt(inStream), endIt;
std::vector<char> buffer(startIt, endIt);
inStream.close();
glShaderBinary(1, &vertShader, GL_SHADER_BINARY_FORMAT_SPIR_V_ARB, buffer.data(), buffer.size());
}
glSpecializeShaderARB( vertShader, "main", 0, 0, 0);
glGetShaderiv(vertShader, GL_COMPILE_STATUS, &status);
if( GL_FALSE == status ) {
std::cerr << "Failed to load vertex shader (SPIR-V)" << std::endl;
std::cerr << getShaderInfoLog(vertShader) << std::endl;
exit(-1);
}
GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);
{
std::cout << "Loading SPIR-V binary: shader/frag.spv" << std::endl;
std::ifstream inStream("shader/frag.spv", std::ios::binary);
std::istreambuf_iterator<char> startIt(inStream), endIt;
std::vector<char> buffer(startIt, endIt);
inStream.close();
glShaderBinary(1, &fragShader, GL_SHADER_BINARY_FORMAT_SPIR_V_ARB, buffer.data(), buffer.size());
}
glSpecializeShaderARB( fragShader, "main", 0, 0, 0);
glGetShaderiv(fragShader, GL_COMPILE_STATUS, &status);
if( GL_FALSE == status ) {
std::cerr << "Failed to load fragment shader (SPIR-V)" << std::endl;
std::cerr << getShaderInfoLog(fragShader) << std::endl;
exit(-1);
}
// Create the program object
programHandle = glCreateProgram();
if (0 == programHandle) {
std::cerr << "Error creating program object." << std::endl;
exit(1);
}
glAttachShader(programHandle, vertShader);
glAttachShader(programHandle, fragShader);
glLinkProgram(programHandle);
glGetProgramiv(programHandle, GL_LINK_STATUS, &status);
if (GL_FALSE == status) {
std::cerr << "Failed to link SPIR-V program" << std::endl;
std::cerr << getProgramInfoLog(programHandle) << std::endl;
exit(EXIT_FAILURE);
}
glUseProgram(programHandle);
}
整体过程和使用glsl源码差不多,但其实有个特殊的地方,就是 glSpecializeShaderARB
,这个函数可以指定程序的入口函数,也就是说SPIR-V可以类似DX那样,vertex和fragment使用同一个二进制的shader object,只不过入口函数不同。
严格来说,DXBC和SPIR-V功能上几乎完全等价,现在也有某些开源项目,来实现DXBC/DXIL和SPIR-V的双向转换(比如龚敏敏大神的Dilithium)。
其实就算没有openGL 4.5,也可以使用 SPIRV-CROSS 将SPIR-V转成GLSL/ESSl shader给低版本的openGL和openGL ES使用。
4. Save a binary (pre-compiled) shader program. (file: “shader/program.bin”)
void writeShaderBinary() {
GLint formats;
glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &formats);
std::cout << "Number of binary formats supported by this driver = " << formats << std::endl;
if( formats > 0 ) {
GLint length;
glGetProgramiv(programHandle, GL_PROGRAM_BINARY_LENGTH, &length);
std::cout << "Program binary length = " << length << std::endl;
std::vector<GLubyte> buffer(length);
GLenum format;
glGetProgramBinary(programHandle, length, NULL, &format, buffer.data());
std::string fName("shader/program.bin");
std::cout << "Writing to " << fName << ", binary format = " << format << std::endl;
std::ofstream out(fName.c_str(), std::ios::binary);
out.write( reinterpret_cast<char *>(buffer.data()), length );
out.close();
} else {
std::cout << "No binary formats supported by this driver. Unable to write shader binary." << std::endl;
}
}
之前提到的3种方法创建的program均可以保存binary program。
openGL program 到 vulkan pipeline
vulkan pipeline作为后发,肯定是比openGL program要先进的。就个人认为,vulkan的pipeline,相当于openGL的program和DX11的pipeline的加和。
vulkan pipeline既包含的openGL program管理各个编程阶段shader,检查变量可用性,剔除无用变量,优化shader的代码的责任,又包含DX11 pipeline设置各个render state属性,设置attribute和uniform变量,管理各个render state某些功能开启和关闭情况的功能。并且vulkan pipeline还能复用,并像openGL program一样存成缓存文件下次程序运行时使用。
可以说vulkan/metal中的pipeline,是对openGL program和DX pipeline的继承和发展。
Comments