Compare commits

...

9 commits

58 changed files with 1167 additions and 896 deletions

View file

@ -1,6 +1,4 @@
# Light # Light
See docs.light7734.com for a comprehensive project documentation See docs.light7734.com for a comprehensive project documentation
<!---FUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUCK ###### “No great thing comes into being all at once, any more than a cluster of grapes or a fig. If you tell me, 'I want a fig,' I will answer that it needs time. Let it flower first, then put forth its fruit and then ripen. I say then, if the fig tree's fruit is not brought to perfection suddenly in a single hour, would you expect to gather the fruit of a persons mind so soon and so easily? I tell you, you must not expect it.” —Epictetus, Discourses 1.15.7-8
MEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!!!!!!!!-->

Binary file not shown.

View file

@ -1,10 +0,0 @@
#version 440 core
in vec4 vso_FragmentColor;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = vso_FragmentColor;
}

Binary file not shown.

View file

@ -1,17 +0,0 @@
#version 440 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec4 a_Color;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 viewProjection;
};
layout(location = 0) out vec4 vso_FragmentColor;
void main()
{
gl_Position = viewProjection * a_Position;
vso_FragmentColor = a_Color;
}

Binary file not shown.

View file

@ -1,12 +0,0 @@
#version 450 core
in vec2 vso_TexCoord;
uniform sampler2D u_Texture;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = texture(u_Texture, vso_TexCoord);
}

Binary file not shown.

View file

@ -1,19 +0,0 @@
#version 450 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 a_TexCoord;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 u_ViewProjection;
};
layout(location = 0) out vec2 vso_TexCoord;
void main()
{
gl_Position = u_ViewProjection * a_Position;
vso_TexCoord = a_TexCoord;
}

View file

@ -1,14 +0,0 @@
#version 450 core
in vec4 vso_Tint;
in vec2 vso_TexCoord;
uniform sampler2D u_Texture;
out vec4 fso_FragmentColor;
void main()
{
fso_FragmentColor = texture(u_Texture, vso_TexCoord) * vso_Tint;
}

View file

@ -1,21 +0,0 @@
#version 450 core
layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec4 a_Tint;
layout(location = 2) in vec2 a_TexCoord;
layout(std140, binding = 0) uniform ub_ViewProjection
{
mat4 u_ViewProjection;
};
out vec4 vso_Tint;
out vec2 vso_TexCoord;
void main()
{
gl_Position = u_ViewProjection * a_Position;
vso_Tint = a_Tint;
vso_TexCoord = a_TexCoord;
}

View file

@ -0,0 +1 @@
The quick brown fox jumps over the lazy dog

View file

@ -0,0 +1,10 @@
#version 450 core
layout(location = 0) in vec3 in_frag_color;
layout(location = 0) out vec4 out_frag_color;
void main()
{
out_frag_color = vec4(in_frag_color, 1.0);
}

View file

@ -0,0 +1,21 @@
#version 450 core
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
vec3 colors[3] = vec3[](
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
layout(location = 0) out vec3 out_frag_color;
void main()
{
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
out_frag_color = colors[gl_VertexIndex];
}

View file

@ -6,9 +6,8 @@ add_subdirectory(./logger)
add_subdirectory(./debug) add_subdirectory(./debug)
add_subdirectory(./math) add_subdirectory(./math)
# #
# add_subdirectory(./asset_baker) add_subdirectory(./asset_baker)
# add_subdirectory(./asset_parser) add_subdirectory(./assets)
# add_subdirectory(./asset_manager)
# #
add_subdirectory(./camera) add_subdirectory(./camera)
add_subdirectory(./input) add_subdirectory(./input)

View file

@ -1,9 +1,17 @@
add_executable_module( add_library_module(libasset_baker
asset_baker entrypoint/baker.cpp bakers.cpp
)
target_link_libraries(libasset_baker
PUBLIC
assets
logger
lt_debug
)
add_test_module(libasset_baker
bakers.test.cpp
) )
target_link_libraries( add_executable_module(asset_baker
asset_baker entrypoint/baker.cpp
PRIVATE asset_parser
PRIVATE logger
) )
target_link_libraries(asset_baker PRIVATE libasset_baker)

View file

View file

@ -0,0 +1,7 @@
#include <asset_baker/bakers.hpp>
#include <test/test.hpp>
using ::lt::test::Case;
using ::lt::test::Suite;
// TODO(Light): add asset baking tests!

View file

@ -1,68 +1,5 @@
#include <asset_baker/bakers.hpp> #include <asset_baker/bakers.hpp>
#include <asset_parser/assets/text.hpp> #include <assets/shader.hpp>
#include <asset_parser/assets/texture.hpp>
#include <asset_parser/parser.hpp>
#include <filesystem>
#include <logger/logger.hpp>
void try_packing_texture(
const std::filesystem::path &in_path,
const std::filesystem::path &out_path
)
{
auto texture_loader = lt::TextureLoaderFactory::create(in_path.extension().string());
if (!texture_loader)
{
// Don't log anything; this is expected.
return;
}
try
{
Assets::TextureAsset::pack(texture_loader->load(in_path), out_path);
log_inf("Packed a texture asset:");
log_inf("\tloader : {}", texture_loader->get_name());
log_inf("\tin path: {}", in_path.string());
log_inf("\tout path: {}", out_path.string());
}
catch (const std::exception &exp)
{
log_err("Failed to pack texture asset:");
log_err("\tloader : {}", texture_loader->get_name());
log_err("\tin path : {}", in_path.string());
log_err("\tout path: {}", out_path.string());
log_err("\texp.what: {}", exp.what());
}
}
void try_packing_text(const std::filesystem::path &in_path, const std::filesystem::path &out_path)
{
auto text_loader = lt::TextLoaderFactory::create(in_path.extension().string());
if (!text_loader)
{
// Don't log anything; this is expected.
return;
}
try
{
Assets::TextAsset::pack(text_loader->load(in_path), out_path);
log_inf("Packed a text asset:");
log_inf("\tloader : {}", text_loader->get_name());
log_inf("\tin path: {}", in_path.string());
log_inf("\tout path: {}", out_path.string());
}
catch (const std::exception &exp)
{
log_err("Failed to pack a text asset:");
log_err("\tloader : {}", text_loader->get_name());
log_err("\tin path : {}", in_path.string());
log_err("\tout path: {}", out_path.string());
log_err("\texp.what: {}", exp.what());
}
}
auto main(int argc, char *argv[]) -> int32_t auto main(int argc, char *argv[]) -> int32_t
try try
@ -81,12 +18,16 @@ try
} }
const auto &in_path = directory_iterator.path(); const auto &in_path = directory_iterator.path();
const auto out_path = std::format("{}.asset", in_path.c_str());
auto out_path = in_path; if (in_path.extension() == ".vert")
out_path.replace_extension(".asset"); {
bake_shader(in_path, out_path, lt::assets::ShaderAsset::Type::vertex);
try_packing_texture(in_path, out_path); }
try_packing_text(in_path, out_path); else if (in_path.extension() == ".frag")
{
bake_shader(in_path, out_path, lt::assets::ShaderAsset::Type::fragment);
}
} }
return EXIT_SUCCESS; return EXIT_SUCCESS;

View file

@ -1,114 +1,64 @@
#pragma once #pragma once
#include <asset_parser/assets/text.hpp> #include <assets/shader.hpp>
#include <asset_parser/assets/texture.hpp>
#include <filesystem>
#include <logger/logger.hpp>
#include <string_view>
#include <unordered_set>
namespace lt { inline void bake_shader(
const std::filesystem::path &in_path,
class Loader const std::filesystem::path &out_path,
lt::assets::ShaderAsset::Type type
)
{ {
public: using lt::assets::ShaderAsset;
[[nodiscard]] virtual auto get_name() const -> std::string_view = 0; using enum lt::assets::ShaderAsset::Type;
Loader() = default; auto glsl_path = in_path.string();
auto spv_path = std::format("{}.spv", glsl_path);
log_trc(
"Compiling {} shader {} -> {}",
type == vertex ? "vertex" : "fragment",
glsl_path,
spv_path
);
Loader(Loader &&) = default; // Don't bother linking to shaderc, just invoke the command with a system call.
// NOLINTNEXTLINE(concurrency-mt-unsafe)
Loader(const Loader &) = delete; system(
auto operator=(Loader &&) -> Loader & = default;
auto operator=(const Loader &) -> Loader & = delete;
virtual ~Loader() = default;
private:
};
class TextureLoader: public Loader
{
public:
TextureLoader() = default;
[[nodiscard]] virtual auto load(std::filesystem::path file_path) const
-> Assets::TextureAsset::PackageData
= 0;
};
class TextureLoaderFactory
{
public:
static auto create(std::string_view file_extension) -> std::unique_ptr<TextureLoader>
{
return {};
}
};
class TextLoader: Loader
{
public:
[[nodiscard]] static auto get_supported_extensions() -> std::unordered_set<std::string_view>
{
return { ".glsl", ".txt", ".hlsl" };
}
[[nodiscard]] auto get_name() const -> std::string_view override
{
return "TextLoader";
}
[[nodiscard]] auto load(const std::filesystem::path &file_path) const
-> Assets::TextAsset::PackageData
{
auto stream = std::ifstream { file_path, std::ios::binary };
if (!stream.good())
{
throw std::runtime_error {
std::format( std::format(
"Failed to open ifstream for text loading of file: {}", "glslc --target-env=vulkan1.4 -std=450core -fshader-stage={} {} -o {}",
file_path.string() type == vertex ? "vert" : "frag",
), glsl_path,
}; spv_path
} )
.c_str()
);
auto file_size = std::filesystem::file_size(file_path); auto stream = std::ifstream(spv_path, std::ios::binary);
lt::ensure(
stream.is_open(),
"Failed to open compiled {} shader at: {}",
type == vertex ? "vert" : "frag",
spv_path
);
auto text_blob = Assets::Blob(file_size); stream.seekg(0, std::ios::end);
const auto size = stream.tellg();
stream.read((char *)(text_blob.data()), static_cast<long>(file_size)); // NOLINT auto bytes = std::vector<std::byte>(size);
stream.seekg(0, std::ios::beg);
stream.read((char *)bytes.data(), size); // NOLINT
log_dbg("BYTES: {}", bytes.size());
stream.close();
std::filesystem::remove(spv_path);
const auto metadata = Assets::Asset::Metadata { ShaderAsset::pack(
.type = Assets::Asset::Type::Text, out_path,
}; lt::assets::AssetMetadata {
.version = lt::assets::current_version,
const auto text_metadata = Assets::TextAsset::Metadata { .type = ShaderAsset::asset_type_identifier,
.lines = {}, },
}; ShaderAsset::Metadata {
.type = type,
return Assets::TextAsset::PackageData { },
.metadata = metadata, std::move(bytes)
.text_metadata = {}, );
.text_blob = std::move(text_blob), }
};
}
};
class TextLoaderFactory
{
public:
static auto create(std::string_view file_extension) -> std::unique_ptr<TextLoader>
{
if (TextLoader::get_supported_extensions().contains(file_extension))
{
return std::make_unique<TextLoader>();
}
return {};
}
};
} // namespace lt

View file

@ -1,9 +0,0 @@
add_library_module(asset_manager
asset_manager.cpp
)
target_link_libraries(
asset_manager
PUBLIC asset_parser
PRIVATE logger
)

View file

@ -1,92 +0,0 @@
#include <asset_manager/asset_manager.hpp>
#include <asset_parser/assets/text.hpp>
#include <asset_parser/assets/texture.hpp>
#include <logger/logger.hpp>
#include <renderer/graphics_context.hpp>
#include <renderer/shader.hpp>
#include <renderer/texture.hpp>
namespace lt {
/* static */ auto AssetManager::instance() -> AssetManager &
{
static auto instance = AssetManager {};
return instance;
}
void AssetManager::load_shader_impl(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
)
{
try
{
log_trc("Loading shader:");
log_trc("\tname : {}", name);
log_trc("\tvertex path: {}", vertex_path.string());
log_trc("\tpixel path : {}", pixel_path.string());
m_shaders[name] = Ref<Shader>(Shader::create(
get_or_load_text_asset(vertex_path.string()),
get_or_load_text_asset(pixel_path),
GraphicsContext::get_shared_context()
));
}
catch (const std::exception &exp)
{
log_err("Failed to load shader:");
log_err("\tname : {}", name);
log_err("\tvertex path: {}", vertex_path.string());
log_err("\tpixel path : {}", pixel_path.string());
log_err("\texception : {}", exp.what());
}
}
void AssetManager::load_texture_impl(const std::string &name, const std::filesystem::path &path)
{
try
{
log_trc("Loading texture:");
log_trc("\tname: {}", name);
log_trc("\tpath: {}", path.string());
m_textures[name] = Ref<Texture>(
Texture::create(get_or_load_texture_asset(path), GraphicsContext::get_shared_context())
);
}
catch (const std::exception &exp)
{
log_err("Failed to load texture:");
log_err("\tname : {}", name);
log_err("\tpath : {}", path.string());
log_err("\texception: {}", exp.what());
}
}
auto AssetManager::get_or_load_text_asset(const std::filesystem::path &path)
-> Ref<Assets::TextAsset>
{
const auto key = std::filesystem::canonical(path).string();
if (!m_text_assets.contains(key))
{
m_text_assets.emplace(key, create_ref<Assets::TextAsset>(path));
}
return m_text_assets[key];
}
auto AssetManager::get_or_load_texture_asset(const std::filesystem::path &path)
-> Ref<Assets::TextureAsset>
{
const auto key = std::filesystem::canonical(path).string();
if (!m_texture_assets.contains(key))
{
m_texture_assets.emplace(key, create_ref<Assets::TextureAsset>(path));
}
return m_texture_assets[key];
}
} // namespace lt

View file

@ -1,78 +0,0 @@
#pragma once
#include <filesystem>
namespace Assets {
class TextAsset;
class TextureAsset;
} // namespace Assets
namespace lt {
class Shader;
class Texture;
/**
* Asset is the data on the disk.
* Resource is the data on the gpu/cpu
*
* eg. TextureAsset is the file on the disk
* eg. Texture is the representation of it in the GPU
*/
class AssetManager
{
public:
static void load_shader(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
)
{
instance().load_shader_impl(name, vertex_path, pixel_path);
}
static void load_texture(const std::string &name, const std::filesystem::path &path)
{
instance().load_texture_impl(name, path);
}
static auto get_shader(const std::string &name) -> Ref<Shader>
{
return instance().m_shaders[name];
}
static auto get_texture(const std::string &name) -> Ref<Texture>
{
return instance().m_textures[name];
}
private:
AssetManager() = default;
static auto instance() -> AssetManager &;
void load_shader_impl(
const std::string &name,
const std::filesystem::path &vertex_path,
const std::filesystem::path &pixel_path
);
void load_texture_impl(const std::string &name, const std::filesystem::path &path);
auto get_or_load_text_asset(const std::filesystem::path &path) -> Ref<Assets::TextAsset>;
auto get_or_load_texture_asset(const std::filesystem::path &path) -> Ref<Assets::TextureAsset>;
std::unordered_map<std::string, Ref<Assets::TextAsset>> m_text_assets;
std::unordered_map<std::string, Ref<Assets::TextureAsset>> m_texture_assets;
std::unordered_map<std::string, Ref<Shader>> m_shaders;
std::unordered_map<std::string, Ref<Texture>> m_textures;
};
} // namespace lt

View file

@ -1,10 +0,0 @@
add_library_module(asset_parser
parser.cpp
assets/texture.cpp
assets/text.cpp
)
target_link_libraries(
asset_parser
PRIVATE logger
)

View file

@ -1,164 +0,0 @@
#include <asset_parser/assets/texture.hpp>
namespace Assets {
/* static */ void TextureAsset::pack(const PackageData &data, const std::filesystem::path &out_path)
{
const auto &[metadata, texture_metadata, pixels] = data;
auto stream = std::ofstream { out_path, std::ios::binary | std::ios::trunc };
if (!stream.is_open())
{
throw std::runtime_error {
std::format("Failed to open ofstream for packing texture at: {}", out_path.string())
};
}
stream.seekp(0);
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
stream.write((char *)&current_version, sizeof(current_version));
stream.write((char *)&metadata, sizeof(metadata));
stream.write((char *)&texture_metadata, sizeof(texture_metadata));
constexpr auto number_of_blobs = uint32_t { 1 };
stream.write((char *)&number_of_blobs, sizeof(number_of_blobs));
auto pixels_metadata = BlobMetadata {
.tag = BlobMetadata::Tag::color,
.offset = static_cast<size_t>(stream.tellp()) + sizeof(BlobMetadata),
.compression_type = CompressionType::None,
.compressed_size = pixels.size(),
.uncompressed_size = pixels.size(),
};
stream.write((char *)&pixels_metadata, sizeof(pixels_metadata));
stream.write((char *)&pixels[0], static_cast<long>(pixels.size()));
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
}
TextureAsset::TextureAsset(const std::filesystem::path &path)
{
m_stream = std::ifstream { path, std::ios::binary };
if (!m_stream.is_open())
{
throw std::runtime_error {
std::format("Failed to open ifstream for loading texture asset at: {}", path.string())
};
}
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
m_stream.read((char *)&version, sizeof(version));
m_stream.read((char *)&m_asset_metadata, sizeof(m_asset_metadata));
m_stream.read((char *)&m_metadata, sizeof(m_metadata));
auto num_blobs = uint32_t {};
m_stream.read((char *)&num_blobs, sizeof(num_blobs));
if (num_blobs != 1)
{
throw std::runtime_error {
std::format("Failed to load texture asset: invalid number of blobs: {}", num_blobs)
};
}
m_stream.read((char *)&m_pixel_blob_metadata, sizeof(m_pixel_blob_metadata));
if (m_pixel_blob_metadata.tag != BlobMetadata::Tag::color)
{
throw std::runtime_error {
std::format(
"Failed to load texture asset: invalid blob tag, expected {}, got {}",
std::to_underlying(BlobMetadata::Tag::color),
std::to_underlying(m_pixel_blob_metadata.tag)
),
};
}
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
}
void TextureAsset::unpack_blob(
BlobMetadata::Tag tag,
std::byte *destination,
size_t destination_capacity
)
{
if (tag != BlobMetadata::Tag::color)
{
throw std::runtime_error {
std::format("Invalid tag for unpack_blob of TextureAsset: {}", std::to_underlying(tag))
};
}
m_stream.seekg(static_cast<long>(m_pixel_blob_metadata.offset));
switch (m_pixel_blob_metadata.compression_type)
{
case Assets::CompressionType::None:
if (m_pixel_blob_metadata.uncompressed_size != m_pixel_blob_metadata.compressed_size)
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: "
"compressed/uncompressed size mismatch for no compression "
"type"
);
}
if (m_pixel_blob_metadata.uncompressed_size > destination_capacity)
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: "
"uncompressed_size > destination_capacity, unpacking "
"would result in segfault"
);
}
if (!m_stream.is_open())
{
throw std::runtime_error(
"Failed to unpack blob from TextureAsset: ifstream is "
"closed"
);
}
m_stream.read(
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
(char *)destination,
static_cast<long>(m_pixel_blob_metadata.uncompressed_size)
);
return;
default:
throw std::runtime_error(
std::format(
"Failed to unpack blob from TextureAsset: unsupported "
"compression type: {}",
std::to_underlying(m_pixel_blob_metadata.compression_type)
)
);
}
}
[[nodiscard]] auto TextureAsset::get_asset_metadata() const -> const Asset::Metadata &
{
return m_asset_metadata;
}
[[nodiscard]] auto TextureAsset::get_metadata() const -> const Metadata &
{
return m_metadata;
}
[[nodiscard]] auto TextureAsset::get_blob_metadata(BlobMetadata::Tag tag) const
-> const BlobMetadata &
{
if (tag != BlobMetadata::Tag::color)
{
throw std::runtime_error { std::format(
"Invalid tag for get_blob_metadata of TextureAsset: {}",
std::to_underlying(tag)
) };
}
return m_pixel_blob_metadata;
}
} // namespace Assets

View file

@ -1,5 +0,0 @@
#include <asset_parser/parser.hpp>
namespace Assets {
} // namespace Assets

View file

@ -1,58 +0,0 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <asset_parser/parser.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
namespace Assets {
class TextAsset: public Asset
{
public:
struct Metadata
{
uint32_t lines {};
};
/** Data required to pack a text asset */
struct PackageData
{
Asset::Metadata metadata;
Metadata text_metadata;
Blob text_blob;
};
static void pack(const PackageData &data, const std::filesystem::path &out_path);
TextAsset(const std::filesystem::path &path);
void unpack_blob(
BlobMetadata::Tag tag,
std::byte *destination,
size_t destination_capacity
) const;
[[nodiscard]] auto get_asset_metadata() const -> const Asset::Metadata &;
[[nodiscard]] auto get_metadata() const -> const Metadata &;
[[nodiscard]] auto get_blob_metadata(BlobMetadata::Tag tag) const -> const BlobMetadata &;
private:
uint32_t version {};
Asset::Metadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_text_blob_metadata {};
mutable std::ifstream m_stream;
};
} // namespace Assets

View file

@ -1,64 +0,0 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <asset_parser/parser.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
namespace Assets {
class TextureAsset: public Asset
{
public:
enum class Format : uint32_t // NOLINT(performance-enum-size)
{
None = 0,
RGBA8,
};
struct Metadata
{
Format format;
uint32_t num_components;
std::array<uint32_t, 3> pixel_size;
};
/** Data required to pack a texture asset */
struct PackageData
{
Asset::Metadata metadata;
Metadata texture_metadata;
Blob pixels;
};
static void pack(const PackageData &data, const std::filesystem::path &out_path);
TextureAsset(const std::filesystem::path &path);
void unpack_blob(BlobMetadata::Tag tag, std::byte *destination, size_t destination_capacity);
[[nodiscard]] auto get_asset_metadata() const -> const Asset::Metadata &;
[[nodiscard]] auto get_metadata() const -> const Metadata &;
[[nodiscard]] auto get_blob_metadata(BlobMetadata::Tag tag) const -> const BlobMetadata &;
private:
uint32_t version {};
Asset::Metadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_pixel_blob_metadata {};
std::ifstream m_stream;
};
} // namespace Assets

View file

@ -1,14 +0,0 @@
#pragma once
#include <cstdint>
namespace Assets {
enum class CompressionType : uint32_t // NOLINT(performance-enum-size)
{
None,
LZ4,
LZ4HC,
};
}

View file

@ -1,68 +0,0 @@
#pragma once
#include <asset_parser/compressors/compressors.hpp>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <logger/logger.hpp>
#include <utility>
#include <vector>
namespace Assets {
constexpr auto current_version = uint32_t { 1 };
struct BlobMetadata
{
enum class Tag : uint8_t
{
text,
color,
depth,
vertices,
indices,
};
Tag tag;
size_t offset;
CompressionType compression_type;
size_t compressed_size;
size_t uncompressed_size;
};
using Blob = std::vector<std::byte>;
class Asset
{
public:
enum class Type : uint32_t // NOLINT(performance-enum-size)
{
None,
Texture,
Text,
Mesh,
Material,
};
struct Metadata
{
Type type;
};
Asset() = default;
/** Directly unpacks from disk to the destination.
*
* @note The destination MUST have at least blob_metadata.unpacked_size bytes available for
* writing, otherwise segfault could occur!
*/
void unpack_blob(BlobMetadata::Tag blob_tag, std::byte *destination);
};
} // namespace Assets

View file

@ -0,0 +1,14 @@
add_library_module(assets
shader.cpp
)
target_link_libraries(
assets
PUBLIC
logger
lt_debug
)
add_test_module(assets
shader.test.cpp
)

View file

@ -0,0 +1,73 @@
#include <assets/shader.hpp>
namespace lt::assets {
ShaderAsset::ShaderAsset(const std::filesystem::path &path)
: m_stream(path, std::ios::binary | std::ios::beg)
{
constexpr auto total_metadata_size = //
sizeof(AssetMetadata) //
+ sizeof(Metadata) //
+ sizeof(BlobMetadata);
ensure(m_stream.is_open(), "Failed to open shader asset at: {}", path.string());
m_stream.seekg(0, std::ifstream::end);
const auto file_size = static_cast<size_t>(m_stream.tellg());
ensure(
file_size > total_metadata_size,
"Failed to open shader asset at: {}, file smaller than metadata: {} < {}",
path.string(),
total_metadata_size,
file_size
);
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
m_stream.seekg(0, std::ifstream::beg);
m_stream.read((char *)&m_asset_metadata, sizeof(m_asset_metadata));
m_stream.read((char *)&m_metadata, sizeof(m_metadata));
m_stream.read((char *)&m_code_blob_metadata, sizeof(m_code_blob_metadata));
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
ensure(
m_asset_metadata.type == asset_type_identifier,
"Failed to open shader asset at: {}, incorrect asset type: {} != {}",
path.string(),
m_asset_metadata.type,
asset_type_identifier
);
ensure(
m_asset_metadata.version == current_version,
"Failed to open shader asset at: {}, version mismatch: {} != {}",
path.string(),
m_asset_metadata.version,
current_version
);
ensure(
std::to_underlying(m_metadata.type) <= std::to_underlying(Type::compute),
"Failed to open shader asset at: {}, invalid shader type: {}",
path.string(),
std::to_underlying(m_metadata.type)
);
ensure(
m_code_blob_metadata.tag == std::to_underlying(BlobTag::code),
"Failed to open shader asset at: {}, invalid blob tag: {}",
path.string(),
m_code_blob_metadata.tag
);
ensure(
m_code_blob_metadata.offset + m_code_blob_metadata.compressed_size <= file_size,
"Failed to open shader asset at: {}, file smaller than blob: {} > {} + {}",
path.string(),
file_size,
m_code_blob_metadata.offset,
m_code_blob_metadata.compressed_size
);
}
} // namespace lt::assets

View file

@ -0,0 +1,89 @@
#include <assets/shader.hpp>
#include <ranges>
#include <test/test.hpp>
using ::lt::assets::AssetMetadata;
using ::lt::assets::BlobMetadata;
using ::lt::assets::ShaderAsset;
using ::lt::test::Case;
using ::lt::test::expect_eq;
using ::lt::test::expect_throw;
using ::lt::test::expect_true;
using ::lt::test::Suite;
const auto test_data_path = std::filesystem::path { "./data/test_assets" };
const auto tmp_path = std::filesystem::path { "/tmp/lt_assets_tests/" };
Suite raii = "shader_raii"_suite = [] {
std::filesystem::current_path(test_data_path);
std::filesystem::create_directories(tmp_path);
Case { "happy path won't throw" } = [] {
};
Case { "many won't freeze/throw" } = [] {
};
Case { "unhappy path throws" } = [] {
expect_throw([] { ShaderAsset { "random_path" }; });
};
};
// NOLINTNEXTLINE(cppcoreguidelines-interfaces-global-init)
Suite packing = "shader_pack"_suite = [] {
Case { "" } = [] {
const auto out_path = tmp_path / "shader_packing";
auto dummy_blob = lt::assets::Blob {};
for (auto idx : std::views::iota(0, 255))
{
dummy_blob.emplace_back(static_cast<std::byte>(idx));
}
const auto expected_size = //
sizeof(AssetMetadata) //
+ sizeof(ShaderAsset::Metadata) //
+ sizeof(BlobMetadata) //
+ dummy_blob.size();
ShaderAsset::pack(
out_path,
lt::assets::AssetMetadata {
.version = lt::assets::current_version,
.type = ShaderAsset::asset_type_identifier,
},
ShaderAsset::Metadata {
.type = ShaderAsset::Type::vertex,
},
std::move(dummy_blob)
);
auto stream = std::ifstream {
out_path,
std::ios::binary | std::ios::beg,
};
expect_true(stream.is_open());
stream.seekg(0, std::ios::end);
const auto file_size = static_cast<size_t>(stream.tellg());
expect_eq(file_size, expected_size);
stream.close();
auto shader_asset = ShaderAsset { out_path };
const auto &asset_metadata = shader_asset.get_asset_metadata();
expect_eq(asset_metadata.type, ShaderAsset::asset_type_identifier);
expect_eq(asset_metadata.version, lt::assets::current_version);
const auto &metadata = shader_asset.get_metadata();
expect_eq(metadata.type, ShaderAsset::Type::vertex);
auto blob = shader_asset.unpack(ShaderAsset::BlobTag::code);
expect_eq(blob.size(), 255);
for (auto idx : std::views::iota(0, 255))
{
expect_eq(blob[idx], static_cast<std::byte>(idx));
}
};
};

View file

@ -0,0 +1,3 @@
#pragma once
// TO BE DOOO

View file

@ -0,0 +1,42 @@
#pragma once
namespace lt::assets {
using Type_T = std::array<const char, 16>;
using Tag_T = uint8_t;
using Version = uint8_t;
using Blob = std::vector<std::byte>;
constexpr auto current_version = Version { 1u };
enum class CompressionType : uint8_t
{
none,
lz4,
lz4_hc,
};
struct AssetMetadata
{
Version version;
Type_T type;
};
struct BlobMetadata
{
Tag_T tag;
size_t offset;
CompressionType compression_type;
size_t compressed_size;
size_t uncompressed_size;
};
} // namespace lt::assets

View file

@ -0,0 +1,132 @@
#pragma once
#include <assets/metadata.hpp>
namespace lt::assets {
class ShaderAsset
{
public:
static constexpr auto asset_type_identifier = Type_T { "SHADER_________" };
enum class BlobTag : Tag_T
{
code,
};
enum class Type : uint8_t
{
vertex,
fragment,
geometry,
compute,
};
struct Metadata
{
Type type;
};
static void pack(
const std::filesystem::path &destination,
AssetMetadata asset_metadata,
Metadata metadata,
Blob code_blob
)
{
auto stream = std::ofstream {
destination,
std::ios::binary | std::ios::trunc,
};
ensure(stream.is_open(), "Failed to pack shader asset to {}", destination.string());
// NOLINTBEGIN(cppcoreguidelines-pro-type-cstyle-cast)
stream.write((char *)&asset_metadata, sizeof(asset_metadata));
stream.write((char *)&metadata, sizeof(metadata));
auto code_blob_metadata = BlobMetadata {
.tag = std::to_underlying(BlobTag::code),
.offset = static_cast<size_t>(stream.tellp()) + sizeof(BlobMetadata),
.compression_type = CompressionType::none,
.compressed_size = code_blob.size(),
.uncompressed_size = code_blob.size(),
};
stream.write((char *)&code_blob_metadata, sizeof(BlobMetadata));
stream.write((char *)code_blob.data(), static_cast<long long>(code_blob.size()));
// NOLINTEND(cppcoreguidelines-pro-type-cstyle-cast)
}
ShaderAsset(const std::filesystem::path &path);
[[nodiscard]] auto get_asset_metadata() const -> const AssetMetadata &
{
return m_asset_metadata;
}
[[nodiscard]] auto get_metadata() const -> const Metadata &
{
return m_metadata;
}
[[nodiscard]] auto get_blob_metadata(BlobTag tag) const -> const BlobMetadata &
{
ensure(
tag == BlobTag::code,
"Invalid blob tag for shader asset: {}",
std::to_underlying(tag)
);
return m_code_blob_metadata;
}
void unpack_to(BlobTag tag, std::span<std::byte> destination) const
{
ensure(
tag == BlobTag::code,
"Invalid blob tag for shader asset: {}",
std::to_underlying(tag)
);
ensure(
destination.size() >= m_code_blob_metadata.uncompressed_size,
"Failed to unpack shader blob {} to destination ({}) of size {} since it's smaller "
"than the blobl's uncompressed size: {}",
std::to_underlying(tag),
(size_t)(destination.data()), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
destination.size(),
m_code_blob_metadata.uncompressed_size
);
m_stream.seekg(static_cast<long long>(m_code_blob_metadata.offset));
m_stream.read(
(char *)destination.data(), // NOLINT(cppcoreguidelines-pro-type-cstyle-cast)
static_cast<long long>(m_code_blob_metadata.uncompressed_size)
);
}
[[nodiscard]] auto unpack(BlobTag tag) const -> Blob
{
ensure(
tag == BlobTag::code,
"Invalid blob tag for shader asset: {}",
std::to_underlying(tag)
);
auto blob = Blob(m_code_blob_metadata.uncompressed_size);
unpack_to(tag, blob);
return blob;
}
private:
AssetMetadata m_asset_metadata {};
Metadata m_metadata {};
BlobMetadata m_code_blob_metadata {};
mutable std::ifstream m_stream;
};
} // namespace lt::assets

View file

@ -6,6 +6,8 @@ add_library_module(renderer
vk/context/device.cpp vk/context/device.cpp
vk/context/swapchain.cpp vk/context/swapchain.cpp
vk/context/context.cpp vk/context/context.cpp
vk/renderer/pass.cpp
vk/renderer/renderer.cpp
vk/pipeline.cpp vk/pipeline.cpp
) )
@ -14,6 +16,8 @@ PUBLIC
app app
ecs ecs
memory memory
assets
time
PRIVATE PRIVATE
surface surface
pthread pthread
@ -28,6 +32,8 @@ add_test_module(renderer
vk/context/device.test.cpp vk/context/device.test.cpp
vk/context/swapchain.test.cpp vk/context/swapchain.test.cpp
vk/context/context.test.cpp vk/context/context.test.cpp
vk/renderer/pass.test.cpp
vk/renderer/renderer.test.cpp
vk/pipeline.test.cpp vk/pipeline.test.cpp
) )
target_link_libraries(renderer_tests target_link_libraries(renderer_tests

View file

@ -2,4 +2,16 @@
namespace lt::renderer { namespace lt::renderer {
void System::on_register()
{
} }
void System::on_unregister()
{
}
void System::tick(app::TickInfo tick)
{
}
} // namespace lt::renderer

View file

@ -3,9 +3,8 @@
namespace lt::renderer::vk { namespace lt::renderer::vk {
Context::Context(const ecs::Entity &surface_entity, Ref<app::SystemStats> system_stats) Context::Context(const ecs::Entity &surface_entity)
: m_stats(std::move(system_stats)) : m_surface(surface_entity)
, m_surface(surface_entity)
, m_device(m_surface) , m_device(m_surface)
, m_swapchain(m_device, m_surface) , m_swapchain(m_device, m_surface)
{ {

View file

@ -18,7 +18,7 @@ using memory::NullOnMove;
class Context class Context
{ {
public: public:
Context(const ecs::Entity &surface_entity, Ref<app::SystemStats> system_stats); Context(const ecs::Entity &surface_entity);
[[nodiscard]] auto instance() const -> VkInstance [[nodiscard]] auto instance() const -> VkInstance
{ {
@ -36,8 +36,6 @@ public:
} }
private: private:
Ref<app::SystemStats> m_stats;
Surface m_surface; Surface m_surface;
Device m_device; Device m_device;

View file

@ -10,9 +10,12 @@ Device::Device(const Surface &surface)
ensure(surface.vk(), "Failed to initialize vk::Device: null vulkan surface"); ensure(surface.vk(), "Failed to initialize vk::Device: null vulkan surface");
initialize_physical_device(); initialize_physical_device();
initialize_queue_indices(surface);
initialize_logical_device(); initialize_logical_device();
Instance::load_device_functions(m_device); Instance::load_device_functions(m_device);
initialize_queue(surface);
vk_get_device_queue(m_device, m_graphics_queue_family_index, 0, &m_graphics_queue);
vk_get_device_queue(m_device, m_present_queue_family_index, 0, &m_present_queue);
} }
Device::~Device() Device::~Device()
@ -54,12 +57,20 @@ void Device::initialize_logical_device()
{ {
const float priorities = .0f; const float priorities = .0f;
auto queue_info = VkDeviceQueueCreateInfo { auto queue_infos = std::vector<VkDeviceQueueCreateInfo> {};
auto queue_families = std::set { m_graphics_queue_family_index, m_present_queue_family_index };
for (auto queue_family : queue_families)
{
queue_infos.emplace_back(
VkDeviceQueueCreateInfo {
.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
.queueFamilyIndex = find_suitable_queue_family(), .queueFamilyIndex = queue_family,
.queueCount = 1u, .queueCount = 1u,
.pQueuePriorities = &priorities, .pQueuePriorities = &priorities,
}; }
);
}
auto physical_device_features = VkPhysicalDeviceFeatures {}; auto physical_device_features = VkPhysicalDeviceFeatures {};
@ -69,8 +80,8 @@ void Device::initialize_logical_device()
auto device_info = VkDeviceCreateInfo { auto device_info = VkDeviceCreateInfo {
.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
.queueCreateInfoCount = 1, .queueCreateInfoCount = static_cast<uint32_t>(queue_infos.size()),
.pQueueCreateInfos = &queue_info, .pQueueCreateInfos = queue_infos.data(),
.enabledExtensionCount = static_cast<uint32_t>(extensions.size()), .enabledExtensionCount = static_cast<uint32_t>(extensions.size()),
.ppEnabledExtensionNames = extensions.data(), .ppEnabledExtensionNames = extensions.data(),
.pEnabledFeatures = &physical_device_features, .pEnabledFeatures = &physical_device_features,
@ -84,30 +95,28 @@ void Device::initialize_logical_device()
[[nodiscard]] auto Device::find_suitable_queue_family() const -> uint32_t [[nodiscard]] auto Device::find_suitable_queue_family() const -> uint32_t
{ {
auto count = 0u; // auto count = 0u;
vk_get_physical_device_queue_family_properties(m_physical_device, &count, nullptr); // vk_get_physical_device_queue_family_properties(m_physical_device, &count, nullptr);
ensure(count != 0u, "Failed to find any physical devices with Vulkan support"); // ensure(count != 0u, "Failed to find any physical devices with Vulkan support");
//
auto families = std::vector<VkQueueFamilyProperties>(count); // auto families = std::vector<VkQueueFamilyProperties>(count);
vk_get_physical_device_queue_family_properties(m_physical_device, &count, families.data()); // vk_get_physical_device_queue_family_properties(m_physical_device, &count, families.data());
//
const auto required_flags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT; // const auto required_flags = VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT;
for (auto idx = 0u; auto &family : families) // for (auto idx = 0u; auto &family : families)
{ // {
if ((family.queueFlags & required_flags) == required_flags) // if ((family.queueFlags & required_flags) == required_flags)
{ // {
return idx; // return idx;
} // }
} // }
//
ensure(false, "Failed to find a suitable Vulkan queue family"); // ensure(false, "Failed to find a suitable Vulkan queue family");
return 0; // return 0;
} }
void Device::initialize_queue(const Surface &surface) void Device::initialize_queue_indices(const Surface &surface)
{ {
vk_get_device_queue(m_device, find_suitable_queue_family(), 0, &m_queue);
auto count = uint32_t { 0u }; auto count = uint32_t { 0u };
vk_get_physical_device_queue_family_properties(m_physical_device, &count, nullptr); vk_get_physical_device_queue_family_properties(m_physical_device, &count, nullptr);

View file

@ -35,12 +35,22 @@ public:
return { m_graphics_queue_family_index, m_present_queue_family_index }; return { m_graphics_queue_family_index, m_present_queue_family_index };
} }
[[nodiscard]] auto get_graphics_queue() const -> VkQueue
{
return m_graphics_queue;
}
[[nodiscard]] auto get_present_queue() const -> VkQueue
{
return m_present_queue;
}
private: private:
void initialize_physical_device(); void initialize_physical_device();
void initialize_logical_device(); void initialize_logical_device();
void initialize_queue(const class Surface &surface); void initialize_queue_indices(const class Surface &surface);
[[nodiscard]] auto find_suitable_queue_family() const -> uint32_t; [[nodiscard]] auto find_suitable_queue_family() const -> uint32_t;
@ -49,7 +59,9 @@ private:
memory::NullOnMove<VkDevice> m_device = VK_NULL_HANDLE; memory::NullOnMove<VkDevice> m_device = VK_NULL_HANDLE;
memory::NullOnMove<VkQueue> m_queue = VK_NULL_HANDLE; memory::NullOnMove<VkQueue> m_graphics_queue = VK_NULL_HANDLE;
memory::NullOnMove<VkQueue> m_present_queue = VK_NULL_HANDLE;
uint32_t m_graphics_queue_family_index = VK_QUEUE_FAMILY_IGNORED; uint32_t m_graphics_queue_family_index = VK_QUEUE_FAMILY_IGNORED;

View file

@ -86,6 +86,8 @@ PFN_vkCmdDraw vk_cmd_draw {};
PFN_vkCmdSetViewport vk_cmd_set_viewport {}; PFN_vkCmdSetViewport vk_cmd_set_viewport {};
PFN_vkCmdSetScissor vk_cmd_set_scissors {}; PFN_vkCmdSetScissor vk_cmd_set_scissors {};
PFN_vkResetCommandBuffer vk_reset_command_buffer {};
PFN_vkGetPhysicalDeviceSurfaceSupportKHR vk_get_physical_device_surface_support {}; PFN_vkGetPhysicalDeviceSurfaceSupportKHR vk_get_physical_device_surface_support {};
PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vk_get_physical_device_surface_capabilities {}; PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR vk_get_physical_device_surface_capabilities {};
PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vk_get_physical_device_surface_formats {}; PFN_vkGetPhysicalDeviceSurfaceFormatsKHR vk_get_physical_device_surface_formats {};
@ -370,6 +372,7 @@ void Instance::load_device_functions_impl(VkDevice device)
load_fn(vk_cmd_draw, "vkCmdDraw"); load_fn(vk_cmd_draw, "vkCmdDraw");
load_fn(vk_cmd_set_viewport, "vkCmdSetViewport"); load_fn(vk_cmd_set_viewport, "vkCmdSetViewport");
load_fn(vk_cmd_set_scissors, "vkCmdSetScissor"); load_fn(vk_cmd_set_scissors, "vkCmdSetScissor");
load_fn(vk_reset_command_buffer, "vkResetCommandBuffer");
} }
auto parse_message_type(VkDebugUtilsMessageTypeFlagsEXT message_types) -> const char * auto parse_message_type(VkDebugUtilsMessageTypeFlagsEXT message_types) -> const char *

View file

@ -7,7 +7,9 @@
namespace lt::renderer::vk { namespace lt::renderer::vk {
Swapchain::Swapchain(const Device &device, const Surface &surface): m_device(device.vk()) Swapchain::Swapchain(const Device &device, const Surface &surface)
: m_device(device.vk())
, m_resolution(surface.get_framebuffer_size())
{ {
auto *physical_device = device.physical(); auto *physical_device = device.physical();
@ -30,6 +32,7 @@ Swapchain::Swapchain(const Device &device, const Surface &surface): m_device(dev
constexpr auto desired_swapchain_image_count = uint32_t { 3 }; constexpr auto desired_swapchain_image_count = uint32_t { 3 };
const auto surface_format = formats.front(); const auto surface_format = formats.front();
const auto queue_indices = device.get_family_indices(); const auto queue_indices = device.get_family_indices();
m_format = surface_format.format;
auto create_info = VkSwapchainCreateInfoKHR { auto create_info = VkSwapchainCreateInfoKHR {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
@ -45,7 +48,7 @@ Swapchain::Swapchain(const Device &device, const Surface &surface): m_device(dev
.pQueueFamilyIndices = queue_indices.data(), .pQueueFamilyIndices = queue_indices.data(),
.preTransform = capabilities.currentTransform, .preTransform = capabilities.currentTransform,
.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR, .compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
.presentMode = VK_PRESENT_MODE_FIFO_RELAXED_KHR, // TODO(Light): parameterize .presentMode = VK_PRESENT_MODE_FIFO_KHR, // TODO(Light): parameterize
.clipped = VK_TRUE, .clipped = VK_TRUE,
.oldSwapchain = nullptr, .oldSwapchain = nullptr,
}; };
@ -84,6 +87,7 @@ Swapchain::Swapchain(const Device &device, const Surface &surface): m_device(dev
vkc(vk_create_image_view(device.vk(), &create_info, nullptr, &view)); vkc(vk_create_image_view(device.vk(), &create_info, nullptr, &view));
} }
} }
Swapchain::~Swapchain() Swapchain::~Swapchain()

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory/pointer_types/null_on_move.hpp> #include <memory/pointer_types/null_on_move.hpp>
#include <renderer/vk/debug/validation.hpp>
#include <renderer/vk/vulkan.hpp> #include <renderer/vk/vulkan.hpp>
namespace lt::renderer::vk { namespace lt::renderer::vk {
@ -20,6 +21,44 @@ public:
auto operator=(const Swapchain &) const -> Swapchain & = delete; auto operator=(const Swapchain &) const -> Swapchain & = delete;
[[nodiscard]] auto vk() const -> VkSwapchainKHR
{
return m_swapchain;
}
[[nodiscard]] auto get_resolution() const -> VkExtent2D
{
return m_resolution;
}
[[nodiscard]] auto get_format() const -> VkFormat
{
return m_format;
}
[[nodiscard]] auto create_framebuffers_for_pass(VkRenderPass pass) const
-> std::vector<VkFramebuffer>
{
auto framebuffers = std::vector<VkFramebuffer>(m_swapchain_image_views.size());
for (auto idx = 0u; auto &framebuffer : framebuffers)
{
auto info = VkFramebufferCreateInfo {
.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO,
.renderPass = pass,
.attachmentCount = 1u,
.pAttachments = &m_swapchain_image_views[idx++],
.width = m_resolution.width,
.height = m_resolution.height,
.layers = 1u
};
vkc(vk_create_frame_buffer(m_device, &info, nullptr, &framebuffer));
}
return framebuffers;
}
private: private:
[[nodiscard]] auto get_optimal_image_count( [[nodiscard]] auto get_optimal_image_count(
VkSurfaceCapabilitiesKHR capabilities, VkSurfaceCapabilitiesKHR capabilities,
@ -33,6 +72,10 @@ private:
std::vector<VkImage> m_swapchain_images; std::vector<VkImage> m_swapchain_images;
std::vector<VkImageView> m_swapchain_image_views; std::vector<VkImageView> m_swapchain_image_views;
VkExtent2D m_resolution;
VkFormat m_format;
}; };
} // namespace lt::renderer::vk } // namespace lt::renderer::vk

View file

@ -24,7 +24,7 @@ public:
.resolution = constants::resolution, .resolution = constants::resolution,
}); });
m_context = create_ref<Context>(*m_surface_entity, m_stats); m_context = create_ref<Context>(*m_surface_entity);
} }
[[nodiscard]] auto context() -> Ref<Context> [[nodiscard]] auto context() -> Ref<Context>
@ -38,8 +38,6 @@ public:
} }
private: private:
Ref<app::SystemStats> m_stats;
Ref<ecs::Registry> m_registry; Ref<ecs::Registry> m_registry;
Ref<surface::System> m_surface_system; Ref<surface::System> m_surface_system;

View file

@ -0,0 +1,280 @@
#pragma once
#include <assets/shader.hpp>
#include <renderer/vk/context/context.hpp>
#include <renderer/vk/debug/validation.hpp>
namespace lt::renderer::vk {
class Pass
{
public:
Pass(
Context &context,
lt::assets::ShaderAsset vertex_shader,
lt::assets::ShaderAsset fragment_shader
)
: m_device(context.device().vk())
{
// auto fragment_blob = vertex_shader.unpack(lt::assets::ShaderAsset::BlobTag::code);
auto *vertex_module = create_module(
vertex_shader.unpack(lt::assets::ShaderAsset::BlobTag::code)
);
auto *fragment_module = create_module(
fragment_shader.unpack(lt::assets::ShaderAsset::BlobTag::code)
);
auto shader_stages = std::array<VkPipelineShaderStageCreateInfo, 2> {
VkPipelineShaderStageCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = vertex_module,
.pName = "main",
},
VkPipelineShaderStageCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = fragment_module,
.pName = "main",
},
};
auto dynamic_states = std::array<VkDynamicState, 2> {
VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR,
};
auto dynamic_state = VkPipelineDynamicStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size()),
.pDynamicStates = dynamic_states.data(),
};
auto vertex_input = VkPipelineVertexInputStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
};
auto input_assembly = VkPipelineInputAssemblyStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST,
.primitiveRestartEnable = VK_FALSE,
};
auto viewport = VkViewport {
.x = 0u,
.y = 0u,
.width = static_cast<float>(context.swapchain().get_resolution().width),
.height = static_cast<float>(context.swapchain().get_resolution().height),
.minDepth = 0.0f,
.maxDepth = 0.0f,
};
auto scissor = VkRect2D {
.offset = { 0u, 0u },
.extent = context.swapchain().get_resolution(),
};
auto viewport_state = VkPipelineViewportStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
.viewportCount = 1u,
.scissorCount = 1u,
};
auto rasterization = VkPipelineRasterizationStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
.depthClampEnable = VK_FALSE,
.rasterizerDiscardEnable = VK_FALSE,
.polygonMode = VK_POLYGON_MODE_FILL,
.cullMode = VK_CULL_MODE_NONE,
.frontFace = VK_FRONT_FACE_CLOCKWISE,
.lineWidth = 1.0,
};
auto multisampling = VkPipelineMultisampleStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
.sampleShadingEnable = VK_FALSE,
.minSampleShading = 1.0,
.pSampleMask = nullptr,
.alphaToCoverageEnable = VK_FALSE,
.alphaToOneEnable = VK_FALSE,
};
auto color_blend_attachment = VkPipelineColorBlendAttachmentState {
.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT
| VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
.blendEnable = VK_FALSE,
.srcColorBlendFactor = VK_BLEND_FACTOR_ONE,
.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO,
.colorBlendOp = VK_BLEND_OP_ADD,
.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE,
.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
.alphaBlendOp = VK_BLEND_OP_ADD,
};
auto color_blend = VkPipelineColorBlendStateCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
.logicOpEnable = VK_FALSE,
.logicOp = VK_LOGIC_OP_COPY,
.attachmentCount = 1,
.pAttachments = &color_blend_attachment,
.blendConstants = { 0.0f, 0.0, 0.0, 0.0 },
};
auto layout_info = VkPipelineLayoutCreateInfo {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = 0u,
.pSetLayouts = nullptr,
.pushConstantRangeCount = 0u,
.pPushConstantRanges = nullptr,
};
vkc(vk_create_pipeline_layout(m_device, &layout_info, nullptr, &m_layout));
auto attachment_description = VkAttachmentDescription {
.format = context.swapchain().get_format(),
.samples = VK_SAMPLE_COUNT_1_BIT,
.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
};
auto color_attachment_ref = VkAttachmentReference {
.attachment = 0,
.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
};
auto subpass_description = VkSubpassDescription {
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
.colorAttachmentCount = 1u,
.pColorAttachments = &color_attachment_ref,
};
auto pass_dependency = VkSubpassDependency {
.srcSubpass = VK_SUBPASS_EXTERNAL,
.dstSubpass = 0u,
.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,
.srcAccessMask = 0u,
.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
};
auto renderpass_info = VkRenderPassCreateInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
.attachmentCount = 1u,
.pAttachments = &attachment_description,
.subpassCount = 1u,
.pSubpasses = &subpass_description,
.dependencyCount = 1u,
.pDependencies = &pass_dependency,
};
vkc(vk_create_render_pass(m_device, &renderpass_info, nullptr, &m_pass));
auto pipeline_info = VkGraphicsPipelineCreateInfo {
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.stageCount = static_cast<uint32_t>(shader_stages.size()),
.pStages = shader_stages.data(),
.pVertexInputState = &vertex_input,
.pInputAssemblyState = &input_assembly,
.pViewportState = &viewport_state,
.pRasterizationState = &rasterization,
.pMultisampleState = &multisampling,
.pDepthStencilState = nullptr,
.pColorBlendState = &color_blend,
.pDynamicState = &dynamic_state,
.layout = m_layout,
.renderPass = m_pass,
.subpass = 0u,
.basePipelineHandle = VK_NULL_HANDLE,
.basePipelineIndex = -1,
};
vkc(vk_create_graphics_pipelines(
m_device,
VK_NULL_HANDLE,
1u,
&pipeline_info,
nullptr,
&m_pipeline
));
vk_destroy_shader_module(m_device, vertex_module, nullptr);
vk_destroy_shader_module(m_device, fragment_module, nullptr);
m_framebuffers = context.swapchain().create_framebuffers_for_pass(m_pass);
}
~Pass()
{
if (!m_device)
{
return;
}
for (auto &framebuffer : m_framebuffers)
{
vk_destroy_frame_buffer(m_device, framebuffer, nullptr);
}
vk_destroy_pipeline(m_device, m_pipeline, nullptr);
vk_destroy_render_pass(m_device, m_pass, nullptr);
vk_destroy_pipeline_layout(m_device, m_layout, nullptr);
}
Pass(Pass &&) = default;
Pass(const Pass &) = delete;
auto operator=(Pass &&) -> Pass & = default;
auto operator=(const Pass &) -> Pass & = delete;
[[nodiscard]] auto get_pass() -> VkRenderPass
{
return m_pass;
}
[[nodiscard]] auto get_pipeline() -> VkPipeline
{
return m_pipeline;
}
[[nodiscard]] auto get_framebuffers() -> std::vector<VkFramebuffer> &
{
return m_framebuffers;
}
private:
auto create_module(lt::assets::Blob blob) -> VkShaderModule
{
log_dbg("BLOB SIZE: {}", blob.size());
auto info = VkShaderModuleCreateInfo {
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = blob.size(),
.pCode = reinterpret_cast<const uint32_t *>(blob.data()) // NOLINT
};
auto *module = VkShaderModule { VK_NULL_HANDLE };
vkc(vk_create_shader_module(m_device, &info, nullptr, &module));
return module;
}
memory::NullOnMove<VkDevice> m_device = VK_NULL_HANDLE;
memory::NullOnMove<VkPipeline> m_pipeline = VK_NULL_HANDLE;
memory::NullOnMove<VkRenderPass> m_pass = VK_NULL_HANDLE;
memory::NullOnMove<VkPipelineLayout> m_layout = VK_NULL_HANDLE;
std::vector<VkFramebuffer> m_framebuffers;
};
} // namespace lt::renderer::vk

View file

@ -0,0 +1,20 @@
#include <renderer/vk/renderer/pass.hpp>
#include <renderer/vk/test_utils.hpp>
using ::lt::assets::ShaderAsset;
using ::lt::renderer::vk::Pass;
Suite raii = "pass_raii"_suite = [] {
Case { "happy path won't throw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
std::ignore = Pass {
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" },
};
expect_false(observer.had_any_messages());
};
};

View file

@ -0,0 +1,203 @@
#pragma once
#include <renderer/vk/context/context.hpp>
#include <renderer/vk/debug/validation.hpp>
#include <renderer/vk/renderer/pass.hpp>
#include <time/timer.hpp>
namespace lt::renderer::vk {
class Renderer
{
public:
Renderer(Context &context, Ref<Pass> pass)
: m_device(context.device().vk())
, m_graphics_queue(context.device().get_graphics_queue())
, m_present_queue(context.device().get_present_queue())
, m_swapchain(context.swapchain().vk())
, m_pass(std::move(pass))
, m_resolution(context.swapchain().get_resolution())
{
auto pool_info = VkCommandPoolCreateInfo {
.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT,
.queueFamilyIndex = context.device().get_family_indices()[0],
};
vkc(vk_create_command_pool(m_device, &pool_info, nullptr, &m_pool));
auto cmd_info = VkCommandBufferAllocateInfo {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
.commandPool = m_pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
.commandBufferCount = 1u,
};
vkc(vk_allocate_command_buffers(m_device, &cmd_info, &m_cmd));
auto semaphore_info = VkSemaphoreCreateInfo {
.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
auto fence_info = VkFenceCreateInfo {
.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
.flags = VK_FENCE_CREATE_SIGNALED_BIT,
};
vkc(vk_create_semaphore(m_device, &semaphore_info, nullptr, &m_image_available_semaphore));
vkc(vk_create_semaphore(m_device, &semaphore_info, nullptr, &m_render_finished_semaphore));
vkc(vk_create_fence(m_device, &fence_info, nullptr, &m_in_flight_fence));
};
void record_cmd(VkCommandBuffer cmd, uint32_t image_idx)
{
auto cmd_begin_info = VkCommandBufferBeginInfo {
.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = {},
.pInheritanceInfo = nullptr,
};
vkc(vk_begin_command_buffer(cmd, &cmd_begin_info));
static auto timer = Timer {};
auto clear_value = VkClearValue {
.color = {
0.93,
0.93,
0.93,
1.0,
},
};
auto pass_begin_info = VkRenderPassBeginInfo {
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.renderPass = m_pass->get_pass(),
.framebuffer = m_pass->get_framebuffers()[image_idx],
.renderArea = { .offset = {}, .extent = m_resolution },
.clearValueCount = 1u,
.pClearValues = &clear_value
};
vk_cmd_begin_render_pass(cmd, &pass_begin_info, VK_SUBPASS_CONTENTS_INLINE);
vk_cmd_bind_pipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, m_pass->get_pipeline());
auto viewport = VkViewport {
.x = 0.0f,
.y = 0.0f,
.width = static_cast<float>(m_resolution.width),
.height = static_cast<float>(m_resolution.height),
.minDepth = 0.0f,
.maxDepth = 1.0f,
};
vk_cmd_set_viewport(cmd, 0, 1, &viewport);
auto scissor = VkRect2D {
.offset = { 0u, 0u },
.extent = m_resolution,
};
vk_cmd_set_scissors(cmd, 0, 1, &scissor);
vk_cmd_draw(cmd, 3, 1, 0, 0);
vk_cmd_end_render_pass(cmd);
vkc(vk_end_command_buffer(cmd));
}
void draw()
{
try
{
vkc(vk_wait_for_fences(m_device, 1u, &m_in_flight_fence, VK_TRUE, UINT64_MAX));
vkc(vk_reset_fences(m_device, 1u, &m_in_flight_fence));
auto image_idx = uint32_t {};
vkc(vk_acquire_next_image_khr(
m_device,
m_swapchain,
UINT64_MAX,
m_image_available_semaphore,
VK_NULL_HANDLE,
&image_idx
));
vkc(vk_reset_command_buffer(m_cmd, {}));
record_cmd(m_cmd, image_idx);
auto wait_stage = VkPipelineStageFlags {
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
};
auto submit_info = VkSubmitInfo {
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.waitSemaphoreCount = 1u,
.pWaitSemaphores = &m_image_available_semaphore,
.pWaitDstStageMask = &wait_stage,
.commandBufferCount = 1u,
.pCommandBuffers = &m_cmd,
.signalSemaphoreCount = 1u,
.pSignalSemaphores = &m_render_finished_semaphore,
};
vkc(vk_queue_submit(m_graphics_queue, 1u, &submit_info, m_in_flight_fence));
auto present_info = VkPresentInfoKHR {
.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
.waitSemaphoreCount = 1u,
.pWaitSemaphores = &m_render_finished_semaphore,
.swapchainCount = 1u,
.pSwapchains = &m_swapchain,
.pImageIndices = &image_idx,
.pResults = nullptr,
};
vk_queue_present_khr(m_present_queue, &present_info);
}
catch (const std::exception &exp)
{
log_dbg("EXCEPTION: {}", exp.what());
}
}
~Renderer()
{
if (!m_device)
{
return;
}
vk_destroy_semaphore(m_device, m_render_finished_semaphore, nullptr);
vk_destroy_semaphore(m_device, m_image_available_semaphore, nullptr);
vk_destroy_fence(m_device, m_in_flight_fence, nullptr);
vk_destroy_command_pool(m_device, m_pool, nullptr);
}
Renderer(Renderer &&) = default;
Renderer(const Renderer &) = delete;
auto operator=(Renderer &&) -> Renderer & = default;
auto operator=(const Renderer &) -> Renderer & = delete;
private:
memory::NullOnMove<VkDevice> m_device = VK_NULL_HANDLE;
memory::NullOnMove<VkCommandPool> m_pool = VK_NULL_HANDLE;
memory::NullOnMove<VkCommandBuffer> m_cmd = VK_NULL_HANDLE;
memory::NullOnMove<VkSemaphore> m_image_available_semaphore = VK_NULL_HANDLE;
memory::NullOnMove<VkSemaphore> m_render_finished_semaphore = VK_NULL_HANDLE;
memory::NullOnMove<VkFence> m_in_flight_fence = VK_NULL_HANDLE;
memory::NullOnMove<VkSwapchainKHR> m_swapchain = VK_NULL_HANDLE;
memory::NullOnMove<VkQueue> m_graphics_queue = VK_NULL_HANDLE;
memory::NullOnMove<VkQueue> m_present_queue = VK_NULL_HANDLE;
Ref<Pass> m_pass;
VkExtent2D m_resolution;
};
} // namespace lt::renderer::vk

View file

@ -0,0 +1,29 @@
#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;
Suite raii = "renderer_raii"_suite = [] {
Case { "happy path won't throw" } = [] {
auto observer = ValidationObserver {};
auto [context, _] = create_context();
auto pass = lt::create_ref<Pass>(
context,
ShaderAsset { "./data/test_assets/triangle.vert.asset" },
ShaderAsset { "./data/test_assets/triangle.frag.asset" }
);
auto renderer = Renderer(context, pass);
for (;;)
{
renderer.draw();
}
expect_false(observer.had_any_messages());
};
};

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <ranges> #include <ranges>
#include <renderer/vk/context/context.hpp>
#include <renderer/vk/context/surface.hpp> #include <renderer/vk/context/surface.hpp>
#include <renderer/vk/debug/messenger.hpp> #include <renderer/vk/debug/messenger.hpp>
#include <surface/components.hpp> #include <surface/components.hpp>
@ -32,7 +33,7 @@ public:
ValidationObserver() ValidationObserver()
: m_messenger( : m_messenger(
Messenger::CreateInfo { Messenger::CreateInfo {
.severity = all_severity, .severity = static_cast<Messenger::Severity>(warning | error),
.type = lt::renderer::vk::Messenger::all_type, .type = lt::renderer::vk::Messenger::all_type,
.callback = &callback, .callback = &callback,
.user_data = &m_had_any_messages, .user_data = &m_had_any_messages,
@ -67,6 +68,22 @@ private:
bool m_had_any_messages = false; 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::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<> template<>
struct std::formatter<VkExtent2D> struct std::formatter<VkExtent2D>
{ {

View file

@ -86,6 +86,8 @@ extern PFN_vkCmdBindPipeline vk_cmd_bind_pipeline;
extern PFN_vkCmdDraw vk_cmd_draw; extern PFN_vkCmdDraw vk_cmd_draw;
extern PFN_vkCmdSetViewport vk_cmd_set_viewport; extern PFN_vkCmdSetViewport vk_cmd_set_viewport;
extern PFN_vkCmdSetScissor vk_cmd_set_scissors; extern PFN_vkCmdSetScissor vk_cmd_set_scissors;
extern PFN_vkResetCommandBuffer vk_reset_command_buffer;
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace lt::renderer::vk } // namespace lt::renderer::vk

View file

@ -0,0 +1,9 @@
#pragma once
namespace lt::renderer {
struct SolidColor
{
};
} // namespace lt::renderer

View file

@ -12,14 +12,16 @@ public:
struct CreateInfo struct CreateInfo
{ {
Ref<ecs::Registry> registry; Ref<ecs::Registry> registry;
ecs::Entity surface_entity; ecs::Entity surface_entity;
Ref<app::SystemStats> system_stats; Ref<app::SystemStats> system_stats;
}; };
[[nodiscard]] System(CreateInfo info) [[nodiscard]] System(CreateInfo info)
: m_registry(std::move(info.registry)) : m_registry(std::move(info.registry))
, m_stats(info.system_stats) , m_stats(info.system_stats)
, m_context(info.surface_entity, info.system_stats) , m_context(info.surface_entity)
{ {
ensure(m_stats, "Failed to initialize system: null stats"); 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");
@ -35,19 +37,11 @@ public:
auto operator=(const System &) -> System & = delete; auto operator=(const System &) -> System & = delete;
void on_register() override void on_register() override;
{
}
void on_unregister() override void on_unregister() override;
{
}
void get_validation_state(); void tick(app::TickInfo tick) override;
void tick(app::TickInfo tick) override
{
}
[[nodiscard]] auto get_stats() const -> const app::SystemStats & [[nodiscard]] auto get_stats() const -> const app::SystemStats &
{ {