Plan recorded OpenGL command streams

This commit is contained in:
2026-06-02 20:48:49 +02:00
parent ce33eaaef2
commit 9a4c595f64
5 changed files with 242 additions and 4 deletions

View File

@@ -273,7 +273,11 @@ Known local toolchain state:
commands and maps render-pass clear masks/values, viewport/scissor state, commands and maps render-pass clear masks/values, viewport/scissor state,
blend/depth/sampler state, texture formats, primitive modes, draw counts, and blend/depth/sampler state, texture formats, primitive modes, draw counts, and
blit filters into GL-facing planned command data while rejecting unsupported 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, Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
depth clear masks, active texture units, and fallback 2D texture unbind depth clear masks, active texture units, and fallback 2D texture unbind
targets while retaining the existing VR SDK/platform bridge shape. targets while retaining the existing VR SDK/platform bridge shape.

View File

@@ -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, commands and maps render-pass clear masks/values, viewport/scissor state,
blend/depth/sampler state, texture formats, primitive modes, draw counts, and blend/depth/sampler state, texture formats, primitive modes, draw counts, and
blit filters into GL-facing planned command data with explicit unsupported-token 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 The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.
@@ -843,8 +847,9 @@ Results:
- `pp_renderer_gl_command_plan_tests` covers the headless OpenGL command - `pp_renderer_gl_command_plan_tests` covers the headless OpenGL command
planner for recorded render-pass clear masks/values, viewport/scissor state, planner for recorded render-pass clear masks/values, viewport/scissor state,
blend/depth/sampler state, texture format mapping, mesh/draw primitive modes, blend/depth/sampler state, texture format mapping, mesh/draw primitive modes,
draw counts, blit filters, planned command names, and unsupported enum draw counts, blit filters, planned command names, unsupported enum rejection,
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 - PowerShell analyze automation returns JSON summaries and includes the shader
validation target and renderer-boundary guard. validation target and renderer-boundary guard.
- `windows-msvc-vcpkg-headless` configured through the Visual Studio bundled - `windows-msvc-vcpkg-headless` configured through the Visual Studio bundled

View File

@@ -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 const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept
@@ -169,4 +185,67 @@ OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCo
return planned; return planned;
} }
OpenGlCommandPlan plan_recorded_render_commands(
std::span<const pp::renderer::RecordedRenderCommand> 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;
}
} }

View File

@@ -3,7 +3,10 @@
#include "renderer_api/recording_renderer.h" #include "renderer_api/recording_renderer.h"
#include "renderer_gl/opengl_capabilities.h" #include "renderer_gl/opengl_capabilities.h"
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <span>
#include <vector>
namespace pp::renderer::gl { namespace pp::renderer::gl {
@@ -42,8 +45,26 @@ struct OpenGlPlannedCommand {
bool supported = false; bool supported = false;
}; };
struct OpenGlCommandPlan {
static constexpr std::size_t npos = static_cast<std::size_t>(-1);
std::vector<OpenGlPlannedCommand> 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]] const char* planned_command_kind_name(OpenGlPlannedCommandKind kind) noexcept;
[[nodiscard]] OpenGlPlannedCommand plan_recorded_render_command( [[nodiscard]] OpenGlPlannedCommand plan_recorded_render_command(
pp::renderer::RecordedRenderCommand command) noexcept; pp::renderer::RecordedRenderCommand command) noexcept;
[[nodiscard]] OpenGlCommandPlan plan_recorded_render_commands(
std::span<const pp::renderer::RecordedRenderCommand> commands);
} }

View File

@@ -2,9 +2,54 @@
#include "test_harness.h" #include "test_harness.h"
#include <string_view> #include <string_view>
#include <vector>
namespace { 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) void maps_render_pass_and_state_commands(pp::tests::Harness& h)
{ {
const auto begin = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand { 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")); == std::string_view("unknown"));
} }
void plans_valid_recorded_command_streams(pp::tests::Harness& h)
{
const std::vector<pp::renderer::RecordedRenderCommand> 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<pp::renderer::RecordedRenderCommand> outside_pass {
viewport_command(),
};
const std::vector<pp::renderer::RecordedRenderCommand> nested_pass {
begin_render_pass_command(),
begin_render_pass_command(),
command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
};
const std::vector<pp::renderer::RecordedRenderCommand> 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<pp::renderer::RecordedRenderCommand> commands {
begin_render_pass_command(),
draw_command(),
command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
command_with_kind(static_cast<pp::renderer::RecordedRenderCommandKind>(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() int main()
@@ -189,5 +315,8 @@ int main()
harness.run("maps_binding_draw_and_blit_commands", maps_binding_draw_and_blit_commands); 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("rejects_unsupported_command_tokens", rejects_unsupported_command_tokens);
harness.run("names_planned_command_kinds", names_planned_command_kinds); 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(); return harness.finish();
} }