Add headless recording renderer api

This commit is contained in:
2026-06-02 09:44:04 +02:00
parent 61f86f5aae
commit 1d44036933
6 changed files with 503 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
#include "renderer_api/renderer_api.h"
#include "renderer_api/recording_renderer.h"
#include "renderer_api/shader_catalog.h"
#include "test_harness.h"
@@ -17,6 +18,11 @@ using pp::renderer::IShaderProgram;
using pp::renderer::MeshDesc;
using pp::renderer::PrimitiveTopology;
using pp::renderer::ReadbackRegion;
using pp::renderer::RecordedRenderCommandKind;
using pp::renderer::RecordingMesh;
using pp::renderer::RecordingRenderDevice;
using pp::renderer::RecordingRenderTarget;
using pp::renderer::RecordingShaderProgram;
using pp::renderer::ShaderProgramDesc;
using pp::renderer::ShaderStageSource;
using pp::renderer::TextureDesc;
@@ -26,6 +32,7 @@ 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::recorded_render_command_kind_name;
using pp::renderer::ShaderCatalogEntry;
using pp::renderer::texture_byte_size;
using pp::renderer::texture_format_name;
@@ -364,6 +371,104 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader"));
}
void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
{
RecordingRenderDevice device;
RecordingRenderTarget target(TextureDesc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
});
RecordingShaderProgram shader("recorded-shader");
RecordingMesh mesh(MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles });
PP_EXPECT(h, device.backend_name() == std::string_view("recording"));
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.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok());
PP_EXPECT(h, context.bind_shader(shader).ok());
PP_EXPECT(h, context.bind_mesh(mesh).ok());
PP_EXPECT(h, context.draw().ok());
context.end_render_pass();
const auto commands = device.commands();
PP_EXPECT(h, commands.size() == 7U);
PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_marker);
PP_EXPECT(h, commands[0].component == std::string_view("renderer"));
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.a == 1.0F);
PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_viewport);
PP_EXPECT(h, commands[2].viewport.height == 32U);
PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::bind_shader);
PP_EXPECT(h, commands[3].name == std::string_view("recorded-shader"));
PP_EXPECT(h, commands[4].kind == RecordedRenderCommandKind::bind_mesh);
PP_EXPECT(h, commands[4].mesh_desc.vertex_count == 3U);
PP_EXPECT(h, commands[5].kind == RecordedRenderCommandKind::draw);
PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::end_render_pass);
PP_EXPECT(h, recorded_render_command_kind_name(commands[5].kind) == std::string_view("draw"));
device.clear();
PP_EXPECT(h, device.commands().empty());
}
void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Harness& h)
{
RecordingRenderDevice device;
RecordingRenderTarget target(TextureDesc {
.extent = Extent2D { .width = 32, .height = 16 },
.format = TextureFormat::rgba8,
.render_target = true,
});
RecordingRenderTarget non_render_target(TextureDesc {
.extent = Extent2D { .width = 32, .height = 16 },
.format = TextureFormat::rgba8,
.render_target = false,
});
RecordingShaderProgram shader("strict-shader");
RecordingMesh mesh(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles });
RecordingMesh empty_mesh(MeshDesc {});
auto& context = device.immediate_context();
const auto draw_before_begin = context.draw();
PP_EXPECT(h, !draw_before_begin.ok());
PP_EXPECT(h, draw_before_begin.code == StatusCode::invalid_argument);
const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {});
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 nested_begin = context.begin_render_pass(target, ClearColor {});
PP_EXPECT(h, !nested_begin.ok());
PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument);
const auto draw_without_bindings = context.draw();
PP_EXPECT(h, !draw_without_bindings.ok());
PP_EXPECT(h, draw_without_bindings.code == StatusCode::invalid_argument);
PP_EXPECT(h, context.bind_shader(shader).ok());
const auto draw_without_mesh = context.draw();
PP_EXPECT(h, !draw_without_mesh.ok());
PP_EXPECT(h, draw_without_mesh.code == StatusCode::invalid_argument);
const auto invalid_mesh = context.bind_mesh(empty_mesh);
PP_EXPECT(h, !invalid_mesh.ok());
PP_EXPECT(h, invalid_mesh.code == StatusCode::invalid_argument);
PP_EXPECT(h, context.bind_mesh(mesh).ok());
PP_EXPECT(h, context.draw().ok());
context.end_render_pass();
const auto viewport_after_end = context.set_viewport(Viewport { .x = 0, .y = 0, .width = 1, .height = 1 });
PP_EXPECT(h, !viewport_after_end.ok());
PP_EXPECT(h, viewport_after_end.code == StatusCode::invalid_argument);
}
}
int main()
@@ -377,5 +482,7 @@ int main()
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);
harness.run("recording_renderer_records_valid_command_sequences", recording_renderer_records_valid_command_sequences);
harness.run("recording_renderer_rejects_invalid_command_order_and_targets", recording_renderer_rejects_invalid_command_order_and_targets);
return harness.finish();
}