From 58f163788bd79dbebb0f42b7e476cd1816abd927 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 16:23:02 +0200 Subject: [PATCH] Add renderer render pass clear contract --- docs/modernization/build-inventory.md | 23 +++--- docs/modernization/roadmap.md | 34 ++++---- src/renderer_api/recording_renderer.cpp | 14 +++- src/renderer_api/recording_renderer.h | 7 +- src/renderer_api/renderer_api.cpp | 21 +++++ src/renderer_api/renderer_api.h | 12 ++- tests/CMakeLists.txt | 2 +- tests/renderer_api/renderer_api_tests.cpp | 97 +++++++++++++++++++++-- tools/pano_cli/main.cpp | 22 ++++- 9 files changed, 191 insertions(+), 41 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index fe536a0..1213fa0 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -285,18 +285,19 @@ Known local toolchain state: source code reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL implementation files. - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates - backend-owned resource creation, command order, scissor state, depth state, - blend state, texture-slot binding, sampler-state binding, texture-upload byte - counts, shader-uniform writes, readback bounds, frame-capture sources, - destination buffer sizes, and render-target blit regions, records - render/scissor/depth/blend/shader-uniform/texture-bind/sampler-bind/upload/ - readback/frame-capture/blit commands, draw mesh inputs, and records trace - markers without a window or GL context. + backend-owned resource creation, command order, render-pass color/depth/ + stencil clear intent, scissor state, depth state, blend state, texture-slot + binding, sampler-state binding, texture-upload byte counts, + shader-uniform writes, readback bounds, frame-capture sources, destination + buffer sizes, and render-target blit regions, records + render-pass-clear/scissor/depth/blend/shader-uniform/texture-bind/ + sampler-bind/upload/readback/frame-capture/blit commands, draw mesh inputs, + and records trace markers without a window or GL context. - `pano_cli record-render` exposes the recording renderer through JSON - automation, including scissor/depth/blend/shader-uniform/texture-bind/ - sampler-bind/upload/readback/frame-capture/blit command and byte totals, - backend resource creation counts, plus draw vertex/index totals, and is - covered by + automation, including render-pass/depth-clear counts, scissor/depth/blend/ + shader-uniform/texture-bind/sampler-bind/upload/readback/frame-capture/blit + command and byte totals, backend resource creation counts, plus draw + vertex/index totals, and is covered by `pano_cli_record_render_smoke` plus `pano_cli_record_render_rejects_oversized_target`. - `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory` diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index f0d00ba..5ecdf1d 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -724,10 +724,11 @@ Results: validation, texture-upload byte-count validation, frame-capture byte-size and command-order validation, render-target blit validation, texture-slot binding validation, blend-state validation, scissor-state validation, - shader-uniform write validation, backend-neutral resource factory validation, - recording scissor/depth/blend/shader-uniform/texture/sampler-bind/upload/ - readback/frame-capture/blit command capture, draw mesh-input capture, and - invalid catalog rejection. + render-pass color/depth/stencil clear validation, shader-uniform write + validation, backend-neutral resource factory validation, recording + render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ + upload/readback/frame-capture/blit command capture, draw mesh-input capture, + and invalid catalog rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. @@ -822,20 +823,21 @@ Results: implementation files. - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict renderer-owned resource factory and - command-order/scissor-state/depth-state/blend-state/texture-bind/ - sampler-bind/shader-uniform/texture-upload/readback/frame-capture/blit - validation; it creates validated textures, render targets, shaders, meshes, - and readback buffers, then records commands, trace markers, scissor state, - depth state, blend state, shader uniform writes, texture/sampler binds, draw - mesh inputs, uploads/readbacks, frame captures, and render-target blits, - giving automation a backend-neutral render path that does not require a - window or GL context. + command-order/render-pass-clear/scissor-state/depth-state/blend-state/ + texture-bind/sampler-bind/shader-uniform/texture-upload/readback/ + frame-capture/blit validation; it creates validated textures, render targets, + shaders, meshes, and readback buffers, then records commands, trace markers, + render-pass color/depth/stencil clear intent, scissor state, depth state, + blend state, shader uniform writes, texture/sampler binds, draw mesh inputs, + uploads/readbacks, frame captures, and render-target blits, giving automation + a backend-neutral render path that does not require a window or GL context. - `pano_cli record-render` exercises that headless recording renderer and emits JSON command counts, resource creation counts, target dimensions, backend - name, trace/draw summary, and draw vertex/index totals, scissor/depth/ - blend-state plus shader-uniform/texture/sampler-bind/upload/readback/ - frame-capture/blit command/byte totals for agent automation, with an - expected-failure smoke for oversized render/readback targets. + name, trace/draw summary, render-pass/depth-clear counts, and draw + vertex/index totals, scissor/depth/blend-state plus shader-uniform/texture/ + sampler-bind/upload/readback/frame-capture/blit command/byte totals for + agent automation, with an expected-failure smoke for oversized + render/readback targets. - `pano_cli simulate-document-history` exercises pure document history apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation. diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index f21e844..fb45256 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -94,7 +94,7 @@ RecordingCommandContext::RecordingCommandContext(std::vector 1.0F)) { + return pp::foundation::Status::out_of_range("render pass clear depth must be within 0..1"); + } + + return pp::foundation::Status::success(); +} + pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept { switch (factor) { diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index ea6a40f..ca03011 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -64,6 +64,15 @@ struct ClearColor { float a = 0.0F; }; +struct RenderPassDesc { + bool clear_color_enabled = true; + ClearColor clear_color; + bool clear_depth_enabled = false; + float clear_depth = 1.0F; + bool clear_stencil_enabled = false; + std::uint8_t clear_stencil = 0; +}; + enum class PrimitiveTopology : std::uint8_t { triangles, triangle_strip, @@ -201,7 +210,7 @@ public: virtual ~ICommandContext() = default; [[nodiscard]] virtual pp::foundation::Status begin_render_pass( IRenderTarget& target, - ClearColor clear_color) noexcept = 0; + RenderPassDesc desc) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status set_viewport(Viewport viewport) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status set_scissor(ScissorRect scissor) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status set_blend_state(BlendState state) noexcept = 0; @@ -260,6 +269,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_scissor(ScissorRect scissor, Extent2D target_extent) noexcept; +[[nodiscard]] pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept; [[nodiscard]] pp::foundation::Status validate_blend_op(BlendOp op) noexcept; [[nodiscard]] pp::foundation::Status validate_blend_state(BlendState state) noexcept; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 543fd26..d973ba3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -365,7 +365,7 @@ if(TARGET pano_cli) COMMAND pano_cli record-render --width 32 --height 16) set_tests_properties(pano_cli_record_render_smoke PROPERTIES LABELS "renderer;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":6.*\"commands\":17.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":6.*\"commands\":17.*\"renderPasses\":1.*\"depthClears\":1.*\"stencilClears\":0.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") add_test(NAME pano_cli_record_render_rejects_oversized_target COMMAND "${CMAKE_COMMAND}" diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 0b186c9..bf50829 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -40,6 +41,7 @@ using pp::renderer::RecordingRenderDevice; using pp::renderer::RecordingRenderTarget; using pp::renderer::RecordingShaderProgram; using pp::renderer::RecordingTexture2D; +using pp::renderer::RenderPassDesc; using pp::renderer::SamplerAddressMode; using pp::renderer::sampler_address_mode_name; using pp::renderer::SamplerDesc; @@ -72,6 +74,7 @@ using pp::renderer::validate_compare_op; using pp::renderer::validate_depth_state; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; +using pp::renderer::validate_render_pass_desc; using pp::renderer::validate_sampler_address_mode; using pp::renderer::validate_sampler_desc; using pp::renderer::validate_sampler_filter; @@ -192,9 +195,14 @@ class FakeCommandContext final : public ICommandContext { public: [[nodiscard]] pp::foundation::Status begin_render_pass( IRenderTarget& target, - ClearColor) noexcept override + RenderPassDesc desc) noexcept override { + const auto render_pass_status = validate_render_pass_desc(desc); + if (!render_pass_status.ok()) { + return render_pass_status; + } in_render_pass = true; + last_render_pass_desc = desc; return validate_extent(target.color_desc().extent); } @@ -405,6 +413,7 @@ public: } bool in_render_pass = false; + RenderPassDesc last_render_pass_desc {}; const char* shader_name = nullptr; const char* last_uniform_name = nullptr; std::size_t last_uniform_bytes = 0; @@ -801,6 +810,50 @@ void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) PP_EXPECT(h, invalid_slot.code == StatusCode::out_of_range); } +void validates_render_pass_descriptors(pp::tests::Harness& h) +{ + const auto valid = validate_render_pass_desc(RenderPassDesc { + .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, + .clear_depth_enabled = true, + .clear_depth = 0.5F, + .clear_stencil_enabled = true, + .clear_stencil = 7, + }); + const auto no_clear = validate_render_pass_desc(RenderPassDesc { + .clear_color_enabled = false, + .clear_color = ClearColor {}, + .clear_depth_enabled = false, + .clear_stencil_enabled = false, + }); + const auto bad_color = validate_render_pass_desc(RenderPassDesc { + .clear_color = ClearColor { + .r = std::numeric_limits::infinity(), + .g = 0.0F, + .b = 0.0F, + .a = 1.0F, + }, + }); + const auto nan_depth = validate_render_pass_desc(RenderPassDesc { + .clear_color = ClearColor {}, + .clear_depth_enabled = true, + .clear_depth = std::numeric_limits::quiet_NaN(), + }); + const auto out_of_range_depth = validate_render_pass_desc(RenderPassDesc { + .clear_color = ClearColor {}, + .clear_depth_enabled = true, + .clear_depth = 1.5F, + }); + + PP_EXPECT(h, valid.ok()); + PP_EXPECT(h, no_clear.ok()); + PP_EXPECT(h, !bad_color.ok()); + PP_EXPECT(h, bad_color.code == StatusCode::invalid_argument); + PP_EXPECT(h, !nan_depth.ok()); + PP_EXPECT(h, nan_depth.code == StatusCode::invalid_argument); + PP_EXPECT(h, !out_of_range_depth.ok()); + PP_EXPECT(h, out_of_range_depth.code == StatusCode::out_of_range); +} + void validates_shader_program_descriptors(pp::tests::Harness& h) { constexpr char vertex_source[] = "#version 330 core\nvoid main(){}"; @@ -951,7 +1004,12 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) PP_EXPECT(h, device.trace_recorder.last_name == std::string_view("begin")); auto& context = device.immediate_context(); - PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }).ok()); + PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc { + .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, + .clear_depth_enabled = true, + .clear_depth = 0.75F, + }) + .ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 5, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { @@ -1007,6 +1065,9 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) PP_EXPECT(h, device.context.last_blend_state.enabled); PP_EXPECT(h, device.context.last_blend_state.source_color == BlendFactor::source_alpha); PP_EXPECT(h, device.context.last_blend_state.destination_color == BlendFactor::one_minus_source_alpha); + PP_EXPECT(h, device.context.last_render_pass_desc.clear_color.a == 1.0F); + PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth_enabled); + PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth == 0.75F); PP_EXPECT(h, device.context.last_depth_state.test_enabled); PP_EXPECT(h, device.context.last_depth_state.write_enabled); PP_EXPECT(h, device.context.last_depth_state.compare == CompareOp::less_or_equal); @@ -1109,7 +1170,7 @@ void recording_renderer_records_shader_uniform_writes(pp::tests::Harness& h) PP_EXPECT(h, !before_begin.ok()); PP_EXPECT(h, before_begin.code == StatusCode::invalid_argument); - PP_EXPECT(h, context.begin_render_pass(target, ClearColor {}).ok()); + PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto before_shader = context.set_shader_uniform("mvp", uniform_bytes); PP_EXPECT(h, !before_shader.ok()); PP_EXPECT(h, before_shader.code == StatusCode::invalid_argument); @@ -1163,7 +1224,14 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) device.trace()->marker("renderer", "frame"); auto& context = device.immediate_context(); - PP_EXPECT(h, context.begin_render_pass(target, ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }).ok()); + PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc { + .clear_color = ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }, + .clear_depth_enabled = true, + .clear_depth = 1.0F, + .clear_stencil_enabled = true, + .clear_stencil = 7, + }) + .ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 6, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { @@ -1201,7 +1269,12 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) PP_EXPECT(h, commands[0].name == std::string_view("frame")); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[1].target_desc.extent.width == 64U); + PP_EXPECT(h, commands[1].clear_color_enabled); PP_EXPECT(h, commands[1].clear_color.a == 1.0F); + PP_EXPECT(h, commands[1].clear_depth_enabled); + PP_EXPECT(h, commands[1].clear_depth == 1.0F); + PP_EXPECT(h, commands[1].clear_stencil_enabled); + PP_EXPECT(h, commands[1].clear_stencil == 7U); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_viewport); PP_EXPECT(h, commands[2].viewport.height == 32U); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::set_scissor); @@ -1350,12 +1423,21 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !sampler_before_begin.ok()); PP_EXPECT(h, sampler_before_begin.code == StatusCode::invalid_argument); - const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {}); + const auto invalid_target = context.begin_render_pass(non_render_target, RenderPassDesc {}); PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); PP_EXPECT(h, device.commands().empty()); - PP_EXPECT(h, context.begin_render_pass(target, ClearColor {}).ok()); + const auto invalid_clear_depth = context.begin_render_pass(target, RenderPassDesc { + .clear_color = ClearColor {}, + .clear_depth_enabled = true, + .clear_depth = -0.25F, + }); + PP_EXPECT(h, !invalid_clear_depth.ok()); + PP_EXPECT(h, invalid_clear_depth.code == StatusCode::out_of_range); + PP_EXPECT(h, device.commands().empty()); + + PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto upload_during_render_pass = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, @@ -1383,7 +1465,7 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !blit_during_render_pass.ok()); PP_EXPECT(h, blit_during_render_pass.code == StatusCode::invalid_argument); - const auto nested_begin = context.begin_render_pass(target, ClearColor {}); + const auto nested_begin = context.begin_render_pass(target, RenderPassDesc {}); PP_EXPECT(h, !nested_begin.ok()); PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument); @@ -1577,6 +1659,7 @@ int main() harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_sampler_contract", validates_sampler_contract); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); + harness.run("validates_render_pass_descriptors", validates_render_pass_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); harness.run("validates_shader_uniform_writes", validates_shader_uniform_writes); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index a38e4fb..5e4202f 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2297,7 +2297,11 @@ int record_render(int argc, char** argv) const auto begin_status = context.begin_render_pass( *target.value(), - pp::renderer::ClearColor { .r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F }); + pp::renderer::RenderPassDesc { + .clear_color = pp::renderer::ClearColor { .r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F }, + .clear_depth_enabled = true, + .clear_depth = 1.0F, + }); if (!begin_status.ok()) { print_error("record-render", begin_status.message); return 2; @@ -2435,6 +2439,9 @@ int record_render(int argc, char** argv) std::size_t capture_commands = 0; std::size_t blit_commands = 0; std::size_t trace_markers = 0; + std::size_t render_passes = 0; + std::size_t depth_clears = 0; + std::size_t stencil_clears = 0; std::uint64_t draw_vertices = 0; std::uint64_t draw_indices = 0; std::uint64_t uniform_bytes = 0; @@ -2446,7 +2453,15 @@ int record_render(int argc, char** argv) std::uint64_t blit_destination_bytes = 0; const auto commands = device.commands(); for (const auto& command : commands) { - if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) { + if (command.kind == pp::renderer::RecordedRenderCommandKind::begin_render_pass) { + ++render_passes; + if (command.clear_depth_enabled) { + ++depth_clears; + } + if (command.clear_stencil_enabled) { + ++stencil_clears; + } + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) { ++draw_commands; draw_vertices += command.mesh_desc.vertex_count; draw_indices += command.mesh_desc.index_count; @@ -2492,6 +2507,9 @@ int record_render(int argc, char** argv) << ",\"format\":\"rgba8\"}" << ",\"createdResources\":" << created_resources << ",\"commands\":" << commands.size() + << ",\"renderPasses\":" << render_passes + << ",\"depthClears\":" << depth_clears + << ",\"stencilClears\":" << stencil_clears << ",\"drawCommands\":" << draw_commands << ",\"drawVertices\":" << draw_vertices << ",\"drawIndices\":" << draw_indices