#include "renderer_api/recording_renderer.h" #include #include namespace pp::renderer { namespace { [[nodiscard]] const char* non_null_name(const char* name) noexcept { return name == nullptr ? "" : name; } void push_command( std::vector* commands, RecordedRenderCommand command) { if (commands != nullptr) { commands->push_back(command); } } template [[nodiscard]] pp::foundation::Result> make_recording_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)); } } RecordingTexture2D::RecordingTexture2D(TextureDesc desc) noexcept : desc_(desc) { } TextureDesc RecordingTexture2D::desc() const noexcept { return desc_; } RecordingRenderTarget::RecordingRenderTarget(TextureDesc color_desc) noexcept : color_desc_(color_desc) { } TextureDesc RecordingRenderTarget::color_desc() const noexcept { return color_desc_; } RecordingShaderProgram::RecordingShaderProgram(const char* debug_name) noexcept : debug_name_(non_null_name(debug_name)) { } const char* RecordingShaderProgram::debug_name() const noexcept { return debug_name_; } RecordingMesh::RecordingMesh(MeshDesc desc) noexcept : desc_(desc) { } MeshDesc RecordingMesh::desc() const noexcept { return desc_; } RecordingReadbackBuffer::RecordingReadbackBuffer(std::uint64_t size_bytes) noexcept : size_bytes_(size_bytes) { } std::uint64_t RecordingReadbackBuffer::size_bytes() const noexcept { return size_bytes_; } RecordingCommandContext::RecordingCommandContext(std::vector& commands) noexcept : commands_(&commands) { } pp::foundation::Status RecordingCommandContext::begin_render_pass( IRenderTarget& target, RenderPassDesc desc) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass is already active"); } active_target_ = target.color_desc(); if (!has_texture_usage(active_target_.usage, TextureUsage::render_target)) { return pp::foundation::Status::invalid_argument("render target texture must allow render_target usage"); } const auto size_status = texture_byte_size(active_target_); if (!size_status.ok()) { return size_status.status(); } const auto render_pass_status = validate_render_pass_desc(desc); if (!render_pass_status.ok()) { return render_pass_status; } in_render_pass_ = true; shader_bound_ = false; mesh_bound_ = false; active_mesh_ = MeshDesc {}; push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::begin_render_pass, .target_desc = active_target_, .clear_color_enabled = desc.clear_color_enabled, .clear_color = desc.clear_color, .clear_depth_enabled = desc.clear_depth_enabled, .clear_depth = desc.clear_depth, .clear_stencil_enabled = desc.clear_stencil_enabled, .clear_stencil = desc.clear_stencil, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::set_viewport(Viewport viewport) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_viewport(viewport, active_target_.extent); if (!status.ok()) { return status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::set_viewport, .viewport = viewport, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::set_scissor(ScissorRect scissor) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto status = validate_scissor(scissor, active_target_.extent); if (!status.ok()) { return status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::set_scissor, .scissor = scissor, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::set_blend_state(BlendState state) noexcept { 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; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::set_blend_state, .blend_state = state, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::set_depth_state(DepthState state) noexcept { 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; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::set_depth_state, .depth_state = state, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::bind_shader(IShaderProgram& shader) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } shader_bound_ = true; push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::bind_shader, .name = shader.debug_name(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::set_shader_uniform( const char* name, std::span bytes) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } if (!shader_bound_) { return pp::foundation::Status::invalid_argument("shader must be bound before setting uniforms"); } const auto status = validate_shader_uniform_write(name, bytes); if (!status.ok()) { return status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::set_shader_uniform, .uniform_bytes = static_cast(bytes.size()), .name = name, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::bind_texture( std::uint32_t slot, ITexture2D& texture) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto slot_status = validate_texture_slot(slot); if (!slot_status.ok()) { return slot_status; } const auto desc = texture.desc(); if (!has_texture_usage(desc.usage, TextureUsage::sampled)) { return pp::foundation::Status::invalid_argument("bound texture must allow sampled usage"); } const auto size_status = texture_byte_size(desc); if (!size_status.ok()) { return size_status.status(); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::bind_texture, .texture_desc = desc, .texture_slot = slot, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::bind_sampler( std::uint32_t slot, SamplerDesc sampler) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto slot_status = validate_texture_slot(slot); if (!slot_status.ok()) { return slot_status; } const auto sampler_status = validate_sampler_desc(sampler); if (!sampler_status.ok()) { return sampler_status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::bind_sampler, .sampler_desc = sampler, .sampler_slot = slot, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::bind_mesh(IMesh& mesh) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } const auto desc = mesh.desc(); const auto status = validate_mesh_desc(desc); if (!status.ok()) { return status; } mesh_bound_ = true; active_mesh_ = desc; push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::bind_mesh, .mesh_desc = desc, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::draw(DrawDesc desc) noexcept { if (!in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass has not begun"); } if (!shader_bound_) { return pp::foundation::Status::invalid_argument("shader must be bound before draw"); } if (!mesh_bound_) { return pp::foundation::Status::invalid_argument("mesh must be bound before draw"); } const auto draw_status = validate_draw_desc(active_mesh_, desc); if (!draw_status.ok()) { return draw_status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::draw, .mesh_desc = active_mesh_, .draw_desc = desc, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::read_texture( ITexture2D& texture, ReadbackRegion region, IReadbackBuffer& destination) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("readback must be outside a render pass"); } const auto desc = texture.desc(); if (!has_texture_usage(desc.usage, TextureUsage::readback_source)) { return pp::foundation::Status::invalid_argument("readback texture must allow readback_source usage"); } const auto bytes = readback_byte_size(desc, region); if (!bytes) { return bytes.status(); } if (destination.size_bytes() < bytes.value()) { return pp::foundation::Status::out_of_range("readback buffer is too small"); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::read_texture, .texture_desc = desc, .readback_region = region, .readback_bytes = bytes.value(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::upload_texture( ITexture2D& texture, ReadbackRegion region, std::span rgba_or_channel_bytes) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("texture upload must be outside a render pass"); } const auto desc = texture.desc(); if (!has_texture_usage(desc.usage, TextureUsage::upload_destination)) { return pp::foundation::Status::invalid_argument("texture upload destination must allow upload_destination usage"); } const auto bytes = readback_byte_size(desc, region); if (!bytes) { return bytes.status(); } if (rgba_or_channel_bytes.size() != bytes.value()) { return pp::foundation::Status::invalid_argument("texture upload byte size does not match the region"); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::upload_texture, .texture_desc = desc, .readback_region = region, .upload_bytes = bytes.value(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::generate_mipmaps(ITexture2D& texture) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("mipmap generation must be outside a render pass"); } const auto desc = texture.desc(); const auto desc_status = validate_mipmap_generation_desc(desc); if (!desc_status.ok()) { return desc_status; } const auto bytes = texture_byte_size(desc); if (!bytes.ok()) { return bytes.status(); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::generate_mipmaps, .texture_desc = desc, .generated_mip_levels = desc.mip_levels, .generated_mip_bytes = bytes.value(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::transition_texture( ITexture2D& texture, TextureState before, TextureState after) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("texture transition must be outside a render pass"); } const auto desc = texture.desc(); const auto desc_status = validate_texture_transition_desc(desc, before, after); if (!desc_status.ok()) { return desc_status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::transition_texture, .texture_desc = desc, .before_state = before, .after_state = after, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::copy_texture( ITexture2D& source, ReadbackRegion source_region, ITexture2D& destination, ReadbackRegion destination_region) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("texture copy must be outside a render pass"); } const auto source_desc = source.desc(); const auto destination_desc = destination.desc(); const auto desc_status = validate_texture_copy_descs( source_desc, source_region, destination_desc, destination_region); if (!desc_status.ok()) { return desc_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(); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::copy_texture, .source_desc = source_desc, .destination_desc = destination_desc, .source_region = source_region, .destination_region = destination_region, .copy_source_bytes = source_bytes.value(), .copy_destination_bytes = destination_bytes.value(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::capture_frame( IRenderTarget& target, IReadbackBuffer& destination) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("frame capture must be outside a render pass"); } const auto desc = target.color_desc(); const auto bytes = frame_capture_byte_size(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"); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::capture_frame, .target_desc = desc, .capture_bytes = bytes.value(), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::blit_render_target( IRenderTarget& source, ReadbackRegion source_region, IRenderTarget& destination, ReadbackRegion destination_region, BlitFilter filter) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("render target blit must be outside a render pass"); } 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(); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::blit_render_target, .source_desc = source_desc, .destination_desc = destination_desc, .source_region = source_region, .destination_region = destination_region, .blit_filter = filter, .blit_source_bytes = source_bytes.value(), .blit_destination_bytes = destination_bytes.value(), }); return pp::foundation::Status::success(); } void RecordingCommandContext::end_render_pass() noexcept { if (!in_render_pass_) { return; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::end_render_pass, }); in_render_pass_ = false; shader_bound_ = false; mesh_bound_ = false; active_mesh_ = MeshDesc {}; } bool RecordingCommandContext::in_render_pass() const noexcept { return in_render_pass_; } void RecordingCommandContext::reset() noexcept { active_target_ = TextureDesc {}; active_mesh_ = MeshDesc {}; in_render_pass_ = false; shader_bound_ = false; mesh_bound_ = false; } RecordingRenderTrace::RecordingRenderTrace(std::vector& commands) noexcept : commands_(&commands) { } pp::foundation::Status RecordingRenderTrace::marker(const char* component, const char* name) noexcept { const auto status = validate_trace_label(component, name); if (!status.ok()) { return status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::trace_marker, .component = non_null_name(component), .name = non_null_name(name), }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingRenderTrace::begin_scope(const char* component, const char* name) noexcept { const auto status = validate_trace_label(component, name); if (!status.ok()) { return status; } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::trace_begin_scope, .component = non_null_name(component), .name = non_null_name(name), }); ++scope_depth_; return pp::foundation::Status::success(); } pp::foundation::Status RecordingRenderTrace::end_scope() noexcept { if (scope_depth_ == 0U) { return pp::foundation::Status::invalid_argument("trace scope has not begun"); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::trace_end_scope, }); --scope_depth_; return pp::foundation::Status::success(); } void RecordingRenderTrace::reset() noexcept { scope_depth_ = 0U; } RecordingRenderDevice::RecordingRenderDevice() noexcept : context_(commands_) , trace_(commands_) { } const char* RecordingRenderDevice::backend_name() const noexcept { return "recording"; } RenderDeviceFeatures RecordingRenderDevice::features() const noexcept { return RenderDeviceFeatures { .explicit_texture_transitions = true, .texture_copy = true, .render_target_blit = true, .frame_capture = true, }; } pp::foundation::Result> RecordingRenderDevice::create_texture( TextureDesc desc) noexcept { const auto desc_status = validate_texture_desc(desc); if (!desc_status.ok()) { return pp::foundation::Result>::failure(desc_status); } const auto bytes = texture_byte_size(desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return make_recording_resource(desc); } pp::foundation::Result> RecordingRenderDevice::create_render_target( TextureDesc color_desc) noexcept { 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 desc_status = validate_texture_desc(color_desc); if (!desc_status.ok()) { return pp::foundation::Result>::failure(desc_status); } const auto bytes = texture_byte_size(color_desc); if (!bytes.ok()) { return pp::foundation::Result>::failure(bytes.status()); } return make_recording_resource(color_desc); } pp::foundation::Result> RecordingRenderDevice::create_shader_program( ShaderProgramDesc desc) noexcept { const auto status = validate_shader_program_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return make_recording_resource(desc.debug_name); } pp::foundation::Result> RecordingRenderDevice::create_mesh( MeshDesc desc) noexcept { const auto status = validate_mesh_desc(desc); if (!status.ok()) { return pp::foundation::Result>::failure(status); } return make_recording_resource(desc); } pp::foundation::Result> RecordingRenderDevice::create_readback_buffer( std::uint64_t size_bytes) noexcept { 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 > max_texture_bytes) { return pp::foundation::Result>::failure( pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit")); } return make_recording_resource(size_bytes); } ICommandContext& RecordingRenderDevice::immediate_context() noexcept { return context_; } IRenderTrace* RecordingRenderDevice::trace() noexcept { return &trace_; } std::span RecordingRenderDevice::commands() const noexcept { return commands_; } void RecordingRenderDevice::clear() noexcept { commands_.clear(); context_.reset(); trace_.reset(); } const char* recorded_render_command_kind_name(RecordedRenderCommandKind kind) noexcept { switch (kind) { case RecordedRenderCommandKind::begin_render_pass: return "begin_render_pass"; case RecordedRenderCommandKind::set_viewport: return "set_viewport"; case RecordedRenderCommandKind::set_scissor: return "set_scissor"; case RecordedRenderCommandKind::set_blend_state: return "set_blend_state"; case RecordedRenderCommandKind::set_depth_state: return "set_depth_state"; case RecordedRenderCommandKind::bind_shader: return "bind_shader"; case RecordedRenderCommandKind::set_shader_uniform: return "set_shader_uniform"; case RecordedRenderCommandKind::bind_texture: return "bind_texture"; case RecordedRenderCommandKind::bind_sampler: return "bind_sampler"; case RecordedRenderCommandKind::bind_mesh: return "bind_mesh"; case RecordedRenderCommandKind::draw: return "draw"; case RecordedRenderCommandKind::upload_texture: return "upload_texture"; case RecordedRenderCommandKind::generate_mipmaps: return "generate_mipmaps"; case RecordedRenderCommandKind::transition_texture: return "transition_texture"; case RecordedRenderCommandKind::copy_texture: return "copy_texture"; case RecordedRenderCommandKind::read_texture: return "read_texture"; case RecordedRenderCommandKind::capture_frame: return "capture_frame"; case RecordedRenderCommandKind::blit_render_target: return "blit_render_target"; case RecordedRenderCommandKind::end_render_pass: return "end_render_pass"; case RecordedRenderCommandKind::trace_marker: return "trace_marker"; case RecordedRenderCommandKind::trace_begin_scope: return "trace_begin_scope"; case RecordedRenderCommandKind::trace_end_scope: return "trace_end_scope"; } return "unknown"; } }