From 75dd5cfdc9bea97d8f35e01ce7ec9b74bf4999aa Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 16:35:38 +0200 Subject: [PATCH] Add renderer texture copy command --- docs/modernization/build-inventory.md | 18 +-- docs/modernization/roadmap.md | 28 ++-- src/renderer_api/recording_renderer.cpp | 45 ++++++ src/renderer_api/recording_renderer.h | 8 + src/renderer_api/renderer_api.cpp | 22 +++ src/renderer_api/renderer_api.h | 10 ++ tests/CMakeLists.txt | 2 +- tests/renderer_api/renderer_api_tests.cpp | 169 +++++++++++++++++++--- tools/pano_cli/main.cpp | 30 ++++ 9 files changed, 286 insertions(+), 46 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 54cf18f..994d05e 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -288,18 +288,18 @@ Known local toolchain state: backend-owned resource creation, command order, render-pass color/depth/ stencil clear intent, scissor state, depth state, blend state, texture-slot binding, sampler-state binding, texture-upload byte counts, - shader-uniform writes, explicit draw descriptor ranges, readback bounds, - frame-capture sources, destination buffer sizes, and render-target blit - regions, records + shader-uniform writes, explicit draw descriptor ranges, texture-copy regions, + readback bounds, frame-capture sources, destination buffer sizes, and + render-target blit regions, records render-pass-clear/scissor/depth/blend/shader-uniform/texture-bind/ - sampler-bind/draw/upload/readback/frame-capture/blit commands, draw mesh - inputs, explicit draw ranges, and records trace markers without a window or - GL context. + sampler-bind/draw/upload/texture-copy/readback/frame-capture/blit commands, + draw mesh inputs, explicit draw ranges, and records trace markers without a + window or GL context. - `pano_cli record-render` exposes the recording renderer through JSON automation, including render-pass/depth-clear counts, scissor/depth/blend/ - shader-uniform/texture-bind/sampler-bind/upload/readback/frame-capture/blit - command and byte totals, backend resource creation counts, plus draw - descriptor vertex/index totals, and is covered by + shader-uniform/texture-bind/sampler-bind/upload/texture-copy/readback/ + frame-capture/blit command and byte totals, backend resource creation counts, + plus draw descriptor vertex/index 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` diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index dd86c64..e66533c 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -726,10 +726,11 @@ Results: validation, blend-state validation, scissor-state validation, render-pass color/depth/stencil clear validation, shader-uniform write validation, draw descriptor/range validation, backend-neutral resource - factory validation, recording + factory validation, texture-copy validation, recording render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ - upload/readback/frame-capture/blit command capture, draw mesh-input capture, - explicit draw-range capture, and invalid catalog rejection. + upload/texture-copy/readback/frame-capture/blit command capture, draw + mesh-input capture, explicit draw-range capture, and invalid catalog + rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. @@ -826,20 +827,21 @@ Results: renderer-owned resource factory and command-order/render-pass-clear/scissor-state/depth-state/blend-state/ texture-bind/sampler-bind/shader-uniform/texture-upload/readback/ - frame-capture/blit validation plus explicit draw descriptor validation; it - creates validated textures, render targets, shaders, meshes, and readback - buffers, then records commands, trace markers, render-pass color/depth/ - stencil clear intent, scissor state, depth state, blend state, shader uniform - writes, texture/sampler binds, draw mesh inputs, explicit draw ranges, - uploads/readbacks, frame captures, and render-target blits, giving automation - a backend-neutral render path that does not require a window or GL context. + frame-capture/blit validation plus explicit draw descriptor and texture-copy + validation; it creates validated textures, render targets, shaders, meshes, + and readback buffers, then records commands, trace markers, render-pass + color/depth/stencil clear intent, scissor state, depth state, blend state, + shader uniform writes, texture/sampler binds, draw mesh inputs, explicit draw + ranges, texture uploads/copies/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, resource creation counts, target dimensions, backend name, trace/draw summary, render-pass/depth-clear counts, and draw descriptor vertex/index totals, scissor/depth/blend-state plus - shader-uniform/texture/sampler-bind/upload/readback/frame-capture/blit - command/byte totals for agent automation, with an expected-failure smoke for oversized - render/readback targets. + shader-uniform/texture/sampler-bind/upload/texture-copy/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 apply/undo/redo behavior and emits JSON layer/frame/history state for agent automation. diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index e10a7ee..cd88d6b 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -397,6 +397,49 @@ pp::foundation::Status RecordingCommandContext::upload_texture( return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::copy_texture( + ITexture2D& source, + ReadbackRegion source_region, + ITexture2D& destination, + ReadbackRegion destination_region) noexcept +{ + if (in_render_pass_) { + return pp::foundation::Status::invalid_argument("texture copy must be outside a render pass"); + } + + const auto source_desc = source.desc(); + const auto destination_desc = destination.desc(); + const auto desc_status = validate_texture_copy_descs( + source_desc, + source_region, + destination_desc, + destination_region); + if (!desc_status.ok()) { + return desc_status; + } + + const auto source_bytes = readback_byte_size(source_desc, source_region); + if (!source_bytes.ok()) { + return source_bytes.status(); + } + + const auto destination_bytes = readback_byte_size(destination_desc, destination_region); + if (!destination_bytes.ok()) { + return destination_bytes.status(); + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::copy_texture, + .source_desc = source_desc, + .destination_desc = destination_desc, + .source_region = source_region, + .destination_region = destination_region, + .copy_source_bytes = source_bytes.value(), + .copy_destination_bytes = destination_bytes.value(), + }); + return pp::foundation::Status::success(); +} + pp::foundation::Status RecordingCommandContext::capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept @@ -626,6 +669,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "draw"; case RecordedRenderCommandKind::upload_texture: return "upload_texture"; + case RecordedRenderCommandKind::copy_texture: + return "copy_texture"; case RecordedRenderCommandKind::read_texture: return "read_texture"; case RecordedRenderCommandKind::capture_frame: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index dc18f55..b316336 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -20,6 +20,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { bind_mesh, draw, upload_texture, + copy_texture, read_texture, capture_frame, blit_render_target, @@ -53,6 +54,8 @@ struct RecordedRenderCommand { ReadbackRegion destination_region {}; BlitFilter blit_filter = BlitFilter::nearest; std::uint64_t upload_bytes = 0; + std::uint64_t copy_source_bytes = 0; + std::uint64_t copy_destination_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; std::uint64_t blit_source_bytes = 0; @@ -138,6 +141,11 @@ public: ITexture2D& texture, ReadbackRegion region, std::span rgba_or_channel_bytes) noexcept override; + [[nodiscard]] pp::foundation::Status copy_texture( + ITexture2D& source, + ReadbackRegion source_region, + ITexture2D& destination, + ReadbackRegion destination_region) noexcept override; [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept override; diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 0a18b84..df5bf49 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -489,6 +489,28 @@ pp::foundation::Result frame_capture_byte_size(TextureDesc desc) return texture_byte_size(desc); } +pp::foundation::Status validate_texture_copy_descs( + TextureDesc source, + ReadbackRegion source_region, + TextureDesc destination, + ReadbackRegion destination_region) noexcept +{ + if (source.format != destination.format) { + return pp::foundation::Status::invalid_argument("texture copy endpoints must use matching formats"); + } + + if (source_region.width != destination_region.width || source_region.height != destination_region.height) { + return pp::foundation::Status::invalid_argument("texture copy regions must have matching dimensions"); + } + + const auto source_status = validate_readback_region(source, source_region); + if (!source_status.ok()) { + return source_status; + } + + return validate_readback_region(destination, destination_region); +} + pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept { switch (filter) { diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index 8bb146e..abb089f 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -243,6 +243,11 @@ public: ITexture2D& texture, ReadbackRegion region, std::span rgba_or_channel_bytes) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status copy_texture( + ITexture2D& source, + ReadbackRegion source_region, + ITexture2D& destination, + ReadbackRegion destination_region) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept = 0; @@ -299,6 +304,11 @@ 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_texture_copy_descs( + TextureDesc source, + ReadbackRegion source_region, + TextureDesc destination, + ReadbackRegion destination_region) noexcept; [[nodiscard]] pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept; [[nodiscard]] pp::foundation::Status validate_blit_descs( TextureDesc source, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d973ba3..562b89a 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.*\"createdResources\":6.*\"commands\":17.*\"renderPasses\":1.*\"depthClears\":1.*\"stencilClears\":0.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":6.*\"commands\":18.*\"renderPasses\":1.*\"depthClears\":1.*\"stencilClears\":0.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"copyCommands\":1.*\"copySourceBytes\":2048.*\"copyDestinationBytes\":2048.*\"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 ed51b69..e8ef64e 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -84,6 +84,7 @@ using pp::renderer::validate_scissor; using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; using pp::renderer::validate_shader_uniform_write; +using pp::renderer::validate_texture_copy_descs; using pp::renderer::validate_texture_slot; using pp::renderer::validate_viewport; @@ -380,6 +381,36 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status copy_texture( + pp::renderer::ITexture2D& source, + ReadbackRegion source_region, + pp::renderer::ITexture2D& destination, + ReadbackRegion destination_region) noexcept override + { + const auto status = validate_texture_copy_descs( + source.desc(), + source_region, + destination.desc(), + destination_region); + if (!status.ok()) { + return status; + } + + const auto source_bytes = readback_byte_size(source.desc(), source_region); + if (!source_bytes.ok()) { + return source_bytes.status(); + } + + const auto destination_bytes = readback_byte_size(destination.desc(), destination_region); + if (!destination_bytes.ok()) { + return destination_bytes.status(); + } + + last_copy_source_bytes = source_bytes.value(); + last_copy_destination_bytes = destination_bytes.value(); + return pp::foundation::Status::success(); + } + [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, pp::renderer::IReadbackBuffer& destination) noexcept override @@ -450,6 +481,8 @@ public: std::uint32_t last_sampler_slot = 0; SamplerDesc last_sampler_desc {}; std::uint64_t last_upload_bytes = 0; + std::uint64_t last_copy_source_bytes = 0; + std::uint64_t last_copy_destination_bytes = 0; std::uint64_t last_readback_bytes = 0; std::uint64_t last_capture_bytes = 0; std::uint64_t last_blit_source_bytes = 0; @@ -698,6 +731,48 @@ void validates_blit_contract(pp::tests::Harness& h) PP_EXPECT(h, blit_filter_name(static_cast(255)) == std::string_view("unknown")); } +void validates_texture_copy_contract(pp::tests::Harness& h) +{ + const TextureDesc rgba_desc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::rgba8, + }; + const TextureDesc r8_desc { + .extent = Extent2D { .width = 16, .height = 8 }, + .format = TextureFormat::r8, + }; + + PP_EXPECT(h, validate_texture_copy_descs( + rgba_desc, + ReadbackRegion { .x = 1, .y = 2, .width = 4, .height = 3 }, + rgba_desc, + ReadbackRegion { .x = 5, .y = 4, .width = 4, .height = 3 }) + .ok()); + + const auto mismatched_format = validate_texture_copy_descs( + rgba_desc, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + r8_desc, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + const auto mismatched_region = validate_texture_copy_descs( + rgba_desc, + ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, + rgba_desc, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + const auto outside_source = validate_texture_copy_descs( + rgba_desc, + ReadbackRegion { .x = 15, .y = 0, .width = 2, .height = 1 }, + rgba_desc, + ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); + + PP_EXPECT(h, !mismatched_format.ok()); + PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); + PP_EXPECT(h, !mismatched_region.ok()); + PP_EXPECT(h, mismatched_region.code == StatusCode::invalid_argument); + PP_EXPECT(h, !outside_source.ok()); + PP_EXPECT(h, outside_source.code == StatusCode::out_of_range); +} + void validates_blend_contract(pp::tests::Harness& h) { const BlendState alpha_blend { @@ -1105,6 +1180,12 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, upload_bytes) .ok()); + PP_EXPECT(h, context.copy_texture( + texture, + ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 4, .height = 5 }) + .ok()); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, @@ -1141,6 +1222,8 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) PP_EXPECT(h, device.context.last_sampler_desc.mag_filter == SamplerFilter::nearest); PP_EXPECT(h, device.context.last_sampler_desc.address_u == SamplerAddressMode::repeat); PP_EXPECT(h, device.context.last_upload_bytes == 80U); + PP_EXPECT(h, device.context.last_copy_source_bytes == 80U); + PP_EXPECT(h, device.context.last_copy_destination_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); @@ -1390,28 +1473,43 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) PP_EXPECT(h, commands_after_upload[12].upload_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[12].kind) == std::string_view("upload_texture")); + PP_EXPECT(h, context.copy_texture( + texture, + ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 3 }) + .ok()); + const auto commands_after_copy = device.commands(); + PP_EXPECT(h, commands_after_copy.size() == 14U); + PP_EXPECT(h, commands_after_copy[13].kind == RecordedRenderCommandKind::copy_texture); + PP_EXPECT(h, commands_after_copy[13].source_region.x == 4U); + PP_EXPECT(h, commands_after_copy[13].destination_region.x == 0U); + PP_EXPECT(h, commands_after_copy[13].copy_source_bytes == 96U); + PP_EXPECT(h, commands_after_copy[13].copy_destination_bytes == 96U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_copy[13].kind) == std::string_view("copy_texture")); + PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, readback_buffer) .ok()); const auto commands_after_readback = device.commands(); - PP_EXPECT(h, commands_after_readback.size() == 14U); - PP_EXPECT(h, commands_after_readback[13].kind == RecordedRenderCommandKind::read_texture); - PP_EXPECT(h, commands_after_readback[13].texture_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_readback[13].readback_region.x == 4U); - PP_EXPECT(h, commands_after_readback[13].readback_region.height == 3U); - PP_EXPECT(h, commands_after_readback[13].readback_bytes == 96U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[13].kind) == std::string_view("read_texture")); + PP_EXPECT(h, commands_after_readback.size() == 15U); + PP_EXPECT(h, commands_after_readback[14].kind == RecordedRenderCommandKind::read_texture); + PP_EXPECT(h, commands_after_readback[14].texture_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_readback[14].readback_region.x == 4U); + PP_EXPECT(h, commands_after_readback[14].readback_region.height == 3U); + PP_EXPECT(h, commands_after_readback[14].readback_bytes == 96U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[14].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() == 15U); - PP_EXPECT(h, commands_after_capture[14].kind == RecordedRenderCommandKind::capture_frame); - PP_EXPECT(h, commands_after_capture[14].target_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_capture[14].target_desc.extent.height == 32U); - PP_EXPECT(h, commands_after_capture[14].capture_bytes == 8192U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[14].kind) == std::string_view("capture_frame")); + PP_EXPECT(h, commands_after_capture.size() == 16U); + PP_EXPECT(h, commands_after_capture[15].kind == RecordedRenderCommandKind::capture_frame); + PP_EXPECT(h, commands_after_capture[15].target_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_capture[15].target_desc.extent.height == 32U); + PP_EXPECT(h, commands_after_capture[15].capture_bytes == 8192U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[15].kind) == std::string_view("capture_frame")); PP_EXPECT(h, context.blit_render_target( target, @@ -1421,16 +1519,16 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) BlitFilter::linear) .ok()); const auto commands_after_blit = device.commands(); - PP_EXPECT(h, commands_after_blit.size() == 16U); - PP_EXPECT(h, commands_after_blit[15].kind == RecordedRenderCommandKind::blit_render_target); - PP_EXPECT(h, commands_after_blit[15].source_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_blit[15].destination_desc.extent.height == 32U); - PP_EXPECT(h, commands_after_blit[15].source_region.width == 16U); - PP_EXPECT(h, commands_after_blit[15].destination_region.x == 2U); - PP_EXPECT(h, commands_after_blit[15].blit_filter == BlitFilter::linear); - PP_EXPECT(h, commands_after_blit[15].blit_source_bytes == 512U); - PP_EXPECT(h, commands_after_blit[15].blit_destination_bytes == 128U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[15].kind) == std::string_view("blit_render_target")); + PP_EXPECT(h, commands_after_blit.size() == 17U); + PP_EXPECT(h, commands_after_blit[16].kind == RecordedRenderCommandKind::blit_render_target); + PP_EXPECT(h, commands_after_blit[16].source_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_blit[16].destination_desc.extent.height == 32U); + PP_EXPECT(h, commands_after_blit[16].source_region.width == 16U); + PP_EXPECT(h, commands_after_blit[16].destination_region.x == 2U); + PP_EXPECT(h, commands_after_blit[16].blit_filter == BlitFilter::linear); + PP_EXPECT(h, commands_after_blit[16].blit_source_bytes == 512U); + PP_EXPECT(h, commands_after_blit[16].blit_destination_bytes == 128U); + PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[16].kind) == std::string_view("blit_render_target")); device.clear(); PP_EXPECT(h, device.commands().empty()); @@ -1517,6 +1615,14 @@ 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 copy_during_render_pass = context.copy_texture( + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + PP_EXPECT(h, !copy_during_render_pass.ok()); + PP_EXPECT(h, copy_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); @@ -1653,6 +1759,22 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !upload_outside_bounds.ok()); PP_EXPECT(h, upload_outside_bounds.code == StatusCode::out_of_range); + const auto copy_mismatched_regions = context.copy_texture( + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + PP_EXPECT(h, !copy_mismatched_regions.ok()); + PP_EXPECT(h, copy_mismatched_regions.code == StatusCode::invalid_argument); + + const auto copy_outside_bounds = context.copy_texture( + texture, + ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); + PP_EXPECT(h, !copy_outside_bounds.ok()); + PP_EXPECT(h, copy_outside_bounds.code == StatusCode::out_of_range); + const auto upload_with_wrong_size = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, @@ -1723,6 +1845,7 @@ int main() 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_texture_copy_contract", validates_texture_copy_contract); harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_sampler_contract", validates_sampler_contract); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index ba6b250..81f731b 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2403,6 +2403,26 @@ int record_render(int argc, char** argv) return 2; } + const auto copy_status = context.copy_texture( + *texture.value(), + pp::renderer::ReadbackRegion { + .x = 0, + .y = 0, + .width = args.width, + .height = args.height, + }, + *texture.value(), + pp::renderer::ReadbackRegion { + .x = 0, + .y = 0, + .width = args.width, + .height = args.height, + }); + if (!copy_status.ok()) { + print_error("record-render", copy_status.message); + return 2; + } + const auto capture_status = context.capture_frame(*target.value(), *readback_buffer.value()); if (!capture_status.ok()) { print_error("record-render", capture_status.message); @@ -2438,6 +2458,7 @@ int record_render(int argc, char** argv) std::size_t bind_texture_commands = 0; std::size_t bind_sampler_commands = 0; std::size_t upload_commands = 0; + std::size_t copy_commands = 0; std::size_t readback_commands = 0; std::size_t capture_commands = 0; std::size_t blit_commands = 0; @@ -2449,6 +2470,8 @@ int record_render(int argc, char** argv) std::uint64_t draw_indices = 0; std::uint64_t uniform_bytes = 0; std::uint64_t upload_bytes = 0; + std::uint64_t copy_source_bytes = 0; + std::uint64_t copy_destination_bytes = 0; std::uint64_t bound_texture_bytes = 0; std::uint64_t readback_bytes = 0; std::uint64_t capture_bytes = 0; @@ -2488,6 +2511,10 @@ int record_render(int argc, char** argv) } else if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) { ++upload_commands; upload_bytes += command.upload_bytes; + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::copy_texture) { + ++copy_commands; + copy_source_bytes += command.copy_source_bytes; + copy_destination_bytes += command.copy_destination_bytes; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::read_texture) { ++readback_commands; readback_bytes += command.readback_bytes; @@ -2526,6 +2553,9 @@ int record_render(int argc, char** argv) << ",\"boundTextureBytes\":" << bound_texture_bytes << ",\"uploadCommands\":" << upload_commands << ",\"uploadBytes\":" << upload_bytes + << ",\"copyCommands\":" << copy_commands + << ",\"copySourceBytes\":" << copy_source_bytes + << ",\"copyDestinationBytes\":" << copy_destination_bytes << ",\"readbackCommands\":" << readback_commands << ",\"readbackBytes\":" << readback_bytes << ",\"captureCommands\":" << capture_commands