Add renderer resource factory contract

This commit is contained in:
2026-06-02 16:09:52 +02:00
parent 881b5271a2
commit 23c308db1b
9 changed files with 389 additions and 48 deletions

View File

@@ -5,7 +5,10 @@
#include <array>
#include <cstddef>
#include <memory>
#include <new>
#include <string_view>
#include <utility>
using pp::foundation::StatusCode;
using pp::renderer::BlitFilter;
@@ -81,42 +84,78 @@ namespace {
class FakeRenderTarget final : public IRenderTarget {
public:
explicit FakeRenderTarget(TextureDesc desc = TextureDesc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
}) noexcept
: desc_(desc)
{
}
[[nodiscard]] TextureDesc color_desc() const noexcept override
{
return TextureDesc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
};
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 "fake-shader";
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 MeshDesc { .vertex_count = 3, .index_count = 0, .topology = PrimitiveTopology::triangles };
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,
.render_target = true,
}) noexcept
: desc_(desc)
{
}
[[nodiscard]] TextureDesc desc() const noexcept override
{
return TextureDesc {
.extent = Extent2D { .width = 64, .height = 32 },
.format = TextureFormat::rgba8,
.render_target = true,
};
return desc_;
}
private:
TextureDesc desc_ {};
};
class FakeReadbackBuffer final : public pp::renderer::IReadbackBuffer {
@@ -369,6 +408,70 @@ public:
return "fake";
}
[[nodiscard]] pp::foundation::Result<std::unique_ptr<pp::renderer::ITexture2D>> create_texture(
TextureDesc desc) noexcept override
{
const auto bytes = texture_byte_size(desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<pp::renderer::ITexture2D>>::failure(bytes.status());
}
return allocate_resource<FakeTexture, pp::renderer::ITexture2D>(desc);
}
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IRenderTarget>> create_render_target(
TextureDesc color_desc) noexcept override
{
if (!color_desc.render_target) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(
pp::foundation::Status::invalid_argument("render target texture must be flagged as render_target"));
}
const auto bytes = texture_byte_size(color_desc);
if (!bytes.ok()) {
return pp::foundation::Result<std::unique_ptr<IRenderTarget>>::failure(bytes.status());
}
return allocate_resource<FakeRenderTarget, IRenderTarget>(color_desc);
}
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IShaderProgram>> create_shader_program(
ShaderProgramDesc desc) noexcept override
{
const auto status = validate_shader_program_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IShaderProgram>>::failure(status);
}
return allocate_resource<FakeShaderProgram, IShaderProgram>(desc.debug_name);
}
[[nodiscard]] pp::foundation::Result<std::unique_ptr<IMesh>> create_mesh(
MeshDesc desc) noexcept override
{
const auto status = validate_mesh_desc(desc);
if (!status.ok()) {
return pp::foundation::Result<std::unique_ptr<IMesh>>::failure(status);
}
return allocate_resource<FakeMesh, IMesh>(desc);
}
[[nodiscard]] pp::foundation::Result<std::unique_ptr<pp::renderer::IReadbackBuffer>> create_readback_buffer(
std::uint64_t size_bytes) noexcept override
{
if (size_bytes == 0U) {
return pp::foundation::Result<std::unique_ptr<pp::renderer::IReadbackBuffer>>::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<std::unique_ptr<pp::renderer::IReadbackBuffer>>::failure(
pp::foundation::Status::out_of_range("readback buffer size exceeds the configured limit"));
}
return allocate_resource<FakeReadbackBuffer, pp::renderer::IReadbackBuffer>(size_bytes);
}
[[nodiscard]] ICommandContext& immediate_context() noexcept override
{
return context;
@@ -381,6 +484,21 @@ public:
FakeCommandContext context;
FakeTrace trace_recorder;
private:
template <typename Resource, typename Interface, typename... Args>
[[nodiscard]] static pp::foundation::Result<std::unique_ptr<Interface>> allocate_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));
}
};
void computes_texture_sizes(pp::tests::Harness& h)
@@ -857,6 +975,74 @@ void renderer_interfaces_support_backend_neutral_dispatch(pp::tests::Harness& h)
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() {}";
RecordingRenderDevice device;
const auto texture = device.create_texture(TextureDesc {
.extent = Extent2D { .width = 8, .height = 4 },
.format = TextureFormat::rgba8,
.render_target = false,
});
const auto target = device.create_render_target(TextureDesc {
.extent = Extent2D { .width = 8, .height = 4 },
.format = TextureFormat::rgba8,
.render_target = true,
});
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,
});
const auto readback = device.create_readback_buffer(8U * 4U * 4U);
PP_EXPECT(h, texture.ok());
PP_EXPECT(h, texture.value()->desc().extent.width == 8U);
PP_EXPECT(h, !texture.value()->desc().render_target);
PP_EXPECT(h, target.ok());
PP_EXPECT(h, target.value()->color_desc().render_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, 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,
.render_target = false,
});
const auto bad_shader = device.create_shader_program(ShaderProgramDesc {
.debug_name = "bad-shader",
.vertex = ShaderStageSource {},
.fragment = ShaderStageSource {},
});
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, !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_valid_command_sequences(pp::tests::Harness& h)
{
RecordingRenderDevice device;
@@ -1302,6 +1488,7 @@ int main()
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_valid_command_sequences", recording_renderer_records_valid_command_sequences);
harness.run("recording_renderer_rejects_invalid_command_order_and_targets", recording_renderer_rejects_invalid_command_order_and_targets);
return harness.finish();