#include "renderer_api/recording_renderer.h" 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); } } } 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, ClearColor clear_color) noexcept { if (in_render_pass_) { return pp::foundation::Status::invalid_argument("render pass is already active"); } active_target_ = target.color_desc(); if (!active_target_.render_target) { return pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target"); } const auto size_status = texture_byte_size(active_target_); if (!size_status.ok()) { return size_status.status(); } in_render_pass_ = true; shader_bound_ = false; mesh_bound_ = false; push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::begin_render_pass, .target_desc = active_target_, .clear_color = clear_color, }); 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::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(); 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; push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::bind_mesh, .mesh_desc = desc, }); return pp::foundation::Status::success(); } pp::foundation::Status RecordingCommandContext::draw() 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"); } push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::draw, }); 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(); 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(); 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::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; } bool RecordingCommandContext::in_render_pass() const noexcept { return in_render_pass_; } RecordingRenderTrace::RecordingRenderTrace(std::vector& commands) noexcept : commands_(&commands) { } void RecordingRenderTrace::marker(const char* component, const char* name) noexcept { push_command(commands_, RecordedRenderCommand { .kind = RecordedRenderCommandKind::trace_marker, .component = non_null_name(component), .name = non_null_name(name), }); } RecordingRenderDevice::RecordingRenderDevice() noexcept : context_(commands_) , trace_(commands_) { } const char* RecordingRenderDevice::backend_name() const noexcept { return "recording"; } 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(); } 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::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::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"; } return "unknown"; } }