diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index b3fe6bb..3ec48bd 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -88,7 +88,8 @@ Known local toolchain state: `pp_ui_core`, `pano_cli`, and their current headless test binaries, including foundation event/logging/task queue coverage, PNG metadata, PPI header, settings document, document frame move/duration coverage, paint - brush/stroke coverage, UI color parsing, and layout XML parse coverage. + brush/stroke coverage, renderer shader descriptor coverage, UI color + parsing, and layout XML parse coverage. - `pano_cli inspect-image` reports PNG IHDR metadata as JSON and is covered by `pano_cli_inspect_png_metadata_smoke` with a tiny IHDR fixture. - `panopainter_validate_shaders` validates the current combined GLSL shader diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 372c6d7..539479f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -364,8 +364,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, mesh, render target, readback, and trace -interface validation. OpenGL classes are not yet behind these interfaces. +context, render device, shader program descriptor, mesh, render target, +readback, and trace interface validation. OpenGL classes are not yet behind +these interfaces. Implementation tasks: @@ -563,7 +564,7 @@ Results: - `pp_paint_stroke_tests` passed. - `pp_document_tests` passed, including frame move, duration, and history invariants. -- `pp_renderer_api_tests` passed. +- `pp_renderer_api_tests` passed, including shader descriptor validation. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index edb7ee4..d1c27ec 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -5,6 +5,34 @@ namespace pp::renderer { +namespace { + +[[nodiscard]] bool is_empty_c_string(const char* text) noexcept +{ + return text == nullptr || text[0] == '\0'; +} + +[[nodiscard]] pp::foundation::Status validate_shader_stage_source( + ShaderStageSource source, + const char* stage_name) noexcept +{ + if (is_empty_c_string(source.entry_point)) { + return pp::foundation::Status::invalid_argument(stage_name); + } + + if (source.source == nullptr || source.source_size == 0U) { + return pp::foundation::Status::invalid_argument("shader source must not be empty"); + } + + if (source.source_size > max_shader_source_bytes) { + return pp::foundation::Status::out_of_range("shader source exceeds the configured limit"); + } + + return pp::foundation::Status::success(); +} + +} + std::uint32_t bytes_per_pixel(TextureFormat format) noexcept { switch (format) { @@ -123,6 +151,29 @@ pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept return pp::foundation::Status::invalid_argument("mesh topology is not supported"); } +pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept +{ + if (desc.debug_name == nullptr) { + return pp::foundation::Status::invalid_argument("shader debug name must not be null"); + } + + const auto vertex_status = validate_shader_stage_source( + desc.vertex, + "vertex shader entry point must not be empty"); + if (!vertex_status.ok()) { + return vertex_status; + } + + const auto fragment_status = validate_shader_stage_source( + desc.fragment, + "fragment shader entry point must not be empty"); + if (!fragment_status.ok()) { + return fragment_status; + } + + return pp::foundation::Status::success(); +} + pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept { const auto extent_status = validate_extent(desc.extent); diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index 486f793..29a5186 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -10,6 +10,7 @@ namespace pp::renderer { constexpr std::uint32_t max_texture_dimension = 32768; constexpr std::uint32_t max_mesh_vertices = 16777216; constexpr std::uint64_t max_texture_bytes = 1024ULL * 1024ULL * 1024ULL; +constexpr std::size_t max_shader_source_bytes = 4ULL * 1024ULL * 1024ULL; enum class TextureFormat : std::uint8_t { rgba8, @@ -136,6 +137,7 @@ public: [[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept; [[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept; [[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept; +[[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept; [[nodiscard]] pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index ecc241e..107c849 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -15,9 +15,12 @@ using pp::renderer::IShaderProgram; using pp::renderer::MeshDesc; using pp::renderer::PrimitiveTopology; using pp::renderer::ReadbackRegion; +using pp::renderer::ShaderProgramDesc; +using pp::renderer::ShaderStageSource; using pp::renderer::TextureDesc; using pp::renderer::TextureFormat; using pp::renderer::Viewport; +using pp::renderer::max_shader_source_bytes; using pp::renderer::max_texture_dimension; using pp::renderer::primitive_topology_name; using pp::renderer::texture_byte_size; @@ -25,6 +28,7 @@ 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_program_desc; using pp::renderer::validate_viewport; namespace { @@ -220,6 +224,56 @@ void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) PP_EXPECT(h, empty_mesh.code == StatusCode::invalid_argument); } +void validates_shader_program_descriptors(pp::tests::Harness& h) +{ + constexpr char vertex_source[] = "#version 330 core\nvoid main(){}"; + constexpr char fragment_source[] = "#version 330 core\nout vec4 color; void main(){ color = vec4(1); }"; + + const ShaderProgramDesc valid { + .debug_name = "solid-color", + .vertex = ShaderStageSource { + .entry_point = "main", + .source = vertex_source, + .source_size = sizeof(vertex_source) - 1U, + }, + .fragment = ShaderStageSource { + .entry_point = "main", + .source = fragment_source, + .source_size = sizeof(fragment_source) - 1U, + }, + }; + + PP_EXPECT(h, validate_shader_program_desc(valid).ok()); + + auto missing_name = valid; + missing_name.debug_name = nullptr; + auto missing_vertex_entry = valid; + missing_vertex_entry.vertex.entry_point = ""; + auto missing_fragment_source = valid; + missing_fragment_source.fragment.source = nullptr; + auto empty_fragment_source = valid; + empty_fragment_source.fragment.source_size = 0; + auto excessive_source = valid; + excessive_source.vertex.source_size = max_shader_source_bytes + 1U; + + const auto missing_name_status = validate_shader_program_desc(missing_name); + const auto missing_vertex_entry_status = validate_shader_program_desc(missing_vertex_entry); + const auto missing_fragment_source_status = validate_shader_program_desc(missing_fragment_source); + const auto empty_fragment_source_status = validate_shader_program_desc(empty_fragment_source); + const auto excessive_source_status = validate_shader_program_desc(excessive_source); + + PP_EXPECT(h, !missing_name_status.ok()); + PP_EXPECT(h, missing_name_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_vertex_entry_status.ok()); + PP_EXPECT(h, missing_vertex_entry_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_fragment_source_status.ok()); + PP_EXPECT(h, missing_fragment_source_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, !empty_fragment_source_status.ok()); + PP_EXPECT(h, empty_fragment_source_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, !excessive_source_status.ok()); + PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range); +} + void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) { FakeRenderDevice device; @@ -255,6 +309,7 @@ int main() harness.run("rejects_invalid_or_excessive_extents", rejects_invalid_or_excessive_extents); 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("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); return harness.finish(); }