From 901aff105105ce7b8abbe0e27c16fcf6725bdcce Mon Sep 17 00:00:00 2001 From: omigamedev Date: Tue, 2 Jun 2026 16:42:53 +0200 Subject: [PATCH] Add renderer texture usage contract --- docs/modernization/build-inventory.md | 12 +- docs/modernization/roadmap.md | 41 +++-- src/renderer_api/recording_renderer.cpp | 30 +++- src/renderer_api/renderer_api.cpp | 78 +++++++- src/renderer_api/renderer_api.h | 37 +++- tests/renderer_api/renderer_api_tests.cpp | 207 +++++++++++++++++++--- tools/pano_cli/main.cpp | 8 +- 7 files changed, 345 insertions(+), 68 deletions(-) diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 994d05e..93f3c28 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -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 - 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, texture-copy regions, - readback bounds, frame-capture sources, destination buffer sizes, and - render-target blit regions, records + backend-owned resource creation, explicit texture usage flags, 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, + 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/texture-copy/readback/frame-capture/blit commands, draw mesh inputs, explicit draw ranges, and records trace markers without a diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index e66533c..57b88c3 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -414,14 +414,15 @@ Goal: make OpenGL an implementation detail and establish parity tests before 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, texture-upload/readback command 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 interface -validation, sampler-state validation, and the canonical PanoPainter shader -catalog now consumed by the legacy OpenGL app initialization path. +with explicit texture usage flags, 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, render-target blit validation, texture-slot +binding validation, blend-state validation, scissor-state validation, +depth-state validation, trace interface validation, sampler-state 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,13 +721,14 @@ Results: per-layer frame duration, and PNG-encoded face-payload export to PPI bytes, 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, texture-upload byte-count validation, frame-capture byte-size and - command-order validation, render-target blit validation, texture-slot binding - 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, texture-copy validation, recording + PanoPainter shader catalog validation, explicit texture usage validation, + readback byte-size and command-order validation, texture-upload byte-count + validation, frame-capture byte-size and command-order validation, + render-target blit validation, texture-slot binding 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, texture-copy + validation, recording render-pass clear/scissor/depth/blend/shader-uniform/texture/sampler-bind/ upload/texture-copy/readback/frame-capture/blit command capture, draw mesh-input capture, explicit draw-range capture, and invalid catalog @@ -826,10 +828,11 @@ Results: - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict 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 and texture-copy - validation; it creates validated textures, render targets, shaders, meshes, - and readback buffers, then records commands, trace markers, render-pass + texture-usage/texture-bind/sampler-bind/shader-uniform/texture-upload/ + readback/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 diff --git a/src/renderer_api/recording_renderer.cpp b/src/renderer_api/recording_renderer.cpp index cd88d6b..3e94f09 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -101,8 +101,8 @@ pp::foundation::Status RecordingCommandContext::begin_render_pass( } active_target_ = target.color_desc(); - if (!active_target_.render_target) { - return pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target"); + if (!has_texture_usage(active_target_.usage, TextureUsage::render_target)) { + return pp::foundation::Status::invalid_argument("render target texture must allow render_target usage"); } const auto size_status = texture_byte_size(active_target_); @@ -256,6 +256,10 @@ pp::foundation::Status RecordingCommandContext::bind_texture( } const auto desc = texture.desc(); + if (!has_texture_usage(desc.usage, TextureUsage::sampled)) { + return pp::foundation::Status::invalid_argument("bound texture must allow sampled usage"); + } + const auto size_status = texture_byte_size(desc); if (!size_status.ok()) { return size_status.status(); @@ -351,6 +355,10 @@ pp::foundation::Status RecordingCommandContext::read_texture( } const auto desc = texture.desc(); + if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) { + return pp::foundation::Status::invalid_argument("readback texture must allow readback_source usage"); + } + const auto bytes = readback_byte_size(desc, region); if (!bytes) { return bytes.status(); @@ -379,6 +387,10 @@ pp::foundation::Status RecordingCommandContext::upload_texture( } const auto desc = texture.desc(); + if (!has_texture_usage(desc.usage, TextureUsage::upload_destination)) { + return pp::foundation::Status::invalid_argument("texture upload destination must allow upload_destination usage"); + } + const auto bytes = readback_byte_size(desc, region); if (!bytes) { return bytes.status(); @@ -560,6 +572,11 @@ const char* RecordingRenderDevice::backend_name() const noexcept pp::foundation::Result> RecordingRenderDevice::create_texture( TextureDesc desc) noexcept { + const auto desc_status = validate_texture_desc(desc); + if (!desc_status.ok()) { + return pp::foundation::Result>::failure(desc_status); + } + const auto bytes = texture_byte_size(desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); @@ -571,9 +588,14 @@ pp::foundation::Result> RecordingRenderDevice::creat pp::foundation::Result> RecordingRenderDevice::create_render_target( TextureDesc color_desc) noexcept { - if (!color_desc.render_target) { + if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) { return pp::foundation::Result>::failure( - pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target")); + pp::foundation::Status::invalid_argument("render target texture must allow render_target usage")); + } + + const auto desc_status = validate_texture_desc(color_desc); + if (!desc_status.ok()) { + return pp::foundation::Result>::failure(desc_status); } const auto bytes = texture_byte_size(color_desc); diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index df5bf49..dab1df6 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -47,6 +47,13 @@ std::uint32_t bytes_per_pixel(TextureFormat format) noexcept return 0; } +bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept +{ + const auto usage_bits = static_cast(usage); + const auto required_bits = static_cast(required); + return required_bits != 0U && (usage_bits & required_bits) == required_bits; +} + pp::foundation::Status validate_extent(Extent2D extent) noexcept { if (extent.width == 0 || extent.height == 0) { @@ -60,19 +67,50 @@ pp::foundation::Status validate_extent(Extent2D extent) noexcept return pp::foundation::Status::success(); } -pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept +pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept +{ + constexpr auto allowed_usage = TextureUsage::sampled + | TextureUsage::render_target + | TextureUsage::upload_destination + | TextureUsage::readback_source + | TextureUsage::copy_source + | TextureUsage::copy_destination; + + const auto usage_bits = static_cast(usage); + const auto allowed_bits = static_cast(allowed_usage); + if (usage_bits == 0U) { + return pp::foundation::Status::invalid_argument("texture usage must not be empty"); + } + + if ((usage_bits & ~allowed_bits) != 0U) { + return pp::foundation::Status::invalid_argument("texture usage contains unsupported flags"); + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept { const auto extent_status = validate_extent(desc.extent); if (!extent_status.ok()) { - return pp::foundation::Result::failure(extent_status); + return extent_status; + } + + if (bytes_per_pixel(desc.format) == 0U) { + return pp::foundation::Status::invalid_argument("texture format is not supported"); + } + + return validate_texture_usage(desc.usage); +} + +pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept +{ + const auto desc_status = validate_texture_desc(desc); + if (!desc_status.ok()) { + return pp::foundation::Result::failure(desc_status); } const auto bpp = static_cast(bytes_per_pixel(desc.format)); - if (bpp == 0) { - return pp::foundation::Result::failure( - pp::foundation::Status::invalid_argument("texture format is not supported")); - } - const auto width = static_cast(desc.extent.width); const auto height = static_cast(desc.extent.height); if (width > std::numeric_limits::max() / height) { @@ -481,11 +519,16 @@ pp::foundation::Result readback_byte_size(TextureDesc desc, Readb pp::foundation::Result frame_capture_byte_size(TextureDesc desc) noexcept { - if (!desc.render_target) { + if (!has_texture_usage(desc.usage, TextureUsage::render_target)) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("frame capture source must be a render target")); } + if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("frame capture source must allow readback")); + } + return texture_byte_size(desc); } @@ -495,6 +538,14 @@ pp::foundation::Status validate_texture_copy_descs( TextureDesc destination, ReadbackRegion destination_region) noexcept { + if (!has_texture_usage(source.usage, TextureUsage::copy_source)) { + return pp::foundation::Status::invalid_argument("texture copy source must allow copy_source usage"); + } + + if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) { + return pp::foundation::Status::invalid_argument("texture copy destination must allow copy_destination usage"); + } + if (source.format != destination.format) { return pp::foundation::Status::invalid_argument("texture copy endpoints must use matching formats"); } @@ -524,10 +575,19 @@ pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc destination) noexcept { - if (!source.render_target || !destination.render_target) { + if (!has_texture_usage(source.usage, TextureUsage::render_target) + || !has_texture_usage(destination.usage, TextureUsage::render_target)) { return pp::foundation::Status::invalid_argument("blit endpoints must be render targets"); } + if (!has_texture_usage(source.usage, TextureUsage::copy_source)) { + return pp::foundation::Status::invalid_argument("blit source must allow copy_source usage"); + } + + if (!has_texture_usage(destination.usage, TextureUsage::copy_destination)) { + return pp::foundation::Status::invalid_argument("blit destination must allow copy_destination usage"); + } + if (source.format != destination.format) { return pp::foundation::Status::invalid_argument("blit endpoints must use matching texture formats"); } diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index abb089f..0a66d00 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -22,6 +22,34 @@ enum class TextureFormat : std::uint8_t { depth24_stencil8, }; +enum class TextureUsage : std::uint32_t { + none = 0, + sampled = 1U << 0U, + render_target = 1U << 1U, + upload_destination = 1U << 2U, + readback_source = 1U << 3U, + copy_source = 1U << 4U, + copy_destination = 1U << 5U, +}; + +[[nodiscard]] constexpr TextureUsage operator|(TextureUsage lhs, TextureUsage rhs) noexcept +{ + return static_cast( + static_cast(lhs) | static_cast(rhs)); +} + +[[nodiscard]] constexpr TextureUsage operator&(TextureUsage lhs, TextureUsage rhs) noexcept +{ + return static_cast( + static_cast(lhs) & static_cast(rhs)); +} + +constexpr TextureUsage& operator|=(TextureUsage& lhs, TextureUsage rhs) noexcept +{ + lhs = lhs | rhs; + return lhs; +} + struct Extent2D { std::uint32_t width = 0; std::uint32_t height = 0; @@ -30,7 +58,11 @@ struct Extent2D { struct TextureDesc { Extent2D extent; TextureFormat format = TextureFormat::rgba8; - bool render_target = false; + TextureUsage usage = TextureUsage::sampled + | TextureUsage::upload_destination + | TextureUsage::readback_source + | TextureUsage::copy_source + | TextureUsage::copy_destination; }; struct ReadbackRegion { @@ -279,7 +311,10 @@ public: }; [[nodiscard]] std::uint32_t bytes_per_pixel(TextureFormat format) noexcept; +[[nodiscard]] bool has_texture_usage(TextureUsage usage, TextureUsage required) noexcept; [[nodiscard]] pp::foundation::Status validate_extent(Extent2D extent) noexcept; +[[nodiscard]] pp::foundation::Status validate_texture_usage(TextureUsage usage) noexcept; +[[nodiscard]] pp::foundation::Status validate_texture_desc(TextureDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept; [[nodiscard]] pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept; [[nodiscard]] pp::foundation::Status validate_render_pass_desc(RenderPassDesc desc) noexcept; diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index e8ef64e..519fcc9 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -26,6 +26,7 @@ using pp::renderer::DepthState; using pp::renderer::DrawDesc; using pp::renderer::Extent2D; using pp::renderer::frame_capture_byte_size; +using pp::renderer::has_texture_usage; using pp::renderer::ICommandContext; using pp::renderer::IMesh; using pp::renderer::IRenderDevice; @@ -53,6 +54,7 @@ using pp::renderer::ShaderProgramDesc; using pp::renderer::ShaderStageSource; using pp::renderer::TextureDesc; using pp::renderer::TextureFormat; +using pp::renderer::TextureUsage; using pp::renderer::Viewport; using pp::renderer::max_shader_source_bytes; using pp::renderer::max_shader_uniform_bytes; @@ -85,7 +87,9 @@ 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_desc; using pp::renderer::validate_texture_slot; +using pp::renderer::validate_texture_usage; using pp::renderer::validate_viewport; namespace { @@ -95,7 +99,7 @@ public: explicit FakeRenderTarget(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }) noexcept : desc_(desc) { @@ -151,7 +155,7 @@ public: explicit FakeTexture(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }) noexcept : desc_(desc) { @@ -204,6 +208,9 @@ public: if (!render_pass_status.ok()) { return render_pass_status; } + if (!has_texture_usage(target.color_desc().usage, TextureUsage::render_target)) { + return pp::foundation::Status::invalid_argument("render target texture must allow render_target usage"); + } in_render_pass = true; last_render_pass_desc = desc; return validate_extent(target.color_desc().extent); @@ -291,6 +298,9 @@ public: if (!slot_status.ok()) { return slot_status; } + if (!has_texture_usage(texture.desc().usage, TextureUsage::sampled)) { + return pp::foundation::Status::invalid_argument("bound texture must allow sampled usage"); + } const auto bytes = texture_byte_size(texture.desc()); if (!bytes) { return bytes.status(); @@ -354,6 +364,9 @@ public: ReadbackRegion region, pp::renderer::IReadbackBuffer& destination) noexcept override { + if (!has_texture_usage(texture.desc().usage, TextureUsage::readback_source)) { + return pp::foundation::Status::invalid_argument("readback texture must allow readback_source usage"); + } const auto bytes = readback_byte_size(texture.desc(), region); if (!bytes) { return bytes.status(); @@ -370,6 +383,9 @@ public: ReadbackRegion region, std::span rgba_or_channel_bytes) noexcept override { + if (!has_texture_usage(texture.desc().usage, TextureUsage::upload_destination)) { + return pp::foundation::Status::invalid_argument("texture upload destination must allow upload_destination usage"); + } const auto bytes = readback_byte_size(texture.desc(), region); if (!bytes) { return bytes.status(); @@ -511,9 +527,9 @@ public: [[nodiscard]] pp::foundation::Result> create_render_target( TextureDesc color_desc) noexcept override { - if (!color_desc.render_target) { + if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) { return pp::foundation::Result>::failure( - pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target")); + pp::foundation::Status::invalid_argument("render target texture must allow render_target usage")); } const auto bytes = texture_byte_size(color_desc); @@ -590,6 +606,13 @@ private: } }; +constexpr TextureUsage all_texture_usages = TextureUsage::sampled + | TextureUsage::render_target + | TextureUsage::upload_destination + | TextureUsage::readback_source + | TextureUsage::copy_source + | TextureUsage::copy_destination; + void computes_texture_sizes(pp::tests::Harness& h) { const auto rgba = texture_byte_size(TextureDesc { @@ -608,6 +631,51 @@ void computes_texture_sizes(pp::tests::Harness& h) PP_EXPECT(h, texture_format_name(TextureFormat::depth24_stencil8) == std::string_view("depth24_stencil8")); } +void validates_texture_usage_contract(pp::tests::Harness& h) +{ + const TextureDesc sampled_desc { + .extent = Extent2D { .width = 4, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled, + }; + const TextureDesc all_usage_desc { + .extent = Extent2D { .width = 4, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = all_texture_usages, + }; + const TextureDesc empty_usage_desc { + .extent = Extent2D { .width = 4, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::none, + }; + const TextureDesc unknown_usage_desc { + .extent = Extent2D { .width = 4, .height = 4 }, + .format = TextureFormat::rgba8, + .usage = static_cast(1U << 31U), + }; + const TextureDesc unknown_format_desc { + .extent = Extent2D { .width = 4, .height = 4 }, + .format = static_cast(255), + }; + + PP_EXPECT(h, has_texture_usage(all_texture_usages, TextureUsage::render_target)); + PP_EXPECT(h, !has_texture_usage(TextureUsage::sampled, TextureUsage::render_target)); + PP_EXPECT(h, validate_texture_usage(TextureUsage::sampled | TextureUsage::copy_source).ok()); + PP_EXPECT(h, validate_texture_desc(sampled_desc).ok()); + PP_EXPECT(h, validate_texture_desc(all_usage_desc).ok()); + + const auto empty_usage = validate_texture_desc(empty_usage_desc); + const auto unknown_usage = validate_texture_desc(unknown_usage_desc); + const auto unknown_format = validate_texture_desc(unknown_format_desc); + + PP_EXPECT(h, !empty_usage.ok()); + PP_EXPECT(h, empty_usage.code == StatusCode::invalid_argument); + PP_EXPECT(h, !unknown_usage.ok()); + PP_EXPECT(h, unknown_usage.code == StatusCode::invalid_argument); + PP_EXPECT(h, !unknown_format.ok()); + PP_EXPECT(h, unknown_format.code == StatusCode::invalid_argument); +} + void rejects_invalid_or_excessive_extents(pp::tests::Harness& h) { const auto zero = validate_extent(Extent2D { .width = 0, .height = 1 }); @@ -630,7 +698,7 @@ void validates_readback_bounds(pp::tests::Harness& h) const TextureDesc desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); @@ -653,12 +721,12 @@ void computes_readback_byte_sizes(pp::tests::Harness& h) const TextureDesc rgba_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc r8_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::r8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const auto rgba = readback_byte_size(rgba_desc, ReadbackRegion { .x = 4, .y = 2, .width = 8, .height = 3 }); @@ -678,12 +746,12 @@ 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, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc texture_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, - .render_target = false, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const auto capture = frame_capture_byte_size(target_desc); @@ -700,17 +768,17 @@ void validates_blit_contract(pp::tests::Harness& h) const TextureDesc target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc r8_target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc texture_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, - .render_target = false, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; PP_EXPECT(h, validate_blit_descs(target_desc, target_desc).ok()); @@ -1239,12 +1307,12 @@ void render_devices_create_validated_resources(pp::tests::Harness& h) const auto texture = device.create_texture(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, - .render_target = false, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); const auto target = device.create_render_target(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); const auto shader = device.create_shader_program(ShaderProgramDesc { .debug_name = "factory-shader", @@ -1260,9 +1328,9 @@ void render_devices_create_validated_resources(pp::tests::Harness& h) PP_EXPECT(h, texture.ok()); PP_EXPECT(h, texture.value()->desc().extent.width == 8U); - PP_EXPECT(h, !texture.value()->desc().render_target); + PP_EXPECT(h, !has_texture_usage(texture.value()->desc().usage, TextureUsage::render_target)); PP_EXPECT(h, target.ok()); - PP_EXPECT(h, target.value()->color_desc().render_target); + PP_EXPECT(h, has_texture_usage(target.value()->color_desc().usage, TextureUsage::render_target)); PP_EXPECT(h, shader.ok()); PP_EXPECT(h, shader.value()->debug_name() == std::string_view("factory-shader")); PP_EXPECT(h, mesh.ok()); @@ -1277,7 +1345,7 @@ void render_devices_create_validated_resources(pp::tests::Harness& h) const auto bad_target = device.create_render_target(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, - .render_target = false, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); const auto bad_shader = device.create_shader_program(ShaderProgramDesc { .debug_name = "bad-shader", @@ -1305,7 +1373,7 @@ void recording_renderer_records_shader_uniform_writes(pp::tests::Harness& h) RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingShaderProgram shader("uniform-shader"); const std::array uniform_bytes {}; @@ -1348,19 +1416,19 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingRenderTarget blit_target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingShaderProgram shader("recorded-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .index_count = 3, .topology = PrimitiveTopology::triangles }); @@ -1540,22 +1608,57 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingRenderTarget non_render_target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, - .render_target = false, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingRenderTarget r8_target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::r8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, - .render_target = true, + .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, + }); + RecordingTexture2D texture_without_sampled(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, + }); + RecordingTexture2D texture_without_upload(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, + }); + RecordingTexture2D texture_without_readback(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::copy_source | TextureUsage::copy_destination, + }); + RecordingTexture2D texture_without_copy_source(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_destination, + }); + RecordingTexture2D texture_without_copy_destination(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source, + }); + RecordingRenderTarget target_without_copy_source(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target | TextureUsage::copy_destination | TextureUsage::readback_source, + }); + RecordingRenderTarget target_without_copy_destination(TextureDesc { + .extent = Extent2D { .width = 32, .height = 16 }, + .format = TextureFormat::rgba8, + .usage = TextureUsage::render_target | TextureUsage::copy_source | TextureUsage::readback_source, }); RecordingReadbackBuffer small_readback_buffer(3U); RecordingReadbackBuffer full_readback_buffer(32U * 16U * 4U); @@ -1692,6 +1795,11 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har const auto bind_texture_bad_slot = context.bind_texture(max_texture_slots, texture); PP_EXPECT(h, !bind_texture_bad_slot.ok()); PP_EXPECT(h, bind_texture_bad_slot.code == StatusCode::out_of_range); + + const auto bind_texture_without_sampled = context.bind_texture(0, texture_without_sampled); + PP_EXPECT(h, !bind_texture_without_sampled.ok()); + PP_EXPECT(h, bind_texture_without_sampled.code == StatusCode::invalid_argument); + PP_EXPECT(h, context.bind_texture(0, texture).ok()); const auto bind_sampler_bad_slot = context.bind_sampler(max_texture_slots, SamplerDesc {}); @@ -1759,6 +1867,13 @@ 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 upload_without_usage = context.upload_texture( + texture_without_upload, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + one_pixel_upload); + PP_EXPECT(h, !upload_without_usage.ok()); + PP_EXPECT(h, upload_without_usage.code == StatusCode::invalid_argument); + const auto copy_mismatched_regions = context.copy_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, @@ -1775,6 +1890,22 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !copy_outside_bounds.ok()); PP_EXPECT(h, copy_outside_bounds.code == StatusCode::out_of_range); + const auto copy_without_source_usage = context.copy_texture( + texture_without_copy_source, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + PP_EXPECT(h, !copy_without_source_usage.ok()); + PP_EXPECT(h, copy_without_source_usage.code == StatusCode::invalid_argument); + + const auto copy_without_destination_usage = context.copy_texture( + texture, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + texture_without_copy_destination, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); + PP_EXPECT(h, !copy_without_destination_usage.ok()); + PP_EXPECT(h, copy_without_destination_usage.code == StatusCode::invalid_argument); + const auto upload_with_wrong_size = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, @@ -1782,6 +1913,13 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !upload_with_wrong_size.ok()); PP_EXPECT(h, upload_with_wrong_size.code == StatusCode::invalid_argument); + const auto read_without_usage = context.read_texture( + texture_without_readback, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + full_readback_buffer); + PP_EXPECT(h, !read_without_usage.ok()); + PP_EXPECT(h, read_without_usage.code == StatusCode::invalid_argument); + const auto read_into_small_buffer = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, @@ -1815,6 +1953,24 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !blit_mismatched_format.ok()); PP_EXPECT(h, blit_mismatched_format.code == StatusCode::invalid_argument); + const auto blit_without_source_usage = context.blit_render_target( + target_without_copy_source, + 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_without_source_usage.ok()); + PP_EXPECT(h, blit_without_source_usage.code == StatusCode::invalid_argument); + + const auto blit_without_destination_usage = context.blit_render_target( + target, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + target_without_copy_destination, + ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, + BlitFilter::nearest); + PP_EXPECT(h, !blit_without_destination_usage.ok()); + PP_EXPECT(h, blit_without_destination_usage.code == StatusCode::invalid_argument); + const auto blit_outside_bounds = context.blit_render_target( target, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, @@ -1840,6 +1996,7 @@ int main() { pp::tests::Harness harness; harness.run("computes_texture_sizes", computes_texture_sizes); + harness.run("validates_texture_usage_contract", validates_texture_usage_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); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index 81f731b..67c86fb 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1642,7 +1642,7 @@ pp::foundation::Status parse_simulate_document_edits_args( const auto render_target_size = pp::renderer::texture_byte_size(pp::renderer::TextureDesc { .extent = pp::renderer::Extent2D { .width = args.width, .height = args.height }, .format = pp::renderer::TextureFormat::rgba8, - .render_target = true, + .usage = pp::renderer::TextureUsage::render_target | pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::upload_destination | pp::renderer::TextureUsage::readback_source | pp::renderer::TextureUsage::copy_source | pp::renderer::TextureUsage::copy_destination, }); if (!render_target_size) { return render_target_size.status(); @@ -2214,17 +2214,17 @@ int record_render(int argc, char** argv) const auto texture = device.create_texture(pp::renderer::TextureDesc { .extent = pp::renderer::Extent2D { .width = args.width, .height = args.height }, .format = pp::renderer::TextureFormat::rgba8, - .render_target = true, + .usage = pp::renderer::TextureUsage::render_target | pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::upload_destination | pp::renderer::TextureUsage::readback_source | pp::renderer::TextureUsage::copy_source | pp::renderer::TextureUsage::copy_destination, }); const auto target = device.create_render_target(pp::renderer::TextureDesc { .extent = pp::renderer::Extent2D { .width = args.width, .height = args.height }, .format = pp::renderer::TextureFormat::rgba8, - .render_target = true, + .usage = pp::renderer::TextureUsage::render_target | pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::upload_destination | pp::renderer::TextureUsage::readback_source | pp::renderer::TextureUsage::copy_source | pp::renderer::TextureUsage::copy_destination, }); const auto blit_target = device.create_render_target(pp::renderer::TextureDesc { .extent = pp::renderer::Extent2D { .width = args.width, .height = args.height }, .format = pp::renderer::TextureFormat::rgba8, - .render_target = true, + .usage = pp::renderer::TextureUsage::render_target | pp::renderer::TextureUsage::sampled | pp::renderer::TextureUsage::upload_destination | pp::renderer::TextureUsage::readback_source | pp::renderer::TextureUsage::copy_source | pp::renderer::TextureUsage::copy_destination, }); const auto readback_buffer = device.create_readback_buffer( static_cast(args.width) * args.height * 4U);