Extract renderer shader catalog

This commit is contained in:
2026-06-01 17:36:25 +02:00
parent ad255a6ddf
commit d61c7f37c3
7 changed files with 204 additions and 55 deletions

View File

@@ -150,7 +150,8 @@ target_link_libraries(pp_document
pp_project_warnings) pp_project_warnings)
add_library(pp_renderer_api STATIC 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 target_include_directories(pp_renderer_api
PUBLIC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")
@@ -209,6 +210,7 @@ if(PP_BUILD_APP)
PUBLIC PUBLIC
pp_project_options pp_project_options
PRIVATE PRIVATE
pp_renderer_api
pp_project_warnings) pp_project_warnings)
target_include_directories(pp_legacy_app target_include_directories(pp_legacy_app

View File

@@ -119,6 +119,9 @@ Known local toolchain state:
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. 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 - `windows-msvc-vcpkg-headless` validates manifest install/configure/build/test
for the current headless component matrix; see DEBT-0007 for remaining app for the current headless component matrix; see DEBT-0007 for remaining app
and platform triplet migration. and platform triplet migration.

View File

@@ -383,8 +383,9 @@ adding new backends.
Status: started. `pp_renderer_api` exists as a headless renderer-neutral target Status: started. `pp_renderer_api` exists as a headless renderer-neutral target
with texture descriptor, byte-size, viewport, mesh, readback bounds, command with texture descriptor, byte-size, viewport, mesh, readback bounds, command
context, render device, shader program descriptor, mesh, render target, context, render device, shader program descriptor, mesh, render target,
readback, and trace interface validation. OpenGL classes are not yet behind readback, trace interface validation, and the canonical PanoPainter shader
these interfaces. catalog now consumed by the legacy OpenGL app initialization path. OpenGL
classes are not yet behind these interfaces.
Implementation tasks: Implementation tasks:
@@ -592,7 +593,8 @@ Results:
- `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face - `pp_document_ppi_import_tests` passed, including decoded PPI dirty-face
payload attachment to `pp_document` layer/frame storage and out-of-range payload attachment to `pp_document` layer/frame storage and out-of-range
payload rejection. 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_paint_renderer_compositor_tests` passed.
- `pp_ui_core_color_tests` passed. - `pp_ui_core_color_tests` passed.
- `pp_ui_core_layout_value_tests` passed. - `pp_ui_core_layout_value_tests` passed.

View File

@@ -1,12 +1,13 @@
#include "pch.h" #include "pch.h"
#include "app.h" #include "app.h"
#include "renderer_api/shader_catalog.h"
#include "shader.h" #include "shader.h"
void App::initShaders() void App::initShaders()
{ {
#ifdef _DEBUG #ifdef _DEBUG
if (!check_uniform_uniqueness()) if (!check_uniform_uniqueness())
std::logic_error("check_uniform_uniqueness() failed"); LOG("check_uniform_uniqueness() failed");
#endif // _DEBUG #endif // _DEBUG
render_task([] { render_task([] {
@@ -45,56 +46,19 @@ void App::initShaders()
LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled"); LOG("Shader Extension shader_framebuffer_fetch: %s", ShaderManager::ext_framebuffer_fetch ? "enabled" : "disabled");
LOG("initializing shaders"); LOG("initializing shaders");
if (!ShaderManager::load(kShader::Texture, "data/shaders/texture.glsl")) const auto shader_catalog = pp::renderer::panopainter_shader_catalog();
LOG("Failed to create shader Texture"); const auto catalog_status = pp::renderer::validate_shader_catalog(shader_catalog);
if (!ShaderManager::load(kShader::TextureAlpha, "data/shaders/texture-alpha.glsl")) if (!catalog_status.ok())
LOG("Failed to create shader TextureAlpha"); {
if (!ShaderManager::load(kShader::TextureMask, "data/shaders/texture-mask.glsl")) LOG("Shader catalog validation failed: %s", catalog_status.message);
LOG("Failed to create shader TextureMask"); return;
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")) for (const auto& shader : shader_catalog)
LOG("Failed to create shader TextureBlend"); {
if (!ShaderManager::load(kShader::StrokePreview, "data/shaders/stroke-preview.glsl")) if (!ShaderManager::load(static_cast<kShader>(const_hash(shader.name)), shader.path))
LOG("Failed to create shader StrokePreview"); LOG("Failed to create shader %s", shader.name);
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");
LOG("shaders initialized"); LOG("shaders initialized");
} }

View File

@@ -0,0 +1,91 @@
#include "renderer_api/shader_catalog.h"
#include <array>
#include <string_view>
namespace pp::renderer {
namespace {
constexpr std::array<ShaderCatalogEntry, 25> 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<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept
{
return pano_catalog;
}
pp::foundation::Status validate_shader_catalog(std::span<const ShaderCatalogEntry> 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();
}
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <span>
namespace pp::renderer {
constexpr std::size_t max_shader_catalog_entries = 256;
struct ShaderCatalogEntry {
const char* name = "";
const char* path = "";
};
[[nodiscard]] std::span<const ShaderCatalogEntry> panopainter_shader_catalog() noexcept;
[[nodiscard]] pp::foundation::Status validate_shader_catalog(
std::span<const ShaderCatalogEntry> catalog) noexcept;
}

View File

@@ -1,6 +1,8 @@
#include "renderer_api/renderer_api.h" #include "renderer_api/renderer_api.h"
#include "renderer_api/shader_catalog.h"
#include "test_harness.h" #include "test_harness.h"
#include <array>
#include <string_view> #include <string_view>
using pp::foundation::StatusCode; using pp::foundation::StatusCode;
@@ -22,12 +24,15 @@ using pp::renderer::TextureFormat;
using pp::renderer::Viewport; using pp::renderer::Viewport;
using pp::renderer::max_shader_source_bytes; using pp::renderer::max_shader_source_bytes;
using pp::renderer::max_texture_dimension; using pp::renderer::max_texture_dimension;
using pp::renderer::panopainter_shader_catalog;
using pp::renderer::primitive_topology_name; using pp::renderer::primitive_topology_name;
using pp::renderer::ShaderCatalogEntry;
using pp::renderer::texture_byte_size; using pp::renderer::texture_byte_size;
using pp::renderer::texture_format_name; using pp::renderer::texture_format_name;
using pp::renderer::validate_extent; using pp::renderer::validate_extent;
using pp::renderer::validate_mesh_desc; using pp::renderer::validate_mesh_desc;
using pp::renderer::validate_readback_region; using pp::renderer::validate_readback_region;
using pp::renderer::validate_shader_catalog;
using pp::renderer::validate_shader_program_desc; using pp::renderer::validate_shader_program_desc;
using pp::renderer::validate_viewport; 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); 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<ShaderCatalogEntry, 2> duplicated {
ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" },
ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture-alpha.glsl" },
};
const std::array<ShaderCatalogEntry, 1> missing_name {
ShaderCatalogEntry { .name = "", .path = "data/shaders/texture.glsl" },
};
const std::array<ShaderCatalogEntry, 1> missing_path {
ShaderCatalogEntry { .name = "texture", .path = "" },
};
const std::array<ShaderCatalogEntry, 1> 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) void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
{ {
FakeRenderDevice device; FakeRenderDevice device;
@@ -310,6 +374,8 @@ int main()
harness.run("validates_readback_bounds", validates_readback_bounds); harness.run("validates_readback_bounds", validates_readback_bounds);
harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); 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_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); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch);
return harness.finish(); return harness.finish();
} }