diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index e8ef8f0..c6838f1 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -286,12 +286,12 @@ Known local toolchain state: legacy OpenGL implementation files. - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates command order, texture-upload byte counts, readback bounds, frame-capture - sources, and destination buffer sizes, records render/upload/readback/ - frame-capture commands, and records trace markers without a window or GL - context. + sources, destination buffer sizes, and render-target blit regions, records + render/upload/readback/frame-capture/blit commands, and records trace markers + without a window or GL context. - `pano_cli record-render` exposes the recording renderer through JSON - automation, including upload/readback/frame-capture command and byte totals, - and is covered by `pano_cli_record_render_smoke` plus + automation, including upload/readback/frame-capture/blit command and 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 diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 6a6cd2b..f7e9416 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -417,9 +417,10 @@ 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, texture-upload/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. +frame-capture byte-size helpers, frame-capture command validation, +render-target blit 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 OpenGL capability detection for framebuffer fetch, map-buffer alignment, and float texture support. It also owns the OpenGL texture upload-type mapping used @@ -720,8 +721,9 @@ Results: - `pp_renderer_api_tests` passed, including shader descriptor validation, PanoPainter shader catalog validation, readback byte-size and command-order validation, texture-upload byte-count validation, frame-capture byte-size and - command-order validation, recording upload/readback/frame-capture command - capture, and invalid catalog rejection. + command-order validation, render-target blit validation, recording + upload/readback/frame-capture/blit 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. @@ -815,13 +817,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/texture-upload/readback/frame-capture validation; it records - commands, trace markers, texture uploads/readbacks, and frame captures, giving - automation a backend-neutral render path that does not require a window or GL - context. + command-order/texture-upload/readback/frame-capture/blit validation; it + records commands, trace markers, texture 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, target dimensions, backend name, trace/draw summary, and - texture-upload/readback/frame-capture command/byte totals for agent + texture-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 diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index 1d8cb2d..182241c 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -256,6 +256,52 @@ pp::foundation::Status RecordingCommandContext::capture_frame( return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::blit_render_target( + IRenderTarget& source, + ReadbackRegion source_region, + IRenderTarget& destination, + ReadbackRegion destination_region, + BlitFilter filter) noexcept +{ + if (in_render_pass_) { + return pp::foundation::Status::invalid_argument("render target blit must be outside a render pass"); + } + + const auto source_desc = source.color_desc(); + const auto destination_desc = destination.color_desc(); + const auto desc_status = validate_blit_descs(source_desc, destination_desc); + if (!desc_status.ok()) { + return desc_status; + } + + const auto filter_status = validate_blit_filter(filter); + if (!filter_status.ok()) { + return filter_status; + } + + const auto source_bytes = readback_byte_size(source_desc, source_region); + if (!source_bytes) { + return source_bytes.status(); + } + + const auto destination_bytes = readback_byte_size(destination_desc, destination_region); + if (!destination_bytes) { + return destination_bytes.status(); + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::blit_render_target, + .source_desc = source_desc, + .destination_desc = destination_desc, + .source_region = source_region, + .destination_region = destination_region, + .blit_filter = filter, + .blit_source_bytes = source_bytes.value(), + .blit_destination_bytes = destination_bytes.value(), + }); + return pp::foundation::Status::success(); +} + void RecordingCommandContext::end_render_pass() noexcept { if (!in_render_pass_) { @@ -339,6 +385,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "read_texture"; case RecordedRenderCommandKind::capture_frame: return "capture_frame"; + case RecordedRenderCommandKind::blit_render_target: + return "blit_render_target"; case RecordedRenderCommandKind::end_render_pass: return "end_render_pass"; case RecordedRenderCommandKind::trace_marker: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index ed80afb..11fb86a 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -16,6 +16,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { upload_texture, read_texture, capture_frame, + blit_render_target, end_render_pass, trace_marker, }; @@ -27,10 +28,17 @@ struct RecordedRenderCommand { Viewport viewport {}; MeshDesc mesh_desc {}; TextureDesc texture_desc {}; + TextureDesc source_desc {}; + TextureDesc destination_desc {}; ReadbackRegion readback_region {}; + ReadbackRegion source_region {}; + ReadbackRegion destination_region {}; + BlitFilter blit_filter = BlitFilter::nearest; std::uint64_t upload_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; + std::uint64_t blit_source_bytes = 0; + std::uint64_t blit_destination_bytes = 0; const char* component = ""; const char* name = ""; }; @@ -102,6 +110,12 @@ public: [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept override; + [[nodiscard]] pp::foundation::Status blit_render_target( + IRenderTarget& source, + ReadbackRegion source_region, + IRenderTarget& destination, + ReadbackRegion destination_region, + BlitFilter filter) noexcept override; void end_render_pass() noexcept override; [[nodiscard]] bool in_render_pass() const noexcept; diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 4f141bc..319075b 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -241,6 +241,40 @@ pp::foundation::Result frame_capture_byte_size(TextureDesc desc) return texture_byte_size(desc); } +pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept +{ + switch (filter) { + case BlitFilter::nearest: + case BlitFilter::linear: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("blit filter is not supported"); +} + +pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc destination) noexcept +{ + if (!source.render_target || !destination.render_target) { + return pp::foundation::Status::invalid_argument("blit endpoints must be render targets"); + } + + if (source.format != destination.format) { + return pp::foundation::Status::invalid_argument("blit endpoints must use matching texture formats"); + } + + const auto source_status = texture_byte_size(source); + if (!source_status.ok()) { + return source_status.status(); + } + + const auto destination_status = texture_byte_size(destination); + if (!destination_status.ok()) { + return destination_status.status(); + } + + return pp::foundation::Status::success(); +} + const char* texture_format_name(TextureFormat format) noexcept { switch (format) { @@ -269,4 +303,16 @@ const char* primitive_topology_name(PrimitiveTopology topology) noexcept return "unknown"; } +const char* blit_filter_name(BlitFilter filter) noexcept +{ + switch (filter) { + case BlitFilter::nearest: + return "nearest"; + case BlitFilter::linear: + return "linear"; + } + + return "unknown"; +} + } diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index caa8d34..0d65ddb 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -59,6 +59,11 @@ enum class PrimitiveTopology : std::uint8_t { lines, }; +enum class BlitFilter : std::uint8_t { + nearest, + linear, +}; + struct MeshDesc { std::uint32_t vertex_count = 0; std::uint32_t index_count = 0; @@ -134,6 +139,12 @@ public: [[nodiscard]] virtual pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status blit_render_target( + IRenderTarget& source, + ReadbackRegion source_region, + IRenderTarget& destination, + ReadbackRegion destination_region, + BlitFilter filter) noexcept = 0; virtual void end_render_pass() noexcept = 0; }; @@ -156,7 +167,12 @@ public: ReadbackRegion region) noexcept; [[nodiscard]] pp::foundation::Result frame_capture_byte_size(TextureDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept; +[[nodiscard]] pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept; +[[nodiscard]] pp::foundation::Status validate_blit_descs( + TextureDesc source, + TextureDesc destination) noexcept; [[nodiscard]] const char* texture_format_name(TextureFormat format) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; +[[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7cb3454..1345e99 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.*\"commands\":10.*\"drawCommands\":1.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":11.*\"drawCommands\":1.*\"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 2dd6f22..8522340 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -8,6 +8,8 @@ #include using pp::foundation::StatusCode; +using pp::renderer::BlitFilter; +using pp::renderer::blit_filter_name; using pp::renderer::ClearColor; using pp::renderer::Extent2D; using pp::renderer::frame_capture_byte_size; @@ -42,6 +44,8 @@ using pp::renderer::ShaderCatalogEntry; using pp::renderer::texture_byte_size; using pp::renderer::texture_format_name; using pp::renderer::validate_extent; +using pp::renderer::validate_blit_descs; +using pp::renderer::validate_blit_filter; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; using pp::renderer::validate_shader_catalog; @@ -200,6 +204,38 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status blit_render_target( + IRenderTarget& source, + ReadbackRegion source_region, + IRenderTarget& destination, + ReadbackRegion destination_region, + BlitFilter filter) noexcept override + { + const auto source_desc = source.color_desc(); + const auto destination_desc = destination.color_desc(); + const auto desc_status = validate_blit_descs(source_desc, destination_desc); + if (!desc_status.ok()) { + return desc_status; + } + const auto filter_status = validate_blit_filter(filter); + if (!filter_status.ok()) { + return filter_status; + } + const auto source_bytes = readback_byte_size(source_desc, source_region); + if (!source_bytes) { + return source_bytes.status(); + } + const auto destination_bytes = readback_byte_size(destination_desc, destination_region); + if (!destination_bytes) { + return destination_bytes.status(); + } + + last_blit_source_bytes = source_bytes.value(); + last_blit_destination_bytes = destination_bytes.value(); + last_blit_filter = filter; + return pp::foundation::Status::success(); + } + void end_render_pass() noexcept override { in_render_pass = false; @@ -210,6 +246,9 @@ public: std::uint64_t last_upload_bytes = 0; std::uint64_t last_readback_bytes = 0; std::uint64_t last_capture_bytes = 0; + std::uint64_t last_blit_source_bytes = 0; + std::uint64_t last_blit_destination_bytes = 0; + BlitFilter last_blit_filter = BlitFilter::nearest; }; class FakeRenderDevice final : public IRenderDevice { @@ -338,6 +377,42 @@ void computes_frame_capture_byte_sizes(pp::tests::Harness& h) PP_EXPECT(h, non_target.status().code == StatusCode::invalid_argument); } +void validates_blit_contract(pp::tests::Harness& h) +{ + const TextureDesc target_desc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::rgba8, + .render_target = true, + }; + const TextureDesc r8_target_desc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::r8, + .render_target = true, + }; + const TextureDesc texture_desc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::rgba8, + .render_target = false, + }; + + PP_EXPECT(h, validate_blit_descs(target_desc, target_desc).ok()); + PP_EXPECT(h, validate_blit_filter(BlitFilter::nearest).ok()); + PP_EXPECT(h, validate_blit_filter(BlitFilter::linear).ok()); + PP_EXPECT(h, blit_filter_name(BlitFilter::linear) == std::string_view("linear")); + + const auto non_target = validate_blit_descs(texture_desc, target_desc); + const auto mismatched_format = validate_blit_descs(target_desc, r8_target_desc); + const auto bad_filter = validate_blit_filter(static_cast(255)); + + PP_EXPECT(h, !non_target.ok()); + PP_EXPECT(h, non_target.code == StatusCode::invalid_argument); + PP_EXPECT(h, !mismatched_format.ok()); + PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_filter.ok()); + PP_EXPECT(h, bad_filter.code == StatusCode::invalid_argument); + PP_EXPECT(h, blit_filter_name(static_cast(255)) == std::string_view("unknown")); +} + void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) { const Extent2D target { .width = 64, .height = 32 }; @@ -512,10 +587,20 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) readback_buffer) .ok()); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); + PP_EXPECT(h, context.blit_render_target( + target, + ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, + target, + ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 10 }, + BlitFilter::linear) + .ok()); PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader")); PP_EXPECT(h, device.context.last_upload_bytes == 80U); PP_EXPECT(h, device.context.last_readback_bytes == 80U); PP_EXPECT(h, device.context.last_capture_bytes == 8192U); + PP_EXPECT(h, device.context.last_blit_source_bytes == 80U); + PP_EXPECT(h, device.context.last_blit_destination_bytes == 320U); + PP_EXPECT(h, device.context.last_blit_filter == BlitFilter::linear); } void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) @@ -533,6 +618,11 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) .format = TextureFormat::rgba8, .render_target = true, }); + RecordingRenderTarget blit_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 }); @@ -601,6 +691,25 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) PP_EXPECT(h, commands_after_capture[9].capture_bytes == 8192U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[9].kind) == std::string_view("capture_frame")); + PP_EXPECT(h, context.blit_render_target( + target, + ReadbackRegion { .x = 0, .y = 0, .width = 16, .height = 8 }, + blit_target, + ReadbackRegion { .x = 2, .y = 3, .width = 8, .height = 4 }, + BlitFilter::linear) + .ok()); + const auto commands_after_blit = device.commands(); + PP_EXPECT(h, commands_after_blit.size() == 11U); + PP_EXPECT(h, commands_after_blit[10].kind == RecordedRenderCommandKind::blit_render_target); + PP_EXPECT(h, commands_after_blit[10].source_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_blit[10].destination_desc.extent.height == 32U); + PP_EXPECT(h, commands_after_blit[10].source_region.width == 16U); + PP_EXPECT(h, commands_after_blit[10].destination_region.x == 2U); + PP_EXPECT(h, commands_after_blit[10].blit_filter == BlitFilter::linear); + PP_EXPECT(h, commands_after_blit[10].blit_source_bytes == 512U); + PP_EXPECT(h, commands_after_blit[10].blit_destination_bytes == 128U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[10].kind) == std::string_view("blit_render_target")); + device.clear(); PP_EXPECT(h, device.commands().empty()); } @@ -618,6 +727,11 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har .format = TextureFormat::rgba8, .render_target = false, }); + RecordingRenderTarget r8_target(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::r8, + .render_target = true, + }); RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, @@ -660,6 +774,15 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !capture_during_render_pass.ok()); PP_EXPECT(h, capture_during_render_pass.code == StatusCode::invalid_argument); + const auto blit_during_render_pass = context.blit_render_target( + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + BlitFilter::nearest); + 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 {}); PP_EXPECT(h, !nested_begin.ok()); PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument); @@ -720,6 +843,42 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har 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); + + const auto blit_non_target = context.blit_render_target( + non_render_target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + BlitFilter::nearest); + PP_EXPECT(h, !blit_non_target.ok()); + PP_EXPECT(h, blit_non_target.code == StatusCode::invalid_argument); + + const auto blit_mismatched_format = context.blit_render_target( + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + r8_target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + BlitFilter::nearest); + PP_EXPECT(h, !blit_mismatched_format.ok()); + PP_EXPECT(h, blit_mismatched_format.code == StatusCode::invalid_argument); + + const auto blit_outside_bounds = context.blit_render_target( + target, + ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + BlitFilter::nearest); + PP_EXPECT(h, !blit_outside_bounds.ok()); + PP_EXPECT(h, blit_outside_bounds.code == StatusCode::out_of_range); + + const auto blit_bad_filter = context.blit_render_target( + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + static_cast(255)); + PP_EXPECT(h, !blit_bad_filter.ok()); + PP_EXPECT(h, blit_bad_filter.code == StatusCode::invalid_argument); } } @@ -732,6 +891,7 @@ int main() 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_blit_contract", validates_blit_contract); 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); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 0c64f21..75a175e 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2221,6 +2221,11 @@ int record_render(int argc, char** argv) .format = pp::renderer::TextureFormat::rgba8, .render_target = true, }); + pp::renderer::RecordingRenderTarget blit_target(pp::renderer::TextureDesc { + .extent = pp::renderer::Extent2D { .width = args.width, .height = args.height }, + .format = pp::renderer::TextureFormat::rgba8, + .render_target = true, + }); pp::renderer::RecordingReadbackBuffer readback_buffer( static_cast(args.width) * args.height * 4U); const std::array upload_pixel { @@ -2305,14 +2310,38 @@ int record_render(int argc, char** argv) return 2; } + const auto blit_status = context.blit_render_target( + target, + pp::renderer::ReadbackRegion { + .x = 0, + .y = 0, + .width = args.width, + .height = args.height, + }, + blit_target, + pp::renderer::ReadbackRegion { + .x = 0, + .y = 0, + .width = args.width, + .height = args.height, + }, + pp::renderer::BlitFilter::nearest); + if (!blit_status.ok()) { + print_error("record-render", blit_status.message); + return 2; + } + std::size_t draw_commands = 0; std::size_t upload_commands = 0; std::size_t readback_commands = 0; std::size_t capture_commands = 0; + std::size_t blit_commands = 0; std::size_t trace_markers = 0; std::uint64_t upload_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; + std::uint64_t blit_source_bytes = 0; + std::uint64_t blit_destination_bytes = 0; const auto commands = device.commands(); for (const auto& command : commands) { if (command.kind == pp::renderer::RecordedRenderCommandKind::draw) { @@ -2326,6 +2355,10 @@ int record_render(int argc, char** argv) } else if (command.kind == pp::renderer::RecordedRenderCommandKind::capture_frame) { ++capture_commands; capture_bytes += command.capture_bytes; + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::blit_render_target) { + ++blit_commands; + blit_source_bytes += command.blit_source_bytes; + blit_destination_bytes += command.blit_destination_bytes; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::trace_marker) { ++trace_markers; } @@ -2344,6 +2377,9 @@ int record_render(int argc, char** argv) << ",\"readbackBytes\":" << readback_bytes << ",\"captureCommands\":" << capture_commands << ",\"captureBytes\":" << capture_bytes + << ",\"blitCommands\":" << blit_commands + << ",\"blitSourceBytes\":" << blit_source_bytes + << ",\"blitDestinationBytes\":" << blit_destination_bytes << ",\"traceMarkers\":" << trace_markers << ",\"first\":\"" << pp::renderer::recorded_render_command_kind_name(commands.front().kind)