diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index dc0a0e4..eed353e 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -286,13 +286,14 @@ Known local toolchain state: legacy OpenGL implementation files. - `pp_renderer_api` exposes a headless `RecordingRenderDevice` that validates command order, scissor state, depth state, blend state, texture-slot binding, - texture-upload byte counts, readback bounds, frame-capture sources, - destination buffer sizes, and render-target blit regions, records - render/scissor/depth/blend/texture-bind/upload/readback/frame-capture/blit - commands, and records trace markers without a window or GL context. + sampler-state binding, texture-upload byte counts, readback bounds, + frame-capture sources, destination buffer sizes, and render-target blit + regions, records render/scissor/depth/blend/texture-bind/sampler-bind/upload/ + readback/frame-capture/blit commands, and records trace markers without a + window or GL context. - `pano_cli record-render` exposes the recording renderer through JSON - automation, including scissor/depth/blend/texture-bind/upload/readback/ - frame-capture/blit command and byte totals, and is covered by + automation, including scissor/depth/blend/texture-bind/sampler-bind/upload/ + readback/frame-capture/blit command and byte totals, and is covered by `pano_cli_record_render_smoke` plus `pano_cli_record_render_rejects_oversized_target`. - `pano_cli simulate-document-history` exposes `pp_document::DocumentHistory` diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 65350c6..6e7fb11 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -420,8 +420,8 @@ 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, and the canonical PanoPainter shader catalog now consumed by the -legacy OpenGL app initialization path. +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 @@ -724,8 +724,8 @@ Results: 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, recording - scissor/depth/blend/texture-bind/upload/readback/frame-capture/blit command - capture, and invalid catalog rejection. + scissor/depth/blend/texture/sampler-bind/upload/readback/frame-capture/blit + command capture, and invalid catalog rejection. - `pp_paint_renderer_compositor_tests` passed. - `pp_ui_core_color_tests` passed. - `pp_ui_core_layout_value_tests` passed. @@ -820,15 +820,16 @@ Results: implementation files. - `pp_renderer_api` now includes a headless `RecordingRenderDevice` with strict command-order/scissor-state/depth-state/blend-state/texture-bind/ - texture-upload/readback/frame-capture/blit validation; it records commands, - trace markers, scissor state, depth state, blend state, texture binds, - uploads/readbacks, frame captures, and render-target blits, giving automation - a backend-neutral render path that does not require a window or GL context. + sampler-bind/texture-upload/readback/frame-capture/blit validation; it + records commands, trace markers, scissor state, depth state, blend state, + texture/sampler binds, uploads/readbacks, frame captures, and render-target + blits, giving automation a backend-neutral render path that does not require + a window or GL context. - `pano_cli record-render` exercises that headless recording renderer and emits JSON command counts, target dimensions, backend name, trace/draw summary, and - scissor/depth/blend-state plus texture-bind/upload/readback/frame-capture/ - blit command/byte totals for agent automation, with an expected-failure smoke - for oversized render/readback targets. + scissor/depth/blend-state plus texture/sampler-bind/upload/readback/ + frame-capture/blit command/byte totals for agent automation, with an + expected-failure smoke for oversized render/readback targets. - `pano_cli simulate-document-history` exercises pure document history 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 91ab4f0..73b6f4e 100644 --- a/src/renderer_api/recording_renderer.cpp +++ b/src/renderer_api/recording_renderer.cpp @@ -217,6 +217,32 @@ pp::foundation::Status RecordingCommandContext::bind_texture( return pp::foundation::Status::success(); } +pp::foundation::Status RecordingCommandContext::bind_sampler( + std::uint32_t slot, + SamplerDesc sampler) noexcept +{ + if (!in_render_pass_) { + return pp::foundation::Status::invalid_argument("render pass has not begun"); + } + + const auto slot_status = validate_texture_slot(slot); + if (!slot_status.ok()) { + return slot_status; + } + + const auto sampler_status = validate_sampler_desc(sampler); + if (!sampler_status.ok()) { + return sampler_status; + } + + push_command(commands_, RecordedRenderCommand { + .kind = RecordedRenderCommandKind::bind_sampler, + .sampler_desc = sampler, + .sampler_slot = slot, + }); + return pp::foundation::Status::success(); +} + pp::foundation::Status RecordingCommandContext::bind_mesh(IMesh& mesh) noexcept { if (!in_render_pass_) { @@ -464,6 +490,8 @@ const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) no return "bind_shader"; case RecordedRenderCommandKind::bind_texture: return "bind_texture"; + case RecordedRenderCommandKind::bind_sampler: + return "bind_sampler"; case RecordedRenderCommandKind::bind_mesh: return "bind_mesh"; case RecordedRenderCommandKind::draw: diff --git a/src/renderer_api/recording_renderer.h b/src/renderer_api/recording_renderer.h index 53a1255..c9aec34 100644 --- a/src/renderer_api/recording_renderer.h +++ b/src/renderer_api/recording_renderer.h @@ -15,6 +15,7 @@ enum class RecordedRenderCommandKind : std::uint8_t { set_depth_state, bind_shader, bind_texture, + bind_sampler, bind_mesh, draw, upload_texture, @@ -36,6 +37,8 @@ struct RecordedRenderCommand { MeshDesc mesh_desc {}; TextureDesc texture_desc {}; std::uint32_t texture_slot = 0; + SamplerDesc sampler_desc {}; + std::uint32_t sampler_slot = 0; TextureDesc source_desc {}; TextureDesc destination_desc {}; ReadbackRegion readback_region {}; @@ -111,6 +114,9 @@ public: [[nodiscard]] pp::foundation::Status bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept override; + [[nodiscard]] pp::foundation::Status bind_sampler( + std::uint32_t slot, + SamplerDesc sampler) noexcept override; [[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override; [[nodiscard]] pp::foundation::Status draw() noexcept override; [[nodiscard]] pp::foundation::Status read_texture( diff --git a/src/renderer_api/renderer_api.cpp b/src/renderer_api/renderer_api.cpp index 89bf7fd..238ec5a 100644 --- a/src/renderer_api/renderer_api.cpp +++ b/src/renderer_api/renderer_api.cpp @@ -242,6 +242,60 @@ pp::foundation::Status validate_depth_state(DepthState state) noexcept return validate_compare_op(state.compare); } +pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept +{ + switch (filter) { + case SamplerFilter::nearest: + case SamplerFilter::linear: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("sampler filter is not supported"); +} + +pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept +{ + switch (mode) { + case SamplerAddressMode::clamp_to_edge: + case SamplerAddressMode::repeat: + case SamplerAddressMode::mirrored_repeat: + case SamplerAddressMode::clamp_to_border: + return pp::foundation::Status::success(); + } + + return pp::foundation::Status::invalid_argument("sampler address mode is not supported"); +} + +pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept +{ + const auto min_filter = validate_sampler_filter(desc.min_filter); + if (!min_filter.ok()) { + return min_filter; + } + + const auto mag_filter = validate_sampler_filter(desc.mag_filter); + if (!mag_filter.ok()) { + return mag_filter; + } + + const auto mip_filter = validate_sampler_filter(desc.mip_filter); + if (!mip_filter.ok()) { + return mip_filter; + } + + const auto address_u = validate_sampler_address_mode(desc.address_u); + if (!address_u.ok()) { + return address_u; + } + + const auto address_v = validate_sampler_address_mode(desc.address_v); + if (!address_v.ok()) { + return address_v; + } + + return validate_sampler_address_mode(desc.address_w); +} + pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept { if (desc.vertex_count == 0) { @@ -493,4 +547,32 @@ const char* compare_op_name(CompareOp op) noexcept return "unknown"; } +const char* sampler_filter_name(SamplerFilter filter) noexcept +{ + switch (filter) { + case SamplerFilter::nearest: + return "nearest"; + case SamplerFilter::linear: + return "linear"; + } + + return "unknown"; +} + +const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept +{ + switch (mode) { + case SamplerAddressMode::clamp_to_edge: + return "clamp_to_edge"; + case SamplerAddressMode::repeat: + return "repeat"; + case SamplerAddressMode::mirrored_repeat: + return "mirrored_repeat"; + case SamplerAddressMode::clamp_to_border: + return "clamp_to_border"; + } + + return "unknown"; +} + } diff --git a/src/renderer_api/renderer_api.h b/src/renderer_api/renderer_api.h index e4cdbcb..464c40b 100644 --- a/src/renderer_api/renderer_api.h +++ b/src/renderer_api/renderer_api.h @@ -99,6 +99,18 @@ enum class CompareOp : std::uint8_t { always, }; +enum class SamplerFilter : std::uint8_t { + nearest, + linear, +}; + +enum class SamplerAddressMode : std::uint8_t { + clamp_to_edge, + repeat, + mirrored_repeat, + clamp_to_border, +}; + struct BlendState { bool enabled = false; BlendFactor source_color = BlendFactor::one; @@ -119,6 +131,15 @@ struct DepthState { CompareOp compare = CompareOp::less_or_equal; }; +struct SamplerDesc { + SamplerFilter min_filter = SamplerFilter::linear; + SamplerFilter mag_filter = SamplerFilter::linear; + SamplerFilter mip_filter = SamplerFilter::linear; + SamplerAddressMode address_u = SamplerAddressMode::clamp_to_edge; + SamplerAddressMode address_v = SamplerAddressMode::clamp_to_edge; + SamplerAddressMode address_w = SamplerAddressMode::clamp_to_edge; +}; + struct MeshDesc { std::uint32_t vertex_count = 0; std::uint32_t index_count = 0; @@ -187,6 +208,9 @@ public: [[nodiscard]] virtual pp::foundation::Status bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept = 0; + [[nodiscard]] virtual pp::foundation::Status bind_sampler( + std::uint32_t slot, + SamplerDesc sampler) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status bind_mesh(IMesh& mesh) noexcept = 0; [[nodiscard]] virtual pp::foundation::Status draw() noexcept = 0; [[nodiscard]] virtual pp::foundation::Status read_texture( @@ -226,6 +250,9 @@ public: [[nodiscard]] pp::foundation::Status validate_blend_state(BlendState state) noexcept; [[nodiscard]] pp::foundation::Status validate_compare_op(CompareOp op) noexcept; [[nodiscard]] pp::foundation::Status validate_depth_state(DepthState state) noexcept; +[[nodiscard]] pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept; +[[nodiscard]] pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept; +[[nodiscard]] pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept; [[nodiscard]] pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept; [[nodiscard]] pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept; @@ -245,5 +272,7 @@ public: [[nodiscard]] const char* blend_factor_name(BlendFactor factor) noexcept; [[nodiscard]] const char* blend_op_name(BlendOp op) noexcept; [[nodiscard]] const char* compare_op_name(CompareOp op) noexcept; +[[nodiscard]] const char* sampler_filter_name(SamplerFilter filter) noexcept; +[[nodiscard]] const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 867c60a..9ec5308 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -365,7 +365,7 @@ if(TARGET pano_cli) COMMAND pano_cli record-render --width 32 --height 16) set_tests_properties(pano_cli_record_render_smoke PROPERTIES LABELS "renderer;integration;desktop-fast" - PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"width\":32.*\"height\":16.*\"commands\":15.*\"drawCommands\":1.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"bindTextureCommands\":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.*\"commands\":16.*\"drawCommands\":1.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048") add_test(NAME pano_cli_record_render_rejects_oversized_target COMMAND "${CMAKE_COMMAND}" diff --git a/tests/renderer_api/renderer_api_tests.cpp b/tests/renderer_api/renderer_api_tests.cpp index 8b4ea2b..1fa2853 100644 --- a/tests/renderer_api/renderer_api_tests.cpp +++ b/tests/renderer_api/renderer_api_tests.cpp @@ -37,6 +37,11 @@ using pp::renderer::RecordingRenderDevice; using pp::renderer::RecordingRenderTarget; using pp::renderer::RecordingShaderProgram; using pp::renderer::RecordingTexture2D; +using pp::renderer::SamplerAddressMode; +using pp::renderer::sampler_address_mode_name; +using pp::renderer::SamplerDesc; +using pp::renderer::SamplerFilter; +using pp::renderer::sampler_filter_name; using pp::renderer::ScissorRect; using pp::renderer::ShaderProgramDesc; using pp::renderer::ShaderStageSource; @@ -63,6 +68,9 @@ using pp::renderer::validate_compare_op; using pp::renderer::validate_depth_state; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_readback_region; +using pp::renderer::validate_sampler_address_mode; +using pp::renderer::validate_sampler_desc; +using pp::renderer::validate_sampler_filter; using pp::renderer::validate_scissor; using pp::renderer::validate_shader_catalog; using pp::renderer::validate_shader_program_desc; @@ -222,6 +230,26 @@ public: return pp::foundation::Status::success(); } + [[nodiscard]] pp::foundation::Status bind_sampler( + std::uint32_t slot, + SamplerDesc sampler) noexcept override + { + if (!in_render_pass) { + return pp::foundation::Status::invalid_argument("render pass has not begun"); + } + const auto slot_status = validate_texture_slot(slot); + if (!slot_status.ok()) { + return slot_status; + } + const auto sampler_status = validate_sampler_desc(sampler); + if (!sampler_status.ok()) { + return sampler_status; + } + last_sampler_slot = slot; + last_sampler_desc = sampler; + return pp::foundation::Status::success(); + } + [[nodiscard]] pp::foundation::Status bind_mesh(IMesh& mesh) noexcept override { return validate_mesh_desc(mesh.desc()); @@ -324,6 +352,8 @@ public: DepthState last_depth_state {}; std::uint32_t last_texture_slot = 0; std::uint64_t last_texture_bytes = 0; + std::uint32_t last_sampler_slot = 0; + SamplerDesc last_sampler_desc {}; std::uint64_t last_upload_bytes = 0; std::uint64_t last_readback_bytes = 0; std::uint64_t last_capture_bytes = 0; @@ -550,6 +580,39 @@ void validates_depth_contract(pp::tests::Harness& h) PP_EXPECT(h, compare_op_name(static_cast(255)) == std::string_view("unknown")); } +void validates_sampler_contract(pp::tests::Harness& h) +{ + const SamplerDesc sampler { + .min_filter = SamplerFilter::linear, + .mag_filter = SamplerFilter::nearest, + .mip_filter = SamplerFilter::linear, + .address_u = SamplerAddressMode::repeat, + .address_v = SamplerAddressMode::mirrored_repeat, + .address_w = SamplerAddressMode::clamp_to_border, + }; + + PP_EXPECT(h, validate_sampler_desc(sampler).ok()); + PP_EXPECT(h, validate_sampler_filter(SamplerFilter::nearest).ok()); + PP_EXPECT(h, validate_sampler_address_mode(SamplerAddressMode::clamp_to_border).ok()); + PP_EXPECT(h, sampler_filter_name(SamplerFilter::linear) == std::string_view("linear")); + PP_EXPECT(h, sampler_address_mode_name(SamplerAddressMode::mirrored_repeat) == std::string_view("mirrored_repeat")); + + auto bad_filter = sampler; + bad_filter.min_filter = static_cast(255); + auto bad_address = sampler; + bad_address.address_w = static_cast(255); + + const auto bad_filter_status = validate_sampler_desc(bad_filter); + const auto bad_address_status = validate_sampler_desc(bad_address); + + PP_EXPECT(h, !bad_filter_status.ok()); + PP_EXPECT(h, bad_filter_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, !bad_address_status.ok()); + PP_EXPECT(h, bad_address_status.code == StatusCode::invalid_argument); + PP_EXPECT(h, sampler_filter_name(static_cast(255)) == std::string_view("unknown")); + PP_EXPECT(h, sampler_address_mode_name(static_cast(255)) == std::string_view("unknown")); +} + void validates_viewports_and_mesh_descriptors(pp::tests::Harness& h) { const Extent2D target { .width = 64, .height = 32 }; @@ -740,6 +803,12 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_texture(2, texture).ok()); + PP_EXPECT(h, context.bind_sampler(2, SamplerDesc { + .min_filter = SamplerFilter::linear, + .mag_filter = SamplerFilter::nearest, + .address_u = SamplerAddressMode::repeat, + }) + .ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw().ok()); context.end_render_pass(); @@ -777,6 +846,9 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) PP_EXPECT(h, device.context.last_depth_state.compare == CompareOp::less_or_equal); PP_EXPECT(h, device.context.last_texture_slot == 2U); PP_EXPECT(h, device.context.last_texture_bytes == 8192U); + PP_EXPECT(h, device.context.last_sampler_slot == 2U); + 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_readback_bytes == 80U); PP_EXPECT(h, device.context.last_capture_bytes == 8192U); @@ -831,12 +903,20 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.bind_texture(1, texture).ok()); + PP_EXPECT(h, context.bind_sampler(1, SamplerDesc { + .min_filter = SamplerFilter::linear, + .mag_filter = SamplerFilter::nearest, + .address_u = SamplerAddressMode::repeat, + .address_v = SamplerAddressMode::clamp_to_edge, + .address_w = SamplerAddressMode::clamp_to_border, + }) + .ok()); PP_EXPECT(h, context.bind_mesh(mesh).ok()); PP_EXPECT(h, context.draw().ok()); context.end_render_pass(); const auto commands = device.commands(); - PP_EXPECT(h, commands.size() == 11U); + PP_EXPECT(h, commands.size() == 12U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_marker); PP_EXPECT(h, commands[0].component == std::string_view("renderer")); PP_EXPECT(h, commands[0].name == std::string_view("frame")); @@ -865,11 +945,16 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) PP_EXPECT(h, commands[7].texture_slot == 1U); PP_EXPECT(h, commands[7].texture_desc.extent.height == 32U); PP_EXPECT(h, recorded_render_command_kind_name(commands[7].kind) == std::string_view("bind_texture")); - PP_EXPECT(h, commands[8].kind == RecordedRenderCommandKind::bind_mesh); - PP_EXPECT(h, commands[8].mesh_desc.vertex_count == 3U); - PP_EXPECT(h, commands[9].kind == RecordedRenderCommandKind::draw); - PP_EXPECT(h, commands[10].kind == RecordedRenderCommandKind::end_render_pass); - PP_EXPECT(h, recorded_render_command_kind_name(commands[9].kind) == std::string_view("draw")); + PP_EXPECT(h, commands[8].kind == RecordedRenderCommandKind::bind_sampler); + PP_EXPECT(h, commands[8].sampler_slot == 1U); + PP_EXPECT(h, commands[8].sampler_desc.mag_filter == SamplerFilter::nearest); + PP_EXPECT(h, commands[8].sampler_desc.address_w == SamplerAddressMode::clamp_to_border); + PP_EXPECT(h, recorded_render_command_kind_name(commands[8].kind) == std::string_view("bind_sampler")); + PP_EXPECT(h, commands[9].kind == RecordedRenderCommandKind::bind_mesh); + PP_EXPECT(h, commands[9].mesh_desc.vertex_count == 3U); + PP_EXPECT(h, commands[10].kind == RecordedRenderCommandKind::draw); + PP_EXPECT(h, commands[11].kind == RecordedRenderCommandKind::end_render_pass); + PP_EXPECT(h, recorded_render_command_kind_name(commands[10].kind) == std::string_view("draw")); PP_EXPECT(h, context.upload_texture( texture, @@ -877,12 +962,12 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) upload_bytes) .ok()); const auto commands_after_upload = device.commands(); - PP_EXPECT(h, commands_after_upload.size() == 12U); - PP_EXPECT(h, commands_after_upload[11].kind == RecordedRenderCommandKind::upload_texture); - PP_EXPECT(h, commands_after_upload[11].texture_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_upload[11].readback_region.x == 4U); - PP_EXPECT(h, commands_after_upload[11].upload_bytes == 96U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[11].kind) == std::string_view("upload_texture")); + PP_EXPECT(h, commands_after_upload.size() == 13U); + PP_EXPECT(h, commands_after_upload[12].kind == RecordedRenderCommandKind::upload_texture); + PP_EXPECT(h, commands_after_upload[12].texture_desc.extent.width == 64U); + PP_EXPECT(h, commands_after_upload[12].readback_region.x == 4U); + 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.read_texture( texture, @@ -890,22 +975,22 @@ void recording_renderer_records_valid_command_sequences(pp::tests::Harness& h) readback_buffer) .ok()); const auto commands_after_readback = device.commands(); - PP_EXPECT(h, commands_after_readback.size() == 13U); - PP_EXPECT(h, commands_after_readback[12].kind == RecordedRenderCommandKind::read_texture); - PP_EXPECT(h, commands_after_readback[12].texture_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_readback[12].readback_region.x == 4U); - PP_EXPECT(h, commands_after_readback[12].readback_region.height == 3U); - PP_EXPECT(h, commands_after_readback[12].readback_bytes == 96U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[12].kind) == std::string_view("read_texture")); + 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, context.capture_frame(target, readback_buffer).ok()); const auto commands_after_capture = device.commands(); - PP_EXPECT(h, commands_after_capture.size() == 14U); - PP_EXPECT(h, commands_after_capture[13].kind == RecordedRenderCommandKind::capture_frame); - PP_EXPECT(h, commands_after_capture[13].target_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_capture[13].target_desc.extent.height == 32U); - PP_EXPECT(h, commands_after_capture[13].capture_bytes == 8192U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[13].kind) == std::string_view("capture_frame")); + 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, context.blit_render_target( target, @@ -915,16 +1000,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() == 15U); - PP_EXPECT(h, commands_after_blit[14].kind == RecordedRenderCommandKind::blit_render_target); - PP_EXPECT(h, commands_after_blit[14].source_desc.extent.width == 64U); - PP_EXPECT(h, commands_after_blit[14].destination_desc.extent.height == 32U); - PP_EXPECT(h, commands_after_blit[14].source_region.width == 16U); - PP_EXPECT(h, commands_after_blit[14].destination_region.x == 2U); - PP_EXPECT(h, commands_after_blit[14].blit_filter == BlitFilter::linear); - PP_EXPECT(h, commands_after_blit[14].blit_source_bytes == 512U); - PP_EXPECT(h, commands_after_blit[14].blit_destination_bytes == 128U); - PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[14].kind) == std::string_view("blit_render_target")); + 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")); device.clear(); PP_EXPECT(h, device.commands().empty()); @@ -978,6 +1063,10 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !depth_before_begin.ok()); PP_EXPECT(h, depth_before_begin.code == StatusCode::invalid_argument); + const auto sampler_before_begin = context.bind_sampler(0, SamplerDesc {}); + PP_EXPECT(h, !sampler_before_begin.ok()); + PP_EXPECT(h, sampler_before_begin.code == StatusCode::invalid_argument); + const auto invalid_target = context.begin_render_pass(non_render_target, ClearColor {}); PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); @@ -1069,6 +1158,18 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, bind_texture_bad_slot.code == StatusCode::out_of_range); PP_EXPECT(h, context.bind_texture(0, texture).ok()); + const auto bind_sampler_bad_slot = context.bind_sampler(max_texture_slots, SamplerDesc {}); + PP_EXPECT(h, !bind_sampler_bad_slot.ok()); + PP_EXPECT(h, bind_sampler_bad_slot.code == StatusCode::out_of_range); + + auto invalid_sampler = SamplerDesc {}; + invalid_sampler.min_filter = static_cast(255); + const auto bind_invalid_sampler = context.bind_sampler(0, invalid_sampler); + PP_EXPECT(h, !bind_invalid_sampler.ok()); + PP_EXPECT(h, bind_invalid_sampler.code == StatusCode::invalid_argument); + + PP_EXPECT(h, context.bind_sampler(0, SamplerDesc {}).ok()); + const auto draw_without_mesh = context.draw(); PP_EXPECT(h, !draw_without_mesh.ok()); PP_EXPECT(h, draw_without_mesh.code == StatusCode::invalid_argument); @@ -1101,6 +1202,10 @@ void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Har PP_EXPECT(h, !bind_texture_after_end.ok()); PP_EXPECT(h, bind_texture_after_end.code == StatusCode::invalid_argument); + const auto bind_sampler_after_end = context.bind_sampler(0, SamplerDesc {}); + PP_EXPECT(h, !bind_sampler_after_end.ok()); + PP_EXPECT(h, bind_sampler_after_end.code == StatusCode::invalid_argument); + const auto read_outside_bounds = context.read_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, @@ -1187,6 +1292,7 @@ int main() harness.run("validates_blit_contract", validates_blit_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); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index a798490..a863ba6 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -2298,6 +2298,14 @@ int record_render(int argc, char** argv) .compare = pp::renderer::CompareOp::less_or_equal, }); const auto bind_texture_status = context.bind_texture(0, texture); + const auto bind_sampler_status = context.bind_sampler(0, pp::renderer::SamplerDesc { + .min_filter = pp::renderer::SamplerFilter::linear, + .mag_filter = pp::renderer::SamplerFilter::linear, + .mip_filter = pp::renderer::SamplerFilter::linear, + .address_u = pp::renderer::SamplerAddressMode::clamp_to_edge, + .address_v = pp::renderer::SamplerAddressMode::clamp_to_edge, + .address_w = pp::renderer::SamplerAddressMode::clamp_to_edge, + }); const auto mesh_status = context.bind_mesh(mesh); const auto draw_status = context.draw(); context.end_render_pass(); @@ -2318,6 +2326,10 @@ int record_render(int argc, char** argv) print_error("record-render", bind_texture_status.message); return 2; } + if (!bind_sampler_status.ok()) { + print_error("record-render", bind_sampler_status.message); + return 2; + } if (!mesh_status.ok()) { print_error("record-render", mesh_status.message); return 2; @@ -2373,6 +2385,7 @@ int record_render(int argc, char** argv) std::size_t blend_commands = 0; std::size_t depth_commands = 0; std::size_t bind_texture_commands = 0; + std::size_t bind_sampler_commands = 0; std::size_t upload_commands = 0; std::size_t readback_commands = 0; std::size_t capture_commands = 0; @@ -2400,6 +2413,8 @@ int record_render(int argc, char** argv) if (bound_bytes.ok()) { bound_texture_bytes += bound_bytes.value(); } + } else if (command.kind == pp::renderer::RecordedRenderCommandKind::bind_sampler) { + ++bind_sampler_commands; } else if (command.kind == pp::renderer::RecordedRenderCommandKind::upload_texture) { ++upload_commands; upload_bytes += command.upload_bytes; @@ -2429,6 +2444,7 @@ int record_render(int argc, char** argv) << ",\"blendCommands\":" << blend_commands << ",\"depthCommands\":" << depth_commands << ",\"bindTextureCommands\":" << bind_texture_commands + << ",\"bindSamplerCommands\":" << bind_sampler_commands << ",\"boundTextureBytes\":" << bound_texture_bytes << ",\"uploadCommands\":" << upload_commands << ",\"uploadBytes\":" << upload_bytes