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)
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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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<kShader>(const_hash(shader.name)), shader.path))
LOG("Failed to create shader %s", shader.name);
}
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/shader_catalog.h"
#include "test_harness.h"
#include <array>
#include <string_view>
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<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)
{
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();
}