Files
panopainter/src/renderer_api/recording_renderer.cpp

831 lines
25 KiB
C++

#include "renderer_api/recording_renderer.h"
#include <new>
#include <utility>
namespace pp::renderer {
namespace {
[[nodiscard]] const char* non_null_name(const char* name) noexcept
{
return name == nullptr ? "" : name;
}
void push_command(
std::vector<RecordedRenderCommand>* commands,
RecordedRenderCommand command)
{
if (commands != nullptr) {
commands->push_back(command);
}
}
template <typename Resource, typename Interface, typename... Args>
[[nodiscard]] pp::foundation::Result<std::unique_ptr<Interface>> make_recording_resource(
Args&&... args) noexcept
{
auto resource = std::unique_ptr<Resource>(new (std::nothrow) Resource(std::forward<Args>(args)...));
if (!resource) {
return pp::foundation::Result<std::unique_ptr<Interface>>::failure(
pp::foundation::Status::out_of_range("renderer resource allocation failed"));
}
std::unique_ptr<Interface> erased = std::move(resource);
return pp::foundation::Result<std::unique_ptr<Interface>>::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<RecordedRenderCommand>& 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<const std::byte> 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<std::uint64_t>(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<const std::byte> 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<RecordedRenderCommand>& 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<std::unique_ptr<ITexture2D>> RecordingRenderDevice::create_texture(
TextureDesc desc) noexcept
{
const auto desc_status = validate_texture_desc(desc);
if (!desc_status.ok()) {
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(desc_status);
}
const auto bytes = texture_byte_size(desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<ITexture2D>>::failure(bytes.status());
}
return make_recording_resource<RecordingTexture2D, ITexture2D>(desc);
}
pp::foundation::Result<std::unique_ptr<IRenderTarget>> RecordingRenderDevice::create_render_target(
TextureDesc color_desc) noexcept
{
if (!has_texture_usage(color_desc.usage, TextureUsage::render_target)) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::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<std::unique_ptr<IRenderTarget>>::failure(desc_status);
}
const auto bytes = texture_byte_size(color_desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(bytes.status());
}
return make_recording_resource<RecordingRenderTarget, IRenderTarget>(color_desc);
}
pp::foundation::Result<std::unique_ptr<IShaderProgram>> RecordingRenderDevice::create_shader_program(
ShaderProgramDesc desc) noexcept
{
const auto status = validate_shader_program_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IShaderProgram>>::failure(status);
}
return make_recording_resource<RecordingShaderProgram, IShaderProgram>(desc.debug_name);
}
pp::foundation::Result<std::unique_ptr<IMesh>> RecordingRenderDevice::create_mesh(
MeshDesc desc) noexcept
{
const auto status = validate_mesh_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IMesh>>::failure(status);
}
return make_recording_resource<RecordingMesh, IMesh>(desc);
}
pp::foundation::Result<std::unique_ptr<IReadbackBuffer>> RecordingRenderDevice::create_readback_buffer(
std::uint64_t size_bytes) noexcept
{
if (size_bytes == 0U) {
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
pp::foundation::Status::invalid_argument("readback buffer size must be greater than zero"));
}
if (size_bytes > max_texture_bytes) {
return pp::foundation::Result<std::unique_ptr<IReadbackBuffer>>::failure(
pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit"));
}
return make_recording_resource<RecordingReadbackBuffer, IReadbackBuffer>(size_bytes);
}
ICommandContext& RecordingRenderDevice::immediate_context() noexcept
{
return context_;
}
IRenderTrace* RecordingRenderDevice::trace() noexcept
{
return &trace_;
}
std::span<const RecordedRenderCommand> 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";
}
}