diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index d5b95c6..b7d60dc 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -273,7 +273,11 @@ Known local toolchain state: commands and maps render-pass clear masks/values, viewport/scissor state, blend/depth/sampler state, texture formats, primitive modes, draw counts, and blit filters into GL-facing planned command data while rejecting unsupported - enum tokens before a real GL context is needed. + enum tokens before a real GL context is needed. It also plans whole recorded + command streams, preserving per-command planned data while counting render + passes, draws, passthrough commands, trace commands, unsupported commands, + and render-pass ordering errors such as state changes outside a pass, nested + passes, and unclosed passes. Desktop VR drawing also consumes backend-owned scissor/depth/blend state, depth clear masks, active texture units, and fallback 2D texture unbind targets while retaining the existing VR SDK/platform bridge shape. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 976b063..c6e1f07 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -532,7 +532,11 @@ The headless OpenGL command planner now consumes `pp_renderer_api` recorded commands and maps render-pass clear masks/values, viewport/scissor state, blend/depth/sampler state, texture formats, primitive modes, draw counts, and blit filters into GL-facing planned command data with explicit unsupported-token -rejection before a runtime GL context is needed. +rejection before a runtime GL context is needed. It also plans whole recorded +command streams, preserving per-command planned data while counting render +passes, draws, passthrough commands, trace commands, unsupported commands, and +render-pass ordering errors such as state changes outside a pass, nested passes, +and unclosed passes. The existing renderer classes are not yet fully behind the renderer interfaces. @@ -843,8 +847,9 @@ Results: - `pp_renderer_gl_command_plan_tests` covers the headless OpenGL command planner for recorded render-pass clear masks/values, viewport/scissor state, blend/depth/sampler state, texture format mapping, mesh/draw primitive modes, - draw counts, blit filters, planned command names, and unsupported enum - rejection. + draw counts, blit filters, planned command names, unsupported enum rejection, + whole recorded stream planning, valid trace/render/draw/blit ordering, and + broken render-pass order detection. - PowerShell analyze automation returns JSON summaries and includes the shader validation target and renderer-boundary guard. - `windows-msvc-vcpkg-headless` configured through the Visual Studio bundled diff --git a/src/renderer_gl/command_plan.cpp b/src/renderer_gl/command_plan.cpp index 040c4be..6fef601 100644 --- a/src/renderer_gl/command_plan.cpp +++ b/src/renderer_gl/command_plan.cpp @@ -31,6 +31,22 @@ namespace { } } +void record_unsupported_command(OpenGlCommandPlan& plan, std::size_t index) noexcept +{ + ++plan.unsupported_command_count; + if (plan.first_unsupported_command == OpenGlCommandPlan::npos) { + plan.first_unsupported_command = index; + } +} + +void record_render_pass_order_error(OpenGlCommandPlan& plan, std::size_t index) noexcept +{ + ++plan.render_pass_order_error_count; + if (plan.first_render_pass_order_error == OpenGlCommandPlan::npos) { + plan.first_render_pass_order_error = index; + } +} + } const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept @@ -169,4 +185,67 @@ OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCo return planned; } +OpenGlCommandPlan plan_recorded_render_commands( + std::span commands) +{ + OpenGlCommandPlan plan; + plan.commands.reserve(commands.size()); + + bool in_render_pass = false; + for (std::size_t index = 0; index < commands.size(); ++index) { + OpenGlPlannedCommand planned = plan_recorded_render_command(commands[index]); + + if (!planned.supported) { + record_unsupported_command(plan, index); + } + + switch (planned.kind) { + case OpenGlPlannedCommandKind::begin_render_pass: + ++plan.render_pass_count; + if (in_render_pass) { + record_render_pass_order_error(plan, index); + } + in_render_pass = true; + break; + case OpenGlPlannedCommandKind::end_render_pass: + if (!in_render_pass) { + record_render_pass_order_error(plan, index); + } + in_render_pass = false; + break; + case OpenGlPlannedCommandKind::draw: + ++plan.draw_command_count; + if (!in_render_pass) { + record_render_pass_order_error(plan, index); + } + break; + case OpenGlPlannedCommandKind::passthrough: + ++plan.passthrough_command_count; + if (planned.requires_render_pass && !in_render_pass) { + record_render_pass_order_error(plan, index); + } + break; + case OpenGlPlannedCommandKind::trace: + ++plan.trace_command_count; + break; + default: + if (planned.requires_render_pass && !in_render_pass) { + record_render_pass_order_error(plan, index); + } + break; + } + + plan.commands.push_back(planned); + } + + plan.ended_in_render_pass = in_render_pass; + if (in_render_pass) { + record_render_pass_order_error(plan, commands.size()); + } + + plan.supported = plan.unsupported_command_count == 0U + && plan.render_pass_order_error_count == 0U; + return plan; +} + } diff --git a/src/renderer_gl/command_plan.h b/src/renderer_gl/command_plan.h index 7559e7d..0c42e5a 100644 --- a/src/renderer_gl/command_plan.h +++ b/src/renderer_gl/command_plan.h @@ -3,7 +3,10 @@ #include "renderer_api/recording_renderer.h" #include "renderer_gl/opengl_capabilities.h" +#include #include +#include +#include namespace pp::renderer::gl { @@ -42,8 +45,26 @@ struct OpenGlPlannedCommand { bool supported = false; }; +struct OpenGlCommandPlan { + static constexpr std::size_t npos = static_cast(-1); + + std::vector commands; + std::uint32_t render_pass_count = 0; + std::uint32_t draw_command_count = 0; + std::uint32_t passthrough_command_count = 0; + std::uint32_t trace_command_count = 0; + std::uint32_t unsupported_command_count = 0; + std::uint32_t render_pass_order_error_count = 0; + std::size_t first_unsupported_command = npos; + std::size_t first_render_pass_order_error = npos; + bool ended_in_render_pass = false; + bool supported = true; +}; + [[nodiscard]] const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept; [[nodiscard]] OpenGlPlannedCommand plan_recorded_render_command( pp::renderer::RecordedRenderCommand command) noexcept; +[[nodiscard]] OpenGlCommandPlan plan_recorded_render_commands( + std::span commands); } diff --git a/tests/renderer_gl/command_plan_tests.cpp b/tests/renderer_gl/command_plan_tests.cpp index fb2dda9..c4cae76 100644 --- a/tests/renderer_gl/command_plan_tests.cpp +++ b/tests/renderer_gl/command_plan_tests.cpp @@ -2,9 +2,54 @@ #include "test_harness.h" #include +#include namespace { +pp::renderer::RecordedRenderCommand begin_render_pass_command() noexcept +{ + pp::renderer::RecordedRenderCommand command; + command.kind = pp::renderer::RecordedRenderCommandKind::begin_render_pass; + command.target_desc.extent = pp::renderer::Extent2D { .width = 64U, .height = 64U }; + command.target_desc.format = pp::renderer::TextureFormat::rgba8; + command.clear_color_enabled = true; + command.clear_depth_enabled = true; + return command; +} + +pp::renderer::RecordedRenderCommand command_with_kind( + pp::renderer::RecordedRenderCommandKind kind) noexcept +{ + pp::renderer::RecordedRenderCommand command; + command.kind = kind; + return command; +} + +pp::renderer::RecordedRenderCommand viewport_command() noexcept +{ + pp::renderer::RecordedRenderCommand command; + command.kind = pp::renderer::RecordedRenderCommandKind::set_viewport; + command.viewport = pp::renderer::Viewport { .width = 64U, .height = 64U }; + return command; +} + +pp::renderer::RecordedRenderCommand draw_command() noexcept +{ + pp::renderer::RecordedRenderCommand command; + command.kind = pp::renderer::RecordedRenderCommandKind::draw; + command.mesh_desc.topology = pp::renderer::PrimitiveTopology::triangles; + command.draw_desc.vertex_count = 3U; + return command; +} + +pp::renderer::RecordedRenderCommand blit_command(pp::renderer::BlitFilter filter) noexcept +{ + pp::renderer::RecordedRenderCommand command; + command.kind = pp::renderer::RecordedRenderCommandKind::blit_render_target; + command.blit_filter = filter; + return command; +} + void maps_render_pass_and_state_commands(pp::tests::Harness& h) { const auto begin = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand { @@ -180,6 +225,87 @@ void names_planned_command_kinds(pp::tests::Harness& h) == std::string_view("unknown")); } +void plans_valid_recorded_command_streams(pp::tests::Harness& h) +{ + const std::vector commands { + command_with_kind(pp::renderer::RecordedRenderCommandKind::trace_begin_scope), + begin_render_pass_command(), + viewport_command(), + command_with_kind(pp::renderer::RecordedRenderCommandKind::bind_shader), + draw_command(), + command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass), + blit_command(pp::renderer::BlitFilter::nearest), + }; + + const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands); + + PP_EXPECT(h, plan.supported); + PP_EXPECT(h, plan.commands.size() == commands.size()); + PP_EXPECT(h, plan.render_pass_count == 1U); + PP_EXPECT(h, plan.draw_command_count == 1U); + PP_EXPECT(h, plan.passthrough_command_count == 1U); + PP_EXPECT(h, plan.trace_command_count == 1U); + PP_EXPECT(h, plan.unsupported_command_count == 0U); + PP_EXPECT(h, plan.render_pass_order_error_count == 0U); + PP_EXPECT(h, plan.first_unsupported_command == pp::renderer::gl::OpenGlCommandPlan::npos); + PP_EXPECT(h, plan.first_render_pass_order_error == pp::renderer::gl::OpenGlCommandPlan::npos); + PP_EXPECT(h, !plan.ended_in_render_pass); + PP_EXPECT(h, plan.commands[1].kind == pp::renderer::gl::OpenGlPlannedCommandKind::begin_render_pass); + PP_EXPECT(h, plan.commands[3].kind == pp::renderer::gl::OpenGlPlannedCommandKind::passthrough); + PP_EXPECT(h, plan.commands[4].kind == pp::renderer::gl::OpenGlPlannedCommandKind::draw); + PP_EXPECT(h, plan.commands[6].kind == pp::renderer::gl::OpenGlPlannedCommandKind::blit_render_target); +} + +void flags_broken_render_pass_command_order(pp::tests::Harness& h) +{ + const std::vector outside_pass { + viewport_command(), + }; + const std::vector nested_pass { + begin_render_pass_command(), + begin_render_pass_command(), + command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass), + }; + const std::vector unclosed_pass { + begin_render_pass_command(), + draw_command(), + }; + + const auto outside = pp::renderer::gl::plan_recorded_render_commands(outside_pass); + const auto nested = pp::renderer::gl::plan_recorded_render_commands(nested_pass); + const auto unclosed = pp::renderer::gl::plan_recorded_render_commands(unclosed_pass); + + PP_EXPECT(h, !outside.supported); + PP_EXPECT(h, outside.render_pass_order_error_count == 1U); + PP_EXPECT(h, outside.first_render_pass_order_error == 0U); + PP_EXPECT(h, !nested.supported); + PP_EXPECT(h, nested.render_pass_order_error_count == 1U); + PP_EXPECT(h, nested.first_render_pass_order_error == 1U); + PP_EXPECT(h, nested.render_pass_count == 2U); + PP_EXPECT(h, !unclosed.supported); + PP_EXPECT(h, unclosed.render_pass_order_error_count == 1U); + PP_EXPECT(h, unclosed.first_render_pass_order_error == unclosed_pass.size()); + PP_EXPECT(h, unclosed.ended_in_render_pass); +} + +void tracks_unsupported_commands_in_streams(pp::tests::Harness& h) +{ + std::vector commands { + begin_render_pass_command(), + draw_command(), + command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass), + command_with_kind(static_cast(255U)), + }; + + const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands); + + PP_EXPECT(h, !plan.supported); + PP_EXPECT(h, plan.unsupported_command_count == 1U); + PP_EXPECT(h, plan.first_unsupported_command == 3U); + PP_EXPECT(h, plan.render_pass_order_error_count == 0U); + PP_EXPECT(h, plan.commands[3].kind == pp::renderer::gl::OpenGlPlannedCommandKind::unknown); +} + } int main() @@ -189,5 +315,8 @@ int main() harness.run("maps_binding_draw_and_blit_commands", maps_binding_draw_and_blit_commands); harness.run("rejects_unsupported_command_tokens", rejects_unsupported_command_tokens); harness.run("names_planned_command_kinds", names_planned_command_kinds); + harness.run("plans_valid_recorded_command_streams", plans_valid_recorded_command_streams); + harness.run("flags_broken_render_pass_command_order", flags_broken_render_pass_command_order); + harness.run("tracks_unsupported_commands_in_streams", tracks_unsupported_commands_in_streams); return harness.finish(); }