From 4212387b707faf59ab7993ffb7929999bb02fc67 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 18:06:14 +0200 Subject: [PATCH] Extract OpenGL shader uniform catalog --- docs/modernization/build-inventory.md | 4 +- docs/modernization/roadmap.md | 10 ++- src/renderer_gl/shader_bindings.cpp | 100 +++++++++++++++++++++++ src/renderer_gl/shader_bindings.h | 10 +++ src/shader.cpp | 59 +------------ tests/renderer_gl/capabilities_tests.cpp | 48 +++++++++++ 6 files changed, 172 insertions(+), 59 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 9d66eb7..15b0e12 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -133,7 +133,9 @@ Known local toolchain state: framebuffer status naming used by `RTT` diagnostics. It also validates Shape index-type and fill/stroke primitive-mode mapping used by the legacy mesh draw path, plus the PanoPainter shader attribute binding catalog used - by legacy `Shader` creation. + by legacy `Shader` creation. It also owns the PanoPainter shader uniform + catalog and legacy hash mapping used by `Shader` active-uniform discovery and + the uniform uniqueness check. - `windows-msvc-vcpkg-headless` validates manifest install/configure/build/test for the current headless component matrix; see DEBT-0007 for remaining app and platform triplet migration. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 4d31116..badc41e 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -395,8 +395,10 @@ format mapping for `Texture2D` image uploads and framebuffer status naming for extension, upload-format, framebuffer diagnostic, and mesh draw-mode interpretation to that backend library. The PanoPainter shader attribute binding catalog also lives in `pp_renderer_gl` and is consumed by legacy -`Shader` creation. The existing renderer classes are not yet fully behind the -renderer interfaces. +`Shader` creation. Shader uniform hashing, catalog validation, active-uniform +mapping, and the legacy uniform uniqueness check now delegate to +`pp_renderer_gl` as well. The existing renderer classes are not yet fully +behind the renderer interfaces. Implementation tasks: @@ -641,7 +643,9 @@ Results: mode mapping. Shader attribute binding catalog validation covers the current `pos`, `uvs`, `uvs2`, `col`, and `nor` bindings and rejects empty, unnamed, null-name, and duplicate-name catalogs while preserving legacy shared - locations. + locations. Shader uniform catalog validation covers the 43 legacy uniform + names used by `Shader`, preserves the legacy hash ids, and rejects empty, + unnamed, null-name, mismatched-hash, and duplicate-name catalogs. - PowerShell analyze automation returns JSON summaries and includes the shader validation target. - `windows-msvc-vcpkg-headless` configured through the Visual Studio bundled diff --git a/src/renderer_gl/shader_bindings.cpp b/src/renderer_gl/shader_bindings.cpp index c264843..37544c8 100644 --- a/src/renderer_gl/shader_bindings.cpp +++ b/src/renderer_gl/shader_bindings.cpp @@ -2,9 +2,23 @@ #include #include +#include namespace pp::renderer::gl { +namespace { + +constexpr std::uint16_t djb2_hash(std::string_view text) noexcept +{ + std::uint16_t value = 5381U; + for (std::size_t i = text.size(); i > 0U; --i) { + value = static_cast(text[i - 1U]) + static_cast(33U * value); + } + return value; +} + +} + std::span panopainter_shader_attribute_bindings() noexcept { static constexpr std::array bindings { @@ -40,4 +54,90 @@ pp::foundation::Status validate_shader_attribute_bindings( return pp::foundation::Status::success(); } +std::span panopainter_shader_uniform_names() noexcept +{ + static constexpr std::array uniforms { + OpenGlUniformName { .name = "mvp", .id = djb2_hash("mvp") }, + OpenGlUniformName { .name = "tex", .id = djb2_hash("tex") }, + OpenGlUniformName { .name = "tex_alpha", .id = djb2_hash("tex_alpha") }, + OpenGlUniformName { .name = "tex_fg", .id = djb2_hash("tex_fg") }, + OpenGlUniformName { .name = "tex_bg", .id = djb2_hash("tex_bg") }, + OpenGlUniformName { .name = "tex_mix", .id = djb2_hash("tex_mix") }, + OpenGlUniformName { .name = "tex_mix_alpha", .id = djb2_hash("tex_mix_alpha") }, + OpenGlUniformName { .name = "tex_mask", .id = djb2_hash("tex_mask") }, + OpenGlUniformName { .name = "tex_dual", .id = djb2_hash("tex_dual") }, + OpenGlUniformName { .name = "tex_stroke", .id = djb2_hash("tex_stroke") }, + OpenGlUniformName { .name = "tex_pattern", .id = djb2_hash("tex_pattern") }, + OpenGlUniformName { .name = "pattern_offset", .id = djb2_hash("pattern_offset") }, + OpenGlUniformName { .name = "pattern_alpha", .id = djb2_hash("pattern_alpha") }, + OpenGlUniformName { .name = "mix_alpha", .id = djb2_hash("mix_alpha") }, + OpenGlUniformName { .name = "opacity", .id = djb2_hash("opacity") }, + OpenGlUniformName { .name = "wet", .id = djb2_hash("wet") }, + OpenGlUniformName { .name = "lock", .id = djb2_hash("lock") }, + OpenGlUniformName { .name = "col", .id = djb2_hash("col") }, + OpenGlUniformName { .name = "tof", .id = djb2_hash("tof") }, + OpenGlUniformName { .name = "tsz", .id = djb2_hash("tsz") }, + OpenGlUniformName { .name = "alpha", .id = djb2_hash("alpha") }, + OpenGlUniformName { .name = "mask", .id = djb2_hash("mask") }, + OpenGlUniformName { .name = "resolution", .id = djb2_hash("resolution") }, + OpenGlUniformName { .name = "highlight", .id = djb2_hash("highlight") }, + OpenGlUniformName { .name = "blend_mode", .id = djb2_hash("blend_mode") }, + OpenGlUniformName { .name = "dual_blend_mode", .id = djb2_hash("dual_blend_mode") }, + OpenGlUniformName { .name = "noise", .id = djb2_hash("noise") }, + OpenGlUniformName { .name = "dir", .id = djb2_hash("dir") }, + OpenGlUniformName { .name = "use_dual", .id = djb2_hash("use_dual") }, + OpenGlUniformName { .name = "use_pattern", .id = djb2_hash("use_pattern") }, + OpenGlUniformName { .name = "light_dir", .id = djb2_hash("light_dir") }, + OpenGlUniformName { .name = "mode", .id = djb2_hash("mode") }, + OpenGlUniformName { .name = "ambient", .id = djb2_hash("ambient") }, + OpenGlUniformName { .name = "pattern_invert", .id = djb2_hash("pattern_invert") }, + OpenGlUniformName { .name = "pattern_scale", .id = djb2_hash("pattern_scale") }, + OpenGlUniformName { .name = "pattern_bright", .id = djb2_hash("pattern_bright") }, + OpenGlUniformName { .name = "pattern_contr", .id = djb2_hash("pattern_contr") }, + OpenGlUniformName { .name = "pattern_depth", .id = djb2_hash("pattern_depth") }, + OpenGlUniformName { .name = "patt_blend_mode", .id = djb2_hash("patt_blend_mode") }, + OpenGlUniformName { .name = "colorize", .id = djb2_hash("colorize") }, + OpenGlUniformName { .name = "dual_alpha", .id = djb2_hash("dual_alpha") }, + OpenGlUniformName { .name = "use_fragcoord", .id = djb2_hash("use_fragcoord") }, + OpenGlUniformName { .name = "draw_outline", .id = djb2_hash("draw_outline") }, + }; + + return uniforms; +} + +std::uint16_t shader_uniform_id(std::string_view name) noexcept +{ + return djb2_hash(name); +} + +pp::foundation::Status validate_shader_uniform_names( + std::span uniforms) noexcept +{ + if (uniforms.empty()) { + return pp::foundation::Status::invalid_argument("shader uniform catalog is empty"); + } + + for (std::size_t i = 0; i < uniforms.size(); ++i) { + if (uniforms[i].name == nullptr || uniforms[i].name[0] == '\0') { + return pp::foundation::Status::invalid_argument("shader uniform has no name"); + } + + if (uniforms[i].id != shader_uniform_id(uniforms[i].name)) { + return pp::foundation::Status::invalid_argument("shader uniform hash does not match its name"); + } + + for (std::size_t j = i + 1; j < uniforms.size(); ++j) { + if (uniforms[j].name != nullptr && std::strcmp(uniforms[i].name, uniforms[j].name) == 0) { + return pp::foundation::Status::invalid_argument("shader uniform name is duplicated"); + } + + if (uniforms[i].id == uniforms[j].id) { + return pp::foundation::Status::invalid_argument("shader uniform hash is duplicated"); + } + } + } + + return pp::foundation::Status::success(); +} + } diff --git a/src/renderer_gl/shader_bindings.h b/src/renderer_gl/shader_bindings.h index bb1bbfb..ac59b68 100644 --- a/src/renderer_gl/shader_bindings.h +++ b/src/renderer_gl/shader_bindings.h @@ -4,6 +4,7 @@ #include #include +#include namespace pp::renderer::gl { @@ -12,8 +13,17 @@ struct OpenGlAttributeBinding { std::uint32_t location = 0; }; +struct OpenGlUniformName { + const char* name = ""; + std::uint16_t id = 0; +}; + [[nodiscard]] std::span panopainter_shader_attribute_bindings() noexcept; [[nodiscard]] pp::foundation::Status validate_shader_attribute_bindings( std::span bindings) noexcept; +[[nodiscard]] std::span panopainter_shader_uniform_names() noexcept; +[[nodiscard]] std::uint16_t shader_uniform_id(std::string_view name) noexcept; +[[nodiscard]] pp::foundation::Status validate_shader_uniform_names( + std::span uniforms) noexcept; } diff --git a/src/shader.cpp b/src/shader.cpp index 2730ef0..2191172 100644 --- a/src/shader.cpp +++ b/src/shader.cpp @@ -262,7 +262,7 @@ bool Shader::create(const std::string& vertex, const std::string& fragment) { glGetActiveUniform(ps, (GLuint)i, bufSize, &length, &size, &type, name); name[length] = 0; - kShaderUniform id = (kShaderUniform)const_hash(name); + kShaderUniform id = static_cast(pp::renderer::gl::shader_uniform_id(name)); if (m_umap.find(id) != m_umap.end()) LOG("UNIFORM ALREADY DEFINED: %s", name); m_umap[id] = glGetUniformLocation(ps, name); @@ -422,58 +422,7 @@ void ShaderManager::invalidate() bool check_uniform_uniqueness() { - std::vector v = { - const_hash("mvp"), - const_hash("tex"), - const_hash("tex_alpha"), - const_hash("tex_fg"), - const_hash("tex_bg"), - const_hash("tex_mix"), - const_hash("tex_mix_alpha"), - const_hash("tex_mask"), - const_hash("tex_dual"), - const_hash("tex_stroke"), - const_hash("tex_pattern"), - const_hash("pattern_offset"), - const_hash("pattern_alpha"), - const_hash("mix_alpha"), - const_hash("opacity"), - const_hash("wet"), - const_hash("lock"), - const_hash("col"), - const_hash("tof"), - const_hash("tsz"), - const_hash("alpha"), - const_hash("mask"), - const_hash("resolution"), - const_hash("highlight"), - const_hash("blend_mode"), - const_hash("dual_blend_mode"), - const_hash("noise"), - const_hash("dir"), - const_hash("use_dual"), - const_hash("use_pattern"), - const_hash("light_dir"), - const_hash("mode"), - const_hash("ambient"), - const_hash("pattern_invert"), - const_hash("pattern_scale"), - const_hash("pattern_bright"), - const_hash("pattern_contr"), - const_hash("pattern_depth"), - const_hash("patt_blend_mode"), - const_hash("colorize"), - const_hash("dual_alpha"), - const_hash("use_fragcoord"), - const_hash("draw_outline"), - }; - std::sort(v.begin(), v.end()); - int last = 0; - for (auto o : v) - { - if (o == last) - return false; - last = o; - } - return true; + return pp::renderer::gl::validate_shader_uniform_names( + pp::renderer::gl::panopainter_shader_uniform_names()) + .ok(); } diff --git a/tests/renderer_gl/capabilities_tests.cpp b/tests/renderer_gl/capabilities_tests.cpp index 400839d..28e6998 100644 --- a/tests/renderer_gl/capabilities_tests.cpp +++ b/tests/renderer_gl/capabilities_tests.cpp @@ -201,6 +201,52 @@ void rejects_invalid_shader_attribute_binding_catalogs(pp::tests::Harness& h) PP_EXPECT(h, pp::renderer::gl::validate_shader_attribute_bindings(duplicate_location).ok()); } +void exposes_shader_uniform_catalog(pp::tests::Harness& h) +{ + const auto uniforms = pp::renderer::gl::panopainter_shader_uniform_names(); + + PP_EXPECT(h, uniforms.size() == 43U); + PP_EXPECT(h, pp::renderer::gl::validate_shader_uniform_names(uniforms).ok()); + PP_EXPECT(h, std::strcmp(uniforms[0].name, "mvp") == 0); + PP_EXPECT(h, uniforms[0].id == pp::renderer::gl::shader_uniform_id("mvp")); + PP_EXPECT(h, uniforms[0].id == 40696U); + PP_EXPECT(h, std::strcmp(uniforms[1].name, "tex") == 0); + PP_EXPECT(h, uniforms[1].id == pp::renderer::gl::shader_uniform_id("tex")); + PP_EXPECT(h, uniforms[1].id == 48854U); + PP_EXPECT(h, pp::renderer::gl::shader_uniform_id("pattern_contr") == 54920U); + PP_EXPECT(h, pp::renderer::gl::shader_uniform_id("draw_outline") == 36178U); +} + +void rejects_invalid_shader_uniform_catalogs(pp::tests::Harness& h) +{ + const std::array empty {}; + const std::array unnamed { + pp::renderer::gl::OpenGlUniformName { .name = "", .id = 0 }, + }; + const std::array null_named { + pp::renderer::gl::OpenGlUniformName { .name = nullptr, .id = 0 }, + }; + const std::array mismatched_hash { + pp::renderer::gl::OpenGlUniformName { .name = "mvp", .id = 1 }, + }; + const std::array duplicate_name { + pp::renderer::gl::OpenGlUniformName { + .name = "mvp", + .id = pp::renderer::gl::shader_uniform_id("mvp"), + }, + pp::renderer::gl::OpenGlUniformName { + .name = "mvp", + .id = pp::renderer::gl::shader_uniform_id("mvp"), + }, + }; + + PP_EXPECT(h, !pp::renderer::gl::validate_shader_uniform_names(empty).ok()); + PP_EXPECT(h, !pp::renderer::gl::validate_shader_uniform_names(unnamed).ok()); + PP_EXPECT(h, !pp::renderer::gl::validate_shader_uniform_names(null_named).ok()); + PP_EXPECT(h, !pp::renderer::gl::validate_shader_uniform_names(mismatched_hash).ok()); + PP_EXPECT(h, !pp::renderer::gl::validate_shader_uniform_names(duplicate_name).ok()); +} + } int main() @@ -216,5 +262,7 @@ int main() harness.run("maps_shape_index_and_primitive_modes", maps_shape_index_and_primitive_modes); harness.run("exposes_shader_attribute_binding_catalog", exposes_shader_attribute_binding_catalog); harness.run("rejects_invalid_shader_attribute_binding_catalogs", rejects_invalid_shader_attribute_binding_catalogs); + harness.run("exposes_shader_uniform_catalog", exposes_shader_uniform_catalog); + harness.run("rejects_invalid_shader_uniform_catalogs", rejects_invalid_shader_uniform_catalogs); return harness.finish(); }