/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ShaderProgram.h" #include "graphics/Color.h" #include "graphics/PreprocessorWrapper.h" #include "graphics/ShaderManager.h" #include "graphics/TextureManager.h" #include "ps/CLogger.h" #include "ps/containers/StaticVector.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "renderer/backend/gl/Device.h" #include "renderer/backend/gl/DeviceCommandContext.h" #define USE_SHADER_XML_VALIDATION 1 #if USE_SHADER_XML_VALIDATION #include "ps/XML/RelaxNG.h" #include "ps/XML/XMLWriter.h" #endif #include #include #include #include namespace Renderer { namespace Backend { namespace GL { namespace { struct Binding { Binding(int a, int b) : first(a), second(b) { } Binding() : first(-1), second(-1) { } /** * Returns whether this uniform attribute is active in the shader. * If not then there's no point calling Uniform() to set its value. */ bool Active() const { return first != -1 || second != -1; } int first; int second; }; int GetStreamMask(const VertexAttributeStream stream) { return 1 << static_cast(stream); } GLint GLSizeFromFormat(const Format format) { GLint size = 1; if (format == Renderer::Backend::Format::R32_SFLOAT || format == Renderer::Backend::Format::R16_SINT) size = 1; else if ( format == Renderer::Backend::Format::R8G8_UNORM || format == Renderer::Backend::Format::R8G8_UINT || format == Renderer::Backend::Format::R16G16_SINT || format == Renderer::Backend::Format::R32G32_SFLOAT) size = 2; else if (format == Renderer::Backend::Format::R32G32B32_SFLOAT) size = 3; else if ( format == Renderer::Backend::Format::R32G32B32A32_SFLOAT || format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT) size = 4; else debug_warn("Unsupported format."); return size; } GLenum GLTypeFromFormat(const Format format) { GLenum type = GL_FLOAT; if (format == Renderer::Backend::Format::R32_SFLOAT || format == Renderer::Backend::Format::R32G32_SFLOAT || format == Renderer::Backend::Format::R32G32B32_SFLOAT || format == Renderer::Backend::Format::R32G32B32A32_SFLOAT) type = GL_FLOAT; else if ( format == Renderer::Backend::Format::R16_SINT || format == Renderer::Backend::Format::R16G16_SINT) type = GL_SHORT; else if ( format == Renderer::Backend::Format::R8G8_UNORM || format == Renderer::Backend::Format::R8G8_UINT || format == Renderer::Backend::Format::R8G8B8A8_UNORM || format == Renderer::Backend::Format::R8G8B8A8_UINT) type = GL_UNSIGNED_BYTE; else debug_warn("Unsupported format."); return type; } GLboolean NormalizedFromFormat(const Format format) { switch (format) { case Format::R8G8_UNORM: FALLTHROUGH; case Format::R8G8B8_UNORM: FALLTHROUGH; case Format::R8G8B8A8_UNORM: FALLTHROUGH; case Format::R16_UNORM: FALLTHROUGH; case Format::R16G16_UNORM: return GL_TRUE; default: break; } return GL_FALSE; } int GetAttributeLocationFromStream( CDevice* device, const VertexAttributeStream stream) { // Old mapping makes sense only if we have an old/low-end hardware. Else we // need to use sequential numbering to fix #3054. We use presence of // compute shaders as a check that the hardware has universal CUs. if (device->GetCapabilities().computeShaders) { return static_cast(stream); } else { // Map known semantics onto the attribute locations documented by NVIDIA: // https://download.nvidia.com/developer/Papers/2005/OpenGL_2.0/NVIDIA_OpenGL_2.0_Support.pdf // https://developer.download.nvidia.com/opengl/glsl/glsl_release_notes.pdf switch (stream) { case VertexAttributeStream::POSITION: return 0; case VertexAttributeStream::NORMAL: return 2; case VertexAttributeStream::COLOR: return 3; case VertexAttributeStream::UV0: return 8; case VertexAttributeStream::UV1: return 9; case VertexAttributeStream::UV2: return 10; case VertexAttributeStream::UV3: return 11; case VertexAttributeStream::UV4: return 12; case VertexAttributeStream::UV5: return 13; case VertexAttributeStream::UV6: return 14; case VertexAttributeStream::UV7: return 15; } } debug_warn("Invalid attribute semantics"); return 0; } bool PreprocessShaderFile( bool arb, const CShaderDefines& defines, const VfsPath& path, const char* stage, CStr& source, std::vector& fileDependencies) { CVFSFile file; if (file.Load(g_VFS, path) != PSRETURN_OK) { LOGERROR("Failed to load shader file: '%s'", path.string8()); return false; } CPreprocessorWrapper preprocessor( [arb, &fileDependencies](const CStr& includePath, CStr& out) -> bool { const VfsPath includeFilePath( (arb ? L"shaders/arb/" : L"shaders/glsl/") + wstring_from_utf8(includePath)); // Add dependencies anyway to reload the shader when the file is // appeared. fileDependencies.push_back(includeFilePath); CVFSFile includeFile; if (includeFile.Load(g_VFS, includeFilePath) != PSRETURN_OK) { LOGERROR("Failed to load shader include file: '%s'", includeFilePath.string8()); return false; } out = includeFile.GetAsString(); return true; }); preprocessor.AddDefines(defines); if (!arb) preprocessor.AddDefine(stage, "1"); #if CONFIG2_GLES if (!arb) { // GLES defines the macro "GL_ES" in its GLSL preprocessor, // but since we run our own preprocessor first, we need to explicitly // define it here preprocessor.AddDefine("GL_ES", "1"); } #endif source = preprocessor.Preprocess(file.GetAsString()); return true; } #if !CONFIG2_GLES std::tuple GetElementTypeAndCountFromString(const CStr& str) { #define CASE(MATCH_STRING, TYPE, ELEMENT_TYPE, ELEMENT_COUNT) \ if (str == MATCH_STRING) return {GL_ ## TYPE, GL_ ## ELEMENT_TYPE, ELEMENT_COUNT} CASE("float", FLOAT, FLOAT, 1); CASE("vec2", FLOAT_VEC2, FLOAT, 2); CASE("vec3", FLOAT_VEC3, FLOAT, 3); CASE("vec4", FLOAT_VEC4, FLOAT, 4); CASE("mat2", FLOAT_MAT2, FLOAT, 4); CASE("mat3", FLOAT_MAT3, FLOAT, 9); CASE("mat4", FLOAT_MAT4, FLOAT, 16); #if !CONFIG2_GLES // GL ES 2.0 doesn't support non-square matrices. CASE("mat2x3", FLOAT_MAT2x3, FLOAT, 6); CASE("mat2x4", FLOAT_MAT2x4, FLOAT, 8); CASE("mat3x2", FLOAT_MAT3x2, FLOAT, 6); CASE("mat3x4", FLOAT_MAT3x4, FLOAT, 12); CASE("mat4x2", FLOAT_MAT4x2, FLOAT, 8); CASE("mat4x3", FLOAT_MAT4x3, FLOAT, 12); #endif // A somewhat incomplete listing, missing "shadow" and "rect" versions // which are interpreted as 2D (NB: our shadowmaps may change // type based on user config). #if CONFIG2_GLES if (str == "sampler1D") debug_warn(L"sampler1D not implemented on GLES"); #else CASE("sampler1D", SAMPLER_1D, TEXTURE_1D, 1); #endif CASE("sampler2D", SAMPLER_2D, TEXTURE_2D, 1); #if CONFIG2_GLES if (str == "sampler2DShadow") debug_warn(L"sampler2DShadow not implemented on GLES"); if (str == "sampler3D") debug_warn(L"sampler3D not implemented on GLES"); #else CASE("sampler2DShadow", SAMPLER_2D_SHADOW, TEXTURE_2D, 1); CASE("sampler3D", SAMPLER_3D, TEXTURE_3D, 1); #endif CASE("samplerCube", SAMPLER_CUBE, TEXTURE_CUBE_MAP, 1); #undef CASE return {0, 0, 0}; } #endif // !CONFIG2_GLES bool CompileGLSL(GLuint shader, const VfsPath& file, const CStr& code) { const char* codeString = code.c_str(); GLint codeLength = code.length(); glShaderSource(shader, 1, &codeString, &codeLength); ogl_WarnIfError(); glCompileShader(shader); GLint ok = 0; glGetShaderiv(shader, GL_COMPILE_STATUS, &ok); GLint length = 0; glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length); // Apparently sometimes GL_INFO_LOG_LENGTH is incorrectly reported as 0 // (http://code.google.com/p/android/issues/detail?id=9953) if (!ok && length == 0) length = 4096; if (length > 1) { std::unique_ptr infolog = std::make_unique(length); glGetShaderInfoLog(shader, length, nullptr, infolog.get()); if (ok) LOGMESSAGE("Info when compiling shader '%s':\n%s", file.string8(), infolog.get()); else LOGERROR("Failed to compile shader '%s':\n%s", file.string8(), infolog.get()); } ogl_WarnIfError(); return ok; } } // anonymous namespace IDevice* CVertexInputLayout::GetDevice() { return m_Device; } #if !CONFIG2_GLES class CShaderProgramARB final : public CShaderProgram { public: CShaderProgramARB( CDevice* device, const VfsPath& path, const VfsPath& vertexFilePath, const VfsPath& fragmentFilePath, const CShaderDefines& defines, const std::map>& vertexIndices, const std::map>& fragmentIndices, int streamflags) : CShaderProgram(streamflags), m_Device(device) { glGenProgramsARB(1, &m_VertexProgram); glGenProgramsARB(1, &m_FragmentProgram); std::vector newFileDependencies = {path, vertexFilePath, fragmentFilePath}; CStr vertexCode; if (!PreprocessShaderFile(true, defines, vertexFilePath, "STAGE_VERTEX", vertexCode, newFileDependencies)) return; CStr fragmentCode; if (!PreprocessShaderFile(true, defines, fragmentFilePath, "STAGE_FRAGMENT", fragmentCode, newFileDependencies)) return; m_FileDependencies = std::move(newFileDependencies); // TODO: replace by scoped bind. m_Device->GetActiveCommandContext()->SetGraphicsPipelineState( MakeDefaultGraphicsPipelineStateDesc()); if (!Compile(GL_VERTEX_PROGRAM_ARB, "vertex", m_VertexProgram, vertexFilePath, vertexCode)) return; if (!Compile(GL_FRAGMENT_PROGRAM_ARB, "fragment", m_FragmentProgram, fragmentFilePath, fragmentCode)) return; for (const auto& index : vertexIndices) { BindingSlot& bindingSlot = GetOrCreateBindingSlot(index.first); bindingSlot.vertexProgramLocation = index.second.second; const auto [type, elementType, elementCount] = GetElementTypeAndCountFromString(index.second.first); bindingSlot.type = type; bindingSlot.elementType = elementType; bindingSlot.elementCount = elementCount; } for (const auto& index : fragmentIndices) { BindingSlot& bindingSlot = GetOrCreateBindingSlot(index.first); bindingSlot.fragmentProgramLocation = index.second.second; const auto [type, elementType, elementCount] = GetElementTypeAndCountFromString(index.second.first); if (bindingSlot.type && type != bindingSlot.type) { LOGERROR("CShaderProgramARB: vertex and fragment program uniforms with the same name should have the same type."); } bindingSlot.type = type; bindingSlot.elementType = elementType; bindingSlot.elementCount = elementCount; } } ~CShaderProgramARB() override { glDeleteProgramsARB(1, &m_VertexProgram); glDeleteProgramsARB(1, &m_FragmentProgram); } bool Compile(GLuint target, const char* targetName, GLuint program, const VfsPath& file, const CStr& code) { ogl_WarnIfError(); glBindProgramARB(target, program); ogl_WarnIfError(); glProgramStringARB(target, GL_PROGRAM_FORMAT_ASCII_ARB, (GLsizei)code.length(), code.c_str()); if (ogl_SquelchError(GL_INVALID_OPERATION)) { GLint errPos = 0; glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errPos); int errLine = std::count(code.begin(), code.begin() + std::min((int)code.length(), errPos + 1), '\n') + 1; char* errStr = (char*)glGetString(GL_PROGRAM_ERROR_STRING_ARB); LOGERROR("Failed to compile %s program '%s' (line %d):\n%s", targetName, file.string8(), errLine, errStr); return false; } glBindProgramARB(target, 0); ogl_WarnIfError(); return true; } void Bind(CShaderProgram* previousShaderProgram) override { if (previousShaderProgram) previousShaderProgram->Unbind(); glBindProgramARB(GL_VERTEX_PROGRAM_ARB, m_VertexProgram); glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, m_FragmentProgram); BindClientStates(); } void Unbind() override { glBindProgramARB(GL_VERTEX_PROGRAM_ARB, 0); glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, 0); UnbindClientStates(); } IDevice* GetDevice() override { return m_Device; } int32_t GetBindingSlot(const CStrIntern name) const override { auto it = m_BindingSlotsMapping.find(name); return it == m_BindingSlotsMapping.end() ? -1 : it->second; } TextureUnit GetTextureUnit(const int32_t bindingSlot) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return { 0, 0, 0 }; TextureUnit textureUnit; textureUnit.type = m_BindingSlots[bindingSlot].type; textureUnit.target = m_BindingSlots[bindingSlot].elementType; textureUnit.unit = m_BindingSlots[bindingSlot].fragmentProgramLocation; return textureUnit; } void SetUniform( const int32_t bindingSlot, const float value) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT) { LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform type (expected float)"); return; } SetUniform(m_BindingSlots[bindingSlot], value, 0.0f, 0.0f, 0.0f); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC2) { LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform type (expected vec2)"); return; } SetUniform(m_BindingSlots[bindingSlot], valueX, valueY, 0.0f, 0.0f); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC3) { LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform type (expected vec3)"); return; } SetUniform(m_BindingSlots[bindingSlot], valueX, valueY, valueZ, 0.0f); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ, const float valueW) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC4) { LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform type (expected vec4)"); return; } SetUniform(m_BindingSlots[bindingSlot], valueX, valueY, valueZ, valueW); } void SetUniform( const int32_t bindingSlot, PS::span values) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].elementType != GL_FLOAT) { LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform element type (expected float)"); return; } if (m_BindingSlots[bindingSlot].elementCount > static_cast(values.size())) { LOGERROR( "CShaderProgramARB::SetUniform(): Invalid uniform element count (expected: %zu passed: %zu)", m_BindingSlots[bindingSlot].elementCount, values.size()); return; } const GLenum type = m_BindingSlots[bindingSlot].type; if (type == GL_FLOAT) SetUniform(m_BindingSlots[bindingSlot], values[0], 0.0f, 0.0f, 0.0f); else if (type == GL_FLOAT_VEC2) SetUniform(m_BindingSlots[bindingSlot], values[0], values[1], 0.0f, 0.0f); else if (type == GL_FLOAT_VEC3) SetUniform(m_BindingSlots[bindingSlot], values[0], values[1], values[2], 0.0f); else if (type == GL_FLOAT_VEC4) SetUniform(m_BindingSlots[bindingSlot], values[0], values[1], values[2], values[3]); else if (type == GL_FLOAT_MAT4) SetUniformMatrix(m_BindingSlots[bindingSlot], values); else LOGERROR("CShaderProgramARB::SetUniform(): Invalid uniform type (expected float, vec2, vec3, vec4, mat4)"); ogl_WarnIfError(); } std::vector GetFileDependencies() const override { return m_FileDependencies; } private: struct BindingSlot { CStrIntern name; int vertexProgramLocation; int fragmentProgramLocation; GLenum type; GLenum elementType; GLint elementCount; }; BindingSlot& GetOrCreateBindingSlot(const CStrIntern name) { auto it = m_BindingSlotsMapping.find(name); if (it == m_BindingSlotsMapping.end()) { m_BindingSlotsMapping[name] = m_BindingSlots.size(); BindingSlot bindingSlot{}; bindingSlot.name = name; bindingSlot.vertexProgramLocation = -1; bindingSlot.fragmentProgramLocation = -1; bindingSlot.elementType = 0; bindingSlot.elementCount = 0; m_BindingSlots.emplace_back(std::move(bindingSlot)); return m_BindingSlots.back(); } else return m_BindingSlots[it->second]; } void SetUniform( const BindingSlot& bindingSlot, const float v0, const float v1, const float v2, const float v3) { SetUniform(GL_VERTEX_PROGRAM_ARB, bindingSlot.vertexProgramLocation, v0, v1, v2, v3); SetUniform(GL_FRAGMENT_PROGRAM_ARB, bindingSlot.fragmentProgramLocation, v0, v1, v2, v3); } void SetUniform( const GLenum target, const int location, const float v0, const float v1, const float v2, const float v3) { if (location >= 0) { glProgramLocalParameter4fARB( target, static_cast(location), v0, v1, v2, v3); } } void SetUniformMatrix( const BindingSlot& bindingSlot, PS::span values) { const size_t mat4ElementCount = 16; ENSURE(values.size() == mat4ElementCount); SetUniformMatrix(GL_VERTEX_PROGRAM_ARB, bindingSlot.vertexProgramLocation, values); SetUniformMatrix(GL_FRAGMENT_PROGRAM_ARB, bindingSlot.fragmentProgramLocation, values); } void SetUniformMatrix( const GLenum target, const int location, PS::span values) { if (location >= 0) { glProgramLocalParameter4fARB( target, static_cast(location + 0), values[0], values[4], values[8], values[12]); glProgramLocalParameter4fARB( target, static_cast(location + 1), values[1], values[5], values[9], values[13]); glProgramLocalParameter4fARB( target, static_cast(location + 2), values[2], values[6], values[10], values[14]); glProgramLocalParameter4fARB( target, static_cast(location + 3), values[3], values[7], values[11], values[15]); } } CDevice* m_Device = nullptr; std::vector m_FileDependencies; GLuint m_VertexProgram; GLuint m_FragmentProgram; std::vector m_BindingSlots; std::unordered_map m_BindingSlotsMapping; }; #endif // !CONFIG2_GLES class CShaderProgramGLSL final : public CShaderProgram { public: CShaderProgramGLSL( CDevice* device, const CStr& name, const VfsPath& programPath, PS::span> shaderStages, const CShaderDefines& defines, const std::map& vertexAttribs, int streamflags) : CShaderProgram(streamflags), m_Device(device), m_Name(name), m_VertexAttribs(vertexAttribs) { for (std::map::iterator it = m_VertexAttribs.begin(); it != m_VertexAttribs.end(); ++it) m_ActiveVertexAttributes.emplace_back(it->second); std::sort(m_ActiveVertexAttributes.begin(), m_ActiveVertexAttributes.end()); m_Program = 0; m_FileDependencies = {programPath}; for (const auto& [path, type] : shaderStages) { UNUSED2(type); m_FileDependencies.emplace_back(path); } // TODO: replace by scoped bind. m_Device->GetActiveCommandContext()->SetGraphicsPipelineState( MakeDefaultGraphicsPipelineStateDesc()); std::vector newFileDependencies = {programPath}; for (const auto& [path, type] : shaderStages) { GLuint shader = glCreateShader(type); newFileDependencies.emplace_back(path); #if !CONFIG2_GLES if (m_Device->GetCapabilities().debugLabels) glObjectLabel(GL_SHADER, shader, -1, path.string8().c_str()); #endif m_ShaderStages.emplace_back(type, shader); const char* stageDefine = "STAGE_UNDEFINED"; switch (type) { case GL_VERTEX_SHADER: stageDefine = "STAGE_VERTEX"; break; case GL_FRAGMENT_SHADER: stageDefine = "STAGE_FRAGMENT"; break; case GL_COMPUTE_SHADER: stageDefine = "STAGE_COMPUTE"; break; default: break; } CStr source; if (!PreprocessShaderFile(false, defines, path, stageDefine, source, newFileDependencies)) return; if (source.empty()) { LOGERROR("Failed to preprocess shader: '%s'", path.string8()); return; } #if CONFIG2_GLES // Ugly hack to replace desktop GLSL 1.10/1.20 with GLSL ES 1.00, // and also to set default float precision for fragment shaders source.Replace("#version 110\n", "#version 100\nprecision highp float;\n"); source.Replace("#version 110\r\n", "#version 100\nprecision highp float;\n"); source.Replace("#version 120\n", "#version 100\nprecision highp float;\n"); source.Replace("#version 120\r\n", "#version 100\nprecision highp float;\n"); #endif if (!CompileGLSL(shader, path, source)) return; } m_FileDependencies = std::move(newFileDependencies); if (!Link(programPath)) return; } ~CShaderProgramGLSL() override { if (m_Program) glDeleteProgram(m_Program); for (ShaderStage& stage : m_ShaderStages) glDeleteShader(stage.shader); } bool Link(const VfsPath& path) { ENSURE(!m_Program); m_Program = glCreateProgram(); #if !CONFIG2_GLES if (m_Device->GetCapabilities().debugLabels) { glObjectLabel(GL_PROGRAM, m_Program, -1, m_Name.c_str()); } #endif for (ShaderStage& stage : m_ShaderStages) { glAttachShader(m_Program, stage.shader); ogl_WarnIfError(); } // Set up the attribute bindings explicitly, since apparently drivers // don't always pick the most efficient bindings automatically, // and also this lets us hardcode indexes into VertexPointer etc for (std::map::iterator it = m_VertexAttribs.begin(); it != m_VertexAttribs.end(); ++it) glBindAttribLocation(m_Program, it->second, it->first.c_str()); glLinkProgram(m_Program); GLint ok = 0; glGetProgramiv(m_Program, GL_LINK_STATUS, &ok); GLint length = 0; glGetProgramiv(m_Program, GL_INFO_LOG_LENGTH, &length); if (!ok && length == 0) length = 4096; if (length > 1) { char* infolog = new char[length]; glGetProgramInfoLog(m_Program, length, NULL, infolog); if (ok) LOGMESSAGE("Info when linking program '%s':\n%s", path.string8(), infolog); else LOGERROR("Failed to link program '%s':\n%s", path.string8(), infolog); delete[] infolog; } ogl_WarnIfError(); if (!ok) return false; Bind(nullptr); ogl_WarnIfError(); // Reorder sampler units to decrease redundant texture unit changes when // samplers bound in a different order. const std::unordered_map requiredUnits = { {CStrIntern("baseTex"), 0}, {CStrIntern("normTex"), 1}, {CStrIntern("specTex"), 2}, {CStrIntern("aoTex"), 3}, {CStrIntern("shadowTex"), 4}, {CStrIntern("losTex"), 5}, }; std::vector occupiedUnits; GLint numUniforms = 0; glGetProgramiv(m_Program, GL_ACTIVE_UNIFORMS, &numUniforms); ogl_WarnIfError(); for (GLint i = 0; i < numUniforms; ++i) { // TODO: use GL_ACTIVE_UNIFORM_MAX_LENGTH for the size. char name[256] = {0}; GLsizei nameLength = 0; GLint size = 0; GLenum type = 0; glGetActiveUniform(m_Program, i, ARRAY_SIZE(name), &nameLength, &size, &type, name); ogl_WarnIfError(); const GLint location = glGetUniformLocation(m_Program, name); // OpenGL specification is a bit vague about a name returned by glGetActiveUniform. // NVIDIA drivers return uniform name with "[0]", Intel Windows drivers without; while (nameLength > 3 && name[nameLength - 3] == '[' && name[nameLength - 2] == '0' && name[nameLength - 1] == ']') { nameLength -= 3; } name[nameLength] = 0; const CStrIntern nameIntern(name); m_BindingSlotsMapping[nameIntern] = m_BindingSlots.size(); BindingSlot bindingSlot{}; bindingSlot.name = nameIntern; bindingSlot.location = location; bindingSlot.size = size; bindingSlot.type = type; bindingSlot.isTexture = false; #define CASE(TYPE, ELEMENT_TYPE, ELEMENT_COUNT) \ case GL_ ## TYPE: \ bindingSlot.elementType = GL_ ## ELEMENT_TYPE; \ bindingSlot.elementCount = ELEMENT_COUNT; \ break; switch (type) { CASE(FLOAT, FLOAT, 1); CASE(FLOAT_VEC2, FLOAT, 2); CASE(FLOAT_VEC3, FLOAT, 3); CASE(FLOAT_VEC4, FLOAT, 4); CASE(INT, INT, 1); CASE(FLOAT_MAT2, FLOAT, 4); CASE(FLOAT_MAT3, FLOAT, 9); CASE(FLOAT_MAT4, FLOAT, 16); #if !CONFIG2_GLES // GL ES 2.0 doesn't support non-square matrices. CASE(FLOAT_MAT2x3, FLOAT, 6); CASE(FLOAT_MAT2x4, FLOAT, 8); CASE(FLOAT_MAT3x2, FLOAT, 6); CASE(FLOAT_MAT3x4, FLOAT, 12); CASE(FLOAT_MAT4x2, FLOAT, 8); CASE(FLOAT_MAT4x3, FLOAT, 12); #endif } #undef CASE // Assign sampler uniforms to sequential texture units. switch (type) { case GL_SAMPLER_2D: bindingSlot.elementType = GL_TEXTURE_2D; bindingSlot.isTexture = true; break; case GL_SAMPLER_CUBE: bindingSlot.elementType = GL_TEXTURE_CUBE_MAP; bindingSlot.isTexture = true; break; #if !CONFIG2_GLES case GL_SAMPLER_2D_SHADOW: bindingSlot.elementType = GL_TEXTURE_2D; bindingSlot.isTexture = true; break; case GL_IMAGE_2D: bindingSlot.elementType = GL_IMAGE_2D; bindingSlot.isTexture = true; break; #endif default: break; } if (bindingSlot.isTexture) { const auto it = requiredUnits.find(nameIntern); const int unit = it == requiredUnits.end() ? -1 : it->second; bindingSlot.elementCount = unit; if (unit != -1) { if (unit >= static_cast(occupiedUnits.size())) occupiedUnits.resize(unit + 1); occupiedUnits[unit] = true; } } if (bindingSlot.elementType == 0) { LOGERROR("CShaderProgramGLSL::Link: unsupported uniform type: 0x%04x", static_cast(type)); } m_BindingSlots.emplace_back(std::move(bindingSlot)); } for (BindingSlot& bindingSlot : m_BindingSlots) { if (!bindingSlot.isTexture) continue; if (bindingSlot.elementCount == -1) { // We need to find a minimal available unit. int unit = 0; while (unit < static_cast(occupiedUnits.size()) && occupiedUnits[unit]) ++unit; if (unit >= static_cast(occupiedUnits.size())) occupiedUnits.resize(unit + 1); occupiedUnits[unit] = true; bindingSlot.elementCount = unit; } // Link uniform to unit. glUniform1i(bindingSlot.location, bindingSlot.elementCount); ogl_WarnIfError(); } // TODO: verify that we're not using more samplers than is supported Unbind(); return true; } void Bind(CShaderProgram* previousShaderProgram) override { CShaderProgramGLSL* previousShaderProgramGLSL = nullptr; if (previousShaderProgram) previousShaderProgramGLSL = static_cast(previousShaderProgram); ENSURE(this != previousShaderProgramGLSL); glUseProgram(m_Program); if (previousShaderProgramGLSL) { std::vector::iterator itPrevious = previousShaderProgramGLSL->m_ActiveVertexAttributes.begin(); std::vector::iterator itNext = m_ActiveVertexAttributes.begin(); while ( itPrevious != previousShaderProgramGLSL->m_ActiveVertexAttributes.end() || itNext != m_ActiveVertexAttributes.end()) { if (itPrevious != previousShaderProgramGLSL->m_ActiveVertexAttributes.end() && itNext != m_ActiveVertexAttributes.end()) { if (*itPrevious == *itNext) { ++itPrevious; ++itNext; } else if (*itPrevious < *itNext) { glDisableVertexAttribArray(*itPrevious); ++itPrevious; } else if (*itPrevious > *itNext) { glEnableVertexAttribArray(*itNext); ++itNext; } } else if (itPrevious != previousShaderProgramGLSL->m_ActiveVertexAttributes.end()) { glDisableVertexAttribArray(*itPrevious); ++itPrevious; } else if (itNext != m_ActiveVertexAttributes.end()) { glEnableVertexAttribArray(*itNext); ++itNext; } } } else { for (const int index : m_ActiveVertexAttributes) glEnableVertexAttribArray(index); } m_ValidStreams = 0; } void Unbind() override { glUseProgram(0); for (const int index : m_ActiveVertexAttributes) glDisableVertexAttribArray(index); } IDevice* GetDevice() override { return m_Device; } int32_t GetBindingSlot(const CStrIntern name) const override { auto it = m_BindingSlotsMapping.find(name); return it == m_BindingSlotsMapping.end() ? -1 : it->second; } TextureUnit GetTextureUnit(const int32_t bindingSlot) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return { 0, 0, 0 }; TextureUnit textureUnit; textureUnit.type = m_BindingSlots[bindingSlot].type; textureUnit.target = m_BindingSlots[bindingSlot].elementType; textureUnit.unit = m_BindingSlots[bindingSlot].elementCount; return textureUnit; } void SetUniform( const int32_t bindingSlot, const float value) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT || m_BindingSlots[bindingSlot].size != 1) { LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform type (expected float) '%s'", m_BindingSlots[bindingSlot].name.c_str()); return; } glUniform1f(m_BindingSlots[bindingSlot].location, value); ogl_WarnIfError(); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC2 || m_BindingSlots[bindingSlot].size != 1) { LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform type (expected vec2) '%s'", m_BindingSlots[bindingSlot].name.c_str()); return; } glUniform2f(m_BindingSlots[bindingSlot].location, valueX, valueY); ogl_WarnIfError(); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC3 || m_BindingSlots[bindingSlot].size != 1) { LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform type (expected vec3) '%s'", m_BindingSlots[bindingSlot].name.c_str()); return; } glUniform3f(m_BindingSlots[bindingSlot].location, valueX, valueY, valueZ); ogl_WarnIfError(); } void SetUniform( const int32_t bindingSlot, const float valueX, const float valueY, const float valueZ, const float valueW) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].type != GL_FLOAT_VEC4 || m_BindingSlots[bindingSlot].size != 1) { LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform type (expected vec4) '%s'", m_BindingSlots[bindingSlot].name.c_str()); return; } glUniform4f(m_BindingSlots[bindingSlot].location, valueX, valueY, valueZ, valueW); ogl_WarnIfError(); } void SetUniform( const int32_t bindingSlot, PS::span values) override { if (bindingSlot < 0 || bindingSlot >= static_cast(m_BindingSlots.size())) return; if (m_BindingSlots[bindingSlot].elementType != GL_FLOAT) { LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform element type (expected float) '%s'", m_BindingSlots[bindingSlot].name.c_str()); return; } if (m_BindingSlots[bindingSlot].size == 1 && m_BindingSlots[bindingSlot].elementCount > static_cast(values.size())) { LOGERROR( "CShaderProgramGLSL::SetUniform(): Invalid uniform element count (expected: %zu passed: %zu) '%s'", m_BindingSlots[bindingSlot].elementCount, values.size(), m_BindingSlots[bindingSlot].name.c_str()); return; } const GLint location = m_BindingSlots[bindingSlot].location; const GLenum type = m_BindingSlots[bindingSlot].type; if (type == GL_FLOAT) glUniform1fv(location, 1, values.data()); else if (type == GL_FLOAT_VEC2) glUniform2fv(location, 1, values.data()); else if (type == GL_FLOAT_VEC3) glUniform3fv(location, 1, values.data()); else if (type == GL_FLOAT_VEC4) glUniform4fv(location, 1, values.data()); else if (type == GL_FLOAT_MAT4) { // For case of array of matrices we might pass less number of matrices. const GLint size = std::min( m_BindingSlots[bindingSlot].size, static_cast(values.size() / 16)); glUniformMatrix4fv(location, size, GL_FALSE, values.data()); } else LOGERROR("CShaderProgramGLSL::SetUniform(): Invalid uniform type (expected float, vec2, vec3, vec4, mat4) '%s'", m_BindingSlots[bindingSlot].name.c_str()); ogl_WarnIfError(); } void VertexAttribPointer( const VertexAttributeStream stream, const Format format, const uint32_t offset, const uint32_t stride, const VertexAttributeRate rate, const void* data) override { const int attributeLocation = GetAttributeLocationFromStream(m_Device, stream); std::vector::const_iterator it = std::lower_bound(m_ActiveVertexAttributes.begin(), m_ActiveVertexAttributes.end(), attributeLocation); if (it == m_ActiveVertexAttributes.end() || *it != attributeLocation) return; const GLint size = GLSizeFromFormat(format); const GLenum type = GLTypeFromFormat(format); const GLboolean normalized = NormalizedFromFormat(format); glVertexAttribPointer( attributeLocation, size, type, normalized, stride, static_cast(data) + offset); #if CONFIG2_GLES ENSURE(!m_Device->GetCapabilities().instancing); UNUSED2(rate); #else if (rate == VertexAttributeRate::PER_INSTANCE) ENSURE(m_Device->GetCapabilities().instancing); if (m_Device->GetCapabilities().instancing) { glVertexAttribDivisorARB(attributeLocation, rate == VertexAttributeRate::PER_INSTANCE ? 1 : 0); } #endif m_ValidStreams |= GetStreamMask(stream); } std::vector GetFileDependencies() const override { return m_FileDependencies; } private: struct ShaderStage { GLenum type; GLuint shader; }; CDevice* m_Device = nullptr; CStr m_Name; std::vector m_FileDependencies; std::map m_VertexAttribs; // Sorted list of active vertex attributes. std::vector m_ActiveVertexAttributes; GLuint m_Program; // 5 = max(compute, vertex + tesselation (control + evaluation) + geometry + fragment). PS::StaticVector m_ShaderStages; struct BindingSlot { CStrIntern name; GLint location; GLint size; GLenum type; GLenum elementType; GLint elementCount; bool isTexture; }; std::vector m_BindingSlots; std::unordered_map m_BindingSlotsMapping; }; CShaderProgram::CShaderProgram(int streamflags) : m_StreamFlags(streamflags), m_ValidStreams(0) { } CShaderProgram::~CShaderProgram() = default; // static std::unique_ptr CShaderProgram::Create(CDevice* device, const CStr& name, const CShaderDefines& baseDefines) { PROFILE2("loading shader"); PROFILE2_ATTR("name: %s", name.c_str()); VfsPath xmlFilename = L"shaders/" + wstring_from_utf8(name) + L".xml"; CXeromyces XeroFile; PSRETURN ret = XeroFile.Load(g_VFS, xmlFilename); if (ret != PSRETURN_OK) return nullptr; #if USE_SHADER_XML_VALIDATION { // Serialize the XMB data and pass it to the validator XMLWriter_File shaderFile; shaderFile.SetPrettyPrint(false); shaderFile.XMB(XeroFile); bool ok = CXeromyces::ValidateEncoded("shader", name, shaderFile.GetOutput()); if (!ok) return nullptr; } #endif // Define all the elements and attributes used in the XML file #define EL(x) int el_##x = XeroFile.GetElementID(#x) #define AT(x) int at_##x = XeroFile.GetAttributeID(#x) EL(compute); EL(define); EL(fragment); EL(stream); EL(uniform); EL(vertex); AT(attribute); AT(file); AT(if); AT(loc); AT(name); AT(type); AT(value); #undef AT #undef EL CPreprocessorWrapper preprocessor; preprocessor.AddDefines(baseDefines); XMBElement root = XeroFile.GetRoot(); const bool isGLSL = root.GetAttributes().GetNamedItem(at_type) == "glsl"; VfsPath vertexFile; VfsPath fragmentFile; CShaderDefines defines = baseDefines; std::map> vertexUniforms; std::map> fragmentUniforms; std::map vertexAttribs; int streamFlags = 0; VfsPath computeFile; XERO_ITER_EL(root, child) { if (child.GetNodeName() == el_define) { defines.Add(CStrIntern(child.GetAttributes().GetNamedItem(at_name)), CStrIntern(child.GetAttributes().GetNamedItem(at_value))); } else if (child.GetNodeName() == el_vertex) { vertexFile = L"shaders/" + child.GetAttributes().GetNamedItem(at_file).FromUTF8(); XERO_ITER_EL(child, param) { XMBAttributeList attributes = param.GetAttributes(); CStr cond = attributes.GetNamedItem(at_if); if (!cond.empty() && !preprocessor.TestConditional(cond)) continue; if (param.GetNodeName() == el_uniform) { vertexUniforms[CStrIntern(attributes.GetNamedItem(at_name))] = std::make_pair(attributes.GetNamedItem(at_type), attributes.GetNamedItem(at_loc).ToInt()); } else if (param.GetNodeName() == el_stream) { const CStr streamName = attributes.GetNamedItem(at_name); const CStr attributeName = attributes.GetNamedItem(at_attribute); if (attributeName.empty() && isGLSL) LOGERROR("Empty attribute name in vertex shader description '%s'", vertexFile.string8().c_str()); VertexAttributeStream stream = VertexAttributeStream::UV7; if (streamName == "pos") stream = VertexAttributeStream::POSITION; else if (streamName == "normal") stream = VertexAttributeStream::NORMAL; else if (streamName == "color") stream = VertexAttributeStream::COLOR; else if (streamName == "uv0") stream = VertexAttributeStream::UV0; else if (streamName == "uv1") stream = VertexAttributeStream::UV1; else if (streamName == "uv2") stream = VertexAttributeStream::UV2; else if (streamName == "uv3") stream = VertexAttributeStream::UV3; else if (streamName == "uv4") stream = VertexAttributeStream::UV4; else if (streamName == "uv5") stream = VertexAttributeStream::UV5; else if (streamName == "uv6") stream = VertexAttributeStream::UV6; else if (streamName == "uv7") stream = VertexAttributeStream::UV7; else LOGERROR("Unknown stream '%s' in vertex shader description '%s'", streamName.c_str(), vertexFile.string8().c_str()); if (isGLSL) { const int attributeLocation = GetAttributeLocationFromStream(device, stream); vertexAttribs[CStrIntern(attributeName)] = attributeLocation; } streamFlags |= GetStreamMask(stream); } } } else if (child.GetNodeName() == el_fragment) { fragmentFile = L"shaders/" + child.GetAttributes().GetNamedItem(at_file).FromUTF8(); XERO_ITER_EL(child, param) { XMBAttributeList attributes = param.GetAttributes(); CStr cond = attributes.GetNamedItem(at_if); if (!cond.empty() && !preprocessor.TestConditional(cond)) continue; if (param.GetNodeName() == el_uniform) { fragmentUniforms[CStrIntern(attributes.GetNamedItem(at_name))] = std::make_pair(attributes.GetNamedItem(at_type), attributes.GetNamedItem(at_loc).ToInt()); } } } else if (child.GetNodeName() == el_compute) { computeFile = L"shaders/" + child.GetAttributes().GetNamedItem(at_file).FromUTF8(); } } if (isGLSL) { if (!computeFile.empty()) { ENSURE(streamFlags == 0); ENSURE(vertexAttribs.empty()); } const PS::StaticVector, 2> shaderStages{computeFile.empty() ? PS::StaticVector, 2>{{vertexFile, GL_VERTEX_SHADER}, {fragmentFile, GL_FRAGMENT_SHADER}} : PS::StaticVector, 2>{{computeFile, GL_COMPUTE_SHADER}}}; return std::make_unique( device, name, xmlFilename, shaderStages, defines, vertexAttribs, streamFlags); } else { #if CONFIG2_GLES LOGERROR("CShaderProgram::Create: '%s'+'%s': ARB shaders not supported on this device", vertexFile.string8(), fragmentFile.string8()); return nullptr; #else return std::make_unique( device, xmlFilename, vertexFile, fragmentFile, defines, vertexUniforms, fragmentUniforms, streamFlags); #endif } } // These should all be overridden by CShaderProgramGLSL, and not used // if a non-GLSL shader was loaded instead: #if CONFIG2_GLES // These should all be overridden by CShaderProgramGLSL // (GLES doesn't support any other types of shader program): void CShaderProgram::VertexPointer(const Renderer::Backend::Format UNUSED(format), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::VertexPointer should be overridden"); } void CShaderProgram::NormalPointer(const Renderer::Backend::Format UNUSED(format), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::NormalPointer should be overridden"); } void CShaderProgram::ColorPointer(const Renderer::Backend::Format UNUSED(format), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::ColorPointer should be overridden"); } void CShaderProgram::TexCoordPointer(GLenum UNUSED(texture), const Renderer::Backend::Format UNUSED(format), GLsizei UNUSED(stride), const void* UNUSED(pointer)) { debug_warn("CShaderProgram::TexCoordPointer should be overridden"); } #else // These are overridden by CShaderProgramGLSL, but fixed-function and ARB shaders // both use the fixed-function vertex attribute pointers so we'll share their // definitions here: void CShaderProgram::VertexPointer(const Renderer::Backend::Format format, GLsizei stride, const void* pointer) { const GLint size = GLSizeFromFormat(format); ENSURE(2 <= size && size <= 4); const GLenum type = GLTypeFromFormat(format); glVertexPointer(size, type, stride, pointer); m_ValidStreams |= GetStreamMask(VertexAttributeStream::POSITION); } void CShaderProgram::NormalPointer(const Renderer::Backend::Format format, GLsizei stride, const void* pointer) { ENSURE(format == Renderer::Backend::Format::R32G32B32_SFLOAT); glNormalPointer(GL_FLOAT, stride, pointer); m_ValidStreams |= GetStreamMask(VertexAttributeStream::NORMAL); } void CShaderProgram::ColorPointer(const Renderer::Backend::Format format, GLsizei stride, const void* pointer) { const GLint size = GLSizeFromFormat(format); ENSURE(3 <= size && size <= 4); const GLenum type = GLTypeFromFormat(format); glColorPointer(size, type, stride, pointer); m_ValidStreams |= GetStreamMask(VertexAttributeStream::COLOR); } void CShaderProgram::TexCoordPointer(GLenum texture, const Renderer::Backend::Format format, GLsizei stride, const void* pointer) { glClientActiveTextureARB(texture); const GLint size = GLSizeFromFormat(format); ENSURE(1 <= size && size <= 4); const GLenum type = GLTypeFromFormat(format); glTexCoordPointer(size, type, stride, pointer); glClientActiveTextureARB(GL_TEXTURE0); m_ValidStreams |= GetStreamMask(VertexAttributeStream::UV0) << (texture - GL_TEXTURE0); } void CShaderProgram::BindClientStates() { ENSURE(m_StreamFlags == (m_StreamFlags & ( GetStreamMask(VertexAttributeStream::POSITION) | GetStreamMask(VertexAttributeStream::NORMAL) | GetStreamMask(VertexAttributeStream::COLOR) | GetStreamMask(VertexAttributeStream::UV0) | GetStreamMask(VertexAttributeStream::UV1)))); // Enable all the desired client states for non-GLSL rendering if (m_StreamFlags & GetStreamMask(VertexAttributeStream::POSITION)) glEnableClientState(GL_VERTEX_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::NORMAL)) glEnableClientState(GL_NORMAL_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::COLOR)) glEnableClientState(GL_COLOR_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::UV0)) { glClientActiveTextureARB(GL_TEXTURE0); glEnableClientState(GL_TEXTURE_COORD_ARRAY); } if (m_StreamFlags & GetStreamMask(VertexAttributeStream::UV1)) { glClientActiveTextureARB(GL_TEXTURE1); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTextureARB(GL_TEXTURE0); } // Rendering code must subsequently call VertexPointer etc for all of the streams // that were activated in this function, else AssertPointersBound will complain // that some arrays were unspecified m_ValidStreams = 0; } void CShaderProgram::UnbindClientStates() { if (m_StreamFlags & GetStreamMask(VertexAttributeStream::POSITION)) glDisableClientState(GL_VERTEX_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::NORMAL)) glDisableClientState(GL_NORMAL_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::COLOR)) glDisableClientState(GL_COLOR_ARRAY); if (m_StreamFlags & GetStreamMask(VertexAttributeStream::UV0)) { glClientActiveTextureARB(GL_TEXTURE0); glDisableClientState(GL_TEXTURE_COORD_ARRAY); } if (m_StreamFlags & GetStreamMask(VertexAttributeStream::UV1)) { glClientActiveTextureARB(GL_TEXTURE1); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glClientActiveTextureARB(GL_TEXTURE0); } } #endif // !CONFIG2_GLES bool CShaderProgram::IsStreamActive(const VertexAttributeStream stream) const { return (m_StreamFlags & GetStreamMask(stream)) != 0; } void CShaderProgram::VertexAttribPointer( const VertexAttributeStream stream, const Format format, const uint32_t offset, const uint32_t stride, const VertexAttributeRate rate, const void* data) { ENSURE(rate == VertexAttributeRate::PER_VERTEX); switch (stream) { case VertexAttributeStream::POSITION: VertexPointer(format, stride, static_cast(data) + offset); break; case VertexAttributeStream::NORMAL: NormalPointer(format, stride, static_cast(data) + offset); break; case VertexAttributeStream::COLOR: ColorPointer(format, stride, static_cast(data) + offset); break; case VertexAttributeStream::UV0: FALLTHROUGH; case VertexAttributeStream::UV1: FALLTHROUGH; case VertexAttributeStream::UV2: FALLTHROUGH; case VertexAttributeStream::UV3: FALLTHROUGH; case VertexAttributeStream::UV4: FALLTHROUGH; case VertexAttributeStream::UV5: FALLTHROUGH; case VertexAttributeStream::UV6: FALLTHROUGH; case VertexAttributeStream::UV7: { const int indexOffset = static_cast(stream) - static_cast(VertexAttributeStream::UV0); TexCoordPointer(GL_TEXTURE0 + indexOffset, format, stride, static_cast(data) + offset); break; } default: debug_warn("Unsupported stream"); }; } void CShaderProgram::AssertPointersBound() { ENSURE((m_StreamFlags & ~m_ValidStreams) == 0); } } // namespace GL } // namespace Backend } // namespace Renderer