Files
panopainter/src/shader.cpp

841 lines
27 KiB
C++

#include "pch.h"
#include "log.h"
#include "shader.h"
#include "asset.h"
#include "app.h"
#include "renderer_gl/opengl_capabilities.h"
#include "renderer_gl/shader_bindings.h"
#include <cstdint>
namespace {
[[nodiscard]] GLenum vertex_shader_stage() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::vertex_shader_stage());
}
[[nodiscard]] GLenum fragment_shader_stage() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::fragment_shader_stage());
}
[[nodiscard]] GLenum shader_compile_status_query() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::shader_compile_status_query());
}
[[nodiscard]] GLenum program_link_status_query() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::program_link_status_query());
}
[[nodiscard]] GLenum active_uniform_count_query() noexcept
{
return static_cast<GLenum>(pp::renderer::gl::active_uniform_count_query());
}
void use_opengl_program(std::uint32_t program) noexcept
{
glUseProgram(static_cast<GLuint>(program));
}
void delete_opengl_program(std::uint32_t program) noexcept
{
glDeleteProgram(static_cast<GLuint>(program));
}
void set_opengl_uniform_4fv(std::int32_t location, std::int32_t count, const float* values) noexcept
{
glUniform4fv(static_cast<GLint>(location), static_cast<GLsizei>(count), values);
}
void set_opengl_uniform_3fv(std::int32_t location, std::int32_t count, const float* values) noexcept
{
glUniform3fv(static_cast<GLint>(location), static_cast<GLsizei>(count), values);
}
void set_opengl_uniform_2fv(std::int32_t location, std::int32_t count, const float* values) noexcept
{
glUniform2fv(static_cast<GLint>(location), static_cast<GLsizei>(count), values);
}
void set_opengl_uniform_matrix_4fv(
std::int32_t location,
std::int32_t count,
std::uint8_t transpose,
const float* values) noexcept
{
glUniformMatrix4fv(
static_cast<GLint>(location),
static_cast<GLsizei>(count),
static_cast<GLboolean>(transpose),
values);
}
void set_opengl_uniform_1i(std::int32_t location, std::int32_t value) noexcept
{
glUniform1i(static_cast<GLint>(location), static_cast<GLint>(value));
}
void set_opengl_uniform_1f(std::int32_t location, float value) noexcept
{
glUniform1f(static_cast<GLint>(location), value);
}
std::int32_t get_opengl_attrib_location(std::uint32_t program, const char* name) noexcept
{
return static_cast<std::int32_t>(glGetAttribLocation(static_cast<GLuint>(program), name));
}
std::uint32_t create_opengl_shader(std::uint32_t stage) noexcept
{
return static_cast<std::uint32_t>(glCreateShader(static_cast<GLenum>(stage)));
}
void set_opengl_shader_source(
std::uint32_t shader,
std::int32_t count,
const char* const* sources) noexcept
{
glShaderSource(
static_cast<GLuint>(shader),
static_cast<GLsizei>(count),
reinterpret_cast<const GLchar* const*>(sources),
nullptr);
}
void compile_opengl_shader(std::uint32_t shader) noexcept
{
glCompileShader(static_cast<GLuint>(shader));
}
void query_opengl_shader_integer(
std::uint32_t shader,
std::uint32_t query,
std::int32_t* value) noexcept
{
glGetShaderiv(
static_cast<GLuint>(shader),
static_cast<GLenum>(query),
reinterpret_cast<GLint*>(value));
}
void get_opengl_shader_info_log(
std::uint32_t shader,
std::int32_t capacity,
std::int32_t* length,
char* info_log) noexcept
{
glGetShaderInfoLog(
static_cast<GLuint>(shader),
static_cast<GLsizei>(capacity),
reinterpret_cast<GLsizei*>(length),
reinterpret_cast<GLchar*>(info_log));
}
void delete_opengl_shader(std::uint32_t shader) noexcept
{
glDeleteShader(static_cast<GLuint>(shader));
}
std::uint32_t create_opengl_program() noexcept
{
return static_cast<std::uint32_t>(glCreateProgram());
}
void attach_opengl_shader(std::uint32_t program, std::uint32_t shader) noexcept
{
glAttachShader(static_cast<GLuint>(program), static_cast<GLuint>(shader));
}
void link_opengl_program(std::uint32_t program) noexcept
{
glLinkProgram(static_cast<GLuint>(program));
}
void bind_opengl_attrib_location(std::uint32_t program, std::uint32_t location, const char* name) noexcept
{
glBindAttribLocation(static_cast<GLuint>(program), static_cast<GLuint>(location), name);
}
void query_opengl_program_integer(
std::uint32_t program,
std::uint32_t query,
std::int32_t* value) noexcept
{
glGetProgramiv(
static_cast<GLuint>(program),
static_cast<GLenum>(query),
reinterpret_cast<GLint*>(value));
}
void get_opengl_program_info_log(
std::uint32_t program,
std::int32_t capacity,
std::int32_t* length,
char* info_log) noexcept
{
glGetProgramInfoLog(
static_cast<GLuint>(program),
static_cast<GLsizei>(capacity),
reinterpret_cast<GLsizei*>(length),
reinterpret_cast<GLchar*>(info_log));
}
void get_opengl_active_uniform(
std::uint32_t program,
std::uint32_t index,
std::int32_t capacity,
std::int32_t* length,
std::int32_t* size,
std::uint32_t* type,
char* name) noexcept
{
glGetActiveUniform(
static_cast<GLuint>(program),
static_cast<GLuint>(index),
static_cast<GLsizei>(capacity),
reinterpret_cast<GLsizei*>(length),
reinterpret_cast<GLint*>(size),
reinterpret_cast<GLenum*>(type),
reinterpret_cast<GLchar*>(name));
}
std::int32_t get_opengl_uniform_location(std::uint32_t program, const char* name) noexcept
{
return static_cast<std::int32_t>(glGetUniformLocation(static_cast<GLuint>(program), name));
}
}
std::map<kShader, Shader> ShaderManager::m_shaders;
Shader* ShaderManager::m_current;
pp::renderer::RenderDeviceFeatures ShaderManager::m_render_device_features {};
bool ShaderManager::ext_framebuffer_fetch = false;
bool ShaderManager::ext_float32 = false;
bool ShaderManager::ext_float32_linear = false;
bool ShaderManager::ext_float16 = false;
bool ShaderManager::ext_map_aligned = false;
std::string Shader::read(const std::string& path)
{
Asset a;
std::string ret;
if (a.open(path.c_str()))
{
struct stat tmp_info;
std::string abs_path = Asset::absolute(path);
if (stat(abs_path.c_str(), &tmp_info) == 0)
m_deps[path] = tmp_info;
std::regex reg_include(R"!(#include "([^"]+)")!");
std::string data((char*)a.read_all(), a.m_len);
// split path
std::string base;
std::regex reg_path(R"((.*)[\\/]([^\\/]+)\.(\w+)$)");
std::smatch m;
if (std::regex_search(path, m, reg_path))
{
base = m[1].str();
}
for (const auto& l : split(data, '\n'))
{
std::smatch include_match;
if (std::regex_search(l, include_match, reg_include))
{
std::string inc = base + "/" + include_match[1].str();
if (Asset::exist(inc.c_str()))
{
std::string subdata = read(inc);
ret.append(subdata + "\n");
}
}
else
{
ret.append(l + "\n");
}
}
}
return ret;
}
void Shader::parse_error(const std::string& msg, const std::string& code)
{
auto code_lines = split(code, '\n');
auto error_lines = split(msg, '\n');
std::smatch m;
std::regex r(R"(\((\d+)\))");
for (const auto& line : error_lines)
{
LOG("%s", line.c_str());
if (std::regex_search(line, m, r))
{
int ln = std::stoi(m[1].str());
if (ln < code_lines.size())
{
int n = 2;
int s = std::max(ln - n, 1);
int e = std::min((int)code_lines.size() - 1, ln + n);
for (int i = s; i < e; i++)
{
LOG("- line %02d: %s", i, code_lines[i - 1].c_str());
}
}
}
}
}
bool Shader::load(const std::string& path)
{
std::string data = read(path);
if (data.empty()) return false;
std::map<std::string, std::shared_ptr<std::string>> sections;
std::shared_ptr<std::string> current_section = nullptr;
std::regex reg_section(R"!(\[\[(.*)\]\])!");
for (const auto& l : split(data, '\n'))
{
std::smatch m;
if (std::regex_search(l, m, reg_section))
{
std::string section_name = m[1].str();
if (!sections[section_name])
sections[section_name] = std::make_shared<std::string>();
current_section = sections[section_name];
}
else
{
// create an un-named section
if (!current_section)
current_section = sections[""] = std::make_shared<std::string>();
current_section->append(l + "\n");
}
}
if (sections.find("vertex") != sections.end() && sections.find("fragment") != sections.end())
{
*sections["vertex"] = SHADER_VERSION + *sections["vertex"];
*sections["fragment"] = SHADER_VERSION + *sections["fragment"];
if (create(*sections["vertex"], *sections["fragment"]))
{
m_path = path;
return true;
}
}
else
{
LOG("could not find [[vertex]] and [[fragment]] sections on %s", path.c_str());
}
return false;
}
bool Shader::reload()
{
if (m_path.empty())
return false;
bool dirty = false;
struct stat tmp_info;
for (auto& d : m_deps)
{
std::string abs_path = Asset::absolute(d.first);
if (stat(abs_path.c_str(), &tmp_info) != 0)
continue;
if (tmp_info.st_mtime > d.second.st_mtime)
{
d.second = tmp_info;
dirty = true;
}
}
if (dirty)
{
LOG("reload shader %s", m_path.c_str());
destroy();
return load(m_path);
}
return false;
}
bool Shader::create(const std::string& vertex, const std::string& fragment)
{
bool ret = true;
App::I->render_task([this, &ret, vertex, fragment]
{
static char infolog[4096];
const auto vertex_shader = pp::renderer::gl::compile_opengl_shader_source(
pp::renderer::gl::vertex_shader_stage(),
vertex.c_str(),
infolog,
static_cast<std::int32_t>(sizeof(infolog)),
pp::renderer::gl::OpenGlShaderCompileDispatch {
.create_shader = create_opengl_shader,
.shader_source = set_opengl_shader_source,
.compile_shader = compile_opengl_shader,
.get_shader_integer = query_opengl_shader_integer,
.get_shader_info_log = get_opengl_shader_info_log,
});
if (!vertex_shader.ok())
{
ret = false;
LOG("Shader::create() vertex compile failed because: %s", vertex_shader.status().message);
return;
}
if (vertex_shader.value().info_log_length > 0)
{
LOG("\nVERTEX SHADER: %s", m_path.c_str());
parse_error(infolog, vertex);
}
if (vertex_shader.value().compile_status == 0)
{
const auto status = pp::renderer::gl::delete_opengl_shader(
vertex_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
if (!status.ok())
LOG("Shader::create() vertex shader cleanup failed because: %s", status.message);
ret = false;
return;
}
const auto fragment_shader = pp::renderer::gl::compile_opengl_shader_source(
pp::renderer::gl::fragment_shader_stage(),
fragment.c_str(),
infolog,
static_cast<std::int32_t>(sizeof(infolog)),
pp::renderer::gl::OpenGlShaderCompileDispatch {
.create_shader = create_opengl_shader,
.shader_source = set_opengl_shader_source,
.compile_shader = compile_opengl_shader,
.get_shader_integer = query_opengl_shader_integer,
.get_shader_info_log = get_opengl_shader_info_log,
});
if (!fragment_shader.ok())
{
const auto status = pp::renderer::gl::delete_opengl_shader(
vertex_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
if (!status.ok())
LOG("Shader::create() vertex shader cleanup failed because: %s", status.message);
ret = false;
LOG("Shader::create() fragment compile failed because: %s", fragment_shader.status().message);
return;
}
if (fragment_shader.value().info_log_length > 0)
{
LOG("\nFRAGMENT SHADER: %s", m_path.c_str());
parse_error(infolog, fragment);
}
if (fragment_shader.value().compile_status == 0)
{
const auto vertex_cleanup = pp::renderer::gl::delete_opengl_shader(
vertex_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
const auto fragment_cleanup = pp::renderer::gl::delete_opengl_shader(
fragment_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
if (!vertex_cleanup.ok())
LOG("Shader::create() vertex shader cleanup failed because: %s", vertex_cleanup.message);
if (!fragment_cleanup.ok())
LOG("Shader::create() fragment shader cleanup failed because: %s", fragment_cleanup.message);
ret = false;
return;
}
const auto program = pp::renderer::gl::link_opengl_shader_program(
vertex_shader.value().shader_id,
fragment_shader.value().shader_id,
pp::renderer::gl::panopainter_shader_attribute_bindings(),
infolog,
static_cast<std::int32_t>(sizeof(infolog)),
pp::renderer::gl::OpenGlProgramLinkDispatch {
.create_program = create_opengl_program,
.attach_shader = attach_opengl_shader,
.delete_shader = delete_opengl_shader,
.link_program = link_opengl_program,
.get_attrib_location = get_opengl_attrib_location,
.bind_attrib_location = bind_opengl_attrib_location,
.get_program_integer = query_opengl_program_integer,
.get_program_info_log = get_opengl_program_info_log,
});
if (!program.ok())
{
const auto vertex_cleanup = pp::renderer::gl::delete_opengl_shader(
vertex_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
const auto fragment_cleanup = pp::renderer::gl::delete_opengl_shader(
fragment_shader.value().shader_id,
pp::renderer::gl::OpenGlShaderDeleteDispatch {
.delete_shader = delete_opengl_shader,
});
if (!vertex_cleanup.ok())
LOG("Shader::create() vertex shader cleanup failed because: %s", vertex_cleanup.message);
if (!fragment_cleanup.ok())
LOG("Shader::create() fragment shader cleanup failed because: %s", fragment_cleanup.message);
ret = false;
LOG("Shader::create() program link failed because: %s", program.status().message);
return;
}
if (program.value().info_log_length > 0)
LOG("LINK SHADER: %s\n%s", m_path.c_str(), infolog);
if (program.value().link_status == 0)
{
const auto status = pp::renderer::gl::delete_opengl_program(
program.value().program_id,
pp::renderer::gl::OpenGlProgramDeleteDispatch {
.use_program = use_opengl_program,
.delete_program = delete_opengl_program,
});
if (!status.ok())
LOG("Shader::create() program cleanup failed because: %s", status.message);
ret = false;
return;
}
const auto cleanup_program = [](std::uint32_t program_id) noexcept {
const auto status = pp::renderer::gl::delete_opengl_program(
program_id,
pp::renderer::gl::OpenGlProgramDeleteDispatch {
.use_program = use_opengl_program,
.delete_program = delete_opengl_program,
});
if (!status.ok())
LOG("Shader::create() program cleanup failed because: %s", status.message);
};
// Parse shader uniforms
{
const auto uniform_count = pp::renderer::gl::query_opengl_program_integer(
program.value().program_id,
pp::renderer::gl::active_uniform_count_query(),
pp::renderer::gl::OpenGlProgramIntegerDispatch {
.get_program_integer = query_opengl_program_integer,
});
if (!uniform_count.ok())
{
LOG("Shader::create() uniform discovery failed because: %s", uniform_count.status().message);
cleanup_program(program.value().program_id);
ret = false;
return;
}
constexpr std::int32_t bufSize = 64; // maximum name length
char uniform_name[bufSize]; // variable name in GLSL
const auto count = uniform_count.value();
for (int i = 0; i < count; i++)
{
const auto uniform = pp::renderer::gl::get_opengl_active_uniform(
program.value().program_id,
static_cast<std::uint32_t>(i),
uniform_name,
bufSize,
pp::renderer::gl::OpenGlActiveUniformDispatch {
.get_active_uniform = get_opengl_active_uniform,
});
if (!uniform.ok())
{
LOG("Shader::create() active uniform discovery failed because: %s", uniform.status().message);
cleanup_program(program.value().program_id);
ret = false;
return;
}
kShaderUniform id = static_cast<kShaderUniform>(pp::renderer::gl::shader_uniform_id(uniform_name));
if (m_umap.find(id) != m_umap.end())
LOG("UNIFORM ALREADY DEFINED: %s", uniform_name);
const auto location = pp::renderer::gl::get_opengl_uniform_location(
program.value().program_id,
uniform_name,
pp::renderer::gl::OpenGlUniformLocationDispatch {
.get_uniform_location = get_opengl_uniform_location,
});
if (!location.ok())
{
LOG("Shader::create() uniform location failed because: %s", location.status().message);
cleanup_program(program.value().program_id);
ret = false;
return;
}
m_umap[id] = location.value();
}
}
prog = program.value().program_id;
});
return ret;
}
void Shader::destroy()
{
if (prog)
{
App::I->render_task_async([prog=prog]
{
const auto status = pp::renderer::gl::delete_opengl_program(
static_cast<std::uint32_t>(prog),
pp::renderer::gl::OpenGlProgramDeleteDispatch {
.use_program = use_opengl_program,
.delete_program = delete_opengl_program,
});
if (!status.ok())
LOG("Shader::destroy() failed because: %s", status.message);
});
prog = 0;
}
m_umap.clear();
}
void Shader::use()
{
const auto status = pp::renderer::gl::use_opengl_program(
static_cast<std::uint32_t>(prog),
pp::renderer::gl::OpenGlProgramUseDispatch {
.use_program = use_opengl_program,
});
if (!status.ok())
LOG("Shader::use() failed because: %s", status.message);
}
void Shader::u_vec4(kShaderUniform id, const glm::vec4& v)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM vec4 %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_vec4(
static_cast<std::int32_t>(m_umap[id]),
glm::value_ptr(v),
pp::renderer::gl::OpenGlUniformVec4Dispatch {
.uniform_4fv = set_opengl_uniform_4fv,
});
if (!status.ok())
LOG("Shader::u_vec4() failed because: %s", status.message);
}
}
void Shader::u_vec3(kShaderUniform id, const glm::vec3& v)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM vec3 %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_vec3(
static_cast<std::int32_t>(m_umap[id]),
glm::value_ptr(v),
pp::renderer::gl::OpenGlUniformVec3Dispatch {
.uniform_3fv = set_opengl_uniform_3fv,
});
if (!status.ok())
LOG("Shader::u_vec3() failed because: %s", status.message);
}
}
void Shader::u_vec2(kShaderUniform id, const glm::vec2& v)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM vec2 %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_vec2(
static_cast<std::int32_t>(m_umap[id]),
glm::value_ptr(v),
pp::renderer::gl::OpenGlUniformVec2Dispatch {
.uniform_2fv = set_opengl_uniform_2fv,
});
if (!status.ok())
LOG("Shader::u_vec2() failed because: %s", status.message);
}
}
void Shader::u_mat4(kShaderUniform id, const glm::mat4& m)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM mat4 %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_mat4(
static_cast<std::int32_t>(m_umap[id]),
glm::value_ptr(m),
pp::renderer::gl::OpenGlUniformMat4Dispatch {
.uniform_matrix_4fv = set_opengl_uniform_matrix_4fv,
});
if (!status.ok())
LOG("Shader::u_mat4() failed because: %s", status.message);
}
}
void Shader::u_int(kShaderUniform id, int i)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM int %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_int(
static_cast<std::int32_t>(m_umap[id]),
static_cast<std::int32_t>(i),
pp::renderer::gl::OpenGlUniformIntDispatch {
.uniform_1i = set_opengl_uniform_1i,
});
if (!status.ok())
LOG("Shader::u_int() failed because: %s", status.message);
}
}
void Shader::u_int(const char* uniform_name, int i)
{
const auto location = pp::renderer::gl::get_opengl_attribute_location(
static_cast<std::uint32_t>(prog),
uniform_name,
pp::renderer::gl::OpenGlAttributeLocationDispatch {
.get_attrib_location = get_opengl_attrib_location,
});
if (!location.ok())
{
LOG("Shader::u_int(name) lookup failed because: %s", location.status().message);
return;
}
const auto status = pp::renderer::gl::set_opengl_uniform_int(
location.value(),
static_cast<std::int32_t>(i),
pp::renderer::gl::OpenGlUniformIntDispatch {
.uniform_1i = set_opengl_uniform_1i,
});
if (!status.ok())
LOG("Shader::u_int(name) failed because: %s", status.message);
}
void Shader::u_float(kShaderUniform id, float f)
{
if (m_umap.count(id) == 0)
LOG("UNIFORM float %d NOT FOUND in shader %d", (int)id, (int)name)
else
{
const auto status = pp::renderer::gl::set_opengl_uniform_float(
static_cast<std::int32_t>(m_umap[id]),
f,
pp::renderer::gl::OpenGlUniformFloatDispatch {
.uniform_1f = set_opengl_uniform_1f,
});
if (!status.ok())
LOG("Shader::u_float() failed because: %s", status.message);
}
}
GLint Shader::GetAttribLocation(const char* attribute_name)
{
const auto location = pp::renderer::gl::get_opengl_attribute_location(
static_cast<std::uint32_t>(prog),
attribute_name,
pp::renderer::gl::OpenGlAttributeLocationDispatch {
.get_attrib_location = get_opengl_attrib_location,
});
if (!location.ok())
{
LOG("Shader::GetAttribLocation() failed because: %s", location.status().message);
return -1;
}
return static_cast<GLint>(location.value());
}
bool ShaderManager::load(kShader id, const std::string& path)
{
m_shaders[id].name = id;
return m_shaders[id].load(path);
}
bool ShaderManager::reload()
{
bool success = false;
for (auto& s : m_shaders)
success |= s.second.reload();
return success;
}
bool ShaderManager::create(kShader id, const std::string& vertex, const std::string& fragment)
{
m_shaders[id].name = id;
return m_shaders[id].create(vertex, fragment);
}
void ShaderManager::use(kShader id)
{
m_current = &m_shaders[id];
m_current->use();
}
void ShaderManager::use(const char* name)
{
m_current = &m_shaders[(kShader)const_hash(name)];
m_current->use();
}
void ShaderManager::u_vec4(kShaderUniform id, const glm::vec4& v)
{
m_current->u_vec4(id, v);
}
void ShaderManager::u_vec3(kShaderUniform id, const glm::vec3& v)
{
m_current->u_vec3(id, v);
}
void ShaderManager::u_vec2(kShaderUniform id, const glm::vec2& v)
{
m_current->u_vec2(id, v);
}
void ShaderManager::u_mat4(kShaderUniform id, const glm::mat4& m)
{
m_current->u_mat4(id, m);
}
void ShaderManager::u_int(kShaderUniform id, int i)
{
m_current->u_int(id, i);
}
void ShaderManager::u_int(const char* name, int i)
{
m_current->u_int(name, i);
}
Shader* ShaderManager::get(kShader id)
{
auto it = m_shaders.find(id);
return (it == m_shaders.end()) ? nullptr : &it->second;
}
void ShaderManager::u_float(kShaderUniform id, float f)
{
m_current->u_float(id, f);
}
void ShaderManager::set_render_device_features(pp::renderer::RenderDeviceFeatures features) noexcept
{
m_render_device_features = features;
}
pp::renderer::RenderDeviceFeatures ShaderManager::render_device_features() noexcept
{
return m_render_device_features;
}
void ShaderManager::invalidate()
{
m_shaders.clear();
}
bool check_uniform_uniqueness()
{
return pp::renderer::gl::validate_shader_uniform_names(
pp::renderer::gl::panopainter_shader_uniform_names())
.ok();
}