From d61c7f37c3884a5552fce0bb694209383a97172f Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 17:36:25 +0200 Subject: [PATCH] Extract renderer shader catalog --- CMakeLists.txt | 4 +- docs/modernization/build-inventory.md | 3 + docs/modernization/roadmap.md | 8 +- src/app_shaders.cpp | 66 ++++------------ src/renderer_api/shader_catalog.cpp | 91 +++++++++++++++++++++++ src/renderer_api/shader_catalog.h | 21 ++++++ tests/renderer_api/renderer_api_tests.cpp | 66 ++++++++++++++++ 7 files changed, 204 insertions(+), 55 deletions(-) create mode 100644 src/renderer_api/shader_catalog.cpp create mode 100644 src/renderer_api/shader_catalog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 01a9f98..85c8e27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,7 +150,8 @@ target_link_libraries(pp_document pp_project_warnings) add_library(pp_renderer_api STATIC - src/renderer_api/renderer_api.cpp) + src/renderer_api/renderer_api.cpp + src/renderer_api/shader_catalog.cpp) target_include_directories(pp_renderer_api PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") @@ -209,6 +210,7 @@ if(PP_BUILD_APP) PUBLIC pp_project_options PRIVATE + pp_renderer_api pp_project_warnings) target_include_directories(pp_legacy_app diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index b39d9cf..47e24ff 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -119,6 +119,9 @@ Known local toolchain state: - `panopainter_validate_shaders` validates the current combined GLSL shader files for one vertex stage marker, one fragment stage marker, valid marker order, and existing relative includes. +- `pp_renderer_api` owns the canonical PanoPainter shader catalog consumed by + the legacy OpenGL app initialization path; `pp_renderer_api_tests` validates + catalog size, key entries, duplicate rejection, and bad path rejection. - `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 99e9343..63065d3 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -383,8 +383,9 @@ adding new backends. Status: started. `pp_renderer_api` exists as a headless renderer-neutral target with texture descriptor, byte-size, viewport, mesh, readback bounds, command context, render device, shader program descriptor, mesh, render target, -readback, and trace interface validation. OpenGL classes are not yet behind -these interfaces. +readback, trace interface validation, and the canonical PanoPainter shader +catalog now consumed by the legacy OpenGL app initialization path. OpenGL +classes are not yet behind these interfaces. Implementation tasks: @@ -592,7 +593,8 @@ Results: - `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face payload attachment to `pp_document` layer/frame storage and out-of-range payload rejection. -- `pp_renderer_api_tests` passed, including shader descriptor validation. +- `pp_renderer_api_tests` passed, including shader descriptor validation, + PanoPainter shader catalog validation, and invalid catalog rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. diff --git a/src/app_shaders.cpp b/src/app_shaders.cpp index 9eb3a1e..008dd02 100644 --- a/src/app_shaders.cpp +++ b/src/app_shaders.cpp @@ -1,12 +1,13 @@ #include "pch.h" #include "app.h" +#include "renderer_api/shader_catalog.h" #include "shader.h" void App::initShaders() { #ifdef _DEBUG if (!check_uniform_uniqueness()) - std::logic_error("check_uniform_uniqueness() failed"); + LOG("check_uniform_uniqueness() failed"); #endif // _DEBUG render_task([] { @@ -45,56 +46,19 @@ void App::initShaders() LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled"); LOG("initializing shaders"); - if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl")) - LOG("Failed to create shader Texture"); - if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl")) - LOG("Failed to create shader TextureAlpha"); - if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl")) - LOG("Failed to create shader TextureMask"); - if (!ShaderManager::load(kShader::TextureColorize, "data/shaders/texture-colorize.glsl")) - LOG("Failed to create shader TextureColorize"); - if (!ShaderManager::load(kShader::TextureBlend, "data/shaders/texture-blend.glsl")) - LOG("Failed to create shader TextureBlend"); - if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl")) - LOG("Failed to create shader StrokePreview"); - if (!ShaderManager::load(kShader::CompErase, "data/shaders/comp-erase.glsl")) - LOG("Failed to create shader CompErase"); - if (!ShaderManager::load(kShader::CompDraw, "data/shaders/comp-draw.glsl")) - LOG("Failed to create shader CompDraw"); - if (!ShaderManager::load(kShader::Color, "data/shaders/color.glsl")) - LOG("Failed to create shader Color"); - if (!ShaderManager::load(kShader::ColorQuad, "data/shaders/color-quad.glsl")) - LOG("Failed to create shader ColorQuad"); - if (!ShaderManager::load(kShader::ColorTri, "data/shaders/color-tri.glsl")) - LOG("Failed to create shader ColorTri"); - if (!ShaderManager::load(kShader::ColorHue, "data/shaders/color-hue.glsl")) - LOG("Failed to create shader ColorHue"); - if (!ShaderManager::load(kShader::UVs, "data/shaders/uvs.glsl")) - LOG("Failed to create shader UVs"); - if (!ShaderManager::load(kShader::Font, "data/shaders/font.glsl")) - LOG("Failed to create shader Font"); - if (!ShaderManager::load(kShader::Atlas, "data/shaders/atlas.glsl")) - LOG("Failed to create shader Atlas"); - if (!ShaderManager::load(kShader::Stroke, "data/shaders/stroke.glsl")) - LOG("Failed to create shader Stroke"); - if (!ShaderManager::load(kShader::StrokePad, "data/shaders/stroke-pad.glsl")) - LOG("Failed to create shader StrokePad"); - if (!ShaderManager::load(kShader::StrokeDilate, "data/shaders/stroke-dilate.glsl")) - LOG("Failed to create shader StrokeDilate"); - if (!ShaderManager::load(kShader::Checkerboard, "data/shaders/checkerboard.glsl")) - LOG("Failed to create shader Checkerboard"); - if (!ShaderManager::load(kShader::Equirect, "data/shaders/equirect.glsl")) - LOG("Failed to create shader Equirect"); - if (!ShaderManager::load(kShader::BrushStroke, "data/shaders/stroke-instanced.glsl")) - LOG("Failed to create shader BrushStroke"); - if (!ShaderManager::load(kShader::VertexColor, "data/shaders/vertex-color.glsl")) - LOG("Failed to create shader VertexColor"); - if (!ShaderManager::load(kShader::Lambert, "data/shaders/lambert.glsl")) - LOG("Failed to create shader Lambert"); - if (!ShaderManager::load(kShader::LambertLightmap, "data/shaders/lightmap.glsl")) - LOG("Failed to create shader LambertLightmap"); - if (!ShaderManager::load(kShader::BakeUV, "data/shaders/bake-uv.glsl")) - LOG("Failed to create shader BakeUV"); + const auto shader_catalog = pp::renderer::panopainter_shader_catalog(); + const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog); + if (!catalog_status.ok()) + { + LOG("Shader catalog validation failed: %s", catalog_status.message); + return; + } + + for (const auto& shader : shader_catalog) + { + if (!ShaderManager::load(static_cast(const_hash(shader.name)), shader.path)) + LOG("Failed to create shader %s", shader.name); + } LOG("shaders initialized"); } diff --git a/src/renderer_api/shader_catalog.cpp b/src/renderer_api/shader_catalog.cpp new file mode 100644 index 0000000..f8301f7 --- /dev/null +++ b/src/renderer_api/shader_catalog.cpp @@ -0,0 +1,91 @@ +#include "renderer_api/shader_catalog.h" + +#include +#include + +namespace pp::renderer { + +namespace { + +constexpr std::array pano_catalog { + ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" }, + ShaderCatalogEntry { .name = "texture-alpha", .path = "data/shaders/texture-alpha.glsl" }, + ShaderCatalogEntry { .name = "texture-mask", .path = "data/shaders/texture-mask.glsl" }, + ShaderCatalogEntry { .name = "texture-colorize", .path = "data/shaders/texture-colorize.glsl" }, + ShaderCatalogEntry { .name = "texture-blend", .path = "data/shaders/texture-blend.glsl" }, + ShaderCatalogEntry { .name = "stroke-preview", .path = "data/shaders/stroke-preview.glsl" }, + ShaderCatalogEntry { .name = "comp-erase", .path = "data/shaders/comp-erase.glsl" }, + ShaderCatalogEntry { .name = "comp-draw", .path = "data/shaders/comp-draw.glsl" }, + ShaderCatalogEntry { .name = "color", .path = "data/shaders/color.glsl" }, + ShaderCatalogEntry { .name = "color-quad", .path = "data/shaders/color-quad.glsl" }, + ShaderCatalogEntry { .name = "color-tri", .path = "data/shaders/color-tri.glsl" }, + ShaderCatalogEntry { .name = "color-hue", .path = "data/shaders/color-hue.glsl" }, + ShaderCatalogEntry { .name = "uvs", .path = "data/shaders/uvs.glsl" }, + ShaderCatalogEntry { .name = "font", .path = "data/shaders/font.glsl" }, + ShaderCatalogEntry { .name = "atlas", .path = "data/shaders/atlas.glsl" }, + ShaderCatalogEntry { .name = "stroke", .path = "data/shaders/stroke.glsl" }, + ShaderCatalogEntry { .name = "stroke-pad", .path = "data/shaders/stroke-pad.glsl" }, + ShaderCatalogEntry { .name = "stroke-dilate", .path = "data/shaders/stroke-dilate.glsl" }, + ShaderCatalogEntry { .name = "checkerboard", .path = "data/shaders/checkerboard.glsl" }, + ShaderCatalogEntry { .name = "equirect", .path = "data/shaders/equirect.glsl" }, + ShaderCatalogEntry { .name = "brush-stroke", .path = "data/shaders/stroke-instanced.glsl" }, + ShaderCatalogEntry { .name = "vertex-color", .path = "data/shaders/vertex-color.glsl" }, + ShaderCatalogEntry { .name = "lambert", .path = "data/shaders/lambert.glsl" }, + ShaderCatalogEntry { .name = "lambert-lightmap", .path = "data/shaders/lightmap.glsl" }, + ShaderCatalogEntry { .name = "bakeuv", .path = "data/shaders/bake-uv.glsl" }, +}; + +[[nodiscard]] bool is_empty_c_string(const char* text) noexcept +{ + return text == nullptr || text[0] == '\0'; +} + +[[nodiscard]] bool has_shader_extension(std::string_view path) noexcept +{ + constexpr std::string_view extension = ".glsl"; + return path.size() >= extension.size() + && path.substr(path.size() - extension.size()) == extension; +} + +} + +std::span panopainter_shader_catalog() noexcept +{ + return pano_catalog; +} + +pp::foundation::Status validate_shader_catalog(std::span catalog) noexcept +{ + if (catalog.empty()) { + return pp::foundation::Status::invalid_argument("shader catalog must not be empty"); + } + + if (catalog.size() > max_shader_catalog_entries) { + return pp::foundation::Status::out_of_range("shader catalog exceeds the configured limit"); + } + + for (std::size_t i = 0; i < catalog.size(); ++i) { + const auto& entry = catalog[i]; + if (is_empty_c_string(entry.name)) { + return pp::foundation::Status::invalid_argument("shader catalog entry name must not be empty"); + } + + if (is_empty_c_string(entry.path)) { + return pp::foundation::Status::invalid_argument("shader catalog entry path must not be empty"); + } + + if (!has_shader_extension(entry.path)) { + return pp::foundation::Status::invalid_argument("shader catalog path must end with .glsl"); + } + + for (std::size_t j = i + 1U; j < catalog.size(); ++j) { + if (std::string_view(entry.name) == std::string_view(catalog[j].name)) { + return pp::foundation::Status::invalid_argument("shader catalog entry name is duplicated"); + } + } + } + + return pp::foundation::Status::success(); +} + +} diff --git a/src/renderer_api/shader_catalog.h b/src/renderer_api/shader_catalog.h new file mode 100644 index 0000000..b4ead33 --- /dev/null +++ b/src/renderer_api/shader_catalog.h @@ -0,0 +1,21 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include + +namespace pp::renderer { + +constexpr std::size_t max_shader_catalog_entries = 256; + +struct ShaderCatalogEntry { + const char* name = ""; + const char* path = ""; +}; + +[[nodiscard]] std::span panopainter_shader_catalog() noexcept; +[[nodiscard]] pp::foundation::Status validate_shader_catalog( + std::span catalog) noexcept; + +} diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 107c849..fdd4015 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -1,6 +1,8 @@ #include "renderer_api/renderer_api.h" +#include "renderer_api/shader_catalog.h" #include "test_harness.h" +#include #include using pp::foundation::StatusCode; @@ -22,12 +24,15 @@ using pp::renderer::TextureFormat; using pp::renderer::Viewport; using pp::renderer::max_shader_source_bytes; using pp::renderer::max_texture_dimension; +using pp::renderer::panopainter_shader_catalog; using pp::renderer::primitive_topology_name; +using pp::renderer::ShaderCatalogEntry; using pp::renderer::texture_byte_size; using pp::renderer::texture_format_name; using pp::renderer::validate_extent; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; +using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; using pp::renderer::validate_viewport; @@ -274,6 +279,65 @@ void validates_shader_program_descriptors(pp::tests::Harness& h) PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range); } +void validates_panopainter_shader_catalog(pp::tests::Harness& h) +{ + const auto catalog = panopainter_shader_catalog(); + + PP_EXPECT(h, catalog.size() == 25U); + PP_EXPECT(h, validate_shader_catalog(catalog).ok()); + PP_EXPECT(h, catalog.front().name == std::string_view("texture")); + PP_EXPECT(h, catalog.front().path == std::string_view("data/shaders/texture.glsl")); + PP_EXPECT(h, catalog.back().name == std::string_view("bakeuv")); + PP_EXPECT(h, catalog.back().path == std::string_view("data/shaders/bake-uv.glsl")); + + bool found_stroke = false; + bool found_brush_stroke = false; + bool found_equirect = false; + for (const auto& entry : catalog) { + found_stroke = found_stroke || std::string_view(entry.name) == "stroke"; + found_brush_stroke = found_brush_stroke || std::string_view(entry.name) == "brush-stroke"; + found_equirect = found_equirect || std::string_view(entry.name) == "equirect"; + } + + PP_EXPECT(h, found_stroke); + PP_EXPECT(h, found_brush_stroke); + PP_EXPECT(h, found_equirect); +} + +void rejects_invalid_shader_catalogs(pp::tests::Harness& h) +{ + const std::array duplicated { + ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" }, + ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture-alpha.glsl" }, + }; + const std::array missing_name { + ShaderCatalogEntry { .name = "", .path = "data/shaders/texture.glsl" }, + }; + const std::array missing_path { + ShaderCatalogEntry { .name = "texture", .path = "" }, + }; + const std::array wrong_extension { + ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.txt" }, + }; + + const auto empty = validate_shader_catalog({}); + const auto duplicate = validate_shader_catalog(duplicated); + const auto no_name = validate_shader_catalog(missing_name); + const auto no_path = validate_shader_catalog(missing_path); + const auto bad_extension = validate_shader_catalog(wrong_extension); + + PP_EXPECT(h, !empty.ok()); + PP_EXPECT(h, empty.code == StatusCode::invalid_argument); + PP_EXPECT(h, !duplicate.ok()); + PP_EXPECT(h, duplicate.code == StatusCode::invalid_argument); + PP_EXPECT(h, !no_name.ok()); + PP_EXPECT(h, no_name.code == StatusCode::invalid_argument); + PP_EXPECT(h, !no_path.ok()); + PP_EXPECT(h, no_path.code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_extension.ok()); + PP_EXPECT(h, bad_extension.code == StatusCode::invalid_argument); +} + void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) { FakeRenderDevice device; @@ -310,6 +374,8 @@ int main() harness.run("validates_readback_bounds", validates_readback_bounds); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); + harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); + harness.run("rejects_invalid_shader_catalogs", rejects_invalid_shader_catalogs); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); return harness.finish(); }