#include "renderer_api/renderer_api.h" #include #include namespace pp::renderer { namespace { [[nodiscard]] bool is_empty_c_string(const char* text) noexcept { return text == nullptr || text[0] == '\0'; } [[nodiscard]] pp::foundation::Status validate_shader_stage_source( ShaderStageSource source, const char* stage_name) noexcept { if (is_empty_c_string(source.entry_point)) { return pp::foundation::Status::invalid_argument(stage_name); } if (source.source == nullptr || source.source_size == 0U) { return pp::foundation::Status::invalid_argument("shader source must not be empty"); } if (source.source_size > max_shader_source_bytes) { return pp::foundation::Status::out_of_range("shader source exceeds the configured limit"); } return pp::foundation::Status::success(); } } std::uint32_t bytes_per_pixel(TextureFormat format) noexcept { switch (format) { case TextureFormat::rgba8: return 4; case TextureFormat::r8: return 1; case TextureFormat::depth24_stencil8: return 4; } return 0; } pp::foundation::Status validate_extent(Extent2D extent) noexcept { if (extent.width == 0 || extent.height == 0) { return pp::foundation::Status::invalid_argument("texture extent must be greater than zero"); } if (extent.width > max_texture_dimension || extent.height > max_texture_dimension) { return pp::foundation::Status::out_of_range("texture extent exceeds the configured limit"); } return pp::foundation::Status::success(); } pp::foundation::Result texture_byte_size(TextureDesc desc) noexcept { const auto extent_status = validate_extent(desc.extent); if (!extent_status.ok()) { return pp::foundation::Result::failure(extent_status); } const auto bpp = static_cast(bytes_per_pixel(desc.format)); if (bpp == 0) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("texture format is not supported")); } const auto width = static_cast(desc.extent.width); const auto height = static_cast(desc.extent.height); if (width > std::numeric_limits::max() / height) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("texture size overflows uint64")); } const auto pixels = width * height; if (pixels > std::numeric_limits::max() / bpp) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("texture byte size overflows uint64")); } const auto bytes = pixels * bpp; if (bytes > max_texture_bytes) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("texture byte size exceeds the configured limit")); } return pp::foundation::Result::success(bytes); } pp::foundation::Status validate_viewport(Viewport viewport, Extent2D target_extent) noexcept { const auto extent_status = validate_extent(target_extent); if (!extent_status.ok()) { return extent_status; } if (viewport.x < 0 || viewport.y < 0) { return pp::foundation::Status::invalid_argument("viewport origin must be non-negative"); } if (viewport.width == 0 || viewport.height == 0) { return pp::foundation::Status::invalid_argument("viewport size must be greater than zero"); } if (!std::isfinite(viewport.min_depth) || !std::isfinite(viewport.max_depth)) { return pp::foundation::Status::invalid_argument("viewport depth range must be finite"); } if (viewport.min_depth < 0.0F || viewport.max_depth > 1.0F || viewport.min_depth > viewport.max_depth) { return pp::foundation::Status::out_of_range("viewport depth range must be within 0..1 and ordered"); } const auto x = static_cast(viewport.x); const auto y = static_cast(viewport.y); if (x > target_extent.width || y > target_extent.height) { return pp::foundation::Status::out_of_range("viewport origin is outside the render target"); } if (viewport.width > target_extent.width - x || viewport.height > target_extent.height - y) { return pp::foundation::Status::out_of_range("viewport exceeds render target bounds"); } return pp::foundation::Status::success(); } pp::foundation::Status validate_scissor(ScissorRect scissor, Extent2D target_extent) noexcept { const auto extent_status = validate_extent(target_extent); if (!extent_status.ok()) { return extent_status; } if (!scissor.enabled) { return pp::foundation::Status::success(); } if (scissor.x < 0 || scissor.y < 0) { return pp::foundation::Status::invalid_argument("scissor origin must be non-negative"); } if (scissor.width == 0 || scissor.height == 0) { return pp::foundation::Status::invalid_argument("scissor size must be greater than zero"); } const auto x = static_cast(scissor.x); const auto y = static_cast(scissor.y); if (x > target_extent.width || y > target_extent.height) { return pp::foundation::Status::out_of_range("scissor origin is outside the render target"); } if (scissor.width > target_extent.width - x || scissor.height > target_extent.height - y) { return pp::foundation::Status::out_of_range("scissor exceeds render target bounds"); } return pp::foundation::Status::success(); } pp::foundation::Status validate_blend_factor(BlendFactor factor) noexcept { switch (factor) { case BlendFactor::zero: case BlendFactor::one: case BlendFactor::source_alpha: case BlendFactor::one_minus_source_alpha: case BlendFactor::destination_alpha: case BlendFactor::one_minus_destination_alpha: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("blend factor is not supported"); } pp::foundation::Status validate_blend_op(BlendOp op) noexcept { switch (op) { case BlendOp::add: case BlendOp::subtract: case BlendOp::reverse_subtract: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("blend operation is not supported"); } pp::foundation::Status validate_blend_state(BlendState state) noexcept { const auto source_color = validate_blend_factor(state.source_color); if (!source_color.ok()) { return source_color; } const auto destination_color = validate_blend_factor(state.destination_color); if (!destination_color.ok()) { return destination_color; } const auto color_op = validate_blend_op(state.color_op); if (!color_op.ok()) { return color_op; } const auto source_alpha = validate_blend_factor(state.source_alpha); if (!source_alpha.ok()) { return source_alpha; } const auto destination_alpha = validate_blend_factor(state.destination_alpha); if (!destination_alpha.ok()) { return destination_alpha; } return validate_blend_op(state.alpha_op); } pp::foundation::Status validate_compare_op(CompareOp op) noexcept { switch (op) { case CompareOp::never: case CompareOp::less: case CompareOp::equal: case CompareOp::less_or_equal: case CompareOp::greater: case CompareOp::not_equal: case CompareOp::greater_or_equal: case CompareOp::always: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("depth compare operation is not supported"); } pp::foundation::Status validate_depth_state(DepthState state) noexcept { return validate_compare_op(state.compare); } pp::foundation::Status validate_sampler_filter(SamplerFilter filter) noexcept { switch (filter) { case SamplerFilter::nearest: case SamplerFilter::linear: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("sampler filter is not supported"); } pp::foundation::Status validate_sampler_address_mode(SamplerAddressMode mode) noexcept { switch (mode) { case SamplerAddressMode::clamp_to_edge: case SamplerAddressMode::repeat: case SamplerAddressMode::mirrored_repeat: case SamplerAddressMode::clamp_to_border: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("sampler address mode is not supported"); } pp::foundation::Status validate_sampler_desc(SamplerDesc desc) noexcept { const auto min_filter = validate_sampler_filter(desc.min_filter); if (!min_filter.ok()) { return min_filter; } const auto mag_filter = validate_sampler_filter(desc.mag_filter); if (!mag_filter.ok()) { return mag_filter; } const auto mip_filter = validate_sampler_filter(desc.mip_filter); if (!mip_filter.ok()) { return mip_filter; } const auto address_u = validate_sampler_address_mode(desc.address_u); if (!address_u.ok()) { return address_u; } const auto address_v = validate_sampler_address_mode(desc.address_v); if (!address_v.ok()) { return address_v; } return validate_sampler_address_mode(desc.address_w); } pp::foundation::Status validate_mesh_desc(MeshDesc desc) noexcept { if (desc.vertex_count == 0) { return pp::foundation::Status::invalid_argument("mesh must contain at least one vertex"); } if (desc.vertex_count > max_mesh_vertices || desc.index_count > max_mesh_vertices) { return pp::foundation::Status::out_of_range("mesh vertex or index count exceeds the configured limit"); } switch (desc.topology) { case PrimitiveTopology::triangles: case PrimitiveTopology::triangle_strip: case PrimitiveTopology::lines: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("mesh topology is not supported"); } pp::foundation::Status validate_texture_slot(std::uint32_t slot) noexcept { if (slot >= max_texture_slots) { return pp::foundation::Status::out_of_range("texture slot exceeds the configured limit"); } return pp::foundation::Status::success(); } pp::foundation::Status validate_shader_program_desc(ShaderProgramDesc desc) noexcept { if (desc.debug_name == nullptr) { return pp::foundation::Status::invalid_argument("shader debug name must not be null"); } const auto vertex_status = validate_shader_stage_source( desc.vertex, "vertex shader entry point must not be empty"); if (!vertex_status.ok()) { return vertex_status; } const auto fragment_status = validate_shader_stage_source( desc.fragment, "fragment shader entry point must not be empty"); if (!fragment_status.ok()) { return fragment_status; } return pp::foundation::Status::success(); } pp::foundation::Status validate_readback_region(TextureDesc desc, ReadbackRegion region) noexcept { const auto extent_status = validate_extent(desc.extent); if (!extent_status.ok()) { return extent_status; } if (region.width == 0 || region.height == 0) { return pp::foundation::Status::invalid_argument("readback region must be greater than zero"); } if (region.x > desc.extent.width || region.y > desc.extent.height) { return pp::foundation::Status::out_of_range("readback origin is outside the texture"); } if (region.width > desc.extent.width - region.x || region.height > desc.extent.height - region.y) { return pp::foundation::Status::out_of_range("readback region exceeds texture bounds"); } return pp::foundation::Status::success(); } pp::foundation::Result readback_byte_size(TextureDesc desc, ReadbackRegion region) noexcept { const auto region_status = validate_readback_region(desc, region); if (!region_status.ok()) { return pp::foundation::Result::failure(region_status); } const auto bpp = static_cast(bytes_per_pixel(desc.format)); if (bpp == 0) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("texture format is not supported")); } const auto width = static_cast(region.width); const auto height = static_cast(region.height); if (width > std::numeric_limits::max() / height) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("readback pixel count overflows uint64")); } const auto pixels = width * height; if (pixels > std::numeric_limits::max() / bpp) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("readback byte size overflows uint64")); } const auto bytes = pixels * bpp; if (bytes > max_texture_bytes) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("readback byte size exceeds the configured limit")); } return pp::foundation::Result::success(bytes); } pp::foundation::Result frame_capture_byte_size(TextureDesc desc) noexcept { if (!desc.render_target) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("frame capture source must be a render target")); } return texture_byte_size(desc); } pp::foundation::Status validate_blit_filter(BlitFilter filter) noexcept { switch (filter) { case BlitFilter::nearest: case BlitFilter::linear: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("blit filter is not supported"); } pp::foundation::Status validate_blit_descs(TextureDesc source, TextureDesc destination) noexcept { if (!source.render_target || !destination.render_target) { return pp::foundation::Status::invalid_argument("blit endpoints must be render targets"); } if (source.format != destination.format) { return pp::foundation::Status::invalid_argument("blit endpoints must use matching texture formats"); } const auto source_status = texture_byte_size(source); if (!source_status.ok()) { return source_status.status(); } const auto destination_status = texture_byte_size(destination); if (!destination_status.ok()) { return destination_status.status(); } return pp::foundation::Status::success(); } const char* texture_format_name(TextureFormat format) noexcept { switch (format) { case TextureFormat::rgba8: return "rgba8"; case TextureFormat::r8: return "r8"; case TextureFormat::depth24_stencil8: return "depth24_stencil8"; } return "unknown"; } const char* primitive_topology_name(PrimitiveTopology topology) noexcept { switch (topology) { case PrimitiveTopology::triangles: return "triangles"; case PrimitiveTopology::triangle_strip: return "triangle_strip"; case PrimitiveTopology::lines: return "lines"; } return "unknown"; } const char* blit_filter_name(BlitFilter filter) noexcept { switch (filter) { case BlitFilter::nearest: return "nearest"; case BlitFilter::linear: return "linear"; } return "unknown"; } const char* blend_factor_name(BlendFactor factor) noexcept { switch (factor) { case BlendFactor::zero: return "zero"; case BlendFactor::one: return "one"; case BlendFactor::source_alpha: return "source_alpha"; case BlendFactor::one_minus_source_alpha: return "one_minus_source_alpha"; case BlendFactor::destination_alpha: return "destination_alpha"; case BlendFactor::one_minus_destination_alpha: return "one_minus_destination_alpha"; } return "unknown"; } const char* blend_op_name(BlendOp op) noexcept { switch (op) { case BlendOp::add: return "add"; case BlendOp::subtract: return "subtract"; case BlendOp::reverse_subtract: return "reverse_subtract"; } return "unknown"; } const char* compare_op_name(CompareOp op) noexcept { switch (op) { case CompareOp::never: return "never"; case CompareOp::less: return "less"; case CompareOp::equal: return "equal"; case CompareOp::less_or_equal: return "less_or_equal"; case CompareOp::greater: return "greater"; case CompareOp::not_equal: return "not_equal"; case CompareOp::greater_or_equal: return "greater_or_equal"; case CompareOp::always: return "always"; } return "unknown"; } const char* sampler_filter_name(SamplerFilter filter) noexcept { switch (filter) { case SamplerFilter::nearest: return "nearest"; case SamplerFilter::linear: return "linear"; } return "unknown"; } const char* sampler_address_mode_name(SamplerAddressMode mode) noexcept { switch (mode) { case SamplerAddressMode::clamp_to_edge: return "clamp_to_edge"; case SamplerAddressMode::repeat: return "repeat"; case SamplerAddressMode::mirrored_repeat: return "mirrored_repeat"; case SamplerAddressMode::clamp_to_border: return "clamp_to_border"; } return "unknown"; } }