#include "renderer_api/renderer_api.h" #include "renderer_api/recording_renderer.h" #include "renderer_api/shader_catalog.h" #include "test_harness.h" #include #include #include #include #include #include #include using pp::foundation::StatusCode; using pp::renderer::BlitFilter; using pp::renderer::blit_filter_name; using pp::renderer::BlendFactor; using pp::renderer::blend_factor_name; using pp::renderer::BlendOp; using pp::renderer::blend_op_name; using pp::renderer::BlendState; using pp::renderer::ClearColor; using pp::renderer::CompareOp; using pp::renderer::compare_op_name; 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; using pp::renderer::IRenderTarget; using pp::renderer::IRenderTrace; using pp::renderer::IShaderProgram; using pp::renderer::MeshDesc; using pp::renderer::PaintFeedbackPath; using pp::renderer::PrimitiveTopology; using pp::renderer::ReadbackRegion; using pp::renderer::RecordedRenderCommandKind; using pp::renderer::RecordingMesh; using pp::renderer::RecordingReadbackBuffer; using pp::renderer::RecordingRenderDevice; using pp::renderer::RecordingRenderTarget; using pp::renderer::RecordingShaderProgram; using pp::renderer::RecordingTexture2D; using pp::renderer::RenderDeviceFeatures; using pp::renderer::RenderPassDesc; 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; 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; using pp::renderer::max_shader_uniform_bytes; using pp::renderer::max_mip_levels_for_extent; using pp::renderer::max_resource_label_bytes; using pp::renderer::max_texture_dimension; using pp::renderer::max_texture_slots; using pp::renderer::max_trace_label_bytes; using pp::renderer::panopainter_shader_catalog; using pp::renderer::paint_feedback_path_name; using pp::renderer::plan_paint_feedback; using pp::renderer::primitive_topology_name; using pp::renderer::readback_byte_size; 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; using pp::renderer::validate_blend_factor; using pp::renderer::validate_blend_op; using pp::renderer::validate_blend_state; using pp::renderer::validate_compare_op; using pp::renderer::validate_depth_state; using pp::renderer::validate_draw_desc; using pp::renderer::validate_mesh_desc; using pp::renderer::validate_mipmap_generation_desc; using pp::renderer::validate_readback_region; using pp::renderer::validate_render_pass_desc; using pp::renderer::validate_resource_label; 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; 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; namespace { class FakeRenderTarget final : public IRenderTarget { public: explicit FakeRenderTarget(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }) noexcept : desc_(desc) { } [[nodiscard]] TextureDesc color_desc() const noexcept override { return desc_; } private: TextureDesc desc_ {}; }; class FakeShaderProgram final : public IShaderProgram { public: explicit FakeShaderProgram(const char* debug_name = "fake-shader") noexcept : debug_name_(debug_name) { } [[nodiscard]] const char* debug_name() const noexcept override { return debug_name_; } private: const char* debug_name_ = ""; }; class FakeMesh final : public IMesh { public: explicit FakeMesh(MeshDesc desc = MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles, }) noexcept : desc_(desc) { } [[nodiscard]] MeshDesc desc() const noexcept override { return desc_; } private: MeshDesc desc_ {}; }; class FakeTexture final : public pp::renderer::ITexture2D { public: explicit FakeTexture(TextureDesc desc = TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }) noexcept : desc_(desc) { } [[nodiscard]] TextureDesc desc() const noexcept override { return desc_; } private: TextureDesc desc_ {}; }; class FakeReadbackBuffer final : public pp::renderer::IReadbackBuffer { public: explicit FakeReadbackBuffer(std::uint64_t size) noexcept : size_(size) { } [[nodiscard]] std::uint64_t size_bytes() const noexcept override { return size_; } private: std::uint64_t size_ = 0; }; class FakeTrace final : public IRenderTrace { public: [[nodiscard]] pp::foundation::Status marker(const char* component, const char* name) noexcept override { const auto status = validate_trace_label(component, name); if (!status.ok()) { return status; } last_component = component; last_name = name; ++marker_count; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status begin_scope(const char* component, const char* name) noexcept override { const auto status = validate_trace_label(component, name); if (!status.ok()) { return status; } last_component = component; last_name = name; ++begin_scope_count; ++scope_depth; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status end_scope() noexcept override { if (scope_depth == 0U) { return pp::foundation::Status::invalid_argument("trace scope has not begun"); } ++end_scope_count; --scope_depth; return pp::foundation::Status::success(); } const char* last_component = nullptr; const char* last_name = nullptr; std::uint32_t marker_count = 0; std::uint32_t begin_scope_count = 0; std::uint32_t end_scope_count = 0; std::uint32_t scope_depth = 0; }; class FakeCommandContext final : public ICommandContext { public: [[nodiscard]] pp::foundation::Status begin_render_pass( IRenderTarget& target, RenderPassDesc desc) noexcept override { const auto render_pass_status = validate_render_pass_desc(desc); 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); } [[nodiscard]] pp::foundation::Status set_viewport(Viewport viewport) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } return validate_viewport(viewport, Extent2D { .width = 64, .height = 32 }); } [[nodiscard]] pp::foundation::Status set_scissor(ScissorRect scissor) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_scissor(scissor, Extent2D { .width = 64, .height = 32 }); if (!status.ok()) { return status; } last_scissor = scissor; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_shader(IShaderProgram& shader) noexcept override { shader_name = shader.debug_name(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status set_shader_uniform( const char* name, std::span bytes) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_shader_uniform_write(name, bytes); if (!status.ok()) { return status; } last_uniform_name = name; last_uniform_bytes = bytes.size(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status set_blend_state(BlendState state) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_blend_state(state); if (!status.ok()) { return status; } last_blend_state = state; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status set_depth_state(DepthState state) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_depth_state(state); if (!status.ok()) { return status; } last_depth_state = state; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status bind_texture( std::uint32_t slot, pp::renderer::ITexture2D& texture) 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; } 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(); } last_texture_slot = slot; last_texture_bytes = bytes.value(); 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 { const auto status = validate_mesh_desc(mesh.desc()); if (!status.ok()) { return status; } last_mesh_desc = mesh.desc(); mesh_bound = true; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status draw(DrawDesc desc) noexcept override { if (!in_render_pass) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } if (!mesh_bound) { return pp::foundation::Status::invalid_argument("mesh must be bound before draw"); } const auto status = validate_draw_desc(last_mesh_desc, desc); if (!status.ok()) { return status; } last_draw_desc = desc; return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status read_texture( pp::renderer::ITexture2D& texture, 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(); } if (destination.size_bytes() < bytes.value()) { return pp::foundation::Status::out_of_range("readback buffer is too small"); } last_readback_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status upload_texture( pp::renderer::ITexture2D& texture, 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(); } if (rgba_or_channel_bytes.size() != bytes.value()) { return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region"); } last_upload_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status generate_mipmaps( pp::renderer::ITexture2D& texture) noexcept override { const auto status = validate_mipmap_generation_desc(texture.desc()); if (!status.ok()) { return status; } const auto bytes = texture_byte_size(texture.desc()); if (!bytes.ok()) { return bytes.status(); } last_generated_mip_levels = texture.desc().mip_levels; last_generated_mip_bytes = bytes.value(); 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, pp::renderer::ITexture2D& destination, ReadbackRegion destination_region) noexcept override { const auto status = validate_texture_copy_descs( source.desc(), source_region, destination.desc(), destination_region); if (!status.ok()) { return status; } const auto source_bytes = readback_byte_size(source.desc(), source_region); if (!source_bytes.ok()) { return source_bytes.status(); } const auto destination_bytes = readback_byte_size(destination.desc(), destination_region); if (!destination_bytes.ok()) { return destination_bytes.status(); } last_copy_source_bytes = source_bytes.value(); last_copy_destination_bytes = destination_bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status capture_frame( IRenderTarget& target, pp::renderer::IReadbackBuffer& destination) noexcept override { const auto bytes = frame_capture_byte_size(target.color_desc()); if (!bytes) { return bytes.status(); } if (destination.size_bytes() < bytes.value()) { return pp::foundation::Status::out_of_range("frame capture buffer is too small"); } last_capture_bytes = bytes.value(); return pp::foundation::Status::success(); } [[nodiscard]] pp::foundation::Status blit_render_target( IRenderTarget& source, ReadbackRegion source_region, IRenderTarget& destination, ReadbackRegion destination_region, BlitFilter filter) noexcept override { const auto source_desc = source.color_desc(); const auto destination_desc = destination.color_desc(); const auto desc_status = validate_blit_descs(source_desc, destination_desc); if (!desc_status.ok()) { return desc_status; } const auto filter_status = validate_blit_filter(filter); if (!filter_status.ok()) { return filter_status; } const auto source_bytes = readback_byte_size(source_desc, source_region); if (!source_bytes) { return source_bytes.status(); } const auto destination_bytes = readback_byte_size(destination_desc, destination_region); if (!destination_bytes) { return destination_bytes.status(); } last_blit_source_bytes = source_bytes.value(); last_blit_destination_bytes = destination_bytes.value(); last_blit_filter = filter; return pp::foundation::Status::success(); } void end_render_pass() noexcept override { in_render_pass = false; mesh_bound = false; last_mesh_desc = MeshDesc {}; } bool in_render_pass = false; bool mesh_bound = false; RenderPassDesc last_render_pass_desc {}; MeshDesc last_mesh_desc {}; DrawDesc last_draw_desc {}; const char* shader_name = nullptr; const char* last_uniform_name = nullptr; std::size_t last_uniform_bytes = 0; ScissorRect last_scissor {}; BlendState last_blend_state {}; 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::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; std::uint64_t last_capture_bytes = 0; std::uint64_t last_blit_source_bytes = 0; std::uint64_t last_blit_destination_bytes = 0; BlitFilter last_blit_filter = BlitFilter::nearest; }; class FakeRenderDevice final : public IRenderDevice { public: [[nodiscard]] const char* backend_name() const noexcept override { return "fake"; } [[nodiscard]] RenderDeviceFeatures features() const noexcept override { return RenderDeviceFeatures { .framebuffer_fetch = true, .explicit_texture_transitions = true, .texture_copy = true, .render_target_blit = true, .frame_capture = true, .float16_render_targets = true, .float32_render_targets = true, }; } [[nodiscard]] pp::foundation::Result> create_texture( TextureDesc desc) noexcept override { const auto bytes = texture_byte_size(desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return allocate_resource(desc); } [[nodiscard]] pp::foundation::Result> create_render_target( TextureDesc color_desc) noexcept override { if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("render target texture must allow render_target usage")); } const auto bytes = texture_byte_size(color_desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return allocate_resource(color_desc); } [[nodiscard]] pp::foundation::Result> create_shader_program( ShaderProgramDesc desc) noexcept override { const auto status = validate_shader_program_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return allocate_resource(desc.debug_name); } [[nodiscard]] pp::foundation::Result> create_mesh( MeshDesc desc) noexcept override { const auto status = validate_mesh_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return allocate_resource(desc); } [[nodiscard]] pp::foundation::Result> create_readback_buffer( std::uint64_t size_bytes) noexcept override { if (size_bytes == 0U) { return pp::foundation::Result>::failure( pp::foundation::Status::invalid_argument("readback buffer size must be greater than zero")); } if (size_bytes > pp::renderer::max_texture_bytes) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit")); } return allocate_resource(size_bytes); } [[nodiscard]] ICommandContext& immediate_context() noexcept override { return context; } [[nodiscard]] IRenderTrace* trace() noexcept override { return &trace_recorder; } FakeCommandContext context; FakeTrace trace_recorder; private: template [[nodiscard]] static pp::foundation::Result> allocate_resource( Args&&... args) noexcept { auto resource = std::unique_ptr(new (std::nothrow) Resource(std::forward(args)...)); if (!resource) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("renderer resource allocation failed")); } std::unique_ptr erased = std::move(resource); return pp::foundation::Result>::success(std::move(erased)); } }; 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 { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, }); const auto r8 = texture_byte_size(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, }); PP_EXPECT(h, rgba.ok()); PP_EXPECT(h, rgba.value() == 512U); PP_EXPECT(h, r8.ok()); PP_EXPECT(h, r8.value() == 128U); PP_EXPECT(h, texture_format_name(TextureFormat::depth24_stencil8) == std::string_view("depth24_stencil8")); const auto mipmapped = texture_byte_size(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, }); PP_EXPECT(h, mipmapped.ok()); PP_EXPECT(h, mipmapped.value() == 84U); } 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 validates_resource_labels(pp::tests::Harness& h) { std::array oversized_label {}; oversized_label.fill('r'); oversized_label[max_resource_label_bytes + 1U] = '\0'; PP_EXPECT(h, validate_resource_label("").ok()); PP_EXPECT(h, validate_resource_label("stroke-target").ok()); PP_EXPECT(h, validate_texture_desc(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .debug_name = "paint-texture", }) .ok()); PP_EXPECT(h, validate_mesh_desc(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles, .debug_name = "brush-quad", }) .ok()); static constexpr char shader_source[] = "void main() {}"; PP_EXPECT(h, validate_shader_program_desc(ShaderProgramDesc { .debug_name = "brush-shader", .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }) .ok()); const auto null_label = validate_resource_label(nullptr); const auto oversized = validate_resource_label(oversized_label.data()); const auto null_texture_label = validate_texture_desc(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .debug_name = nullptr, }); const auto oversized_mesh_label = validate_mesh_desc(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles, .debug_name = oversized_label.data(), }); const auto null_shader_label = validate_shader_program_desc(ShaderProgramDesc { .debug_name = nullptr, .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }); const auto oversized_shader_label = validate_shader_program_desc(ShaderProgramDesc { .debug_name = oversized_label.data(), .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }); PP_EXPECT(h, !null_label.ok()); PP_EXPECT(h, null_label.code == StatusCode::invalid_argument); PP_EXPECT(h, !oversized.ok()); PP_EXPECT(h, oversized.code == StatusCode::out_of_range); PP_EXPECT(h, !null_texture_label.ok()); PP_EXPECT(h, null_texture_label.code == StatusCode::invalid_argument); PP_EXPECT(h, !oversized_mesh_label.ok()); PP_EXPECT(h, oversized_mesh_label.code == StatusCode::out_of_range); PP_EXPECT(h, !null_shader_label.ok()); PP_EXPECT(h, null_shader_label.code == StatusCode::invalid_argument); PP_EXPECT(h, !oversized_shader_label.ok()); PP_EXPECT(h, oversized_shader_label.code == StatusCode::out_of_range); } void validates_mipmap_generation_contract(pp::tests::Harness& h) { const TextureDesc mipmapped_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc one_mip_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 1, .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc too_many_mips_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 4, .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc missing_sampled_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, .usage = TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc missing_copy_source_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, .usage = TextureUsage::sampled | TextureUsage::copy_destination, }; const TextureDesc missing_copy_destination_desc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, .usage = TextureUsage::sampled | TextureUsage::copy_source, }; PP_EXPECT(h, max_mip_levels_for_extent(Extent2D { .width = 1, .height = 1 }) == 1U); PP_EXPECT(h, max_mip_levels_for_extent(Extent2D { .width = 4, .height = 3 }) == 3U); PP_EXPECT(h, max_mip_levels_for_extent(Extent2D { .width = max_texture_dimension, .height = 1 }) == 16U); PP_EXPECT(h, validate_mipmap_generation_desc(mipmapped_desc).ok()); const auto one_mip = validate_mipmap_generation_desc(one_mip_desc); const auto too_many_mips = validate_mipmap_generation_desc(too_many_mips_desc); const auto missing_sampled = validate_mipmap_generation_desc(missing_sampled_desc); const auto missing_copy_source = validate_mipmap_generation_desc(missing_copy_source_desc); const auto missing_copy_destination = validate_mipmap_generation_desc(missing_copy_destination_desc); PP_EXPECT(h, !one_mip.ok()); PP_EXPECT(h, one_mip.code == StatusCode::invalid_argument); PP_EXPECT(h, !too_many_mips.ok()); PP_EXPECT(h, too_many_mips.code == StatusCode::out_of_range); PP_EXPECT(h, !missing_sampled.ok()); PP_EXPECT(h, missing_sampled.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); } 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 }); const auto huge = validate_extent(Extent2D { .width = max_texture_dimension + 1U, .height = 1 }); const auto excessive_bytes = texture_byte_size(TextureDesc { .extent = Extent2D { .width = max_texture_dimension, .height = max_texture_dimension }, .format = TextureFormat::rgba8, }); PP_EXPECT(h, !zero.ok()); PP_EXPECT(h, zero.code == StatusCode::invalid_argument); PP_EXPECT(h, !huge.ok()); PP_EXPECT(h, huge.code == StatusCode::out_of_range); PP_EXPECT(h, !excessive_bytes.ok()); PP_EXPECT(h, excessive_bytes.status().code == StatusCode::out_of_range); } void validates_readback_bounds(pp::tests::Harness& h) { const TextureDesc desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc unsupported_usage_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::readback_source | static_cast(1U << 31U), }; PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 1, .height = 1 }).ok()); const auto empty = validate_readback_region(desc, ReadbackRegion { .x = 0, .y = 0, .width = 0, .height = 1 }); const auto origin_outside = validate_readback_region(desc, ReadbackRegion { .x = 65, .y = 0, .width = 1, .height = 1 }); const auto overrun = validate_readback_region(desc, ReadbackRegion { .x = 63, .y = 31, .width = 2, .height = 1 }); const auto unsupported_usage = validate_readback_region( unsupported_usage_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !empty.ok()); PP_EXPECT(h, empty.code == StatusCode::invalid_argument); PP_EXPECT(h, !origin_outside.ok()); PP_EXPECT(h, origin_outside.code == StatusCode::out_of_range); PP_EXPECT(h, !overrun.ok()); PP_EXPECT(h, overrun.code == StatusCode::out_of_range); PP_EXPECT(h, !unsupported_usage.ok()); PP_EXPECT(h, unsupported_usage.code == StatusCode::invalid_argument); } void computes_readback_byte_sizes(pp::tests::Harness& h) { const TextureDesc rgba_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .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, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc unknown_format_desc { .extent = Extent2D { .width = 64, .height = 32 }, .format = static_cast(255), .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 }); const auto r8 = readback_byte_size(r8_desc, ReadbackRegion { .x = 0, .y = 0, .width = 5, .height = 7 }); const auto overrun = readback_byte_size(rgba_desc, ReadbackRegion { .x = 63, .y = 0, .width = 2, .height = 1 }); const auto unknown_format = readback_byte_size( unknown_format_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, rgba.ok()); PP_EXPECT(h, rgba.value() == 96U); PP_EXPECT(h, r8.ok()); PP_EXPECT(h, r8.value() == 35U); PP_EXPECT(h, !overrun.ok()); PP_EXPECT(h, overrun.status().code == StatusCode::out_of_range); PP_EXPECT(h, !unknown_format.ok()); PP_EXPECT(h, unknown_format.status().code == StatusCode::invalid_argument); } void computes_frame_capture_byte_sizes(pp::tests::Harness& h) { const TextureDesc target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .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, .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc unsupported_usage_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::readback_source | static_cast(1U << 31U), }; const TextureDesc unknown_format_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = static_cast(255), .usage = TextureUsage::render_target | TextureUsage::readback_source, }; const auto capture = frame_capture_byte_size(target_desc); const auto non_target = frame_capture_byte_size(texture_desc); const auto unsupported_usage = frame_capture_byte_size(unsupported_usage_desc); const auto unknown_format = frame_capture_byte_size(unknown_format_desc); PP_EXPECT(h, capture.ok()); PP_EXPECT(h, capture.value() == 512U); PP_EXPECT(h, !non_target.ok()); PP_EXPECT(h, non_target.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !unsupported_usage.ok()); PP_EXPECT(h, unsupported_usage.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !unknown_format.ok()); PP_EXPECT(h, unknown_format.status().code == StatusCode::invalid_argument); } void validates_blit_contract(pp::tests::Harness& h) { const TextureDesc target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .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, .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, .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc unknown_format_target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = static_cast(255), .usage = TextureUsage::render_target | TextureUsage::copy_source | TextureUsage::copy_destination, }; const TextureDesc unsupported_usage_target_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::copy_source | TextureUsage::copy_destination | static_cast(1U << 31U), }; PP_EXPECT(h, validate_blit_descs(target_desc, target_desc).ok()); PP_EXPECT(h, validate_blit_filter(BlitFilter::nearest).ok()); PP_EXPECT(h, validate_blit_filter(BlitFilter::linear).ok()); PP_EXPECT(h, blit_filter_name(BlitFilter::linear) == std::string_view("linear")); const auto non_target = validate_blit_descs(texture_desc, target_desc); const auto mismatched_format = validate_blit_descs(target_desc, r8_target_desc); const auto unknown_format = validate_blit_descs(unknown_format_target_desc, unknown_format_target_desc); const auto unsupported_usage = validate_blit_descs(unsupported_usage_target_desc, target_desc); const auto bad_filter = validate_blit_filter(static_cast(255)); PP_EXPECT(h, !non_target.ok()); PP_EXPECT(h, non_target.code == StatusCode::invalid_argument); PP_EXPECT(h, !mismatched_format.ok()); PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); PP_EXPECT(h, !unknown_format.ok()); PP_EXPECT(h, unknown_format.code == StatusCode::invalid_argument); PP_EXPECT(h, !unsupported_usage.ok()); PP_EXPECT(h, unsupported_usage.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_filter.ok()); PP_EXPECT(h, bad_filter.code == StatusCode::invalid_argument); PP_EXPECT(h, blit_filter_name(static_cast(255)) == std::string_view("unknown")); } void plans_paint_feedback_paths(pp::tests::Harness& h) { const TextureDesc render_target { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "paint-target", }; const TextureDesc render_only_target { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target, .debug_name = "paint-render-only", }; const TextureDesc depth_target { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::depth24_stencil8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "paint-depth", }; const RenderDeviceFeatures framebuffer_fetch_features { .framebuffer_fetch = true, .explicit_texture_transitions = true, }; const RenderDeviceFeatures copy_features { .texture_copy = true, }; const RenderDeviceFeatures blit_features { .render_target_blit = true, }; const auto simple = plan_paint_feedback(copy_features, render_only_target, false); PP_EXPECT(h, simple); if (simple) { PP_EXPECT(h, simple.value().path == PaintFeedbackPath::none); PP_EXPECT(h, !simple.value().reads_destination_color); PP_EXPECT(h, simple.value().target_bytes == 8192U); } const auto fetch = plan_paint_feedback(framebuffer_fetch_features, render_only_target, true); PP_EXPECT(h, fetch); if (fetch) { PP_EXPECT(h, fetch.value().path == PaintFeedbackPath::framebuffer_fetch); PP_EXPECT(h, fetch.value().reads_destination_color); PP_EXPECT(h, !fetch.value().requires_auxiliary_texture); PP_EXPECT(h, !fetch.value().requires_texture_copy); PP_EXPECT(h, fetch.value().requires_explicit_transition); } const auto ping_pong_copy = plan_paint_feedback(copy_features, render_target, true); PP_EXPECT(h, ping_pong_copy); if (ping_pong_copy) { PP_EXPECT(h, ping_pong_copy.value().path == PaintFeedbackPath::ping_pong_textures); PP_EXPECT(h, ping_pong_copy.value().requires_auxiliary_texture); PP_EXPECT(h, ping_pong_copy.value().requires_texture_copy); PP_EXPECT(h, !ping_pong_copy.value().requires_render_target_blit); PP_EXPECT(h, ping_pong_copy.value().auxiliary_desc.extent.width == render_target.extent.width); } const auto ping_pong_blit = plan_paint_feedback(blit_features, render_target, true); PP_EXPECT(h, ping_pong_blit); if (ping_pong_blit) { PP_EXPECT(h, ping_pong_blit.value().path == PaintFeedbackPath::ping_pong_textures); PP_EXPECT(h, !ping_pong_blit.value().requires_texture_copy); PP_EXPECT(h, ping_pong_blit.value().requires_render_target_blit); } const auto unsupported = plan_paint_feedback(RenderDeviceFeatures {}, render_target, true); const auto missing_usage = plan_paint_feedback(copy_features, render_only_target, true); const auto depth = plan_paint_feedback(copy_features, depth_target, true); PP_EXPECT(h, !unsupported.ok()); PP_EXPECT(h, unsupported.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !missing_usage.ok()); PP_EXPECT(h, missing_usage.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !depth.ok()); PP_EXPECT(h, depth.status().code == StatusCode::invalid_argument); PP_EXPECT(h, paint_feedback_path_name(PaintFeedbackPath::framebuffer_fetch) == std::string_view("framebuffer_fetch")); PP_EXPECT(h, paint_feedback_path_name(PaintFeedbackPath::ping_pong_textures) == std::string_view("ping_pong_textures")); PP_EXPECT(h, paint_feedback_path_name(static_cast(255)) == std::string_view("unknown")); } void validates_texture_copy_contract(pp::tests::Harness& h) { const TextureDesc rgba_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, }; const TextureDesc r8_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::r8, }; const TextureDesc unknown_format_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = static_cast(255), }; const TextureDesc unsupported_usage_desc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .usage = TextureUsage::copy_source | static_cast(1U << 31U), }; PP_EXPECT(h, validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 1, .y = 2, .width = 4, .height = 3 }, rgba_desc, ReadbackRegion { .x = 5, .y = 4, .width = 4, .height = 3 }) .ok()); const auto mismatched_format = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, r8_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); const auto mismatched_region = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }, rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); const auto outside_source = validate_texture_copy_descs( rgba_desc, ReadbackRegion { .x = 15, .y = 0, .width = 2, .height = 1 }, rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); const auto invalid_format_endpoints = validate_texture_copy_descs( unknown_format_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, unknown_format_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); const auto unsupported_source_usage = validate_texture_copy_descs( unsupported_usage_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, rgba_desc, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !mismatched_format.ok()); PP_EXPECT(h, mismatched_format.code == StatusCode::invalid_argument); PP_EXPECT(h, !mismatched_region.ok()); PP_EXPECT(h, mismatched_region.code == StatusCode::invalid_argument); PP_EXPECT(h, !outside_source.ok()); PP_EXPECT(h, outside_source.code == StatusCode::out_of_range); PP_EXPECT(h, !invalid_format_endpoints.ok()); PP_EXPECT(h, invalid_format_endpoints.code == StatusCode::invalid_argument); PP_EXPECT(h, !unsupported_source_usage.ok()); PP_EXPECT(h, unsupported_source_usage.code == StatusCode::invalid_argument); } void validates_blend_contract(pp::tests::Harness& h) { const BlendState alpha_blend { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, .color_op = BlendOp::add, .source_alpha = BlendFactor::one, .destination_alpha = BlendFactor::one_minus_source_alpha, .alpha_op = BlendOp::add, }; PP_EXPECT(h, validate_blend_state(alpha_blend).ok()); PP_EXPECT(h, validate_blend_factor(BlendFactor::destination_alpha).ok()); PP_EXPECT(h, validate_blend_op(BlendOp::reverse_subtract).ok()); PP_EXPECT(h, blend_factor_name(BlendFactor::one_minus_destination_alpha) == std::string_view("one_minus_destination_alpha")); PP_EXPECT(h, blend_op_name(BlendOp::subtract) == std::string_view("subtract")); auto bad_source = alpha_blend; bad_source.source_color = static_cast(255); auto bad_op = alpha_blend; bad_op.alpha_op = static_cast(255); const auto bad_source_status = validate_blend_state(bad_source); const auto bad_op_status = validate_blend_state(bad_op); PP_EXPECT(h, !bad_source_status.ok()); PP_EXPECT(h, bad_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_op_status.ok()); PP_EXPECT(h, bad_op_status.code == StatusCode::invalid_argument); PP_EXPECT(h, blend_factor_name(static_cast(255)) == std::string_view("unknown")); PP_EXPECT(h, blend_op_name(static_cast(255)) == std::string_view("unknown")); } void validates_depth_contract(pp::tests::Harness& h) { const DepthState read_write_depth { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }; PP_EXPECT(h, validate_depth_state(read_write_depth).ok()); PP_EXPECT(h, validate_compare_op(CompareOp::always).ok()); PP_EXPECT(h, compare_op_name(CompareOp::greater_or_equal) == std::string_view("greater_or_equal")); auto bad_compare = read_write_depth; bad_compare.compare = static_cast(255); const auto bad_compare_status = validate_depth_state(bad_compare); PP_EXPECT(h, !bad_compare_status.ok()); PP_EXPECT(h, bad_compare_status.code == StatusCode::invalid_argument); 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 }; PP_EXPECT(h, validate_viewport( Viewport { .x = 0, .y = 0, .width = 64, .height = 32, .min_depth = 0.0F, .max_depth = 1.0F }, target) .ok()); PP_EXPECT(h, validate_scissor(ScissorRect {}, target).ok()); PP_EXPECT(h, validate_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 64, .height = 32 }, target) .ok()); PP_EXPECT(h, validate_scissor(ScissorRect { .enabled = true, .x = 63, .y = 31, .width = 1, .height = 1 }, target) .ok()); PP_EXPECT(h, validate_mesh_desc(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }).ok()); PP_EXPECT(h, primitive_topology_name(PrimitiveTopology::lines) == std::string_view("lines")); const auto negative_origin = validate_viewport(Viewport { .x = -1, .y = 0, .width = 1, .height = 1 }, target); const auto too_wide = validate_viewport(Viewport { .x = 63, .y = 0, .width = 2, .height = 1 }, target); const auto bad_depth = validate_viewport( Viewport { .x = 0, .y = 0, .width = 1, .height = 1, .min_depth = 0.75F, .max_depth = 0.25F }, target); const auto empty_mesh = validate_mesh_desc(MeshDesc {}); const auto negative_scissor = validate_scissor(ScissorRect { .enabled = true, .x = -1, .y = 0, .width = 1, .height = 1 }, target); const auto empty_scissor = validate_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 0, .height = 1 }, target); const auto scissor_overrun = validate_scissor(ScissorRect { .enabled = true, .x = 63, .y = 0, .width = 2, .height = 1 }, target); PP_EXPECT(h, !negative_origin.ok()); PP_EXPECT(h, negative_origin.code == StatusCode::invalid_argument); PP_EXPECT(h, !too_wide.ok()); PP_EXPECT(h, too_wide.code == StatusCode::out_of_range); PP_EXPECT(h, !bad_depth.ok()); PP_EXPECT(h, bad_depth.code == StatusCode::out_of_range); PP_EXPECT(h, !empty_mesh.ok()); PP_EXPECT(h, empty_mesh.code == StatusCode::invalid_argument); PP_EXPECT(h, !negative_scissor.ok()); PP_EXPECT(h, negative_scissor.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_scissor.ok()); PP_EXPECT(h, empty_scissor.code == StatusCode::invalid_argument); PP_EXPECT(h, !scissor_overrun.ok()); PP_EXPECT(h, scissor_overrun.code == StatusCode::out_of_range); PP_EXPECT(h, validate_texture_slot(0).ok()); PP_EXPECT(h, validate_texture_slot(max_texture_slots - 1U).ok()); const auto invalid_slot = validate_texture_slot(max_texture_slots); PP_EXPECT(h, !invalid_slot.ok()); PP_EXPECT(h, invalid_slot.code == StatusCode::out_of_range); } void validates_draw_descriptors(pp::tests::Harness& h) { const MeshDesc vertex_mesh { .vertex_count = 8, .index_count = 0, .topology = PrimitiveTopology::triangles, }; const MeshDesc indexed_mesh { .vertex_count = 8, .index_count = 12, .topology = PrimitiveTopology::triangles, }; PP_EXPECT(h, validate_draw_desc(vertex_mesh, DrawDesc { .first_vertex = 2, .vertex_count = 3 }).ok()); PP_EXPECT(h, validate_draw_desc(indexed_mesh, DrawDesc { .first_index = 3, .index_count = 6 }).ok()); PP_EXPECT(h, validate_draw_desc(indexed_mesh, DrawDesc { .index_count = 12, .instance_count = 4 }).ok()); const auto empty_draw = validate_draw_desc(vertex_mesh, DrawDesc {}); const auto zero_instances = validate_draw_desc(vertex_mesh, DrawDesc { .vertex_count = 3, .instance_count = 0 }); const auto vertex_overrun = validate_draw_desc(vertex_mesh, DrawDesc { .first_vertex = 7, .vertex_count = 2 }); const auto indexed_without_indices = validate_draw_desc(vertex_mesh, DrawDesc { .index_count = 3 }); const auto index_overrun = validate_draw_desc(indexed_mesh, DrawDesc { .first_index = 11, .index_count = 2 }); PP_EXPECT(h, !empty_draw.ok()); PP_EXPECT(h, empty_draw.code == StatusCode::invalid_argument); PP_EXPECT(h, !zero_instances.ok()); PP_EXPECT(h, zero_instances.code == StatusCode::invalid_argument); PP_EXPECT(h, !vertex_overrun.ok()); PP_EXPECT(h, vertex_overrun.code == StatusCode::out_of_range); PP_EXPECT(h, !indexed_without_indices.ok()); PP_EXPECT(h, indexed_without_indices.code == StatusCode::invalid_argument); PP_EXPECT(h, !index_overrun.ok()); PP_EXPECT(h, index_overrun.code == StatusCode::out_of_range); } void validates_render_pass_descriptors(pp::tests::Harness& h) { const auto valid = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 0.5F, .clear_stencil_enabled = true, .clear_stencil = 7, }); const auto no_clear = validate_render_pass_desc(RenderPassDesc { .clear_color_enabled = false, .clear_color = ClearColor {}, .clear_depth_enabled = false, .clear_stencil_enabled = false, }); const auto bad_color = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor { .r = std::numeric_limits::infinity(), .g = 0.0F, .b = 0.0F, .a = 1.0F, }, }); const auto nan_depth = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = std::numeric_limits::quiet_NaN(), }); const auto out_of_range_depth = validate_render_pass_desc(RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = 1.5F, }); PP_EXPECT(h, valid.ok()); PP_EXPECT(h, no_clear.ok()); PP_EXPECT(h, !bad_color.ok()); PP_EXPECT(h, bad_color.code == StatusCode::invalid_argument); PP_EXPECT(h, !nan_depth.ok()); PP_EXPECT(h, nan_depth.code == StatusCode::invalid_argument); PP_EXPECT(h, !out_of_range_depth.ok()); PP_EXPECT(h, out_of_range_depth.code == StatusCode::out_of_range); } void validates_shader_program_descriptors(pp::tests::Harness& h) { constexpr char vertex_source[] = "#version 330 core\nvoid main(){}"; constexpr char fragment_source[] = "#version 330 core\nout vec4 color; void main(){ color = vec4(1); }"; const ShaderProgramDesc valid { .debug_name = "solid-color", .vertex = ShaderStageSource { .entry_point = "main", .source = vertex_source, .source_size = sizeof(vertex_source) - 1U, }, .fragment = ShaderStageSource { .entry_point = "main", .source = fragment_source, .source_size = sizeof(fragment_source) - 1U, }, }; PP_EXPECT(h, validate_shader_program_desc(valid).ok()); auto missing_name = valid; missing_name.debug_name = nullptr; auto missing_vertex_entry = valid; missing_vertex_entry.vertex.entry_point = ""; auto missing_fragment_source = valid; missing_fragment_source.fragment.source = nullptr; auto empty_fragment_source = valid; empty_fragment_source.fragment.source_size = 0; auto excessive_source = valid; excessive_source.vertex.source_size = max_shader_source_bytes + 1U; const auto missing_name_status = validate_shader_program_desc(missing_name); const auto missing_vertex_entry_status = validate_shader_program_desc(missing_vertex_entry); const auto missing_fragment_source_status = validate_shader_program_desc(missing_fragment_source); const auto empty_fragment_source_status = validate_shader_program_desc(empty_fragment_source); const auto excessive_source_status = validate_shader_program_desc(excessive_source); PP_EXPECT(h, !missing_name_status.ok()); PP_EXPECT(h, missing_name_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !missing_vertex_entry_status.ok()); PP_EXPECT(h, missing_vertex_entry_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !missing_fragment_source_status.ok()); PP_EXPECT(h, missing_fragment_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_fragment_source_status.ok()); PP_EXPECT(h, empty_fragment_source_status.code == StatusCode::invalid_argument); PP_EXPECT(h, !excessive_source_status.ok()); PP_EXPECT(h, excessive_source_status.code == StatusCode::out_of_range); } void validates_shader_uniform_writes(pp::tests::Harness& h) { const std::array matrix_bytes {}; const std::array one_byte {}; static const std::array excessive_bytes {}; PP_EXPECT(h, validate_shader_uniform_write("mvp", matrix_bytes).ok()); PP_EXPECT(h, validate_shader_uniform_write("opacity", one_byte).ok()); const auto null_name = validate_shader_uniform_write(nullptr, matrix_bytes); const auto empty_name = validate_shader_uniform_write("", matrix_bytes); const auto empty_bytes = validate_shader_uniform_write("mvp", std::span {}); const auto excessive = validate_shader_uniform_write("mvp", excessive_bytes); PP_EXPECT(h, !null_name.ok()); PP_EXPECT(h, null_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_name.ok()); PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_bytes.ok()); PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); PP_EXPECT(h, !excessive.ok()); PP_EXPECT(h, excessive.code == StatusCode::out_of_range); } void validates_trace_labels(pp::tests::Harness& h) { std::array oversized_label {}; oversized_label.fill('x'); oversized_label[max_trace_label_bytes + 1U] = '\0'; PP_EXPECT(h, validate_trace_label("renderer", "frame").ok()); const auto empty_component = validate_trace_label("", "frame"); const auto null_component = validate_trace_label(nullptr, "frame"); const auto empty_name = validate_trace_label("renderer", ""); const auto oversized_component = validate_trace_label(oversized_label.data(), "frame"); const auto oversized_name = validate_trace_label("renderer", oversized_label.data()); PP_EXPECT(h, !empty_component.ok()); PP_EXPECT(h, empty_component.code == StatusCode::invalid_argument); PP_EXPECT(h, !null_component.ok()); PP_EXPECT(h, null_component.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_name.ok()); PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !oversized_component.ok()); PP_EXPECT(h, oversized_component.code == StatusCode::out_of_range); PP_EXPECT(h, !oversized_name.ok()); PP_EXPECT(h, oversized_name.code == StatusCode::out_of_range); } void validates_panopainter_shader_catalog(pp::tests::Harness& h) { const auto catalog = panopainter_shader_catalog(); PP_EXPECT(h, catalog.size() == 25U); PP_EXPECT(h, validate_shader_catalog(catalog).ok()); PP_EXPECT(h, catalog.front().name == std::string_view("texture")); PP_EXPECT(h, catalog.front().path == std::string_view("data/shaders/texture.glsl")); PP_EXPECT(h, catalog.back().name == std::string_view("bakeuv")); PP_EXPECT(h, catalog.back().path == std::string_view("data/shaders/bake-uv.glsl")); bool found_stroke = false; bool found_brush_stroke = false; bool found_equirect = false; for (const auto& entry : catalog) { found_stroke = found_stroke || std::string_view(entry.name) == "stroke"; found_brush_stroke = found_brush_stroke || std::string_view(entry.name) == "brush-stroke"; found_equirect = found_equirect || std::string_view(entry.name) == "equirect"; } PP_EXPECT(h, found_stroke); PP_EXPECT(h, found_brush_stroke); PP_EXPECT(h, found_equirect); } void rejects_invalid_shader_catalogs(pp::tests::Harness& h) { const std::array duplicated { ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.glsl" }, ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture-alpha.glsl" }, }; const std::array missing_name { ShaderCatalogEntry { .name = "", .path = "data/shaders/texture.glsl" }, }; const std::array missing_path { ShaderCatalogEntry { .name = "texture", .path = "" }, }; const std::array wrong_extension { ShaderCatalogEntry { .name = "texture", .path = "data/shaders/texture.txt" }, }; const auto empty = validate_shader_catalog({}); const auto duplicate = validate_shader_catalog(duplicated); const auto no_name = validate_shader_catalog(missing_name); const auto no_path = validate_shader_catalog(missing_path); const auto bad_extension = validate_shader_catalog(wrong_extension); PP_EXPECT(h, !empty.ok()); PP_EXPECT(h, empty.code == StatusCode::invalid_argument); PP_EXPECT(h, !duplicate.ok()); PP_EXPECT(h, duplicate.code == StatusCode::invalid_argument); PP_EXPECT(h, !no_name.ok()); PP_EXPECT(h, no_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !no_path.ok()); PP_EXPECT(h, no_path.code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_extension.ok()); PP_EXPECT(h, bad_extension.code == StatusCode::invalid_argument); } void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h) { FakeRenderDevice device; FakeRenderTarget target; FakeTexture texture; FakeReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; const std::array uniform_bytes {}; FakeShaderProgram shader; FakeMesh mesh; PP_EXPECT(h, device.backend_name() == std::string_view("fake")); const auto features = device.features(); PP_EXPECT(h, features.framebuffer_fetch); PP_EXPECT(h, features.explicit_texture_transitions); PP_EXPECT(h, features.texture_copy); PP_EXPECT(h, features.render_target_blit); PP_EXPECT(h, features.frame_capture); PP_EXPECT(h, features.float16_render_targets); PP_EXPECT(h, features.float32_render_targets); PP_EXPECT(h, device.trace()->begin_scope("renderer", "dispatch").ok()); PP_EXPECT(h, device.trace()->marker("renderer", "begin").ok()); PP_EXPECT(h, device.trace()->end_scope().ok()); PP_EXPECT(h, device.trace_recorder.last_component == std::string_view("renderer")); PP_EXPECT(h, device.trace_recorder.last_name == std::string_view("begin")); PP_EXPECT(h, device.trace_recorder.marker_count == 1U); PP_EXPECT(h, device.trace_recorder.begin_scope_count == 1U); PP_EXPECT(h, device.trace_recorder.end_scope_count == 1U); auto& context = device.immediate_context(); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc { .clear_color = ClearColor { .r = 0.1F, .g = 0.2F, .b = 0.3F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 0.75F, }) .ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 5, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .ok()); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.set_shader_uniform("mvp", uniform_bytes).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(DrawDesc { .vertex_count = 3 }).ok()); context.end_render_pass(); const auto draw_after_end = context.draw(DrawDesc { .vertex_count = 3 }); PP_EXPECT(h, !draw_after_end.ok()); PP_EXPECT(h, draw_after_end.code == StatusCode::invalid_argument); PP_EXPECT(h, context.upload_texture( texture, 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 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 4, .height = 5 }) .ok()); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, readback_buffer) .ok()); PP_EXPECT(h, context.capture_frame(target, readback_buffer).ok()); PP_EXPECT(h, context.blit_render_target( target, ReadbackRegion { .x = 2, .y = 3, .width = 4, .height = 5 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 10 }, BlitFilter::linear) .ok()); PP_EXPECT(h, device.context.shader_name == std::string_view("fake-shader")); PP_EXPECT(h, device.context.last_scissor.enabled); PP_EXPECT(h, device.context.last_scissor.x == 4); PP_EXPECT(h, device.context.last_scissor.height == 8U); PP_EXPECT(h, device.context.last_blend_state.enabled); PP_EXPECT(h, device.context.last_blend_state.source_color == BlendFactor::source_alpha); PP_EXPECT(h, device.context.last_blend_state.destination_color == BlendFactor::one_minus_source_alpha); PP_EXPECT(h, device.context.last_render_pass_desc.clear_color.a == 1.0F); PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth_enabled); PP_EXPECT(h, device.context.last_render_pass_desc.clear_depth == 0.75F); PP_EXPECT(h, device.context.last_depth_state.test_enabled); PP_EXPECT(h, device.context.last_depth_state.write_enabled); PP_EXPECT(h, device.context.last_depth_state.compare == CompareOp::less_or_equal); PP_EXPECT(h, device.context.last_uniform_name == std::string_view("mvp")); PP_EXPECT(h, device.context.last_uniform_bytes == 64U); PP_EXPECT(h, device.context.last_draw_desc.vertex_count == 3U); PP_EXPECT(h, device.context.last_draw_desc.instance_count == 1U); 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_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); PP_EXPECT(h, device.context.last_capture_bytes == 8192U); PP_EXPECT(h, device.context.last_blit_source_bytes == 80U); PP_EXPECT(h, device.context.last_blit_destination_bytes == 320U); PP_EXPECT(h, device.context.last_blit_filter == BlitFilter::linear); } void render_devices_create_validated_resources(pp::tests::Harness& h) { static constexpr char shader_source[] = "void main() {}"; std::array oversized_label {}; oversized_label.fill('s'); oversized_label[max_resource_label_bytes + 1U] = '\0'; RecordingRenderDevice device; const auto texture = device.create_texture(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, .usage = TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "factory-texture", }); const auto target = device.create_render_target(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 = "factory-target", }); const auto shader = device.create_shader_program(ShaderProgramDesc { .debug_name = "factory-shader", .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }); const auto mesh = device.create_mesh(MeshDesc { .vertex_count = 6, .index_count = 6, .topology = PrimitiveTopology::triangles, .debug_name = "factory-mesh", }); const auto readback = device.create_readback_buffer(8U * 4U * 4U); const auto features = device.features(); PP_EXPECT(h, !features.framebuffer_fetch); PP_EXPECT(h, features.explicit_texture_transitions); PP_EXPECT(h, features.texture_copy); PP_EXPECT(h, features.render_target_blit); PP_EXPECT(h, features.frame_capture); PP_EXPECT(h, !features.float16_render_targets); PP_EXPECT(h, !features.float32_render_targets); PP_EXPECT(h, texture.ok()); PP_EXPECT(h, texture.value()->desc().extent.width == 8U); PP_EXPECT(h, !has_texture_usage(texture.value()->desc().usage, TextureUsage::render_target)); PP_EXPECT(h, texture.value()->desc().debug_name == std::string_view("factory-texture")); PP_EXPECT(h, target.ok()); PP_EXPECT(h, has_texture_usage(target.value()->color_desc().usage, TextureUsage::render_target)); PP_EXPECT(h, target.value()->color_desc().debug_name == std::string_view("factory-target")); PP_EXPECT(h, shader.ok()); PP_EXPECT(h, shader.value()->debug_name() == std::string_view("factory-shader")); PP_EXPECT(h, mesh.ok()); PP_EXPECT(h, mesh.value()->desc().index_count == 6U); PP_EXPECT(h, mesh.value()->desc().debug_name == std::string_view("factory-mesh")); PP_EXPECT(h, readback.ok()); PP_EXPECT(h, readback.value()->size_bytes() == 128U); const auto bad_texture = device.create_texture(TextureDesc { .extent = Extent2D { .width = 0, .height = 4 }, .format = TextureFormat::rgba8, }); const auto bad_target = device.create_render_target(TextureDesc { .extent = Extent2D { .width = 8, .height = 4 }, .format = TextureFormat::rgba8, .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", .vertex = ShaderStageSource {}, .fragment = ShaderStageSource {}, }); const auto oversized_shader_label = device.create_shader_program(ShaderProgramDesc { .debug_name = oversized_label.data(), .vertex = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, .fragment = ShaderStageSource { .source = shader_source, .source_size = sizeof(shader_source) - 1U }, }); const auto bad_mesh = device.create_mesh(MeshDesc {}); const auto bad_readback = device.create_readback_buffer(0); PP_EXPECT(h, !bad_texture.ok()); PP_EXPECT(h, bad_texture.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_target.ok()); PP_EXPECT(h, bad_target.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_shader.ok()); PP_EXPECT(h, bad_shader.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !oversized_shader_label.ok()); PP_EXPECT(h, oversized_shader_label.status().code == StatusCode::out_of_range); PP_EXPECT(h, !bad_mesh.ok()); PP_EXPECT(h, bad_mesh.status().code == StatusCode::invalid_argument); PP_EXPECT(h, !bad_readback.ok()); PP_EXPECT(h, bad_readback.status().code == StatusCode::invalid_argument); } void recording_renderer_records_shader_uniform_writes(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .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 {}; auto& context = device.immediate_context(); const auto before_begin = context.set_shader_uniform("mvp", uniform_bytes); PP_EXPECT(h, !before_begin.ok()); PP_EXPECT(h, before_begin.code == StatusCode::invalid_argument); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto before_shader = context.set_shader_uniform("mvp", uniform_bytes); PP_EXPECT(h, !before_shader.ok()); PP_EXPECT(h, before_shader.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_shader(shader).ok()); PP_EXPECT(h, context.set_shader_uniform("mvp", uniform_bytes).ok()); const auto empty_name = context.set_shader_uniform("", uniform_bytes); const auto empty_bytes = context.set_shader_uniform("mvp", std::span {}); context.end_render_pass(); PP_EXPECT(h, !empty_name.ok()); PP_EXPECT(h, empty_name.code == StatusCode::invalid_argument); PP_EXPECT(h, !empty_bytes.ok()); PP_EXPECT(h, empty_bytes.code == StatusCode::invalid_argument); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 4U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::bind_shader); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::set_shader_uniform); PP_EXPECT(h, commands[2].name == std::string_view("mvp")); PP_EXPECT(h, commands[2].uniform_bytes == 64U); PP_EXPECT(h, recorded_render_command_kind_name(commands[2].kind) == std::string_view("set_shader_uniform")); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::end_render_pass); } void recording_renderer_records_mipmap_generation(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 3, .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingTexture2D one_mip_texture(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .mip_levels = 1, .usage = TextureUsage::sampled | TextureUsage::copy_source | TextureUsage::copy_destination, }); RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 4, .height = 4 }, .format = TextureFormat::rgba8, .usage = all_texture_usages, }); auto& context = device.immediate_context(); PP_EXPECT(h, context.generate_mipmaps(texture).ok()); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 1U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::generate_mipmaps); PP_EXPECT(h, commands[0].texture_desc.mip_levels == 3U); PP_EXPECT(h, commands[0].generated_mip_levels == 3U); PP_EXPECT(h, commands[0].generated_mip_bytes == 84U); PP_EXPECT(h, recorded_render_command_kind_name(commands[0].kind) == std::string_view("generate_mipmaps")); const auto one_mip = context.generate_mipmaps(one_mip_texture); PP_EXPECT(h, !one_mip.ok()); PP_EXPECT(h, one_mip.code == StatusCode::invalid_argument); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto during_render_pass = context.generate_mipmaps(texture); 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_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; RecordingTexture2D texture(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "recorded-texture", }); RecordingReadbackBuffer readback_buffer(64U * 32U * 4U); const std::array upload_bytes {}; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "recorded-target", }); RecordingRenderTarget blit_target(TextureDesc { .extent = Extent2D { .width = 64, .height = 32 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::upload_destination | TextureUsage::readback_source | TextureUsage::copy_source | TextureUsage::copy_destination, .debug_name = "recorded-blit-target", }); RecordingShaderProgram shader("recorded-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .index_count = 3, .topology = PrimitiveTopology::triangles, .debug_name = "recorded-mesh", }); PP_EXPECT(h, device.backend_name() == std::string_view("recording")); PP_EXPECT(h, device.trace()->begin_scope("renderer", "recorded-frame").ok()); PP_EXPECT(h, device.trace()->marker("renderer", "frame").ok()); PP_EXPECT(h, device.trace()->end_scope().ok()); auto& context = device.immediate_context(); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc { .clear_color = ClearColor { .r = 0.2F, .g = 0.3F, .b = 0.4F, .a = 1.0F }, .clear_depth_enabled = true, .clear_depth = 1.0F, .clear_stencil_enabled = true, .clear_stencil = 7, }) .ok()); PP_EXPECT(h, context.set_viewport(Viewport { .x = 0, .y = 0, .width = 64, .height = 32 }).ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 4, .y = 6, .width = 16, .height = 8 }).ok()); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, .source_alpha = BlendFactor::one, .destination_alpha = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .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(DrawDesc { .vertex_count = 3, .index_count = 3 }).ok()); context.end_render_pass(); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 14U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_begin_scope); PP_EXPECT(h, commands[0].component == std::string_view("renderer")); PP_EXPECT(h, commands[0].name == std::string_view("recorded-frame")); PP_EXPECT(h, recorded_render_command_kind_name(commands[0].kind) == std::string_view("trace_begin_scope")); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::trace_marker); PP_EXPECT(h, commands[1].component == std::string_view("renderer")); PP_EXPECT(h, commands[1].name == std::string_view("frame")); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::trace_end_scope); PP_EXPECT(h, recorded_render_command_kind_name(commands[2].kind) == std::string_view("trace_end_scope")); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[3].target_desc.extent.width == 64U); PP_EXPECT(h, commands[3].target_desc.debug_name == std::string_view("recorded-target")); PP_EXPECT(h, commands[3].clear_color_enabled); PP_EXPECT(h, commands[3].clear_color.a == 1.0F); PP_EXPECT(h, commands[3].clear_depth_enabled); PP_EXPECT(h, commands[3].clear_depth == 1.0F); PP_EXPECT(h, commands[3].clear_stencil_enabled); PP_EXPECT(h, commands[3].clear_stencil == 7U); PP_EXPECT(h, commands[4].kind == RecordedRenderCommandKind::set_viewport); PP_EXPECT(h, commands[4].viewport.height == 32U); PP_EXPECT(h, commands[5].kind == RecordedRenderCommandKind::set_scissor); PP_EXPECT(h, commands[5].scissor.enabled); PP_EXPECT(h, commands[5].scissor.x == 4); PP_EXPECT(h, commands[5].scissor.height == 8U); PP_EXPECT(h, recorded_render_command_kind_name(commands[5].kind) == std::string_view("set_scissor")); PP_EXPECT(h, commands[6].kind == RecordedRenderCommandKind::set_blend_state); PP_EXPECT(h, commands[6].blend_state.enabled); PP_EXPECT(h, commands[6].blend_state.destination_color == BlendFactor::one_minus_source_alpha); PP_EXPECT(h, recorded_render_command_kind_name(commands[6].kind) == std::string_view("set_blend_state")); PP_EXPECT(h, commands[7].kind == RecordedRenderCommandKind::set_depth_state); PP_EXPECT(h, commands[7].depth_state.test_enabled); PP_EXPECT(h, commands[7].depth_state.write_enabled); PP_EXPECT(h, commands[7].depth_state.compare == CompareOp::less_or_equal); PP_EXPECT(h, recorded_render_command_kind_name(commands[7].kind) == std::string_view("set_depth_state")); PP_EXPECT(h, commands[8].kind == RecordedRenderCommandKind::bind_shader); PP_EXPECT(h, commands[8].name == std::string_view("recorded-shader")); PP_EXPECT(h, commands[9].kind == RecordedRenderCommandKind::bind_texture); PP_EXPECT(h, commands[9].texture_slot == 1U); PP_EXPECT(h, commands[9].texture_desc.extent.height == 32U); PP_EXPECT(h, commands[9].texture_desc.debug_name == std::string_view("recorded-texture")); PP_EXPECT(h, recorded_render_command_kind_name(commands[9].kind) == std::string_view("bind_texture")); PP_EXPECT(h, commands[10].kind == RecordedRenderCommandKind::bind_sampler); PP_EXPECT(h, commands[10].sampler_slot == 1U); PP_EXPECT(h, commands[10].sampler_desc.mag_filter == SamplerFilter::nearest); PP_EXPECT(h, commands[10].sampler_desc.address_w == SamplerAddressMode::clamp_to_border); PP_EXPECT(h, recorded_render_command_kind_name(commands[10].kind) == std::string_view("bind_sampler")); PP_EXPECT(h, commands[11].kind == RecordedRenderCommandKind::bind_mesh); PP_EXPECT(h, commands[11].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[11].mesh_desc.index_count == 3U); PP_EXPECT(h, commands[11].mesh_desc.debug_name == std::string_view("recorded-mesh")); PP_EXPECT(h, commands[12].kind == RecordedRenderCommandKind::draw); PP_EXPECT(h, commands[12].mesh_desc.vertex_count == 3U); PP_EXPECT(h, commands[12].mesh_desc.index_count == 3U); PP_EXPECT(h, commands[12].mesh_desc.topology == PrimitiveTopology::triangles); PP_EXPECT(h, commands[12].draw_desc.vertex_count == 3U); PP_EXPECT(h, commands[12].draw_desc.index_count == 3U); PP_EXPECT(h, commands[12].draw_desc.instance_count == 1U); PP_EXPECT(h, commands[13].kind == RecordedRenderCommandKind::end_render_pass); PP_EXPECT(h, recorded_render_command_kind_name(commands[12].kind) == std::string_view("draw")); PP_EXPECT(h, context.upload_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, upload_bytes) .ok()); const auto commands_after_upload = device.commands(); PP_EXPECT(h, commands_after_upload.size() == 15U); PP_EXPECT(h, commands_after_upload[14].kind == RecordedRenderCommandKind::upload_texture); PP_EXPECT(h, commands_after_upload[14].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_upload[14].readback_region.x == 4U); PP_EXPECT(h, commands_after_upload[14].upload_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_upload[14].kind) == std::string_view("upload_texture")); PP_EXPECT(h, context.copy_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 8, .height = 3 }) .ok()); const auto commands_after_copy = device.commands(); PP_EXPECT(h, commands_after_copy.size() == 16U); PP_EXPECT(h, commands_after_copy[15].kind == RecordedRenderCommandKind::copy_texture); PP_EXPECT(h, commands_after_copy[15].source_region.x == 4U); PP_EXPECT(h, commands_after_copy[15].destination_region.x == 0U); PP_EXPECT(h, commands_after_copy[15].copy_source_bytes == 96U); PP_EXPECT(h, commands_after_copy[15].copy_destination_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_copy[15].kind) == std::string_view("copy_texture")); PP_EXPECT(h, context.read_texture( texture, ReadbackRegion { .x = 4, .y = 5, .width = 8, .height = 3 }, readback_buffer) .ok()); const auto commands_after_readback = device.commands(); PP_EXPECT(h, commands_after_readback.size() == 17U); PP_EXPECT(h, commands_after_readback[16].kind == RecordedRenderCommandKind::read_texture); PP_EXPECT(h, commands_after_readback[16].texture_desc.extent.width == 64U); PP_EXPECT(h, commands_after_readback[16].readback_region.x == 4U); PP_EXPECT(h, commands_after_readback[16].readback_region.height == 3U); PP_EXPECT(h, commands_after_readback[16].readback_bytes == 96U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_readback[16].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() == 18U); PP_EXPECT(h, commands_after_capture[17].kind == RecordedRenderCommandKind::capture_frame); PP_EXPECT(h, commands_after_capture[17].target_desc.extent.width == 64U); PP_EXPECT(h, commands_after_capture[17].target_desc.extent.height == 32U); PP_EXPECT(h, commands_after_capture[17].capture_bytes == 8192U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_capture[17].kind) == std::string_view("capture_frame")); PP_EXPECT(h, context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 16, .height = 8 }, blit_target, ReadbackRegion { .x = 2, .y = 3, .width = 8, .height = 4 }, BlitFilter::linear) .ok()); const auto commands_after_blit = device.commands(); PP_EXPECT(h, commands_after_blit.size() == 19U); PP_EXPECT(h, commands_after_blit[18].kind == RecordedRenderCommandKind::blit_render_target); PP_EXPECT(h, commands_after_blit[18].source_desc.extent.width == 64U); PP_EXPECT(h, commands_after_blit[18].destination_desc.extent.height == 32U); PP_EXPECT(h, commands_after_blit[18].source_region.width == 16U); PP_EXPECT(h, commands_after_blit[18].destination_region.x == 2U); PP_EXPECT(h, commands_after_blit[18].blit_filter == BlitFilter::linear); PP_EXPECT(h, commands_after_blit[18].blit_source_bytes == 512U); PP_EXPECT(h, commands_after_blit[18].blit_destination_bytes == 128U); PP_EXPECT(h, recorded_render_command_kind_name(commands_after_blit[18].kind) == std::string_view("blit_render_target")); device.clear(); PP_EXPECT(h, device.commands().empty()); } void recording_renderer_clear_resets_context_and_trace_state(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 16, .height = 8 }, .format = TextureFormat::rgba8, .usage = TextureUsage::render_target | TextureUsage::sampled | TextureUsage::readback_source, .debug_name = "reset-target", }); auto* trace = device.trace(); PP_EXPECT(h, trace != nullptr); if (trace == nullptr) { return; } PP_EXPECT(h, trace->begin_scope("renderer", "interrupted-frame").ok()); auto& context = device.immediate_context(); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); PP_EXPECT(h, !device.commands().empty()); device.clear(); PP_EXPECT(h, device.commands().empty()); const auto trace_end_after_clear = trace->end_scope(); PP_EXPECT(h, !trace_end_after_clear.ok()); PP_EXPECT(h, trace_end_after_clear.code == StatusCode::invalid_argument); PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, trace->begin_scope("renderer", "next-frame").ok()); PP_EXPECT(h, trace->end_scope().ok()); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); context.end_render_pass(); const auto commands = device.commands(); PP_EXPECT(h, commands.size() == 4U); PP_EXPECT(h, commands[0].kind == RecordedRenderCommandKind::trace_begin_scope); PP_EXPECT(h, commands[1].kind == RecordedRenderCommandKind::trace_end_scope); PP_EXPECT(h, commands[2].kind == RecordedRenderCommandKind::begin_render_pass); PP_EXPECT(h, commands[3].kind == RecordedRenderCommandKind::end_render_pass); } void recording_renderer_rejects_invalid_command_order_and_targets(pp::tests::Harness& h) { RecordingRenderDevice device; RecordingRenderTarget target(TextureDesc { .extent = Extent2D { .width = 32, .height = 16 }, .format = TextureFormat::rgba8, .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, .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, .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, .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); const std::array one_pixel_upload {}; const std::array undersized_upload {}; RecordingShaderProgram shader("strict-shader"); RecordingMesh mesh(MeshDesc { .vertex_count = 3, .topology = PrimitiveTopology::triangles }); RecordingMesh empty_mesh(MeshDesc {}); RecordingRenderDevice trace_device; auto* trace = trace_device.trace(); const auto trace_marker_empty_component = trace->marker("", "frame"); PP_EXPECT(h, !trace_marker_empty_component.ok()); PP_EXPECT(h, trace_marker_empty_component.code == StatusCode::invalid_argument); const auto trace_scope_empty_name = trace->begin_scope("renderer", ""); PP_EXPECT(h, !trace_scope_empty_name.ok()); PP_EXPECT(h, trace_scope_empty_name.code == StatusCode::invalid_argument); const auto trace_end_without_begin = trace->end_scope(); PP_EXPECT(h, !trace_end_without_begin.ok()); PP_EXPECT(h, trace_end_without_begin.code == StatusCode::invalid_argument); PP_EXPECT(h, trace->begin_scope("renderer", "strict").ok()); PP_EXPECT(h, trace->end_scope().ok()); auto& context = device.immediate_context(); const auto draw_before_begin = context.draw(DrawDesc { .vertex_count = 3 }); PP_EXPECT(h, !draw_before_begin.ok()); PP_EXPECT(h, draw_before_begin.code == StatusCode::invalid_argument); const auto blend_before_begin = context.set_blend_state(BlendState {}); PP_EXPECT(h, !blend_before_begin.ok()); PP_EXPECT(h, blend_before_begin.code == StatusCode::invalid_argument); const auto scissor_before_begin = context.set_scissor(ScissorRect {}); PP_EXPECT(h, !scissor_before_begin.ok()); PP_EXPECT(h, scissor_before_begin.code == StatusCode::invalid_argument); const auto depth_before_begin = context.set_depth_state(DepthState {}); 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, RenderPassDesc {}); PP_EXPECT(h, !invalid_target.ok()); PP_EXPECT(h, invalid_target.code == StatusCode::invalid_argument); PP_EXPECT(h, device.commands().empty()); const auto invalid_clear_depth = context.begin_render_pass(target, RenderPassDesc { .clear_color = ClearColor {}, .clear_depth_enabled = true, .clear_depth = -0.25F, }); PP_EXPECT(h, !invalid_clear_depth.ok()); PP_EXPECT(h, invalid_clear_depth.code == StatusCode::out_of_range); PP_EXPECT(h, device.commands().empty()); PP_EXPECT(h, context.begin_render_pass(target, RenderPassDesc {}).ok()); const auto upload_during_render_pass = context.upload_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, one_pixel_upload); PP_EXPECT(h, !upload_during_render_pass.ok()); PP_EXPECT(h, upload_during_render_pass.code == StatusCode::invalid_argument); const auto read_during_render_pass = context.read_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, full_readback_buffer); PP_EXPECT(h, !read_during_render_pass.ok()); PP_EXPECT(h, read_during_render_pass.code == StatusCode::invalid_argument); const auto copy_during_render_pass = context.copy_texture( texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !copy_during_render_pass.ok()); PP_EXPECT(h, copy_during_render_pass.code == StatusCode::invalid_argument); const auto capture_during_render_pass = context.capture_frame(target, full_readback_buffer); PP_EXPECT(h, !capture_during_render_pass.ok()); PP_EXPECT(h, capture_during_render_pass.code == StatusCode::invalid_argument); const auto blit_during_render_pass = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_during_render_pass.ok()); PP_EXPECT(h, blit_during_render_pass.code == StatusCode::invalid_argument); const auto nested_begin = context.begin_render_pass(target, RenderPassDesc {}); PP_EXPECT(h, !nested_begin.ok()); PP_EXPECT(h, nested_begin.code == StatusCode::invalid_argument); auto invalid_blend = BlendState {}; invalid_blend.source_color = static_cast(255); const auto invalid_blend_state = context.set_blend_state(invalid_blend); PP_EXPECT(h, !invalid_blend_state.ok()); PP_EXPECT(h, invalid_blend_state.code == StatusCode::invalid_argument); PP_EXPECT(h, context.set_blend_state(BlendState { .enabled = true, .source_color = BlendFactor::source_alpha, .destination_color = BlendFactor::one_minus_source_alpha, }) .ok()); PP_EXPECT(h, context.set_scissor(ScissorRect { .enabled = true, .x = 0, .y = 0, .width = 16, .height = 8, }) .ok()); const auto invalid_scissor = context.set_scissor(ScissorRect { .enabled = true, .x = 31, .y = 15, .width = 2, .height = 1, }); PP_EXPECT(h, !invalid_scissor.ok()); PP_EXPECT(h, invalid_scissor.code == StatusCode::out_of_range); auto invalid_depth = DepthState {}; invalid_depth.compare = static_cast(255); const auto invalid_depth_state = context.set_depth_state(invalid_depth); PP_EXPECT(h, !invalid_depth_state.ok()); PP_EXPECT(h, invalid_depth_state.code == StatusCode::invalid_argument); PP_EXPECT(h, context.set_depth_state(DepthState { .test_enabled = true, .write_enabled = true, .compare = CompareOp::less_or_equal, }) .ok()); const auto draw_without_bindings = context.draw(DrawDesc { .vertex_count = 3 }); PP_EXPECT(h, !draw_without_bindings.ok()); PP_EXPECT(h, draw_without_bindings.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_shader(shader).ok()); 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 {}); 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(DrawDesc { .vertex_count = 3 }); PP_EXPECT(h, !draw_without_mesh.ok()); PP_EXPECT(h, draw_without_mesh.code == StatusCode::invalid_argument); const auto invalid_mesh = context.bind_mesh(empty_mesh); PP_EXPECT(h, !invalid_mesh.ok()); PP_EXPECT(h, invalid_mesh.code == StatusCode::invalid_argument); PP_EXPECT(h, context.bind_mesh(mesh).ok()); const auto draw_outside_mesh = context.draw(DrawDesc { .first_vertex = 2, .vertex_count = 2 }); PP_EXPECT(h, !draw_outside_mesh.ok()); PP_EXPECT(h, draw_outside_mesh.code == StatusCode::out_of_range); PP_EXPECT(h, context.draw(DrawDesc { .vertex_count = 3 }).ok()); context.end_render_pass(); const auto viewport_after_end = context.set_viewport(Viewport { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !viewport_after_end.ok()); PP_EXPECT(h, viewport_after_end.code == StatusCode::invalid_argument); const auto blend_after_end = context.set_blend_state(BlendState {}); PP_EXPECT(h, !blend_after_end.ok()); PP_EXPECT(h, blend_after_end.code == StatusCode::invalid_argument); const auto scissor_after_end = context.set_scissor(ScissorRect {}); PP_EXPECT(h, !scissor_after_end.ok()); PP_EXPECT(h, scissor_after_end.code == StatusCode::invalid_argument); const auto depth_after_end = context.set_depth_state(DepthState {}); PP_EXPECT(h, !depth_after_end.ok()); PP_EXPECT(h, depth_after_end.code == StatusCode::invalid_argument); const auto bind_texture_after_end = context.bind_texture(0, texture); 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 }, full_readback_buffer); PP_EXPECT(h, !read_outside_bounds.ok()); PP_EXPECT(h, read_outside_bounds.code == StatusCode::out_of_range); const auto upload_outside_bounds = context.upload_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, one_pixel_upload); 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 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }); PP_EXPECT(h, !copy_mismatched_regions.ok()); PP_EXPECT(h, copy_mismatched_regions.code == StatusCode::invalid_argument); const auto copy_outside_bounds = context.copy_texture( texture, ReadbackRegion { .x = 31, .y = 15, .width = 2, .height = 1 }, texture, ReadbackRegion { .x = 0, .y = 0, .width = 2, .height = 1 }); PP_EXPECT(h, !copy_outside_bounds.ok()); PP_EXPECT(h, copy_outside_bounds.code == StatusCode::out_of_range); const auto 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 }, undersized_upload); 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 }, small_readback_buffer); PP_EXPECT(h, !read_into_small_buffer.ok()); PP_EXPECT(h, read_into_small_buffer.code == StatusCode::out_of_range); const auto capture_into_small_buffer = context.capture_frame(target, small_readback_buffer); PP_EXPECT(h, !capture_into_small_buffer.ok()); PP_EXPECT(h, capture_into_small_buffer.code == StatusCode::out_of_range); const auto capture_non_target = context.capture_frame(non_render_target, full_readback_buffer); PP_EXPECT(h, !capture_non_target.ok()); PP_EXPECT(h, capture_non_target.code == StatusCode::invalid_argument); const auto blit_non_target = context.blit_render_target( non_render_target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_non_target.ok()); PP_EXPECT(h, blit_non_target.code == StatusCode::invalid_argument); const auto blit_mismatched_format = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, r8_target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_mismatched_format.ok()); PP_EXPECT(h, blit_mismatched_format.code == StatusCode::invalid_argument); const auto blit_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 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, BlitFilter::nearest); PP_EXPECT(h, !blit_outside_bounds.ok()); PP_EXPECT(h, blit_outside_bounds.code == StatusCode::out_of_range); const auto blit_bad_filter = context.blit_render_target( target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, target, ReadbackRegion { .x = 0, .y = 0, .width = 1, .height = 1 }, static_cast(255)); PP_EXPECT(h, !blit_bad_filter.ok()); PP_EXPECT(h, blit_bad_filter.code == StatusCode::invalid_argument); } } 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("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); harness.run("computes_frame_capture_byte_sizes", computes_frame_capture_byte_sizes); harness.run("validates_blit_contract", validates_blit_contract); harness.run("plans_paint_feedback_paths", plans_paint_feedback_paths); harness.run("validates_texture_copy_contract", validates_texture_copy_contract); harness.run("validates_blend_contract", validates_blend_contract); harness.run("validates_depth_contract", validates_depth_contract); harness.run("validates_sampler_contract", validates_sampler_contract); harness.run("validates_viewports_and_mesh_descriptors", validates_viewports_and_mesh_descriptors); harness.run("validates_draw_descriptors", validates_draw_descriptors); harness.run("validates_render_pass_descriptors", validates_render_pass_descriptors); harness.run("validates_shader_program_descriptors", validates_shader_program_descriptors); harness.run("validates_shader_uniform_writes", validates_shader_uniform_writes); harness.run("validates_trace_labels", validates_trace_labels); harness.run("validates_panopainter_shader_catalog", validates_panopainter_shader_catalog); harness.run("rejects_invalid_shader_catalogs", rejects_invalid_shader_catalogs); harness.run("renderer_interfaces_support_backend_neutral_dispatch", renderer_interfaces_support_backend_neutral_dispatch); 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_clear_resets_context_and_trace_state", recording_renderer_clear_resets_context_and_trace_state); harness.run("recording_renderer_rejects_invalid_command_order_and_targets", recording_renderer_rejects_invalid_command_order_and_targets); return harness.finish(); }