test(renderer): overhaul tests & fix many bugs
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
light7734 2025-10-07 16:09:50 +03:30
parent e7c61b2faf
commit 61473c2758
Signed by: light7734
GPG key ID: 8C30176798F1A6BA
50 changed files with 1030 additions and 832 deletions

View file

@ -3,7 +3,6 @@ add_library_module(renderer
# Vulkan - backend
backend/vk/messenger.cpp
backend/vk/context/context.cpp
backend/vk/context/device.cpp
backend/vk/context/gpu.cpp
backend/vk/context/instance.cpp
@ -11,11 +10,15 @@ add_library_module(renderer
backend/vk/context/swapchain.cpp
backend/vk/renderer/pass.cpp
backend/vk/renderer/renderer.cpp
# Vulkan - frontend
frontend/messenger.cpp
frontend/context/context.cpp
frontend/context/device.cpp
frontend/context/gpu.cpp
frontend/context/instance.cpp
frontend/context/surface.cpp
frontend/context/swapchain.cpp
frontend/renderer/renderer.cpp
frontend/renderer/pass.cpp
)
target_link_libraries(renderer
@ -31,21 +34,29 @@ PRIVATE
pthread
)
# add_test_module(renderer
# system.test.cpp
# vk/test_utils.cpp
# vk/debug/messenger.test.cpp
# vk/context/instance.test.cpp
# vk/context/surface.test.cpp
# vk/context/device.test.cpp
# vk/context/swapchain.test.cpp
# vk/context/context.test.cpp
# vk/renderer/pass.test.cpp
# vk/renderer/renderer.test.cpp
# vk/pipeline.test.cpp
# )
# target_link_libraries(renderer_tests
# PRIVATE
# surface
# pthread
# )
add_test_module(renderer
test/utils.cpp
system.test.cpp
# general backend tests through the frontend
frontend/messenger.test.cpp
frontend/context/surface.test.cpp
frontend/context/device.test.cpp
frontend/context/swapchain.test.cpp
frontend/renderer/pass.test.cpp
frontend/renderer/renderer.test.cpp
# backend specific tests -- vk
backend/vk/context/instance.test.cpp
# backend specific tests -- dx
# backend specific tests -- mt
)
target_link_libraries(renderer_tests
PRIVATE
surface
pthread
)

View file

@ -1,41 +0,0 @@
#include <memory/scope.hpp>
#include <renderer/backend/vk/context/context.hpp>
#include <renderer/backend/vk/context/device.hpp>
#include <renderer/backend/vk/context/gpu.hpp>
#include <renderer/backend/vk/context/instance.hpp>
#include <renderer/backend/vk/context/surface.hpp>
#include <renderer/backend/vk/context/swapchain.hpp>
namespace lt::renderer::vk {
Context::Context(const ecs::Entity &surface_entity)
: m_instance(Instance::get())
, m_surface(memory::create_scope<Surface>(m_instance, surface_entity))
, m_gpu(memory::create_scope<Gpu>(m_instance))
, m_device(memory::create_scope<Device>(m_gpu.get(), m_surface.get()))
, m_swapchain(memory::create_scope<Swapchain>(m_surface.get(), m_gpu.get(), m_device.get()))
{
ensure(
static_cast<Instance *>(m_instance)->vk(),
"Failed to create vulkan context: null instance"
);
ensure(
static_cast<Surface *>(m_surface.get())->vk(),
"Failed to create vulkan context: null surface"
);
ensure(static_cast<Gpu *>(m_gpu.get())->vk(), "Failed to create vulkan context: null gpu");
ensure(
static_cast<Device *>(m_device.get())->vk(),
"Failed to create vulkan context: null device"
);
ensure(
static_cast<Swapchain *>(m_swapchain.get())->vk(),
"Failed to create vulkan context: null swapchain"
);
}
} // namespace lt::renderer::vk

View file

@ -1,66 +0,0 @@
#pragma once
#include <ecs/entity.hpp>
#include <memory/pointer_types/null_on_move.hpp>
#include <memory/scope.hpp>
#include <renderer/backend/vk/context/swapchain.hpp>
#include <renderer/frontend/context/context.hpp>
namespace lt::renderer::vk {
using memory::NullOnMove;
/** A layer of glue between main graphics objects. */
class Context: public IContext
{
public:
Context(const ecs::Entity &surface_entity);
[[nodiscard]] auto instance() const -> IInstance * override
{
return m_instance;
}
[[nodiscard]] auto gpu() const -> IGpu * override
{
return m_gpu.get();
}
[[nodiscard]] auto device() const -> IDevice * override
{
return m_device.get();
}
[[nodiscard]] auto swapchain() const -> ISwapchain * override
{
return m_swapchain.get();
}
[[nodiscard]] auto surface() const -> ISurface * override
{
return m_surface.get();
}
void recreate_swapchain() override
{
m_swapchain.reset();
m_swapchain = memory::create_scope<vk::Swapchain>(
m_surface.get(),
m_gpu.get(),
m_device.get()
);
}
private:
IInstance *m_instance;
memory::Scope<ISurface> m_surface;
memory::Scope<IGpu> m_gpu;
memory::Scope<IDevice> m_device;
memory::Scope<ISwapchain> m_swapchain;
};
} // namespace lt::renderer::vk

View file

@ -91,7 +91,7 @@ public:
[[nodiscard]] auto acquire_image(
VkSwapchainKHR swapchain,
VkSemaphore semaphore,
uint64_t timeout = 1'000'000
uint64_t timeout = 100'000'000
) -> std::optional<uint32_t>;
[[nodiscard]] auto get_swapchain_images(VkSwapchainKHR swapchain) const -> std::vector<VkImage>;

View file

@ -0,0 +1,90 @@
#include <renderer/backend/vk/context/instance.hpp>
#include <renderer/backend/vk/vulkan.hpp>
#include <test/test.hpp>
using namespace lt;
using renderer::vk::Instance;
using test::Case;
using test::expect_not_nullptr;
using test::Suite;
// NOLINTNEXTLINE
Suite raii = "raii"_suite = [] {
Case { "post singleton insantiation state is correct" } = [] {
expect_not_nullptr(Instance::get());
using namespace renderer::vk;
expect_not_nullptr(vk_get_physical_device_properties);
expect_not_nullptr(vk_get_physical_device_queue_family_properties);
expect_not_nullptr(vk_create_device);
expect_not_nullptr(vk_get_device_proc_address);
expect_not_nullptr(vk_destroy_device);
expect_not_nullptr(vk_get_physical_device_features);
expect_not_nullptr(vk_enumerate_device_extension_properties);
expect_not_nullptr(vk_cmd_begin_debug_label);
expect_not_nullptr(vk_cmd_end_debug_label);
expect_not_nullptr(vk_cmd_insert_debug_label);
expect_not_nullptr(vk_create_debug_messenger);
expect_not_nullptr(vk_destroy_debug_messenger);
expect_not_nullptr(vk_queue_begin_debug_label);
expect_not_nullptr(vk_queue_end_debug_label);
expect_not_nullptr(vk_queue_insert_debug_label);
expect_not_nullptr(vk_set_debug_object_name);
expect_not_nullptr(vk_set_debug_object_tag);
expect_not_nullptr(vk_submit_debug_message);
expect_not_nullptr(vk_get_physical_device_surface_support);
expect_not_nullptr(vk_get_physical_device_surface_capabilities);
expect_not_nullptr(vk_get_physical_device_surface_formats);
// TODO(Light): add test for platform-dependant functions
// expect_not_nullptr(vk_create_xlib_surface_khr);
// expect_not_nullptr(vk_destroy_surface_khr);
};
Case { "post load device functions state is correct" } = [] {
using namespace renderer::vk;
expect_not_nullptr(Instance::get());
expect_not_nullptr(vk_get_device_queue);
expect_not_nullptr(vk_create_command_pool);
expect_not_nullptr(vk_destroy_command_pool);
expect_not_nullptr(vk_allocate_command_buffers);
expect_not_nullptr(vk_free_command_buffers);
expect_not_nullptr(vk_begin_command_buffer);
expect_not_nullptr(vk_end_command_buffer);
expect_not_nullptr(vk_cmd_pipeline_barrier);
expect_not_nullptr(vk_queue_submit);
expect_not_nullptr(vk_queue_wait_idle);
expect_not_nullptr(vk_device_wait_idle);
expect_not_nullptr(vk_create_fence);
expect_not_nullptr(vk_destroy_fence);
expect_not_nullptr(vk_wait_for_fences);
expect_not_nullptr(vk_reset_fences);
expect_not_nullptr(vk_create_semaphore);
expect_not_nullptr(vk_destroy_semaphore);
expect_not_nullptr(vk_create_swapchain_khr);
expect_not_nullptr(vk_destroy_swapchain_khr);
expect_not_nullptr(vk_get_swapchain_images_khr);
expect_not_nullptr(vk_acquire_next_image_khr);
expect_not_nullptr(vk_queue_present_khr);
expect_not_nullptr(vk_create_image_view);
expect_not_nullptr(vk_destroy_image_view);
expect_not_nullptr(vk_create_render_pass);
expect_not_nullptr(vk_destroy_render_pass);
expect_not_nullptr(vk_create_frame_buffer);
expect_not_nullptr(vk_destroy_frame_buffer);
expect_not_nullptr(vk_create_shader_module);
expect_not_nullptr(vk_destroy_shader_module);
expect_not_nullptr(vk_create_pipeline_layout);
expect_not_nullptr(vk_destroy_pipeline_layout);
expect_not_nullptr(vk_create_graphics_pipelines);
expect_not_nullptr(vk_destroy_pipeline);
expect_not_nullptr(vk_cmd_begin_render_pass);
expect_not_nullptr(vk_cmd_end_render_pass);
expect_not_nullptr(vk_cmd_bind_pipeline);
expect_not_nullptr(vk_cmd_draw);
expect_not_nullptr(vk_cmd_set_viewport);
expect_not_nullptr(vk_cmd_set_scissors);
};
};

View file

@ -32,15 +32,9 @@ Surface::~Surface()
m_instance->destroy_surface(m_surface);
}
[[nodiscard]] auto Surface::get_framebuffer_size() const -> VkExtent2D
[[nodiscard]] auto Surface::get_framebuffer_size() const -> math::uvec2
{
const auto &[width, height] = //
m_surface_entity.get<surface::SurfaceComponent>().get_resolution();
return {
.width = width,
.height = height,
};
return m_surface_entity.get<surface::SurfaceComponent>().get_resolution();
}
} // namespace lt::renderer::vk

View file

@ -30,7 +30,7 @@ public:
return m_surface;
}
[[nodiscard]] auto get_framebuffer_size() const -> VkExtent2D;
[[nodiscard]] auto get_framebuffer_size() const -> math::uvec2 override;
private:
class Instance *m_instance {};

View file

@ -4,10 +4,24 @@ namespace lt::renderer::vk {
Messenger::Messenger(IInstance *instance, ecs::Entity entity)
: m_instance(static_cast<Instance *>(instance))
, m_entity(std::move(entity))
// Move this to heap for pointer-stability of .pUserData
, m_entity(memory::create_scope<ecs::Entity>(std::move(entity)))
{
const auto &component = m_entity.get<MessengerComponent>();
const auto &component = m_entity->get<MessengerComponent>();
ensure(
component.get_severities() != MessageSeverity::none,
"Failed to create vk::Messenger: severities == none"
);
ensure(
component.get_types() != MessageType::none,
"Failed to create vk::Messenger: types == none"
);
ensure(component.get_callback(), "Failed to create vk::Messenger: null callback");
m_debug_messenger = m_instance->create_messenger(
VkDebugUtilsMessengerCreateInfoEXT {
@ -15,7 +29,7 @@ Messenger::Messenger(IInstance *instance, ecs::Entity entity)
.messageSeverity = to_native_severity(component.get_severities()),
.messageType = to_native_type(component.get_types()),
.pfnUserCallback = &native_callback,
.pUserData = this,
.pUserData = m_entity.get(),
}
);
}
@ -41,8 +55,8 @@ Messenger::~Messenger()
{
ensure(vulkan_user_data, "Null vulkan_user_data received in messenger callback");
auto *messenger = (Messenger *)vulkan_user_data; // NOLINT
auto &component = messenger->m_entity.get<MessengerComponent>();
auto *messenger = std::bit_cast<ecs::Entity *>(vulkan_user_data);
auto &component = messenger->get<MessengerComponent>();
component.get_callback()(
from_native_severity(severity),
from_native_type(type),
@ -102,22 +116,22 @@ Messenger::~Messenger()
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT)
{
flags &= std::to_underlying(error);
flags |= std::to_underlying(error);
}
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT)
{
flags &= std::to_underlying(warning);
flags |= std::to_underlying(warning);
}
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT)
{
flags &= std::to_underlying(info);
flags |= std::to_underlying(info);
}
if (severity & VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT)
{
flags &= std::to_underlying(verbose);
flags |= std::to_underlying(verbose);
}
return static_cast<MessageSeverity>(flags);

View file

@ -24,7 +24,6 @@ public:
auto operator=(const Messenger &) const -> Messenger & = delete;
private:
static auto native_callback(
VkDebugUtilsMessageSeverityFlagBitsEXT severity,
@ -45,7 +44,7 @@ private:
memory::NullOnMove<class Instance *> m_instance {};
ecs::Entity m_entity;
memory::Scope<ecs::Entity> m_entity;
VkDebugUtilsMessengerEXT m_debug_messenger = VK_NULL_HANDLE;

View file

@ -1,6 +1,5 @@
#include <renderer/backend/vk/context/surface.hpp>
#include <renderer/backend/vk/debug/messenger.hpp>
#include <renderer/backend/vk/test_utils.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/expects.hpp>

View file

@ -5,11 +5,12 @@
namespace lt::renderer::vk {
Pass::Pass(
IContext &context,
lt::assets::ShaderAsset vertex_shader,
lt::assets::ShaderAsset fragment_shader
IDevice *device,
ISwapchain *swapchain,
const lt::assets::ShaderAsset &vertex_shader,
const lt::assets::ShaderAsset &fragment_shader
)
: m_device(static_cast<Device *>(context.device()))
: m_device(static_cast<Device *>(device))
{
auto *vertex_module = create_module(
vertex_shader.unpack(lt::assets::ShaderAsset::BlobTag::code)
@ -113,7 +114,7 @@ Pass::Pass(
);
auto attachment_description = VkAttachmentDescription {
.format = static_cast<Swapchain *>(context.swapchain())->get_format(),
.format = static_cast<Swapchain *>(swapchain)->get_format(),
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
@ -176,8 +177,7 @@ Pass::Pass(
}
);
m_framebuffers = static_cast<Swapchain *>(context.swapchain())
->create_framebuffers_for_pass(m_pass);
m_framebuffers = static_cast<Swapchain *>(swapchain)->create_framebuffers_for_pass(m_pass);
m_device->destroy_shader_module(vertex_module);

View file

@ -3,7 +3,6 @@
#include <assets/shader.hpp>
#include <memory/pointer_types/null_on_move.hpp>
#include <renderer/backend/vk/utils.hpp>
#include <renderer/frontend/context/context.hpp>
#include <renderer/frontend/renderer/pass.hpp>
namespace lt::renderer::vk {
@ -12,9 +11,10 @@ class Pass: public IPass
{
public:
Pass(
IContext &context,
lt::assets::ShaderAsset vertex_shader,
lt::assets::ShaderAsset fragment_shader
class IDevice *device,
class ISwapchain *swapchain,
const lt::assets::ShaderAsset &vertex_shader,
const lt::assets::ShaderAsset &fragment_shader
);
~Pass() override;

View file

@ -4,9 +4,9 @@
namespace lt::renderer::vk {
Renderer::Renderer(IContext &context, uint32_t max_frames_in_flight)
: m_device(static_cast<Device *>(context.device()))
, m_swapchain(static_cast<Swapchain *>(context.swapchain()))
Renderer::Renderer(IDevice *device, ISwapchain *swapchain, uint32_t max_frames_in_flight)
: m_device(static_cast<Device *>(device))
, m_swapchain(static_cast<Swapchain *>(swapchain))
, m_resolution(m_swapchain->get_resolution())
, m_max_frames_in_flight(max_frames_in_flight)
{
@ -15,7 +15,8 @@ Renderer::Renderer(IContext &context, uint32_t max_frames_in_flight)
// TODO(Light): HARDCODED PASS!!!
m_pass = memory::create_ref<vk::Pass>(
context,
m_device,
m_swapchain,
assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);

View file

@ -2,11 +2,9 @@
#include <memory/reference.hpp>
#include <ranges>
#include <renderer/backend/vk/context/context.hpp>
#include <renderer/backend/vk/context/device.hpp>
#include <renderer/backend/vk/renderer/pass.hpp>
#include <renderer/backend/vk/utils.hpp>
#include <renderer/frontend/context/context.hpp>
#include <renderer/frontend/renderer/pass.hpp>
#include <renderer/frontend/renderer/renderer.hpp>
@ -15,7 +13,7 @@ namespace lt::renderer::vk {
class Renderer: public IRenderer
{
public:
Renderer(IContext &context, uint32_t max_frames_in_flight);
Renderer(class IDevice *device, class ISwapchain *swapchain, uint32_t max_frames_in_flight);
~Renderer() override;

View file

@ -1 +0,0 @@
#include <renderer/vk/test_utils.hpp>

View file

@ -1,115 +1,18 @@
#pragma once
#include <memory/reference.hpp>
#include <ranges>
#include <renderer/backend/vk/context/context.hpp>
#include <renderer/backend/vk/context/surface.hpp>
#include <renderer/backend/vk/debug/messenger.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
using ::lt::test::Case;
using ::lt::test::expect_eq;
using ::lt::test::expect_false;
using ::lt::test::expect_not_nullptr;
using ::lt::test::expect_throw;
using ::lt::test::expect_true;
using ::lt::test::Suite;
using ::std::ignore;
namespace constants {
constexpr auto resolution = lt::math::uvec2 { 800u, 600u };
}
class ValidationObserver
{
using Messenger = lt::renderer::vk::Messenger;
using enum Messenger::Type;
using enum Messenger::Severity;
public:
ValidationObserver()
: m_messenger(
Messenger::CreateInfo {
.severity = static_cast<Messenger::Severity>(warning | error),
.type = lt::renderer::vk::Messenger::all_type,
.callback = &callback,
.user_data = &m_had_any_messages,
}
)
{
}
[[nodiscard]] auto had_any_messages() const -> bool
{
return m_had_any_messages;
}
private:
static void callback(
Messenger::Severity message_severity,
Messenger::Type message_type,
Messenger::CallbackData_T vulkan_data,
void *user_data
)
{
std::ignore = message_severity;
std::ignore = message_type;
for (auto idx = 0; idx < vulkan_data->objectCount; ++idx)
{
auto object = vulkan_data->pObjects[idx];
std::println(
"0x{:x}({}) = {}",
object.objectHandle,
string_VkObjectType(object.objectType),
object.pObjectName ? object.pObjectName : "unnamed"
);
}
std::println("Validation message: {}", vulkan_data->pMessage);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
*(bool *)user_data = true;
}
Messenger m_messenger;
bool m_had_any_messages = false;
};
[[nodiscard]] inline auto create_context()
-> std::pair<lt::renderer::vk::Context, lt::surface::System>
{
using lt::surface::SurfaceComponent;
auto registry = lt::memory::create_ref<lt::ecs::Registry>();
auto entity = lt::ecs::Entity { registry, registry->create_entity() };
auto surface_system = lt::surface::System(registry);
entity.add<SurfaceComponent>(SurfaceComponent::CreateInfo {
.title = "",
.resolution = constants::resolution,
});
return { lt::renderer::vk::Context { entity }, std::move(surface_system) };
}
template<>
struct std::formatter<VkExtent2D>
{
constexpr auto parse(std::format_parse_context &context)
{
return context.begin();
}
auto format(const VkExtent2D &val, std::format_context &context) const
{
return std::format_to(context.out(), "{}, {}", val.width, val.height);
}
};
inline auto operator==(VkExtent2D lhs, VkExtent2D rhs) -> bool
{
return lhs.width == rhs.width && lhs.height == rhs.height;
}
// template<>
// struct std::formatter<VkExtent2D>
// {
// constexpr auto parse(std::format_parse_context &context)
// {
// return context.begin();
// }
//
// auto format(const VkExtent2D &val, std::format_context &context) const
// {
// return std::format_to(context.out(), "{}, {}", val.width, val.height);
// }
// };
//
// inline auto operator==(VkExtent2D lhs, VkExtent2D rhs) -> bool
// {
// return lhs.width == rhs.width && lhs.height == rhs.height;
// }

View file

@ -1,18 +0,0 @@
#include <memory/scope.hpp>
#include <renderer/api.hpp>
#include <renderer/backend/vk/context/context.hpp>
#include <renderer/frontend/context/context.hpp>
namespace lt::renderer {
auto IContext::create(API target_api, const ecs::Entity &surface_entity)
-> memory::Scope<IContext>
{
switch (target_api)
{
case API::Vulkan: return memory::create_scope<vk::Context>(surface_entity);
default: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,46 +0,0 @@
#pragma once
#include <ecs/entity.hpp>
#include <memory/scope.hpp>
#include <renderer/api.hpp>
#include <renderer/frontend/context/device.hpp>
#include <renderer/frontend/context/gpu.hpp>
#include <renderer/frontend/context/instance.hpp>
#include <renderer/frontend/context/surface.hpp>
#include <renderer/frontend/context/swapchain.hpp>
namespace lt::renderer {
class IContext
{
public:
static auto create(API target_api, const ecs::Entity &surface_entity)
-> memory::Scope<IContext>;
IContext() = default;
virtual ~IContext() = default;
IContext(IContext &&) = default;
IContext(const IContext &) = delete;
auto operator=(IContext &&) -> IContext & = default;
auto operator=(const IContext &) -> IContext & = delete;
virtual void recreate_swapchain() = 0;
[[nodiscard]] virtual auto instance() const -> IInstance * = 0;
[[nodiscard]] virtual auto surface() const -> ISurface * = 0;
[[nodiscard]] virtual auto gpu() const -> IGpu * = 0;
[[nodiscard]] virtual auto device() const -> IDevice * = 0;
[[nodiscard]] virtual auto swapchain() const -> ISwapchain * = 0;
};
} // namespace lt::renderer

View file

@ -1,56 +0,0 @@
#include <memory/reference.hpp>
#include <memory/scope.hpp>
#include <ranges>
#include <renderer/interface/context.hpp>
#include <renderer/vk/context/context.hpp>
#include <renderer/vk/context/surface.hpp>
#include <renderer/vk/debug/messenger.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
using lt::renderer::API;
using lt::renderer::IContext;
using ::lt::test::Case;
using ::lt::test::expect_eq;
using ::lt::test::expect_false;
using ::lt::test::expect_not_nullptr;
using ::lt::test::expect_throw;
using ::lt::test::expect_true;
using ::lt::test::Suite;
using ::std::ignore;
namespace constants {
constexpr auto resolution = lt::math::uvec2 { 800u, 600u };
}
class Fixture
{
public:
Fixture()
: m_registry(lt::memory::create_ref<lt::ecs::Registry>())
, m_surface_system(lt::memory::create_scope<lt::surface::System>(m_registry))
, m_surface_entity(m_registry, m_registry->create_entity())
{
}
auto get_surface_entity() -> lt::ecs::Entity
{
return m_surface_entity;
}
private:
lt::memory::Ref<lt::ecs::Registry> m_registry;
lt::memory::Scope<lt::surface::System> m_surface_system;
lt::ecs::Entity m_surface_entity;
};
Suite raii = "context_raii"_suite = [] {
Case { "Happy path won't throw" } = [] {
auto fixture = Fixture {};
IContext::create(API::Vulkan, fixture.get_surface_entity());
};
};

View file

@ -0,0 +1,21 @@
#include <renderer/backend/vk/context/device.hpp>
#include <renderer/frontend/context/device.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto IDevice::create(Api target_api, IGpu *gpu, ISurface *surface)
-> memory::Scope<IDevice>
{
ensure(gpu, "Failed to create renderer::IDevice: null gpu");
ensure(surface, "Failed to create renderer::IDevice: null surface");
switch (target_api)
{
case Api::vulkan: return memory::create_scope<vk::Device>(gpu, surface);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,10 +1,16 @@
#pragma once
#include <memory/scope.hpp>
#include <renderer/api.hpp>
namespace lt::renderer {
class IDevice
{
public:
[[nodiscard]] static auto create(Api target_api, class IGpu *gpu, class ISurface *surface)
-> memory::Scope<IDevice>;
IDevice() = default;
virtual ~IDevice() = default;

View file

@ -1,100 +1,55 @@
#include <memory/reference.hpp>
#include <ranges>
#include <renderer/vk/context/device.hpp>
#include <renderer/vk/context/surface.hpp>
#include <renderer/frontend/context/device.hpp>
#include <renderer/frontend/context/surface.hpp>
#include <renderer/test/utils.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
using namespace lt;
using renderer::vk::Device;
using renderer::vk::Surface;
using test::Case;
using test::expect_ne;
using test::expect_throw;
using test::Suite;
constexpr auto resolution = math::uvec2 { 800u, 600u };
Suite raii = "device_raii"_suite = [] {
Case { "happy path won't throw" } = [] {
auto registry = memory::create_ref<ecs::Registry>();
auto surface_system = surface::System { registry };
auto entity = ecs::Entity { registry, registry->create_entity() };
entity.add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.resolution = resolution,
.visible = true,
});
auto surface = Surface { entity };
auto device = Device { surface };
};
Case { "many won't freeze/throw" } = [] {
auto registry = memory::create_ref<ecs::Registry>();
auto surface_system = surface::System { registry };
auto entity = ecs::Entity { registry, registry->create_entity() };
entity.add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.resolution = resolution,
.visible = true,
});
auto surface = Surface { entity };
// it takes a loong time to initialize vulkan + setup device
for (auto idx : std::views::iota(0, 10))
{
Device { surface };
}
auto fixture = Fixture_SurfaceGpu {};
std::ignore = lt::renderer::IDevice::create(
constants::api,
fixture.gpu(),
fixture.surface()
);
};
Case { "unhappy path throws" } = [] {
auto registry = memory::create_ref<ecs::Registry>();
auto surface_system = surface::System { registry };
auto entity = ecs::Entity { registry, registry->create_entity() };
entity.add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.resolution = resolution,
.visible = true,
auto fixture = Fixture_SurfaceGpu {};
expect_throw([&] {
ignore = lt::renderer::IDevice::create(constants::api, nullptr, fixture.surface());
});
auto moved_out_surface = Surface { entity };
auto surface = std::move(moved_out_surface);
expect_throw([&] { Device { moved_out_surface }; });
};
Case { "post construct has correct state" } = [] {
auto registry = memory::create_ref<ecs::Registry>();
auto surface_system = surface::System { registry };
auto entity = ecs::Entity { registry, registry->create_entity() };
entity.add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.resolution = resolution,
.visible = true,
expect_throw([&] {
ignore = lt::renderer::IDevice::create(constants::api, fixture.gpu(), nullptr);
});
auto surface = Surface { entity };
auto device = Device { surface };
for (auto &index : device.get_family_indices())
{
expect_ne(index, VK_QUEUE_FAMILY_IGNORED);
}
test::expect_true(device.physical());
test::expect_true(device.vk());
};
Case { "post destruct has correct state" } = [] {
auto registry = memory::create_ref<ecs::Registry>();
auto surface_system = surface::System { registry };
auto entity = ecs::Entity { registry, registry->create_entity() };
entity.add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.resolution = resolution,
.visible = true,
expect_throw([&] {
ignore = lt::renderer::IDevice::create(
lt::renderer::Api::none,
fixture.gpu(),
fixture.surface()
);
});
auto surface = Surface { entity };
expect_throw([&] {
ignore = lt::renderer::IDevice::create(
lt::renderer::Api::direct_x,
fixture.gpu(),
fixture.surface()
);
});
{
auto device = Device { surface };
}
expect_throw([&] {
ignore = lt::renderer::IDevice::create(
lt::renderer::Api::metal,
fixture.gpu(),
fixture.surface()
);
});
};
};

View file

@ -0,0 +1,18 @@
#include <renderer/backend/vk/context/gpu.hpp>
#include <renderer/frontend/context/gpu.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto IGpu::create(Api target_api, IInstance *instance)
-> memory::Scope<IGpu>
{
switch (target_api)
{
case Api::vulkan: return memory::create_scope<vk::Gpu>(instance);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,10 +1,16 @@
#pragma once
#include <memory/scope.hpp>
#include <renderer/api.hpp>
namespace lt::renderer {
class IGpu
{
public:
[[nodiscard]] static auto create(Api target_api, class IInstance *instance)
-> memory::Scope<IGpu>;
IGpu() = default;
virtual ~IGpu() = default;

View file

@ -0,0 +1,17 @@
#include <renderer/backend/vk/context/instance.hpp>
#include <renderer/frontend/context/instance.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto IInstance::get(Api target_api) -> IInstance *
{
switch (target_api)
{
case Api::vulkan: return vk::Instance::get();
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,10 +1,14 @@
#pragma once
#include <renderer/api.hpp>
namespace lt::renderer {
class IInstance
{
public:
[[nodiscard]] static auto get(Api target_api) -> IInstance *;
IInstance() = default;
virtual ~IInstance() = default;

View file

@ -1,95 +0,0 @@
#include <renderer/vk/context/instance.hpp>
#include <test/test.hpp>
using namespace lt;
using renderer::vk::Instance;
using test::Case;
using test::expect_not_nullptr;
using test::Suite;
// NOLINTNEXTLINE
Suite raii = "raii"_suite = [] {
Case { "post singleton insantiation state is correct" } = [] {
expect_not_nullptr(Instance::get());
using namespace renderer::vk;
expect_not_nullptr(vk_get_instance_proc_address);
expect_not_nullptr(vk_create_instance);
expect_not_nullptr(vk_enumerate_instance_extension_properties);
expect_not_nullptr(vk_enumerate_instance_layer_properties);
expect_not_nullptr(vk_destroy_instance);
expect_not_nullptr(vk_enumerate_physical_devices);
expect_not_nullptr(vk_get_physical_device_properties);
expect_not_nullptr(vk_get_physical_device_queue_family_properties);
expect_not_nullptr(vk_create_device);
expect_not_nullptr(vk_get_device_proc_address);
expect_not_nullptr(vk_destroy_device);
expect_not_nullptr(vk_get_physical_device_features);
expect_not_nullptr(vk_enumerate_device_extension_properties);
expect_not_nullptr(vk_cmd_begin_debug_label);
expect_not_nullptr(vk_cmd_end_debug_label);
expect_not_nullptr(vk_cmd_insert_debug_label);
expect_not_nullptr(vk_create_debug_messenger);
expect_not_nullptr(vk_destroy_debug_messenger);
expect_not_nullptr(vk_queue_begin_debug_label);
expect_not_nullptr(vk_queue_end_debug_label);
expect_not_nullptr(vk_queue_insert_debug_label);
expect_not_nullptr(vk_set_debug_object_name);
expect_not_nullptr(vk_set_debug_object_tag);
expect_not_nullptr(vk_submit_debug_message);
expect_not_nullptr(vk_get_physical_device_surface_support);
expect_not_nullptr(vk_get_physical_device_surface_capabilities);
expect_not_nullptr(vk_get_physical_device_surface_formats);
expect_not_nullptr(vk_create_xlib_surface_khr);
expect_not_nullptr(vk_destroy_surface_khr);
};
Case { "post load device functions state is correct" } = [] {
using namespace renderer::vk;
expect_not_nullptr(Instance::get());
expect_not_nullptr(vk_get_device_queue);
expect_not_nullptr(vk_create_command_pool);
expect_not_nullptr(vk_destroy_command_pool);
expect_not_nullptr(vk_allocate_command_buffers);
expect_not_nullptr(vk_free_command_buffers);
expect_not_nullptr(vk_begin_command_buffer);
expect_not_nullptr(vk_end_command_buffer);
expect_not_nullptr(vk_cmd_pipeline_barrier);
expect_not_nullptr(vk_queue_submit);
expect_not_nullptr(vk_queue_wait_idle);
expect_not_nullptr(vk_device_wait_idle);
expect_not_nullptr(vk_create_fence);
expect_not_nullptr(vk_destroy_fence);
expect_not_nullptr(vk_wait_for_fences);
expect_not_nullptr(vk_reset_fences);
expect_not_nullptr(vk_create_semaphore);
expect_not_nullptr(vk_destroy_semaphore);
expect_not_nullptr(vk_create_swapchain_khr);
expect_not_nullptr(vk_destroy_swapchain_khr);
expect_not_nullptr(vk_get_swapchain_images_khr);
expect_not_nullptr(vk_acquire_next_image_khr);
expect_not_nullptr(vk_queue_present_khr);
expect_not_nullptr(vk_create_image_view);
expect_not_nullptr(vk_destroy_image_view);
expect_not_nullptr(vk_create_render_pass);
expect_not_nullptr(vk_destroy_render_pass);
expect_not_nullptr(vk_create_frame_buffer);
expect_not_nullptr(vk_destroy_frame_buffer);
expect_not_nullptr(vk_create_shader_module);
expect_not_nullptr(vk_destroy_shader_module);
expect_not_nullptr(vk_create_pipeline_layout);
expect_not_nullptr(vk_destroy_pipeline_layout);
expect_not_nullptr(vk_create_graphics_pipelines);
expect_not_nullptr(vk_destroy_pipeline);
expect_not_nullptr(vk_cmd_begin_render_pass);
expect_not_nullptr(vk_cmd_end_render_pass);
expect_not_nullptr(vk_cmd_bind_pipeline);
expect_not_nullptr(vk_cmd_draw);
expect_not_nullptr(vk_cmd_set_viewport);
expect_not_nullptr(vk_cmd_set_scissors);
};
};

View file

@ -0,0 +1,24 @@
#include <renderer/backend/vk/context/surface.hpp>
#include <renderer/frontend/context/surface.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto ISurface::create(
Api target_api,
IInstance *instance,
const ecs::Entity &surface_entity
) -> memory::Scope<ISurface>
{
ensure(instance, "Failed to create renderer::ISurface: null instance");
switch (target_api)
{
case Api::vulkan: return memory::create_scope<vk::Surface>(instance, surface_entity);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,10 +1,21 @@
#pragma once
#include <ecs/entity.hpp>
#include <math/vec2.hpp>
#include <memory/scope.hpp>
#include <renderer/api.hpp>
namespace lt::renderer {
class ISurface
{
public:
[[nodiscard]] static auto create(
Api target_api,
class IInstance *instance,
const ecs::Entity &surface_entity
) -> memory::Scope<ISurface>;
ISurface() = default;
virtual ~ISurface() = default;
@ -16,6 +27,8 @@ public:
auto operator=(ISurface &&) -> ISurface & = default;
auto operator=(const ISurface &) -> ISurface & = delete;
[[nodiscard]] virtual auto get_framebuffer_size() const -> math::uvec2 = 0;
};
} // namespace lt::renderer

View file

@ -1,52 +1,88 @@
#include <memory/reference.hpp>
#include <renderer/vk/context/surface.hpp>
#include <renderer/vk/debug/messenger.hpp>
#include <renderer/vk/test_utils.hpp>
#include <renderer/frontend/context/instance.hpp>
#include <renderer/frontend/context/surface.hpp>
#include <renderer/test/utils.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
using ::lt::ecs::Entity;
using ::lt::ecs::EntityId;
using ::lt::ecs::Registry;
using ::lt::renderer::vk::Surface;
using ::lt::surface::SurfaceComponent;
using ::lt::surface::System;
Suite raii = "surface"_suite = [] {
Case { "happy path won't throw" } = [&] {
auto observer = ValidationObserver {};
auto fixture = Fixture_SurfaceSystem {};
auto registry = lt::memory::create_ref<Registry>();
auto entity = Entity { registry, registry->create_entity() };
auto surface_system = System(registry);
entity.add<SurfaceComponent>(SurfaceComponent::CreateInfo {
.title = "",
.resolution = constants::resolution,
.visible = true,
});
const auto surface = Surface { entity };
const auto &[x, y] = surface.get_framebuffer_size();
const auto surface = lt::renderer::ISurface::create(
constants::api,
lt::renderer::IInstance::get(constants::api),
fixture.surface_entity()
);
const auto &[x, y] = surface->get_framebuffer_size();
expect_eq(x, constants::resolution.x);
expect_eq(y, constants::resolution.y);
expect_not_nullptr(surface.vk());
expect_false(observer.had_any_messages());
};
Case { "unhappy path throws" } = [&] {
auto observer = ValidationObserver {};
auto registry = lt::memory::create_ref<Registry>();
auto entity = Entity { registry, registry->create_entity() };
auto registry = lt::memory::create_ref<lt::ecs::Registry>();
auto entity = lt::ecs::Entity { registry, registry->create_entity() };
auto system = lt::surface::System(registry);
entity.add<SurfaceComponent>(SurfaceComponent::CreateInfo {
.title = "",
.resolution = constants::resolution,
.visible = true,
expect_throw([&] {
std::ignore = lt::renderer::ISurface::create(
constants::api,
lt::renderer::IInstance::get(constants::api),
entity
);
});
expect_throw([&] { Surface { entity }; });
expect_false(observer.had_any_messages());
system.create_surface_component(
entity.id(),
lt::surface::SurfaceComponent::CreateInfo {
.title = "",
.resolution = constants::resolution,
}
);
expect_throw([&] {
std::ignore = lt::renderer::ISurface::create(constants::api, nullptr, entity);
});
expect_throw([&] {
std::ignore = lt::renderer::ISurface::create(
lt::renderer::Api::none,
lt::renderer::IInstance::get(constants::api),
entity
);
});
expect_throw([&] {
std::ignore = lt::renderer::ISurface::create(
lt::renderer::Api::direct_x,
lt::renderer::IInstance::get(constants::api),
entity
);
});
expect_throw([&] {
std::ignore = lt::renderer::ISurface::create(
lt::renderer::Api::metal,
lt::renderer::IInstance::get(constants::api),
entity
);
});
// Ensure base creation info is non-throwing
std::ignore = lt::renderer::ISurface::create(
constants::api,
lt::renderer::IInstance::get(constants::api),
entity
);
};
// TODO(Light): add torture tests
Case { "torture tests" } = [] {
};
};

View file

@ -0,0 +1,23 @@
#include <renderer/backend/vk/context/swapchain.hpp>
#include <renderer/frontend/context/swapchain.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto ISwapchain::create(
Api target_api,
ISurface *surface,
IGpu *gpu,
IDevice *device
) -> memory::Scope<ISwapchain>
{
switch (target_api)
{
case Api::vulkan: return memory::create_scope<vk::Swapchain>(surface, gpu, device);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,10 +1,20 @@
#pragma once
#include <memory/scope.hpp>
#include <renderer/api.hpp>
namespace lt::renderer {
class ISwapchain
{
public:
[[nodiscard]] static auto create(
Api target_api,
class ISurface *surface,
class IGpu *gpu,
class IDevice *device
) -> memory::Scope<ISwapchain>;
ISwapchain() = default;
virtual ~ISwapchain() = default;

View file

@ -5,16 +5,17 @@
namespace lt::renderer {
[[nodiscard]] /* static */ auto IMessenger::create(
API target_api,
Api target_api,
IInstance *instance,
ecs::Entity entity
) -> memory::Scope<IMessenger>
{
switch (target_api)
{
case API::Vulkan: return memory::create_scope<vk::Messenger>(instance, std::move(entity));
case API::Metal:
case API::DirectX: throw std::runtime_error { "Invalid API" };
case Api::vulkan: return memory::create_scope<vk::Messenger>(instance, std::move(entity));
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -0,0 +1,29 @@
#include <assets/shader.hpp>
#include <renderer/backend/vk/renderer/pass.hpp>
#include <renderer/frontend/context/gpu.hpp>
#include <renderer/frontend/renderer/pass.hpp>
namespace lt::renderer {
[[nodiscard]] /* static */ auto IPass::create(
lt::renderer::Api target_api,
IDevice *device,
ISwapchain *swapchain,
const lt::assets::ShaderAsset &vertex_shader,
const lt::assets::ShaderAsset &fragment_shader
) -> memory::Scope<IPass>
{
ensure(device, "Failed to create renderer::IPass: null device");
ensure(swapchain, "Failed to create renderer::IPass: null swapchain");
switch (target_api)
{
case Api::vulkan:
return memory::create_scope<vk::Pass>(device, swapchain, vertex_shader, fragment_shader);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}
} // namespace lt::renderer

View file

@ -1,12 +1,25 @@
#pragma once
#include <renderer/api.hpp>
#include <renderer/frontend/context/swapchain.hpp>
namespace lt::assets {
class ShaderAsset;
}
namespace lt::renderer {
class IPass
{
public:
[[nodiscard]] static auto create(
lt::renderer::Api target_api,
class IDevice *device,
class ISwapchain *swapchain,
const class lt::assets::ShaderAsset &vertex_shader,
const class lt::assets::ShaderAsset &fragment_shader
) -> memory::Scope<IPass>;
IPass() = default;
virtual ~IPass() = default;

View file

@ -1,20 +1,74 @@
#include <renderer/vk/renderer/pass.hpp>
#include <renderer/vk/test_utils.hpp>
using ::lt::assets::ShaderAsset;
using ::lt::renderer::vk::Pass;
#include <assets/shader.hpp>
#include <renderer/frontend/renderer/pass.hpp>
#include <renderer/test/utils.hpp>
Suite raii = "pass_raii"_suite = [] {
Case { "happy path won't throw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
auto fixture = Fixture_RendererSystem {};
auto &system = fixture.renderer_system();
std::ignore = Pass {
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" },
};
std::ignore = lt::renderer::IPass::create(
constants::api,
system.get_device(),
system.get_swapchain(),
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
};
expect_false(observer.had_any_messages());
Case { "unhappy path throws" } = [] {
auto fixture = Fixture_RendererSystem {};
auto &system = fixture.renderer_system();
expect_throw([&] {
std::ignore = lt::renderer::IPass::create(
constants::api,
nullptr,
system.get_swapchain(),
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
});
expect_throw([&] {
std::ignore = lt::renderer::IPass::create(
constants::api,
system.get_device(),
nullptr,
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
});
expect_throw([&] {
std::ignore = lt::renderer::IPass::create(
lt::renderer::Api::none,
system.get_device(),
system.get_swapchain(),
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
});
expect_throw([&] {
std::ignore = lt::renderer::IPass::create(
lt::renderer::Api::direct_x,
system.get_device(),
system.get_swapchain(),
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
});
expect_throw([&] {
std::ignore = lt::renderer::IPass::create(
lt::renderer::Api::metal,
system.get_device(),
system.get_swapchain(),
lt::assets::ShaderAsset { "./data/test_assets/triangle.vert.asset" },
lt::assets::ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
});
};
};

View file

@ -5,13 +5,33 @@
namespace lt::renderer {
auto IRenderer::create(API target_api, IContext &context, uint32_t max_frames_in_flight)
-> memory::Scope<IRenderer>
[[nodiscard]] /* static */ auto IRenderer::create(
Api target_api,
IDevice *device,
ISwapchain *swapchain,
uint32_t max_frames_in_flight
) -> memory::Scope<IRenderer>
{
ensure(device, "Failed to create renderer::IRenderer: null device");
ensure(swapchain, "Failed to create renderer::IRenderer: null swapchain");
ensure(
std::clamp(max_frames_in_flight, frames_in_flight_lower_limit, frames_in_flight_upper_limit)
== max_frames_in_flight,
"Failed to initialize renderer::System: max_frames_in_flight ({}) not within bounds ({} -> "
"{}) ",
max_frames_in_flight,
frames_in_flight_lower_limit,
frames_in_flight_upper_limit
);
switch (target_api)
{
case API::Vulkan: return memory::create_scope<vk::Renderer>(context, max_frames_in_flight);
default: throw std::runtime_error { "Invalid API" };
case Api::vulkan:
return memory::create_scope<vk::Renderer>(device, swapchain, max_frames_in_flight);
case Api::none:
case Api::metal:
case Api::direct_x: throw std::runtime_error { "Invalid API" };
}
}

View file

@ -8,6 +8,10 @@ namespace lt::renderer {
class IRenderer
{
public:
static constexpr auto frames_in_flight_upper_limit = 5u;
static constexpr auto frames_in_flight_lower_limit = 1u;
enum class DrawResult : uint8_t
{
success = 0,
@ -15,8 +19,12 @@ public:
error,
};
static auto create(API target_api, class IContext &context, uint32_t max_frames_in_flight)
-> memory::Scope<IRenderer>;
[[nodiscard]] static auto create(
Api target_api,
class IDevice *device,
class ISwapchain *swapchain,
uint32_t max_frames_in_flight
) -> memory::Scope<IRenderer>;
IRenderer() = default;

View file

@ -1,74 +1,96 @@
#include <memory/reference.hpp>
#include <renderer/vk/renderer/renderer.hpp>
#include <renderer/vk/test_utils.hpp>
using ::lt::assets::ShaderAsset;
using ::lt::renderer::vk::Pass;
using ::lt::renderer::vk::Renderer;
#include <renderer/frontend/renderer/renderer.hpp>
#include <renderer/test/utils.hpp>
Suite raii = "renderer_raii"_suite = [] {
Case { "happy path won't throw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
std::ignore = Renderer(
context,
lt::memory::create_ref<Pass>(
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" }
)
auto fixture = FixtureDeviceSwapchain {};
ignore = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
fixture.swapchain(),
constants::frames_in_flight
);
};
expect_false(observer.had_any_messages());
Case { "unhappy path throws" } = [] {
auto fixture = FixtureDeviceSwapchain {};
expect_throw([&] {
ignore = lt::renderer::IRenderer::create(
constants::api,
nullptr,
fixture.swapchain(),
constants::frames_in_flight
);
});
expect_throw([&] {
ignore = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
nullptr,
constants::frames_in_flight
);
});
expect_throw([&] {
ignore = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
nullptr,
lt::renderer::IRenderer::frames_in_flight_upper_limit + 1
);
});
expect_throw([&] {
ignore = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
nullptr,
lt::renderer::IRenderer::frames_in_flight_lower_limit - 1
);
});
};
};
Suite draw = "renderer_draw"_suite = [] {
Case { "renderer draw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
using enum lt::renderer::IRenderer::DrawResult;
auto renderer = Renderer(
context,
lt::memory::create_ref<Pass>(
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" }
)
Case { "renderer draw" } = [] {
auto fixture = FixtureDeviceSwapchain {};
auto renderer = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
fixture.swapchain(),
constants::frames_in_flight
);
for (auto frame_idx : std::views::iota(0u, 30u))
{
expect_true(renderer.draw(frame_idx % Renderer::max_frames_in_flight));
expect_eq(renderer->draw(frame_idx % constants::frames_in_flight), success);
}
expect_false(observer.had_any_messages());
};
Case { "post swapchain replacement renderer draw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
auto pass = lt::memory::create_ref<Pass>(
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" }
auto fixture = FixtureDeviceSwapchain {};
auto renderer = lt::renderer::IRenderer::create(
constants::api,
fixture.device(),
fixture.swapchain(),
constants::frames_in_flight
);
auto renderer = Renderer { context, pass };
for (auto frame_idx : std::views::iota(0u, 15u))
{
expect_true(renderer.draw(frame_idx % Renderer::max_frames_in_flight));
expect_eq(renderer->draw(frame_idx % constants::frames_in_flight), success);
}
context.recreate_swapchain();
renderer.replace_swapchain(context.swapchain());
pass->replace_swapchain(context.swapchain());
fixture.recreate_swapchain();
renderer->replace_swapchain(fixture.swapchain());
for (auto frame_idx : std::views::iota(0u, 15u))
{
expect_true(renderer.draw(frame_idx % Renderer::max_frames_in_flight));
expect_eq(renderer->draw(frame_idx % constants::frames_in_flight), success);
}
expect_false(observer.had_any_messages());
};
};

View file

@ -1,5 +1,9 @@
#include <renderer/components/messenger.hpp>
#include <renderer/frontend/context/context.hpp>
#include <renderer/frontend/context/device.hpp>
#include <renderer/frontend/context/gpu.hpp>
#include <renderer/frontend/context/instance.hpp>
#include <renderer/frontend/context/surface.hpp>
#include <renderer/frontend/context/swapchain.hpp>
#include <renderer/frontend/messenger.hpp>
#include <renderer/frontend/renderer/pass.hpp>
#include <renderer/frontend/renderer/renderer.hpp>
@ -9,20 +13,65 @@
namespace lt::renderer {
System::System(CreateInfo info)
: m_api(info.config.target_api)
: m_surface_entity(info.surface_entity)
, m_api(info.config.target_api)
, m_registry(std::move(info.registry))
, m_context(IContext::create(m_api, info.surface_entity))
, m_surface_entity(info.surface_entity)
, m_instance(IInstance::get(m_api))
, m_max_frames_in_flight(info.config.max_frames_in_flight)
{
// ensure(m_stats, "Failed to initialize system: null stats");
ensure(m_registry, "Failed to initialize renderer system: null registry");
ensure(m_registry, "Failed to initialize renderer::System: null registry");
ensure(
std::clamp(
info.config.max_frames_in_flight,
frames_in_flight_lower_limit,
frames_in_flight_upper_limit
) == info.config.max_frames_in_flight,
"Failed to initialize renderer::System: max_frames_in_flight ({}) not within bounds ({} -> "
"{}) ",
info.config.max_frames_in_flight,
frames_in_flight_lower_limit,
frames_in_flight_upper_limit
);
m_renderer = IRenderer::create(m_api, *m_context, info.config.max_frames_in_flight);
if (info.messenger_info.has_value())
{
ensure(
create_messenger_component(m_registry->create_entity(), info.messenger_info.value()),
"Failed to initialize renderer::System: failed to create messenger component"
);
}
else
{
log_wrn(
"Creating renderer::System without a default messenger component, this is not "
"recommended"
);
}
m_surface = ISurface::create(m_api, m_instance, m_surface_entity);
m_gpu = IGpu::create(m_api, m_instance);
m_device = IDevice::create(m_api, m_gpu.get(), m_surface.get());
m_swapchain = ISwapchain::create(m_api, m_surface.get(), m_gpu.get(), m_device.get());
m_renderer = { IRenderer::create(
m_api,
m_device.get(),
m_swapchain.get(),
info.config.max_frames_in_flight
) };
}
System::~System()
{
auto entities_to_remove = std::vector<ecs::EntityId> {};
for (auto &[entity, surface] : m_registry->view<MessengerComponent>())
{
entities_to_remove.emplace_back(entity);
}
for (auto entity : entities_to_remove)
{
m_registry->remove<MessengerComponent>(entity);
}
}
void System::on_register()
@ -41,37 +90,41 @@ void System::tick(app::TickInfo tick)
{
if (std::holds_alternative<surface::ResizedEvent>(event))
{
m_context->recreate_swapchain();
m_renderer->replace_swapchain(m_context->swapchain());
// m_pass->replace_swapchain(m_context->swapchain());
m_swapchain.reset();
m_swapchain = ISwapchain::create(m_api, m_surface.get(), m_gpu.get(), m_device.get());
m_renderer->replace_swapchain(m_swapchain.get());
}
}
if (m_renderer->draw(m_frame_idx) != IRenderer::DrawResult::success)
{
m_context->recreate_swapchain();
m_renderer->replace_swapchain(m_context->swapchain());
// m_pass->replace_swapchain(m_context->swapchain());
m_swapchain.reset();
m_swapchain = ISwapchain::create(m_api, m_surface.get(), m_gpu.get(), m_device.get());
m_renderer->replace_swapchain(m_swapchain.get());
std::ignore = m_renderer->draw(m_frame_idx); // drop the frame if failed twice
}
m_frame_idx = (m_frame_idx + 1) % m_max_frames_in_flight;
}
void System::create_messenger_component(ecs::EntityId entity, MessengerComponent::CreateInfo info)
[[nodiscard]] auto System::create_messenger_component(
ecs::EntityId entity,
MessengerComponent::CreateInfo info
) -> bool
try
{
auto &component = m_registry->add<MessengerComponent>(entity, std::move(info));
component.m_implementation = IMessenger::create(
m_api,
m_context->instance(),
{ m_registry, entity }
);
component.m_implementation = IMessenger::create(m_api, m_instance, { m_registry, entity });
// component.m_user_data = info.user_data;
return true;
}
catch (const std::exception &exp)
{
log_err("Failed to create renderer::MessengerComponent:");
log_err("\twhat: {}", exp.what());
m_registry->remove<MessengerComponent>(entity);
return false;
}

View file

@ -1,9 +1,8 @@
#include <memory/reference.hpp>
#include <memory/scope.hpp>
#include <ranges>
#include <renderer/frontend/renderer/renderer.hpp>
#include <renderer/system.hpp>
#include <renderer/vk/context/context.hpp>
#include <renderer/vk/renderer/renderer.hpp>
#include <renderer/test/utils.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
@ -15,10 +14,9 @@ using test::expect_throw;
using test::expect_true;
using test::Suite;
using lt::renderer::MessageSeverity;
using renderer::System;
constexpr auto resolution = math::uvec2 { 800, 600 };
struct SurfaceContext
{
surface::System system;
@ -31,140 +29,67 @@ struct RendererContext
System system;
};
[[nodiscard]] auto create_surface() -> SurfaceContext
{
using surface::SurfaceComponent;
auto surface_registry = memory::create_ref<ecs::Registry>();
auto surface_entity = surface_registry->create_entity();
auto surface_system = surface::System(surface_registry);
surface_registry->add<SurfaceComponent>(
surface_entity,
SurfaceComponent::CreateInfo {
.title = "",
.resolution = resolution,
}
);
return {
.system = std::move(surface_system),
.entity = ecs::Entity { surface_registry, surface_entity },
};
}
[[nodiscard]] auto create_system() -> std::pair<SurfaceContext, RendererContext>
{
auto surface_context = create_surface();
auto &[surface_system, surface_entity] = surface_context;
auto registry = memory::create_ref<ecs::Registry>();
auto stats = memory::create_ref<app::SystemStats>();
return {
std::move(surface_context),
RendererContext {
.registry = registry,
.system = System(
{
.registry = registry,
.surface_entity = surface_entity,
.system_stats = stats,
}
),
},
};
}
class SystemTest
{
public:
SystemTest()
{
m_surface_entity->add<surface::SurfaceComponent>(surface::SurfaceComponent::CreateInfo {
.title = "",
.resolution = resolution,
});
}
[[nodiscard]] auto registry() const -> memory::Ref<ecs::Registry>
{
return m_registry;
}
[[nodiscard]] auto surface_entity() const -> const ecs::Entity &
{
return *m_surface_entity;
}
[[nodiscard]] auto stats() const -> memory::Ref<app::SystemStats>
{
return m_stats;
}
private:
memory::Ref<app::SystemStats> m_stats = memory::create_ref<app::SystemStats>();
memory::Ref<ecs::Registry> m_registry = memory::create_ref<ecs::Registry>();
memory::Ref<surface::System> m_surface_system = memory::create_ref<surface::System>(
m_registry
);
memory::Scope<ecs::Entity> m_surface_entity = memory::create_scope<ecs::Entity>(
m_registry,
m_registry->create_entity()
);
};
Suite raii = "raii"_suite = [] {
Case { "happy path won't throw" } = [&] {
ignore = create_system();
Case { "happy path won't throw" } = [] {
ignore = Fixture_RendererSystem {};
};
Case { "happy path has no validation errors" } = [&] {
auto fixture = SystemTest {};
std::ignore = System(
{
.registry = fixture.registry(),
.surface_entity = fixture.surface_entity(),
.system_stats = fixture.stats(),
}
);
expect_true(fixture.stats()->empty_diagnosis());
Case { "happy path has no errors" } = [] {
auto fixture = Fixture_RendererSystem {};
expect_false(fixture.has_any_messages_of(MessageSeverity::error));
expect_false(fixture.has_any_messages_of(MessageSeverity::warning));
};
Case { "unhappy path throws" } = [] {
auto fixture = SystemTest {};
auto fixture = Fixture_SurfaceSystem {};
auto empty_entity = ecs::Entity { fixture.registry(), fixture.registry()->create_entity() };
auto info = fixture.renderer_system_create_info();
expect_throw([&] {
ignore = System(
{
.registry = {},
.surface_entity = fixture.surface_entity(),
.system_stats = fixture.stats(),
}
);
expect_throw([=] mutable {
info.registry = nullptr;
ignore = System { info };
});
expect_throw([&] {
ignore = System(
System::CreateInfo {
.registry = fixture.registry(),
.surface_entity = empty_entity,
.system_stats = fixture.stats(),
}
);
expect_throw([=] mutable {
info.surface_entity = ecs::Entity({}, {});
ignore = System { info };
});
expect_throw([&] {
ignore = System(
System::CreateInfo {
.registry = fixture.registry(),
.surface_entity = fixture.surface_entity(),
.system_stats = {},
}
);
expect_throw([=] mutable {
info.config.target_api = lt::renderer::Api::none;
ignore = System { info };
});
// unsupported Apis
expect_throw([=] mutable {
info.config.target_api = lt::renderer::Api::direct_x;
ignore = System { info };
});
expect_throw([=] mutable {
info.config.target_api = lt::renderer::Api::metal;
ignore = System { info };
});
expect_throw([=] mutable {
constexpr auto limit = lt::renderer::System::frames_in_flight_upper_limit;
info.config.max_frames_in_flight = limit + 1u;
ignore = System { info };
});
expect_throw([=] mutable {
constexpr auto limit = lt::renderer::System::frames_in_flight_lower_limit;
info.config.max_frames_in_flight = limit - 1u;
ignore = System { info };
});
expect_throw([=] mutable {
info.messenger_info = lt::renderer::MessengerComponent::CreateInfo {};
ignore = System { info };
});
// Make sure the base info is not at fault for unhappiness.
ignore = System { info };
};
};

View file

@ -0,0 +1,5 @@
#pragma once
namespace constants {
};

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,199 @@
#pragma once
#include <memory/reference.hpp>
#include <renderer/components/messenger.hpp>
#include <renderer/frontend/context/device.hpp>
#include <renderer/frontend/context/gpu.hpp>
#include <renderer/frontend/context/instance.hpp>
#include <renderer/frontend/context/surface.hpp>
#include <renderer/frontend/context/swapchain.hpp>
#include <renderer/frontend/messenger.hpp>
#include <renderer/system.hpp>
#include <surface/components.hpp>
#include <surface/system.hpp>
#include <test/test.hpp>
using ::lt::test::Case;
using ::lt::test::expect_eq;
using ::lt::test::expect_false;
using ::lt::test::expect_not_nullptr;
using ::lt::test::expect_throw;
using ::lt::test::expect_true;
using ::lt::test::Suite;
using ::std::ignore;
namespace constants {
constexpr auto api = lt::renderer::Api::vulkan;
constexpr auto resolution = lt::math::uvec2 { 800u, 600u };
constexpr auto frames_in_flight = uint32_t { 3u };
} // namespace constants
class Fixture_SurfaceSystem
{
public:
Fixture_SurfaceSystem()
{
m_system.create_surface_component(
m_entity.id(),
lt::surface::SurfaceComponent::CreateInfo {
.title = "",
.resolution = constants::resolution,
}
);
}
[[nodiscard]] auto renderer_system_create_info() -> lt::renderer::System::CreateInfo
{
return lt::renderer::System::CreateInfo{
.config = lt::renderer::System::Configuration{
.target_api = constants::api,
.max_frames_in_flight = constants::frames_in_flight,
},
.registry = registry(),
.surface_entity = surface_entity(),
} ;
}
[[nodiscard]] auto registry() -> lt::memory::Ref<lt::ecs::Registry> &
{
return m_registry;
}
[[nodiscard]] auto surface_entity() -> lt::ecs::Entity &
{
return m_entity;
}
[[nodiscard]] auto surface_system() -> lt::surface::System &
{
return m_system;
}
private:
lt::memory::Ref<lt::ecs::Registry> m_registry = lt::memory::create_ref<lt::ecs::Registry>();
lt::ecs::Entity m_entity { m_registry, m_registry->create_entity() };
lt::surface::System m_system = lt::surface::System(m_registry);
};
class Fixture_SurfaceGpu: public Fixture_SurfaceSystem
{
public:
Fixture_SurfaceGpu() = default;
[[nodiscard]] auto surface() -> lt::renderer::ISurface *
{
return m_surface.get();
}
[[nodiscard]] auto gpu() -> lt::renderer::IGpu *
{
return m_gpu.get();
}
private:
lt::memory::Scope<lt::renderer::ISurface> m_surface { lt::renderer::ISurface::create(
constants::api,
lt::renderer::IInstance::get(constants::api),
surface_entity()
) };
lt::memory::Scope<lt::renderer::IGpu> m_gpu {
lt::renderer::IGpu::create(constants::api, lt::renderer::IInstance::get(constants::api))
};
};
class FixtureDeviceSwapchain: public Fixture_SurfaceGpu
{
public:
FixtureDeviceSwapchain() = default;
[[nodiscard]] auto device() -> lt::renderer::IDevice *
{
return m_device.get();
}
[[nodiscard]] auto swapchain() -> lt::renderer::ISwapchain *
{
return m_swapchain.get();
}
void recreate_swapchain()
{
m_swapchain.reset();
m_swapchain = lt::renderer::ISwapchain::create(
constants::api,
surface(),
gpu(),
m_device.get()
);
}
private:
lt::memory::Scope<lt::renderer::IDevice> m_device {
lt::renderer::IDevice::create(constants::api, gpu(), surface())
};
lt::memory::Scope<lt::renderer::ISwapchain> m_swapchain {
lt::renderer::ISwapchain::create(constants::api, surface(), gpu(), m_device.get())
};
};
class Fixture_RendererSystem: public Fixture_SurfaceSystem
{
public:
Fixture_RendererSystem() = default;
[[nodiscard]] auto renderer_system() -> lt::renderer::System &
{
return m_system;
}
[[nodiscard]] auto has_any_messages() const -> bool
{
return m_has_any_messages;
}
[[nodiscard]] auto has_any_messages_of(lt::renderer::MessageSeverity severity) const -> uint32_t
{
return m_severity_counter.contains(severity);
}
private:
static void messenger_callback(
lt::renderer::MessageSeverity severity,
lt::renderer::MessageType type,
lt::renderer::MessageData data,
std::any &user_data
)
{
std::ignore = type;
std::ignore = data;
auto *fixture = std::any_cast<Fixture_RendererSystem *>(user_data);
fixture->m_has_any_messages = true;
++fixture->m_severity_counter[severity];
}
std::unordered_map<lt::renderer::MessageSeverity, uint32_t> m_severity_counter;
bool m_has_any_messages {};
lt::renderer::System m_system = lt::renderer::System::CreateInfo {
.config = {
.target_api = constants::api,
.max_frames_in_flight = constants::frames_in_flight,
},
.registry = registry(),
.surface_entity = surface_entity(),
.messenger_info = lt::renderer::MessengerComponent::CreateInfo {
.severities = lt::renderer::MessageSeverity::all,
.types = lt::renderer::MessageType::all,
.callback = &messenger_callback,
.user_data = this,
}
};
};

View file

@ -2,11 +2,13 @@
namespace lt::renderer {
enum class API : uint8_t
enum class Api : uint8_t
{
Vulkan,
DirectX,
Metal,
none = 0,
vulkan,
direct_x,
metal,
};
}

View file

@ -29,7 +29,7 @@ enum class MessageType : uint8_t
all = general | validation | performance,
};
struct MessengerCallbackData
struct MessageData
{
std::string message;
};
@ -37,7 +37,7 @@ struct MessengerCallbackData
using Callback_T = std::function<void(
MessageSeverity message_severity,
MessageType message_type,
MessengerCallbackData data,
MessageData data,
std::any &user_data
)>;

View file

@ -9,7 +9,7 @@ namespace lt::renderer {
class IMessenger
{
public:
[[nodiscard]] static auto create(API target_api, class IInstance *instance, ecs::Entity entity)
[[nodiscard]] static auto create(Api target_api, class IInstance *instance, ecs::Entity entity)
-> memory::Scope<IMessenger>;
IMessenger() = default;

View file

@ -7,15 +7,22 @@
#include <memory/scope.hpp>
#include <renderer/api.hpp>
#include <renderer/components/messenger.hpp>
#include <renderer/frontend/renderer/renderer.hpp>
namespace lt::renderer {
class System: public app::ISystem
{
public:
/** config.max_frames_in_flight should not be higher than this value. */
static constexpr auto frames_in_flight_upper_limit = IRenderer::frames_in_flight_upper_limit;
/** config.max_frames_in_flight should not be lower than this value. */
static constexpr auto frames_in_flight_lower_limit = IRenderer::frames_in_flight_lower_limit;
struct Configuration
{
API target_api;
Api target_api;
uint32_t max_frames_in_flight;
};
@ -27,6 +34,8 @@ public:
memory::Ref<ecs::Registry> registry;
ecs::Entity surface_entity;
std::optional<MessengerComponent::CreateInfo> messenger_info;
};
System(CreateInfo info);
@ -47,7 +56,35 @@ public:
void tick(app::TickInfo tick) override;
void create_messenger_component(ecs::EntityId entity, MessengerComponent::CreateInfo info);
[[nodiscard]] auto get_surface() -> class ISurface *
{
return m_surface.get();
}
[[nodiscard]] auto get_gpu() -> class IGpu *
{
return m_gpu.get();
}
[[nodiscard]] auto get_device() -> class IDevice *
{
return m_device.get();
}
[[nodiscard]] auto get_swapchain() -> class ISwapchain *
{
return m_swapchain.get();
}
[[nodiscard]] auto get_renderer() -> class IRenderer *
{
return m_renderer.get();
}
[[nodiscard]] auto create_messenger_component(
ecs::EntityId entity,
MessengerComponent::CreateInfo info
) -> bool;
[[nodiscard]] auto get_last_tick_result() const -> const app::TickResult & override
{
@ -55,13 +92,23 @@ public:
}
private:
API m_api;
Api m_api;
memory::Ref<ecs::Registry> m_registry;
ecs::Entity m_surface_entity;
memory::Scope<class IContext> m_context;
memory::Scope<class IMessenger> m_messenger;
class IInstance *m_instance;
memory::Scope<class ISurface> m_surface;
memory::Scope<class IGpu> m_gpu;
memory::Scope<class IDevice> m_device;
memory::Scope<class ISwapchain> m_swapchain;
memory::Scope<class IRenderer> m_renderer;