From 18617cdbd2f51f6653fa87b86983778931fd08c0 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 17:13:44 +0200 Subject: [PATCH] Add renderer texture transition contract --- docs/modernization/build-inventory.md | 6 +- docs/modernization/roadmap.md | 8 +- src/renderer_api/recording_renderer.cpp | 26 +++ src/renderer_api/recording_renderer.h | 7 + src/renderer_api/renderer_api.cpp | 96 ++++++++++ src/renderer_api/renderer_api.h | 21 ++ tests/CMakeLists.txt | 2 +- tests/renderer_api/renderer_api_tests.cpp | 223 ++++++++++++++++++++++ tools/pano_cli/main.cpp | 51 +++++ 9 files changed, 432 insertions(+), 8 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 678561e..ecca991 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -289,16 +289,16 @@ Known local toolchain state: render-pass color/depth/stencil clear intent, scissor state, depth state, blend state, texture-slot binding, sampler-state binding, texture-upload byte counts, texture mip-level counts, resource debug labels, mipmap-generation commands, - shader-uniform writes, explicit draw descriptor ranges, texture-copy regions, + texture-state transitions, 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/mipmap-generation/texture-copy/readback/ + sampler-bind/draw/upload/mipmap-generation/texture-transition/texture-copy/readback/ frame-capture/blit commands, draw mesh inputs, explicit draw ranges, and records trace markers and scopes 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/mipmap-generation/texture-copy/readback/ + shader-uniform/texture-bind/sampler-bind/upload/mipmap-generation/texture-transition/texture-copy/readback/ frame-capture/blit command and byte totals, trace marker/scope counts, labeled descriptor counts, backend resource creation counts, plus draw descriptor vertex/index totals, and is covered by diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 48a6654..7a0a848 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -419,7 +419,7 @@ mesh, readback bounds, command context, render device, shader program descriptor, mesh, render target, readback byte-size helpers, texture mip-level validation, resource debug-label validation, texture-upload/readback command validation, -mipmap-generation command validation, frame-capture byte-size helpers, +mipmap-generation command validation, texture-state transition validation, frame-capture byte-size helpers, frame-capture command validation, render-target blit validation, texture-slot binding validation, blend-state validation, scissor-state validation, depth-state validation, trace marker/scope validation, sampler-state validation, @@ -833,13 +833,13 @@ Results: renderer-owned resource factory and command-order/render-pass-clear/scissor-state/depth-state/blend-state/ texture-usage/texture-bind/sampler-bind/shader-uniform/texture-upload/ - mipmap-generation/readback/frame-capture/blit validation plus explicit draw + mipmap-generation/texture-transition/readback/frame-capture/blit validation plus explicit draw descriptor and texture-copy validation; it creates validated textures, render targets, shaders, meshes, and readback buffers with validated debug labels, then records commands, trace markers/scopes, 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/mipmap generations/copies/readbacks, frame captures, + ranges, texture uploads/mipmap generations/state transitions/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 @@ -847,7 +847,7 @@ Results: name, trace marker/scope and draw summary, labeled descriptor counts, render-pass/depth-clear counts, and draw descriptor vertex/index totals, scissor/depth/blend-state plus - shader-uniform/texture/sampler-bind/upload/mipmap-generation/texture-copy/readback/ + shader-uniform/texture/sampler-bind/upload/mipmap-generation/texture-transition/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 diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index 914a7a0..651257e 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -435,6 +435,30 @@ pp::foundation::Status RecordingCommandContext::generate_mipmaps(ITexture2D& tex return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::transition_texture( + ITexture2D& texture, + TextureState before, + TextureState after) noexcept +{ + if (in_render_pass_) { + return pp::foundation::Status::invalid_argument("texture transition must be outside a render pass"); + } + + const auto desc = texture.desc(); + const auto desc_status = validate_texture_transition_desc(desc, before, after); + if (!desc_status.ok()) { + return desc_status; + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::transition_texture, + .texture_desc = desc, + .before_state = before, + .after_state = after, + }); + return pp::foundation::Status::success(); +} + pp::foundation::Status RecordingCommandContext::copy_texture( ITexture2D& source, ReadbackRegion source_region, @@ -754,6 +778,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "upload_texture"; case RecordedRenderCommandKind::generate_mipmaps: return "generate_mipmaps"; + case RecordedRenderCommandKind::transition_texture: + return "transition_texture"; case RecordedRenderCommandKind::copy_texture: return "copy_texture"; case RecordedRenderCommandKind::read_texture: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index 7dedd17..faad25c 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -21,6 +21,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { draw, upload_texture, generate_mipmaps, + transition_texture, copy_texture, read_texture, capture_frame, @@ -52,6 +53,8 @@ struct RecordedRenderCommand { std::uint32_t sampler_slot = 0; TextureDesc source_desc {}; TextureDesc destination_desc {}; + TextureState before_state = TextureState::undefined; + TextureState after_state = TextureState::undefined; ReadbackRegion readback_region {}; ReadbackRegion source_region {}; ReadbackRegion destination_region {}; @@ -148,6 +151,10 @@ public: std::span rgba_or_channel_bytes) noexcept override; [[nodiscard]] pp::foundation::Status generate_mipmaps( ITexture2D& texture) noexcept override; + [[nodiscard]] pp::foundation::Status transition_texture( + ITexture2D& texture, + TextureState before, + TextureState after) noexcept override; [[nodiscard]] pp::foundation::Status copy_texture( ITexture2D& source, ReadbackRegion source_region, diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 97ba887..d5caa0d 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -692,6 +692,78 @@ pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcep return pp::foundation::Status::success(); } +pp::foundation::Status validate_texture_state(TextureState state) noexcept +{ + switch (state) { + case TextureState::undefined: + case TextureState::shader_read: + case TextureState::render_target: + case TextureState::upload_destination: + case TextureState::copy_source: + case TextureState::copy_destination: + case TextureState::readback_source: + case TextureState::present: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("texture state is not supported"); +} + +pp::foundation::Status validate_texture_transition_desc( + TextureDesc desc, + TextureState before, + TextureState after) noexcept +{ + const auto desc_status = validate_texture_desc(desc); + if (!desc_status.ok()) { + return desc_status; + } + + const auto before_status = validate_texture_state(before); + if (!before_status.ok()) { + return before_status; + } + + const auto after_status = validate_texture_state(after); + if (!after_status.ok()) { + return after_status; + } + + if (before == after) { + return pp::foundation::Status::invalid_argument("texture transition must change state"); + } + + if (after == TextureState::undefined) { + return pp::foundation::Status::invalid_argument("texture transition destination must not be undefined"); + } + + if (after == TextureState::shader_read && !has_texture_usage(desc.usage, TextureUsage::sampled)) { + return pp::foundation::Status::invalid_argument("shader-read transition requires sampled usage"); + } + + if (after == TextureState::render_target && !has_texture_usage(desc.usage, TextureUsage::render_target)) { + return pp::foundation::Status::invalid_argument("render-target transition requires render_target usage"); + } + + if (after == TextureState::upload_destination && !has_texture_usage(desc.usage, TextureUsage::upload_destination)) { + return pp::foundation::Status::invalid_argument("upload transition requires upload_destination usage"); + } + + if (after == TextureState::copy_source && !has_texture_usage(desc.usage, TextureUsage::copy_source)) { + return pp::foundation::Status::invalid_argument("copy-source transition requires copy_source usage"); + } + + if (after == TextureState::copy_destination && !has_texture_usage(desc.usage, TextureUsage::copy_destination)) { + return pp::foundation::Status::invalid_argument("copy-destination transition requires copy_destination usage"); + } + + if (after == TextureState::readback_source && !has_texture_usage(desc.usage, TextureUsage::readback_source)) { + return pp::foundation::Status::invalid_argument("readback transition requires readback_source usage"); + } + + return pp::foundation::Status::success(); +} + pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept { switch (filter) { @@ -749,6 +821,30 @@ const char* texture_format_name(TextureFormat format) noexcept return "unknown"; } +const char* texture_state_name(TextureState state) noexcept +{ + switch (state) { + case TextureState::undefined: + return "undefined"; + case TextureState::shader_read: + return "shader_read"; + case TextureState::render_target: + return "render_target"; + case TextureState::upload_destination: + return "upload_destination"; + case TextureState::copy_source: + return "copy_source"; + case TextureState::copy_destination: + return "copy_destination"; + case TextureState::readback_source: + return "readback_source"; + case TextureState::present: + return "present"; + } + + return "unknown"; +} + const char* primitive_topology_name(PrimitiveTopology topology) noexcept { switch (topology) { diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index fb81e9e..c76c713 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -35,6 +35,17 @@ enum class TextureUsage : std::uint32_t { copy_destination = 1U << 5U, }; +enum class TextureState : std::uint8_t { + undefined, + shader_read, + render_target, + upload_destination, + copy_source, + copy_destination, + readback_source, + present, +}; + [[nodiscard]] constexpr TextureUsage operator|(TextureUsage lhs, TextureUsage rhs) noexcept { return static_cast( @@ -285,6 +296,10 @@ public: std::span rgba_or_channel_bytes) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status generate_mipmaps( ITexture2D& texture) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status transition_texture( + ITexture2D& texture, + TextureState before, + TextureState after) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status copy_texture( ITexture2D& source, ReadbackRegion source_region, @@ -358,11 +373,17 @@ public: TextureDesc destination, ReadbackRegion destination_region) noexcept; [[nodiscard]] pp::foundation::Status validate_mipmap_generation_desc(TextureDesc desc) noexcept; +[[nodiscard]] pp::foundation::Status validate_texture_state(TextureState state) noexcept; +[[nodiscard]] pp::foundation::Status validate_texture_transition_desc( + TextureDesc desc, + TextureState before, + TextureState after) 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* texture_state_name(TextureState state) noexcept; [[nodiscard]] const char* primitive_topology_name(PrimitiveTopology topology) noexcept; [[nodiscard]] const char* blit_filter_name(BlitFilter filter) noexcept; [[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c9503aa..6e93251 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\":7.*\"labeledCommandDescriptors\":12.*\"commands\":21.*\"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.*\"mipmapCommands\":1.*\"mipmapLevels\":3.*\"mipmapBytes\":84.*\"copyCommands\":1.*\"copySourceBytes\":2048.*\"copyDestinationBytes\":2048.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048.*\"traceMarkers\":1.*\"traceBeginScopes\":1.*\"traceEndScopes\":1") + PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"createdResources\":7.*\"labeledCommandDescriptors\":16.*\"commands\":25.*\"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.*\"mipmapCommands\":1.*\"mipmapLevels\":3.*\"mipmapBytes\":84.*\"transitionCommands\":4.*\"transitionToUpload\":1.*\"transitionToShaderRead\":2.*\"copyCommands\":1.*\"copySourceBytes\":2048.*\"copyDestinationBytes\":2048.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048.*\"traceMarkers\":1.*\"traceBeginScopes\":1.*\"traceEndScopes\":1") 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 5170c80..0861796 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -54,6 +54,7 @@ using pp::renderer::ShaderProgramDesc; using pp::renderer::ShaderStageSource; using pp::renderer::TextureDesc; using pp::renderer::TextureFormat; +using pp::renderer::TextureState; using pp::renderer::TextureUsage; using pp::renderer::Viewport; using pp::renderer::max_shader_source_bytes; @@ -70,6 +71,7 @@ using pp::renderer::recorded_render_command_kind_name; using pp::renderer::ShaderCatalogEntry; using pp::renderer::texture_byte_size; using pp::renderer::texture_format_name; +using pp::renderer::texture_state_name; using pp::renderer::validate_extent; using pp::renderer::validate_blit_descs; using pp::renderer::validate_blit_filter; @@ -94,6 +96,8 @@ using pp::renderer::validate_shader_uniform_write; using pp::renderer::validate_texture_copy_descs; using pp::renderer::validate_texture_desc; using pp::renderer::validate_texture_slot; +using pp::renderer::validate_texture_state; +using pp::renderer::validate_texture_transition_desc; using pp::renderer::validate_texture_usage; using pp::renderer::validate_trace_label; using pp::renderer::validate_viewport; @@ -457,6 +461,21 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status transition_texture( + pp::renderer::ITexture2D& texture, + TextureState before, + TextureState after) noexcept override + { + const auto status = validate_texture_transition_desc(texture.desc(), before, after); + if (!status.ok()) { + return status; + } + + last_transition_before = before; + last_transition_after = after; + return pp::foundation::Status::success(); + } + [[nodiscard]] pp::foundation::Status copy_texture( pp::renderer::ITexture2D& source, ReadbackRegion source_region, @@ -559,6 +578,8 @@ public: std::uint64_t last_upload_bytes = 0; std::uint32_t last_generated_mip_levels = 0; std::uint64_t last_generated_mip_bytes = 0; + TextureState last_transition_before = TextureState::undefined; + TextureState last_transition_after = TextureState::undefined; std::uint64_t last_copy_source_bytes = 0; std::uint64_t last_copy_destination_bytes = 0; std::uint64_t last_readback_bytes = 0; @@ -852,6 +873,133 @@ void validates_mipmap_generation_contract(pp::tests::Harness& h) PP_EXPECT(h, missing_copy_destination.code == StatusCode::invalid_argument); } +void validates_texture_transition_contract(pp::tests::Harness& h) +{ + const TextureDesc full_usage_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, + }; + const TextureDesc missing_sampled_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::upload_destination | TextureUsage::copy_source | TextureUsage::copy_destination, + }; + const TextureDesc missing_render_target_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::copy_source | TextureUsage::copy_destination, + }; + const TextureDesc missing_upload_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, + }; + const TextureDesc missing_copy_source_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::copy_destination, + }; + const TextureDesc missing_copy_destination_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::copy_source, + }; + const TextureDesc missing_readback_desc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, + }; + + PP_EXPECT(h, validate_texture_state(TextureState::present).ok()); + PP_EXPECT(h, validate_texture_transition_desc( + full_usage_desc, + TextureState::undefined, + TextureState::upload_destination) + .ok()); + PP_EXPECT(h, validate_texture_transition_desc( + full_usage_desc, + TextureState::upload_destination, + TextureState::shader_read) + .ok()); + PP_EXPECT(h, validate_texture_transition_desc( + full_usage_desc, + TextureState::shader_read, + TextureState::render_target) + .ok()); + PP_EXPECT(h, validate_texture_transition_desc( + full_usage_desc, + TextureState::copy_destination, + TextureState::readback_source) + .ok()); + PP_EXPECT(h, validate_texture_transition_desc( + full_usage_desc, + TextureState::render_target, + TextureState::present) + .ok()); + PP_EXPECT(h, texture_state_name(TextureState::copy_destination) == std::string_view("copy_destination")); + + const auto invalid_state = validate_texture_state(static_cast(255)); + const auto same_state = validate_texture_transition_desc( + full_usage_desc, + TextureState::shader_read, + TextureState::shader_read); + const auto undefined_destination = validate_texture_transition_desc( + full_usage_desc, + TextureState::shader_read, + TextureState::undefined); + const auto invalid_before = validate_texture_transition_desc( + full_usage_desc, + static_cast(255), + TextureState::shader_read); + const auto missing_sampled = validate_texture_transition_desc( + missing_sampled_desc, + TextureState::copy_destination, + TextureState::shader_read); + const auto missing_render_target = validate_texture_transition_desc( + missing_render_target_desc, + TextureState::shader_read, + TextureState::render_target); + const auto missing_upload = validate_texture_transition_desc( + missing_upload_desc, + TextureState::undefined, + TextureState::upload_destination); + const auto missing_copy_source = validate_texture_transition_desc( + missing_copy_source_desc, + TextureState::shader_read, + TextureState::copy_source); + const auto missing_copy_destination = validate_texture_transition_desc( + missing_copy_destination_desc, + TextureState::shader_read, + TextureState::copy_destination); + const auto missing_readback = validate_texture_transition_desc( + missing_readback_desc, + TextureState::copy_source, + TextureState::readback_source); + + PP_EXPECT(h, !invalid_state.ok()); + PP_EXPECT(h, invalid_state.code == StatusCode::invalid_argument); + PP_EXPECT(h, !same_state.ok()); + PP_EXPECT(h, same_state.code == StatusCode::invalid_argument); + PP_EXPECT(h, !undefined_destination.ok()); + PP_EXPECT(h, undefined_destination.code == StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_before.ok()); + PP_EXPECT(h, invalid_before.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_sampled.ok()); + PP_EXPECT(h, missing_sampled.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_render_target.ok()); + PP_EXPECT(h, missing_render_target.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_upload.ok()); + PP_EXPECT(h, missing_upload.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_copy_source.ok()); + PP_EXPECT(h, missing_copy_source.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_copy_destination.ok()); + PP_EXPECT(h, missing_copy_destination.code == StatusCode::invalid_argument); + PP_EXPECT(h, !missing_readback.ok()); + PP_EXPECT(h, missing_readback.code == StatusCode::invalid_argument); + PP_EXPECT(h, texture_state_name(static_cast(255)) == std::string_view("unknown")); +} + void rejects_invalid_or_excessive_extents(pp::tests::Harness& h) { const auto zero = validate_extent(Extent2D { .width = 0, .height = 1 }); @@ -1455,6 +1603,11 @@ 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.transition_texture( + texture, + TextureState::upload_destination, + TextureState::shader_read) + .ok()); PP_EXPECT(h, context.copy_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, @@ -1497,6 +1650,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_transition_before == TextureState::upload_destination); + PP_EXPECT(h, device.context.last_transition_after == TextureState::shader_read); 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); @@ -1665,6 +1820,72 @@ void recording_renderer_records_mipmap_generation(pp::tests::Harness& h) context.end_render_pass(); } +void recording_renderer_records_texture_transitions(pp::tests::Harness& h) +{ + RecordingRenderDevice device; + RecordingTexture2D texture(TextureDesc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, + .debug_name = "transition-texture", + }); + RecordingTexture2D texture_without_sampled(TextureDesc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::upload_destination | TextureUsage::copy_source | TextureUsage::copy_destination, + }); + RecordingRenderTarget target(TextureDesc { + .extent = Extent2D { .width = 8, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = all_texture_usages, + }); + + auto& context = device.immediate_context(); + PP_EXPECT(h, context.transition_texture( + texture, + TextureState::undefined, + TextureState::upload_destination) + .ok()); + PP_EXPECT(h, context.transition_texture( + texture, + TextureState::upload_destination, + TextureState::shader_read) + .ok()); + + const auto commands = device.commands(); + PP_EXPECT(h, commands.size() == 2U); + PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::transition_texture); + PP_EXPECT(h, commands[0].texture_desc.debug_name == std::string_view("transition-texture")); + PP_EXPECT(h, commands[0].before_state == TextureState::undefined); + PP_EXPECT(h, commands[0].after_state == TextureState::upload_destination); + PP_EXPECT(h, commands[1].before_state == TextureState::upload_destination); + PP_EXPECT(h, commands[1].after_state == TextureState::shader_read); + PP_EXPECT(h, recorded_render_command_kind_name(commands[0].kind) == std::string_view("transition_texture")); + + const auto no_op = context.transition_texture( + texture, + TextureState::shader_read, + TextureState::shader_read); + PP_EXPECT(h, !no_op.ok()); + PP_EXPECT(h, no_op.code == StatusCode::invalid_argument); + + const auto missing_usage = context.transition_texture( + texture_without_sampled, + TextureState::copy_destination, + TextureState::shader_read); + PP_EXPECT(h, !missing_usage.ok()); + PP_EXPECT(h, missing_usage.code == StatusCode::invalid_argument); + + PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); + const auto during_render_pass = context.transition_texture( + texture, + TextureState::shader_read, + TextureState::render_target); + PP_EXPECT(h, !during_render_pass.ok()); + PP_EXPECT(h, during_render_pass.code == StatusCode::invalid_argument); + context.end_render_pass(); +} + void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) { RecordingRenderDevice device; @@ -2290,6 +2511,7 @@ int main() harness.run("validates_texture_usage_contract", validates_texture_usage_contract); harness.run("validates_resource_labels", validates_resource_labels); harness.run("validates_mipmap_generation_contract", validates_mipmap_generation_contract); + harness.run("validates_texture_transition_contract", validates_texture_transition_contract); 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); @@ -2311,6 +2533,7 @@ int main() harness.run("render_devices_create_validated_resources", render_devices_create_validated_resources); harness.run("recording_renderer_records_shader_uniform_writes", recording_renderer_records_shader_uniform_writes); harness.run("recording_renderer_records_mipmap_generation", recording_renderer_records_mipmap_generation); + harness.run("recording_renderer_records_texture_transitions", recording_renderer_records_texture_transitions); 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(); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index aa17129..7de5f85 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2313,6 +2313,15 @@ int record_render(int argc, char** argv) } auto& context = device.immediate_context(); + const auto transition_upload_status = context.transition_texture( + *texture.value(), + pp::renderer::TextureState::undefined, + pp::renderer::TextureState::upload_destination); + if (!transition_upload_status.ok()) { + print_error("record-render", transition_upload_status.message); + return 2; + } + const auto upload_status = context.upload_texture( *texture.value(), pp::renderer::ReadbackRegion { @@ -2327,12 +2336,39 @@ int record_render(int argc, char** argv) return 2; } + const auto transition_shader_read_status = context.transition_texture( + *texture.value(), + pp::renderer::TextureState::upload_destination, + pp::renderer::TextureState::shader_read); + if (!transition_shader_read_status.ok()) { + print_error("record-render", transition_shader_read_status.message); + return 2; + } + + const auto transition_mip_destination_status = context.transition_texture( + *mip_texture.value(), + pp::renderer::TextureState::undefined, + pp::renderer::TextureState::copy_destination); + if (!transition_mip_destination_status.ok()) { + print_error("record-render", transition_mip_destination_status.message); + return 2; + } + const auto mipmap_status = context.generate_mipmaps(*mip_texture.value()); if (!mipmap_status.ok()) { print_error("record-render", mipmap_status.message); return 2; } + const auto transition_mip_shader_read_status = context.transition_texture( + *mip_texture.value(), + pp::renderer::TextureState::copy_destination, + pp::renderer::TextureState::shader_read); + if (!transition_mip_shader_read_status.ok()) { + print_error("record-render", transition_mip_shader_read_status.message); + return 2; + } + const auto begin_status = context.begin_render_pass( *target.value(), pp::renderer::RenderPassDesc { @@ -2497,6 +2533,9 @@ int record_render(int argc, char** argv) std::size_t bind_sampler_commands = 0; std::size_t upload_commands = 0; std::size_t mipmap_commands = 0; + std::size_t transition_commands = 0; + std::size_t transition_to_upload = 0; + std::size_t transition_to_shader_read = 0; std::size_t copy_commands = 0; std::size_t readback_commands = 0; std::size_t capture_commands = 0; @@ -2567,6 +2606,15 @@ int record_render(int argc, char** argv) count_label(command.texture_desc.debug_name); mipmap_levels += command.generated_mip_levels; mipmap_bytes += command.generated_mip_bytes; + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::transition_texture) { + ++transition_commands; + count_label(command.texture_desc.debug_name); + if (command.after_state == pp::renderer::TextureState::upload_destination) { + ++transition_to_upload; + } + if (command.after_state == pp::renderer::TextureState::shader_read) { + ++transition_to_shader_read; + } } else if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) { ++upload_commands; count_label(command.texture_desc.debug_name); @@ -2627,6 +2675,9 @@ int record_render(int argc, char** argv) << ",\"mipmapCommands\":" << mipmap_commands << ",\"mipmapLevels\":" << mipmap_levels << ",\"mipmapBytes\":" << mipmap_bytes + << ",\"transitionCommands\":" << transition_commands + << ",\"transitionToUpload\":" << transition_to_upload + << ",\"transitionToShaderRead\":" << transition_to_shader_read << ",\"copyCommands\":" << copy_commands << ",\"copySourceBytes\":" << copy_source_bytes << ",\"copyDestinationBytes\":" << copy_destination_bytes