Add renderer frame capture contract
This commit is contained in:
@@ -285,12 +285,12 @@ 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
|
||||
command order, readback bounds, and destination buffer sizes, records render
|
||||
and readback commands, and records trace markers without a window or GL
|
||||
context.
|
||||
command order, readback bounds, frame-capture sources, and destination buffer
|
||||
sizes, records render/readback/frame-capture commands, and records trace
|
||||
markers without a window or GL context.
|
||||
- `pano_cli record-render` exposes the recording renderer through JSON
|
||||
automation, including readback command/byte totals, and is covered by
|
||||
`pano_cli_record_render_smoke` plus
|
||||
automation, including readback and frame-capture command/byte 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`
|
||||
apply/undo/redo state through JSON automation and is covered by
|
||||
|
||||
@@ -416,7 +416,8 @@ 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 program descriptor, mesh, render target,
|
||||
readback byte-size helpers, readback command validation, trace interface
|
||||
readback byte-size helpers, readback command validation, frame-capture
|
||||
byte-size helpers, frame-capture command validation, trace interface
|
||||
validation, and the canonical PanoPainter shader catalog now consumed by the
|
||||
legacy OpenGL app initialization path.
|
||||
`pp_renderer_gl` now exists as the first OpenGL backend library and owns pure
|
||||
@@ -718,8 +719,8 @@ Results:
|
||||
plus malformed payload rejection at the export boundary.
|
||||
- `pp_renderer_api_tests` passed, including shader descriptor validation,
|
||||
PanoPainter shader catalog validation, readback byte-size and command-order
|
||||
validation, recording readback command capture, and invalid catalog
|
||||
rejection.
|
||||
validation, frame-capture byte-size and command-order validation, recording
|
||||
readback/frame-capture command capture, and invalid catalog rejection.
|
||||
- `pp_paint_renderer_compositor_tests` passed.
|
||||
- `pp_ui_core_color_tests` passed.
|
||||
- `pp_ui_core_layout_value_tests` passed.
|
||||
@@ -813,13 +814,13 @@ Results:
|
||||
reintroduces raw `GL_*`/`WGL_*` constants outside the allowed legacy OpenGL
|
||||
implementation files.
|
||||
- `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict
|
||||
command-order/readback validation and command/trace/readback capture, giving
|
||||
automation a backend-neutral render path that does not require a window or GL
|
||||
context.
|
||||
command-order/readback/frame-capture validation; it records commands, trace
|
||||
markers, texture readbacks, and frame captures, 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, target dimensions, backend name, trace/draw summary, and
|
||||
readback command/byte totals for agent automation, with an expected-failure
|
||||
smoke for oversized render/readback targets.
|
||||
readback/frame-capture 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.
|
||||
|
||||
@@ -202,6 +202,32 @@ pp::foundation::Status RecordingCommandContext::read_texture(
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status RecordingCommandContext::capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept
|
||||
{
|
||||
if (in_render_pass_) {
|
||||
return pp::foundation::Status::invalid_argument("frame capture must be outside a render pass");
|
||||
}
|
||||
|
||||
const auto desc = target.color_desc();
|
||||
const auto bytes = frame_capture_byte_size(desc);
|
||||
if (!bytes) {
|
||||
return bytes.status();
|
||||
}
|
||||
|
||||
if (destination.size_bytes() < bytes.value()) {
|
||||
return pp::foundation::Status::out_of_range("frame capture buffer is too small");
|
||||
}
|
||||
|
||||
push_command(commands_, RecordedRenderCommand {
|
||||
.kind = RecordedRenderCommandKind::capture_frame,
|
||||
.target_desc = desc,
|
||||
.capture_bytes = bytes.value(),
|
||||
});
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
void RecordingCommandContext::end_render_pass() noexcept
|
||||
{
|
||||
if (!in_render_pass_) {
|
||||
@@ -281,6 +307,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no
|
||||
return "draw";
|
||||
case RecordedRenderCommandKind::read_texture:
|
||||
return "read_texture";
|
||||
case RecordedRenderCommandKind::capture_frame:
|
||||
return "capture_frame";
|
||||
case RecordedRenderCommandKind::end_render_pass:
|
||||
return "end_render_pass";
|
||||
case RecordedRenderCommandKind::trace_marker:
|
||||
|
||||
@@ -14,6 +14,7 @@ enum class RecordedRenderCommandKind : std::uint8_t {
|
||||
bind_mesh,
|
||||
draw,
|
||||
read_texture,
|
||||
capture_frame,
|
||||
end_render_pass,
|
||||
trace_marker,
|
||||
};
|
||||
@@ -27,6 +28,7 @@ struct RecordedRenderCommand {
|
||||
TextureDesc texture_desc {};
|
||||
ReadbackRegion readback_region {};
|
||||
std::uint64_t readback_bytes = 0;
|
||||
std::uint64_t capture_bytes = 0;
|
||||
const char* component = "";
|
||||
const char* name = "";
|
||||
};
|
||||
@@ -91,6 +93,9 @@ public:
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
IReadbackBuffer& destination) noexcept override;
|
||||
[[nodiscard]] pp::foundation::Status capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept override;
|
||||
void end_render_pass() noexcept override;
|
||||
|
||||
[[nodiscard]] bool in_render_pass() const noexcept;
|
||||
|
||||
@@ -231,6 +231,16 @@ pp::foundation::Result<std::uint64_t> readback_byte_size(TextureDesc desc, Readb
|
||||
return pp::foundation::Result<std::uint64_t>::success(bytes);
|
||||
}
|
||||
|
||||
pp::foundation::Result<std::uint64_t> frame_capture_byte_size(TextureDesc desc) noexcept
|
||||
{
|
||||
if (!desc.render_target) {
|
||||
return pp::foundation::Result<std::uint64_t>::failure(
|
||||
pp::foundation::Status::invalid_argument("frame capture source must be a render target"));
|
||||
}
|
||||
|
||||
return texture_byte_size(desc);
|
||||
}
|
||||
|
||||
const char* texture_format_name(TextureFormat format) noexcept
|
||||
{
|
||||
switch (format) {
|
||||
|
||||
@@ -126,6 +126,9 @@ public:
|
||||
ITexture2D& texture,
|
||||
ReadbackRegion region,
|
||||
IReadbackBuffer& destination) noexcept = 0;
|
||||
[[nodiscard]] virtual pp::foundation::Status capture_frame(
|
||||
IRenderTarget& target,
|
||||
IReadbackBuffer& destination) noexcept = 0;
|
||||
virtual void end_render_pass() noexcept = 0;
|
||||
};
|
||||
|
||||
@@ -146,6 +149,7 @@ public:
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> readback_byte_size(
|
||||
TextureDesc desc,
|
||||
ReadbackRegion region) noexcept;
|
||||
[[nodiscard]] pp::foundation::Result<std::uint64_t> frame_capture_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;
|
||||
[[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept;
|
||||
|
||||
@@ -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.*\"commands\":8.*\"drawCommands\":1.*\"readbackCommands\":1.*\"readbackBytes\":2048")
|
||||
PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":9.*\"drawCommands\":1.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048")
|
||||
|
||||
add_test(NAME pano_cli_record_render_rejects_oversized_target
|
||||
COMMAND "${CMAKE_COMMAND}"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
using pp::foundation::StatusCode;
|
||||
using pp::renderer::ClearColor;
|
||||
using pp::renderer::Extent2D;
|
||||
using pp::renderer::frame_capture_byte_size;
|
||||
using pp::renderer::ICommandContext;
|
||||
using pp::renderer::IMesh;
|
||||
using pp::renderer::IRenderDevice;
|
||||
@@ -167,6 +168,21 @@ public:
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
[[nodiscard]] pp::foundation::Status capture_frame(
|
||||
IRenderTarget& target,
|
||||
pp::renderer::IReadbackBuffer& destination) noexcept override
|
||||
{
|
||||
const auto bytes = frame_capture_byte_size(target.color_desc());
|
||||
if (!bytes) {
|
||||
return bytes.status();
|
||||
}
|
||||
if (destination.size_bytes() < bytes.value()) {
|
||||
return pp::foundation::Status::out_of_range("frame capture buffer is too small");
|
||||
}
|
||||
last_capture_bytes = bytes.value();
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
void end_render_pass() noexcept override
|
||||
{
|
||||
in_render_pass = false;
|
||||
@@ -175,6 +191,7 @@ public:
|
||||
bool in_render_pass = false;
|
||||
const char* shader_name = nullptr;
|
||||
std::uint64_t last_readback_bytes = 0;
|
||||
std::uint64_t last_capture_bytes = 0;
|
||||
};
|
||||
|
||||
class FakeRenderDevice final : public IRenderDevice {
|
||||
@@ -281,6 +298,28 @@ void computes_readback_byte_sizes(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, overrun.status().code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
void computes_frame_capture_byte_sizes(pp::tests::Harness& h)
|
||||
{
|
||||
const TextureDesc target_desc {
|
||||
.extent = Extent2D { .width = 16, .height = 8 },
|
||||
.format = TextureFormat::rgba8,
|
||||
.render_target = true,
|
||||
};
|
||||
const TextureDesc texture_desc {
|
||||
.extent = Extent2D { .width = 16, .height = 8 },
|
||||
.format = TextureFormat::rgba8,
|
||||
.render_target = false,
|
||||
};
|
||||
|
||||
const auto capture = frame_capture_byte_size(target_desc);
|
||||
const auto non_target = frame_capture_byte_size(texture_desc);
|
||||
|
||||
PP_EXPECT(h, capture.ok());
|
||||
PP_EXPECT(h, capture.value() == 512U);
|
||||
PP_EXPECT(h, !non_target.ok());
|
||||
PP_EXPECT(h, non_target.status().code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h)
|
||||
{
|
||||
const Extent2D target { .width = 64, .height = 32 };
|
||||
@@ -448,8 +487,10 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
|
||||
ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 },
|
||||
readback_buffer)
|
||||
.ok());
|
||||
PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok());
|
||||
PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader"));
|
||||
PP_EXPECT(h, device.context.last_readback_bytes == 80U);
|
||||
PP_EXPECT(h, device.context.last_capture_bytes == 8192U);
|
||||
}
|
||||
|
||||
void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
|
||||
@@ -512,6 +553,15 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, commands_after_readback[7].readback_bytes == 96U);
|
||||
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[7].kind) == std::string_view("read_texture"));
|
||||
|
||||
PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok());
|
||||
const auto commands_after_capture = device.commands();
|
||||
PP_EXPECT(h, commands_after_capture.size() == 9U);
|
||||
PP_EXPECT(h, commands_after_capture[8].kind == RecordedRenderCommandKind::capture_frame);
|
||||
PP_EXPECT(h, commands_after_capture[8].target_desc.extent.width == 64U);
|
||||
PP_EXPECT(h, commands_after_capture[8].target_desc.extent.height == 32U);
|
||||
PP_EXPECT(h, commands_after_capture[8].capture_bytes == 8192U);
|
||||
PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[8].kind) == std::string_view("capture_frame"));
|
||||
|
||||
device.clear();
|
||||
PP_EXPECT(h, device.commands().empty());
|
||||
}
|
||||
@@ -558,6 +608,10 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har
|
||||
PP_EXPECT(h, !read_during_render_pass.ok());
|
||||
PP_EXPECT(h, read_during_render_pass.code == StatusCode::invalid_argument);
|
||||
|
||||
const auto capture_during_render_pass = context.capture_frame(target, full_readback_buffer);
|
||||
PP_EXPECT(h, !capture_during_render_pass.ok());
|
||||
PP_EXPECT(h, capture_during_render_pass.code == StatusCode::invalid_argument);
|
||||
|
||||
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);
|
||||
@@ -596,6 +650,14 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har
|
||||
small_readback_buffer);
|
||||
PP_EXPECT(h, !read_into_small_buffer.ok());
|
||||
PP_EXPECT(h, read_into_small_buffer.code == StatusCode::out_of_range);
|
||||
|
||||
const auto capture_into_small_buffer = context.capture_frame(target, small_readback_buffer);
|
||||
PP_EXPECT(h, !capture_into_small_buffer.ok());
|
||||
PP_EXPECT(h, capture_into_small_buffer.code == StatusCode::out_of_range);
|
||||
|
||||
const auto capture_non_target = context.capture_frame(non_render_target, full_readback_buffer);
|
||||
PP_EXPECT(h, !capture_non_target.ok());
|
||||
PP_EXPECT(h, capture_non_target.code == StatusCode::invalid_argument);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -607,6 +669,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("computes_readback_byte_sizes", computes_readback_byte_sizes);
|
||||
harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes);
|
||||
harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors);
|
||||
harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors);
|
||||
harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog);
|
||||
|
||||
@@ -2279,10 +2279,18 @@ int record_render(int argc, char** argv)
|
||||
return 2;
|
||||
}
|
||||
|
||||
const auto capture_status = context.capture_frame(target, readback_buffer);
|
||||
if (!capture_status.ok()) {
|
||||
print_error("record-render", capture_status.message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::size_t draw_commands = 0;
|
||||
std::size_t readback_commands = 0;
|
||||
std::size_t capture_commands = 0;
|
||||
std::size_t trace_markers = 0;
|
||||
std::uint64_t readback_bytes = 0;
|
||||
std::uint64_t capture_bytes = 0;
|
||||
const auto commands = device.commands();
|
||||
for (const auto& command : commands) {
|
||||
if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) {
|
||||
@@ -2290,6 +2298,9 @@ int record_render(int argc, char** argv)
|
||||
} else if (command.kind == pp::renderer::RecordedRenderCommandKind::read_texture) {
|
||||
++readback_commands;
|
||||
readback_bytes += command.readback_bytes;
|
||||
} else if (command.kind == pp::renderer::RecordedRenderCommandKind::capture_frame) {
|
||||
++capture_commands;
|
||||
capture_bytes += command.capture_bytes;
|
||||
} else if (command.kind == pp::renderer::RecordedRenderCommandKind::trace_marker) {
|
||||
++trace_markers;
|
||||
}
|
||||
@@ -2304,6 +2315,8 @@ int record_render(int argc, char** argv)
|
||||
<< ",\"drawCommands\":" << draw_commands
|
||||
<< ",\"readbackCommands\":" << readback_commands
|
||||
<< ",\"readbackBytes\":" << readback_bytes
|
||||
<< ",\"captureCommands\":" << capture_commands
|
||||
<< ",\"captureBytes\":" << capture_bytes
|
||||
<< ",\"traceMarkers\":" << trace_markers
|
||||
<< ",\"first\":\""
|
||||
<< pp::renderer::recorded_render_command_kind_name(commands.front().kind)
|
||||
|
||||
Reference in New Issue
Block a user