initial commit
All checks were successful
Build (Arch Linux) / build (push) Successful in 3m10s

This commit is contained in:
2025-04-16 01:58:29 +01:00
commit a8d8b9b9ab
116 changed files with 106633 additions and 0 deletions

23
include/cli/cli.hpp Normal file
View File

@@ -0,0 +1,23 @@
#pragma once
#include <cstdint>
#include <expected>
#include <filesystem>
#include <string_view>
enum class cli_args_parse_err : std::uint8_t {
not_enough_args,
invalid_argument,
};
class cli {
public:
static std::expected<cli, cli_args_parse_err> try_construct(int argc, char** argv);
static void print_usage();
static std::string_view cli_args_parse_err_str(cli_args_parse_err err);
public:
std::filesystem::path root_path {};
bool prefer_x11 = false;
};

View File

@@ -0,0 +1,20 @@
#pragma once
#include <glm/ext/scalar_constants.hpp> // glm::pi
#include <glm/vec3.hpp>
namespace kuiper
{
namespace component
{
struct camera_props {
glm::vec3 euler {0.0f, -(glm::pi<float>() / 2.0f), (glm::pi<float>() / 2.0f)}; // looking down the -Z axis
float fov = (glm::pi<float>() / 2.0f); // 90 degrees
};
} // namespace component
} // namespace kuiper

View File

@@ -0,0 +1,22 @@
#pragma once
#include <glm/vec3.hpp>
namespace kuiper
{
namespace component
{
struct directional_light {
glm::vec3 direction {0.0f, -1.0f, 0.0f}; // Initial points straight down
};
struct point_light {
glm::vec3 colour {1.0f, 1.0f, 1.0f};
float intensity {1.0f};
};
} // namespace component
} // namespace kuiper

View File

@@ -0,0 +1,16 @@
#pragma once
namespace kuiper
{
namespace component
{
struct physics_props {
float mass {0.0f};
float velocity {0.0f};
};
} // namespace component
} // namespace kuiper

View File

@@ -0,0 +1,17 @@
#pragma once
#include "resources/resource_manager.hpp"
namespace kuiper
{
namespace component
{
struct model_resource {
resource_id_t id {0};
};
} // namespace component
} // namespace kuiper

View File

@@ -0,0 +1,152 @@
#pragma once
#include "containers/sparse_set.hpp"
#include <cstddef> // std::size_t
#include <format> // std::format
#include <stdexcept> // std::out_of_range
#include <utility> // std::get
#include <vector> // values container
namespace kuiper
{
namespace containers
{
template<typename K, typename V>
class sparse_map {
public:
using sparse_set_t = containers::sparse_set<K>;
using values_container_t = std::vector<V>;
public:
// Capacity
/// Reserve a number of slots in the underlying containers
void reserve(std::size_t size) {
if (size > m_capacity) {
m_values.resize(size);
m_capacity = size;
}
return;
}
// Access
/// Access value specified by key (non-const)
V& at(const K& key) {
if (!m_sparse_set.contains(key)) {
throw std::out_of_range(std::format("key \"{}\" does not exist in the sparse map", key));
}
const auto dense_idx = m_sparse_set.sparse()[key];
return m_values[dense_idx];
}
/// Access value specified by key (const, read-only)
const V& at(const K& key) const {
if (!m_sparse_set.contains(key)) {
throw std::out_of_range(std::format("key \"{}\" does not exist in the sparse map", key));
}
const auto dense_idx = m_sparse_set.sparse()[key];
return m_values[dense_idx];
}
// Modifiers
void insert(const K& key, const V& value) {
// 1. Insert key into the underlying sparse set
// 2. Insert value into the corresponding index in the sparse map
if (!m_sparse_set.contains(key)) {
// Key does not yet exist in set
const auto insertion = m_sparse_set.insert(key);
if (!std::get<1>(insertion))
return;
const auto dense_idx = std::get<0>(insertion);
reserve(dense_idx + 1); // no-op if there's already enough space
m_values[dense_idx] = value;
++m_size;
return;
} else {
// Key already exists in set
const auto dense_idx = m_sparse_set.sparse()[key];
m_values[dense_idx] = value;
++m_size;
return;
}
}
/// Remove an element from the map
void erase(const K& key) {
// 1. Retrieve dense array index from set
// 2. Swap to back
// 3. Erase from set
// 4. Decrement number of elements
if (contains(key)) {
const auto dense_idx = m_sparse_set.sparse()[key];
m_values[dense_idx] = m_values[m_size - 1];
m_sparse_set.erase(key);
--m_size;
}
return;
}
// Lookup
/// Check if the key exists in the map
bool contains(const K& key) const {
return m_sparse_set.contains(key);
}
// Observers
/// Access to the underlying sparse_set container
const sparse_set_t& sparse_set() const noexcept {
return m_sparse_set;
}
/// Access to the underlying values container
const values_container_t& values() const noexcept {
return m_values;
}
// Operator overloads
/// Access/insert element specified by key
V& operator[](const K& key) {
const auto k = key;
if (!contains(k)) {
insert(k, {});
}
return at(k);
}
/// Access element specified by key (const, read-only)
const V& operator[](const K& key) const {
return at(key);
}
private:
containers::sparse_set<K> m_sparse_set {};
values_container_t m_values {};
std::size_t m_size = 0;
std::size_t m_capacity = 0;
};
} // namespace containers
} // namespace kuiper

View File

@@ -0,0 +1,109 @@
#pragma once
#include <concepts> // std::unsigned_integral
#include <cstddef> // std::size_t
#include <iterator> // std::distance
#include <utility> // std::pair
#include <vector> // Underlying arrays
namespace kuiper
{
namespace containers
{
template<typename T>
requires std::unsigned_integral<T>
class sparse_set {
public:
using sparse_container_t = std::vector<T>;
using dense_container_t = std::vector<T>;
public:
// Capacity
/// Reserve a number of slots in the underlying containers
void reserve(std::size_t size) {
if (size > m_capacity) {
m_sparse.resize(size);
m_dense.resize(size);
m_capacity = size;
}
return;
}
// Modifiers
/// Add an element to the set, returns `(dense index, inserted)` pair
std::pair<std::size_t, bool> insert(const T& value) {
// 1. Append to next slot in the dense array
// 2. Record dense array index (current m_size) in sparse array
// 3. Increment number of elements
if (!contains(value)) {
reserve(value + 1); // no-op if there's already enough space
m_dense[m_size] = value;
m_sparse[value] = m_size;
++m_size;
return std::make_pair(std::distance(m_dense.cbegin(), m_dense.cbegin() + m_sparse[value]), true);
} else {
// Value already exists in set, return the index, but not inserted flag
return std::make_pair(std::distance(m_dense.cbegin(), m_dense.cbegin() + m_sparse[value]), false);
}
}
/// Remove an element from the set
void erase(const T& value) {
// 1. Swap element to be removed to the back of the dense array
// 2. Update the dense array index for the value which was swapped
// 3. Decrement number of elements
if (contains(value)) {
m_dense[m_sparse[value]] = m_dense[m_size - 1];
// m_dense[m_size - 1] = m_dense[m_sparse[value]];
m_sparse[m_dense[m_size - 1]] = m_sparse[value];
--m_size;
}
return;
}
/// Clear all elements from the set
void clear() {
m_size = 0;
return;
}
// Lookup
/// Check if a value is in the set
bool contains(const T& value) const {
// First condition will short-circuit if `value` is out-of-range
return !(value >= m_capacity) && (m_sparse[value] < m_size) && (m_dense[m_sparse[value]] == value);
}
// Observers
/// Access to the underlying sparse container
const sparse_container_t& sparse() const noexcept {
return m_sparse;
}
/// Access to the underlying dense container
const dense_container_t& dense() const noexcept {
return m_dense;
}
private:
sparse_container_t m_sparse {};
dense_container_t m_dense {};
std::size_t m_size = 0; // Number of elements, also the next index into m_dense
std::size_t m_capacity = 0; // Number of elements + 1
};
} // namespace containers
} // namespace kuiper

View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
namespace kuiper
{
class engine;
struct application_spec {
std::u8string name = u8"Kuiper Engine Application";
std::uint32_t width = 640;
std::uint32_t height = 480;
bool fullscreen = false;
};
class application {
public:
application(const application_spec& app_spec = application_spec())
: m_app_spec(app_spec) {};
virtual ~application() = default;
public:
virtual void initialise(kuiper::engine* engine) = 0;
virtual void shutdown() = 0;
virtual void pre_render(float dt) = 0;
virtual void render(float dt) = 0;
virtual void post_render(float dt) = 0;
inline const application_spec& get_app_spec() {
return m_app_spec;
}
private:
application_spec m_app_spec = application_spec();
};
/// This function must be implement by the user,
/// defining & creating the application which will be run by the Kuiper engine.
/// Consider this function your application's entrypoint, you can perform initialisation that must
/// occur before your application starts here.
/// NOTE: This function is expected to return a *unique* pointer to the application, to be owned by the engine.
/// Don't store a copy of this pointer to use later as there are no guarantees to whether the pointer is valid.
std::unique_ptr<application> create_application(int argc, char* argv[]);
} // namespace kuiper

82
include/core/engine.hpp Normal file
View File

@@ -0,0 +1,82 @@
#pragma once
#include "cli/cli.hpp"
#include "components/camera.hpp"
#include "core/application.hpp"
#include "graphics/opengl/framebuffer.hpp"
#include "platform/glfw/glfw_window.hpp"
#include "renderer/gl_renderer.hpp"
#include "resources/resource_manager.hpp"
#include <flecs.h>
#include <expected>
#include <memory>
#include <system_error>
#include <vector>
namespace kuiper
{
struct engine_metrics {
float rendering_time = 0.0f;
gl::renderer_metrics renderer_metrics {};
};
class engine {
public:
engine(std::unique_ptr<application>&& app);
~engine();
engine(const engine& other) = delete;
engine& operator=(const engine& other) = delete;
engine(engine&& other) noexcept = delete;
engine& operator=(engine&& other) noexcept = delete;
/// Initialise core engine subsystems
static std::expected<void, std::error_code> initialise(const cli& cli_args) noexcept;
void start();
public:
std::shared_ptr<glfw_window> window() noexcept {
return m_window;
}
engine_metrics metrics() const noexcept {
return m_metrics;
}
flecs::world& world() noexcept {
return m_world;
}
resource_manager& resources() noexcept {
return m_resources;
}
void add_render_target(const gl::framebuffer::s_ptr& target) noexcept;
void remove_render_target(const gl::framebuffer::s_ptr& target) noexcept;
/// Only draw to render targets added with `add_render_target()`
void disable_default_framebuffer() noexcept;
/// Draw to default framebuffer as well as any additional render targets
void enable_default_framebuffer() noexcept;
void set_render_camera(const glm::vec3& pos, const component::camera_props& cam_props) noexcept;
private:
std::unique_ptr<application> m_application {nullptr};
std::shared_ptr<glfw_window> m_window {nullptr};
engine_metrics m_metrics {};
std::vector<gl::framebuffer::s_ptr> m_render_targets {};
bool m_render_to_default {true};
glm::vec3 m_cam_pos {};
component::camera_props m_cam_props {};
resource_manager m_resources {};
flecs::world m_world {};
};
// Some assumptions that I make
static_assert(sizeof(char8_t) == sizeof(char));
} // namespace kuiper

View File

@@ -0,0 +1,15 @@
#pragma once
#if defined(_WIN32)
// TODO: Windows entrypoint
#else
/// Standard C entrypoint
int main(int argc, char** argv);
#endif
namespace kuiper
{
/// Actual entrypoint, as far as the engine is concerned.
/// Once we reach the real entrypoint, `WinMain` should've converted any UTF-16 wchar strings into UTF-8 C-style strings
int entrypoint(int argc, char** argv);
} // namespace kuiper

21
include/core/io.hpp Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <span>
#include <string>
#include <vector>
namespace kuiper
{
class io {
public:
static std::string read_text_file(const std::filesystem::path& path) noexcept;
static std::filesystem::path get_home_dir() noexcept;
};
} // namespace kuiper

16
include/errors/errors.hpp Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
namespace kuiper
{
enum class error : std::uint8_t {
unknown,
failed_to_load,
resource_missing,
resource_invalid,
opengl_error
};
} // namespace kuiper

View File

@@ -0,0 +1,30 @@
#pragma once
#include <exception>
#include <filesystem>
namespace kuiper
{
namespace exceptions
{
class file_not_found : public std::exception {
public:
file_not_found(const std::filesystem::path& path)
: m_path(path) {};
public:
inline std::filesystem::path path() const noexcept {
return m_path;
}
// Implementing std::exception
inline const char* what() const noexcept override {
return "file not found";
}
private:
std::filesystem::path m_path;
};
} // namespace exceptions
} // namespace kuiper

View File

@@ -0,0 +1,60 @@
#pragma once
#include <glad/gl.h>
#include <cstddef>
#include <memory>
namespace kuiper
{
namespace gl
{
class buffer {
public:
using s_ptr = std::shared_ptr<buffer>;
explicit buffer(GLuint id, GLenum target)
: m_id(id), m_target(target) {}
~buffer() {
destroy();
}
buffer(const buffer& other) = delete;
buffer& operator=(const buffer& other) = delete;
buffer(buffer&& other) noexcept = default;
buffer& operator=(buffer&& other) noexcept = default;
static s_ptr make(GLenum target) noexcept;
public:
GLuint id() const noexcept {
return m_id;
}
std::size_t size() const noexcept {
return m_size;
}
void destroy() noexcept;
void bind() const noexcept;
void bind_to_location(std::uint32_t location) const noexcept;
void upload(GLenum usage, std::size_t size, const void* data) noexcept;
void update(std::size_t offset, std::size_t size, const void* data) noexcept;
void set_vertex_attrib(std::uint32_t index,
std::size_t size,
GLenum type,
std::size_t stride,
std::size_t offset,
bool normalised = false) noexcept;
private:
GLuint m_id {0};
GLenum m_target {0};
std::size_t m_size {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,79 @@
#pragma once
#include "graphics/opengl/texture.hpp"
#include <glad/gl.h>
#include <glm/vec2.hpp>
#include <array>
#include <cstdint>
#include <memory>
namespace kuiper
{
namespace gl
{
class framebuffer {
public:
using s_ptr = std::shared_ptr<framebuffer>;
// OpenGL mandates at least 8 colour buffers
static constexpr std::uint32_t max_colour_attachments = 8;
using col_tex_storage = std::array<texture::s_ptr, max_colour_attachments>;
explicit framebuffer(GLuint id)
: m_id(id) {}
~framebuffer() {
destroy();
}
framebuffer(const framebuffer& other) = delete;
framebuffer& operator=(const framebuffer& other) = delete;
framebuffer(framebuffer&& other) noexcept = default;
framebuffer& operator=(framebuffer&& other) noexcept = default;
static s_ptr make() noexcept;
static s_ptr make_default(std::uint32_t width, std::uint32_t height) noexcept;
public:
void destroy() noexcept;
void bind() const noexcept;
void resize(std::uint32_t width, std::uint32_t height) noexcept;
GLuint id() const noexcept {
return m_id;
}
const col_tex_storage& colour_tex() const noexcept {
return m_colour_texs;
}
const texture::s_ptr& depth_tex() const noexcept {
return m_depth_tex;
}
glm::u32vec2 dimensions() const noexcept {
return {m_width, m_height};
}
// Attach a colour texture to the framebuffer, optionally at a given attachment point. Takes ownership of `tex`.
void attach_colour(texture::s_ptr&& tex, std::uint32_t attachment_point = 0) noexcept;
// Attach a depth AND stencil texture to the framebuffer. Takes ownership of `tex`.
void attach_depth(texture::s_ptr&& tex) noexcept;
private:
GLuint m_id {0};
col_tex_storage m_colour_texs {};
texture::s_ptr m_depth_tex {nullptr};
std::uint32_t m_width {0};
std::uint32_t m_height {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,70 @@
#pragma once
#include "glm/ext/vector_float3.hpp"
#include "graphics/opengl/shader.hpp"
#include <glad/gl.h>
#include <glm/mat3x3.hpp>
#include <glm/mat4x4.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <cstdint>
#include <initializer_list>
#include <memory>
namespace kuiper
{
namespace gl
{
class program {
public:
using s_ptr = std::shared_ptr<program>;
explicit program(GLuint id)
: m_id(id) {}
~program() {
destroy();
}
program(const program& other) = delete;
program& operator=(const program& other) = delete;
program(program&& other) noexcept = default;
program& operator=(program&& other) noexcept = default;
static s_ptr make() noexcept;
static s_ptr from_shaders(std::initializer_list<shader::s_ptr> shaders);
public:
GLuint id() const noexcept {
return m_id;
}
void destroy() noexcept;
void use() const noexcept;
void attach_shader(const shader::s_ptr& shader) noexcept;
bool link() noexcept;
// Uniforms
GLint uniform_location(const char* name) const noexcept;
void set_uint(const char* uniform, std::uint32_t value) noexcept;
void set_int(const char* uniform, std::int32_t value) noexcept;
void set_int_array(const char* uniform, const std::int32_t* value_arr, std::size_t len) noexcept;
void set_float(const char* uniform, float value) noexcept;
void set_vec3(const char* uniform, const glm::vec3& value) noexcept;
void set_uvec3(const char* uniform, const glm::uvec3& value) noexcept;
void set_uvec2(const char* uniform, const glm::uvec2& value) noexcept;
void set_mat3(const char* uniform, const glm::mat3& value) noexcept;
void set_mat4(const char* uniform, const glm::mat4& value) noexcept;
private:
GLuint m_id {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,74 @@
#pragma once
#include "graphics/opengl/framebuffer.hpp"
#include <glad/gl.h>
#include <GLFW/glfw3.h>
#include <cstdint>
#include <type_traits>
namespace kuiper
{
namespace gl
{
void initialise();
void dump_hardware_info();
#if !defined(NDEBUG)
void debug_callback(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar* message,
const void* userParam) noexcept;
#endif
class render_context {
public:
/// Binds a framebuffer to the renderer pipeline
static void bind_framebuffer(const framebuffer::s_ptr& fb) noexcept;
/// Binds the default framebuffer
static void bind_default_framebuffer() noexcept;
/// Set the viewport size over the currently bound framebuffer
static void set_viewport_size(std::uint32_t width, std::uint32_t height) noexcept;
/// Enables an OpenGL feature
static void enable_feature(GLenum feature) noexcept;
/// Disables an OpenGL feature
static void disable_feature(GLenum feature) noexcept;
template<typename T>
static T get(GLenum param) noexcept {
T ret {};
if constexpr (std::is_same<T, GLint>::value) {
glGetIntegerv(param, &ret);
} else if constexpr (std::is_same<T, GLint64>::value) {
glGetInteger64v(param, &ret);
} else if constexpr (std::is_same<T, GLfloat>::value) {
glGetFloatv(param, &ret);
} else if constexpr (std::is_same<T, GLdouble>::value) {
glGetDoublev(param, &ret);
} else if constexpr (std::is_same<T, GLboolean>::value) {
glGetBooleanv(param, &ret);
} else {
static_assert(!"non-exhaustive if-constexpr!");
}
return ret;
}
static constexpr const char* GLSL_VERSION = "#version 460 core";
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,57 @@
#pragma once
#include <glad/gl.h>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string_view>
namespace kuiper
{
namespace gl
{
enum class shader_type : std::uint8_t {
none = 0,
vertex,
fragment,
compute
};
class shader {
public:
using s_ptr = std::shared_ptr<shader>;
explicit shader(GLuint id, GLenum type)
: m_id(id), m_type(type) {}
~shader() {
destroy();
}
shader(const shader& other) = delete;
shader& operator=(const shader& other) = delete;
shader(shader&& other) noexcept = default;
shader& operator=(shader&& other) noexcept = default;
static s_ptr make(shader_type type) noexcept;
static s_ptr from_path(const std::filesystem::path& shader_path, shader_type type = shader_type::none) noexcept;
static s_ptr from_source(std::string_view source, shader_type type) noexcept;
public:
void destroy() noexcept;
void attach_source(std::string_view source) noexcept;
void compile() noexcept;
GLuint id() const noexcept {
return m_id;
}
private:
GLuint m_id {0};
GLenum m_type {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,97 @@
#pragma once
#include <glad/gl.h>
#include <glm/vec2.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
namespace kuiper
{
namespace gl
{
enum class texture_type : std::uint8_t {
none = 0,
tex_2d
};
class texture {
public:
using s_ptr = std::shared_ptr<texture>;
explicit texture(GLuint id, GLenum target)
: m_id(id), m_target(target) {}
~texture() {
destroy();
}
texture(const texture& other) = delete;
texture& operator=(const texture& other) = delete;
texture(texture&& other) noexcept = default;
texture& operator=(texture&& other) noexcept = default;
/// Makes a new, uninitialised texture
static s_ptr make(texture_type type = texture_type::tex_2d) noexcept;
/// Makes a placeholder 4x4 texture
static s_ptr make_placeholder() noexcept;
/// Makes a texture from a path to an encoded image
static s_ptr from_image_path(const std::filesystem::path& image_path, bool flip_y = true) noexcept;
/// Makes a texture from a pointer to encoded image data
static s_ptr from_image_data(const std::uint8_t* buffer, std::size_t length, bool flip_y = true) noexcept;
public:
/// Explicitly delete the OpenGL texture object
void destroy() noexcept;
/// Bind the texture
void bind() noexcept;
/// Upload image data to the texture object on the GPU
void upload(
GLint internal_fmt, GLsizei width, GLsizei height, GLenum format, GLenum type, const void* data) noexcept;
/// Resize underlying texture buffer
void resize(std::uint32_t width, std::uint32_t height) noexcept;
/// Set a texture parameter
void set_param(GLenum name, GLint param) noexcept;
/// Generate texture mipmaps
void gen_mipmaps() noexcept;
/// Get the dimensions of the texture
glm::uvec2 dimensions() const noexcept {
return {m_width, m_height};
}
GLint internal_format() const noexcept {
return m_internal_fmt;
}
GLenum pixel_format() const noexcept {
return m_pixel_fmt;
}
GLenum pixel_type() const noexcept {
return m_pixel_type;
}
/// Get the internal OpenGL object name
GLuint id() const noexcept {
return m_id;
}
private:
GLuint m_id {0};
GLenum m_target {0};
std::uint32_t m_width {0};
std::uint32_t m_height {0};
GLint m_internal_fmt {0};
GLenum m_pixel_fmt {0};
GLenum m_pixel_type {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,74 @@
#pragma once
#include <glad/gl.h>
#include <glm/vec3.hpp>
#include <cstddef>
#include <cstdint>
#include <memory>
namespace kuiper
{
namespace gl
{
class texture_array {
public:
using s_ptr = std::shared_ptr<texture_array>;
explicit texture_array(GLuint id, GLenum target)
: m_id(id), m_target(target) {}
~texture_array() {
destroy();
}
texture_array(const texture_array& other) = delete;
texture_array& operator=(const texture_array& other) = delete;
texture_array(texture_array&& other) noexcept = default;
texture_array& operator=(texture_array&& other) noexcept = default;
static s_ptr make(GLenum target) noexcept;
public:
void destroy() noexcept;
void bind(std::uint32_t unit = 0) const noexcept;
void set_storage(GLenum internal_fmt, std::uint32_t width, std::uint32_t height, std::uint32_t depth) noexcept;
void upload(std::uint32_t x_offset,
std::uint32_t y_offset,
std::uint32_t z_offset,
std::uint32_t width,
std::uint32_t height,
std::uint32_t depth,
GLenum format,
GLenum type,
const void* data,
std::uint32_t mip_level = 0) noexcept;
void set_param(GLenum name, GLint param) noexcept;
void gen_mipmaps() noexcept;
glm::uvec3 dimensions() const noexcept {
return {m_width, m_height, m_depth};
}
GLuint id() const noexcept {
return m_id;
}
GLenum internal_format() const noexcept {
return m_internal_fmt;
}
private:
GLuint m_id {0};
GLenum m_target {0};
GLenum m_internal_fmt {0};
std::uint32_t m_width {0};
std::uint32_t m_height {0};
std::uint32_t m_depth {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,48 @@
#pragma once
#include <glad/gl.h>
#include <memory>
namespace kuiper
{
namespace gl
{
class vertex_array {
public:
using s_ptr = std::shared_ptr<vertex_array>;
explicit vertex_array(GLuint id)
: m_id(id) {}
~vertex_array() {
destroy();
}
vertex_array(const vertex_array& other) = delete;
vertex_array& operator=(const vertex_array& other) = delete;
vertex_array(vertex_array&& other) noexcept = default;
vertex_array& operator=(vertex_array&& other) noexcept = default;
static s_ptr make() noexcept;
public:
GLuint id() const noexcept {
return m_id;
}
void destroy() noexcept;
void bind() const noexcept;
void enable_attrib(std::uint32_t index) noexcept;
void disable_attrib(std::uint32_t index) noexcept;
void set_attrib_divisor(std::uint32_t attrib, std::uint32_t divisor) noexcept;
void set_binding_divisor(std::uint32_t binding, std::uint32_t divisor) noexcept;
private:
GLuint m_id {0};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,56 @@
#pragma once
#include "logging/logger.hpp"
namespace kuiper
{
/// @brief Singleton class providing engine-wide logging facilities
class engine_logger {
private:
engine_logger() = default;
public:
~engine_logger() = default;
public:
static inline engine_logger& get(void) {
static engine_logger s_instance;
return s_instance;
}
template<typename... Args>
inline void trace(const char* fmt, Args... args) {
m_log.trace(fmt, args...);
}
template<typename... Args>
inline void debug(const char* fmt, Args... args) {
m_log.debug(fmt, args...);
}
template<typename... Args>
inline void info(const char* fmt, Args... args) {
m_log.info(fmt, args...);
}
template<typename... Args>
inline void warn(const char* fmt, Args... args) {
m_log.warn(fmt, args...);
}
template<typename... Args>
inline void error(const char* fmt, Args... args) {
m_log.error(fmt, args...);
}
template<typename... Args>
inline void critical(const char* fmt, Args... args) {
m_log.critical(fmt, args...);
}
private:
kuiper::logger m_log {"engine"};
};
} // namespace kuiper

View File

@@ -0,0 +1,67 @@
#pragma once
#include <memory>
#include <string_view> // TODO: Use std::format_string when available
#include <utility>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/spdlog.h>
namespace kuiper
{
class logger {
public:
logger()
: m_log(spdlog::stdout_color_st("log")) {
#if !defined(NDEBUG)
m_log->set_level(spdlog::level::debug);
#else
m_log->set_level(spdlog::level::info);
#endif
}
logger(const char* name)
: m_log(spdlog::stdout_color_st(name)) {
#if !defined(NDEBUG)
m_log->set_level(spdlog::level::debug);
#else
m_log->set_level(spdlog::level::info);
#endif
}
~logger() = default;
template<typename... Args>
inline void trace(std::string_view fmt, Args&&... args) {
m_log->trace(fmt, std::forward<Args>(args)...);
}
template<typename... Args>
inline void debug(std::string_view fmt, Args&&... args) {
m_log->debug(fmt, std::forward<Args>(args)...);
}
template<typename... Args>
inline void info(std::string_view fmt, Args&&... args) {
m_log->info(fmt, std::forward<Args>(args)...);
}
template<typename... Args>
inline void warn(std::string_view fmt, Args&&... args) {
m_log->warn(fmt, std::forward<Args>(args)...);
}
template<typename... Args>
inline void error(std::string_view fmt, Args&&... args) {
m_log->error(fmt, std::forward<Args>(args)...);
}
template<typename... Args>
inline void critical(std::string_view fmt, Args&&... args) {
m_log->critical(fmt, std::forward<Args>(args)...);
}
private:
std::shared_ptr<spdlog::logger> m_log {nullptr};
};
} // namespace kuiper

View File

@@ -0,0 +1,18 @@
#pragma once
#include <concepts>
namespace kuiper
{
namespace maths
{
template<typename T, typename V>
requires(std::floating_point<V> || std::integral<V>) && std::floating_point<T>
constexpr inline T aspect_ratio(V width, V height) {
return static_cast<T>(width) / static_cast<T>(height);
}
} // namespace maths
} // namespace kuiper

37
include/maths/pow2.hpp Normal file
View File

@@ -0,0 +1,37 @@
#pragma once
#include <cstdint>
namespace kuiper
{
// https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
inline std::uint64_t next_pow2(std::uint64_t value) {
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
value |= value >> 32;
value++;
value += (value == 0); // Case for if value == 0 (returns 0 as the next power of 2, which 0 is not)
return value;
}
inline std::uint32_t next_pow2(std::uint32_t value) {
value--;
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
value++;
value += (value == 0);
return value;
}
} // namespace kuiper

View File

@@ -0,0 +1,52 @@
#pragma once
#include "glm/gtc/quaternion.hpp"
#include <glm/ext/matrix_transform.hpp>
#include <glm/ext/quaternion_float.hpp>
#include <glm/ext/quaternion_trigonometric.hpp>
#include <glm/mat4x4.hpp>
#include <glm/trigonometric.hpp>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
namespace kuiper
{
namespace maths
{
struct transform_quat {
glm::vec3 position = {0.0f, 0.0f, 0.0f};
glm::quat rotation = {1.0f, 0.0f, 0.0f, 0.0f};
glm::vec3 scale = {1.0f, 1.0f, 1.0f};
glm::mat4 to_mat4() const noexcept {
static constexpr glm::mat4 identity_mat = glm::mat4(1.0f);
const glm::mat4 tm = glm::translate(identity_mat, position);
const glm::mat4 rm = glm::mat4_cast(rotation);
const glm::mat4 sm = glm::scale(identity_mat, scale);
return tm * rm * sm;
}
};
struct transform_euler {
glm::vec3 position = {0.0f, 0.0f, 0.0f};
glm::vec3 rotation = {0.0f, 0.0f, 0.0f};
glm::vec3 scale = {1.0f, 1.0f, 1.0f};
glm::mat4 to_mat4() const noexcept {
static constexpr glm::mat4 identity_mat = glm::mat4(1.0f);
const glm::mat4 tm = glm::translate(identity_mat, position);
const glm::mat4 rm = glm::mat4_cast(glm::quat(rotation));
const glm::mat4 sm = glm::scale(identity_mat, scale);
return tm * rm * sm;
}
};
} // namespace maths
} // namespace kuiper

View File

@@ -0,0 +1,89 @@
#pragma once
#include "graphics/opengl/framebuffer.hpp"
#include "window/input_system.hpp"
#include "window/window.hpp"
#include <GLFW/glfw3.h>
#include <glm/vec2.hpp>
#include <cstdint>
#include <memory>
#include <string_view>
namespace kuiper
{
class glfw_window : public window {
public:
glfw_window(
std::uint32_t width, std::uint32_t height, std::u8string_view title, GLFWmonitor* monitor, GLFWwindow* share);
~glfw_window() override;
// Only allow moving
glfw_window(const glfw_window& other) = delete;
glfw_window& operator=(const glfw_window& other) = delete;
glfw_window(glfw_window&& other) noexcept = default;
glfw_window& operator=(glfw_window&& other) noexcept = default;
public:
// Implement window
virtual void* get_native_handle() const noexcept override;
virtual void set_title(std::u8string_view title) noexcept override;
virtual void poll_events() noexcept override;
virtual void swap_buffers() noexcept override;
virtual void set_swap_interval(std::uint32_t interval) noexcept override;
virtual void bind_context() noexcept override;
virtual gl::framebuffer::s_ptr back_buffer() const noexcept override;
virtual bool should_close() const noexcept override;
virtual bool is_minimised() const noexcept override;
virtual bool is_cursor_locked() const noexcept override;
virtual void lock_cursor() noexcept override;
virtual void unlock_cursor() noexcept override;
virtual void enable_raw_input() noexcept override;
virtual void disable_raw_input() noexcept override;
virtual glm::uvec2 get_size() const noexcept override;
virtual glm::uvec2 get_framebuffer_size() const noexcept override; // should this go in render context?
virtual glm::vec2 get_content_scale() const noexcept override;
input_system& input() noexcept {
return m_input_state;
}
private:
void resize_back_buffer() noexcept;
public:
static void initialise_glfw(bool prefer_x11);
private:
static GLFWwindow* create_glfw_window(std::uint32_t width,
std::uint32_t height,
std::u8string_view title,
GLFWmonitor* monitor,
GLFWwindow* share) noexcept;
static void push_window_hints() noexcept;
// GLFW Callbacks
static void glfw_error_callback(int error_code, const char* description);
static void glfw_framebuffer_size_callback(GLFWwindow* window, int width, int height);
static void glfw_content_scale_callback(GLFWwindow* window, float xscale, float yscale);
static void glfw_key_event_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
static void glfw_cursor_pos_callback(GLFWwindow* window, double xpos, double ypos);
static void glfw_scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
static void glfw_dragdrop_callback(GLFWwindow* window, int path_count, const char* paths[]);
private:
GLFWwindow* m_window_handle = nullptr;
input_system m_input_state {};
bool m_cursor_locked = false;
gl::framebuffer::s_ptr m_default_framebuffer = nullptr;
};
} // namespace kuiper

View File

@@ -0,0 +1,163 @@
#pragma once
#include "components/camera.hpp"
#include "components/lights.hpp"
#include "graphics/opengl/buffer.hpp"
#include "graphics/opengl/framebuffer.hpp"
#include "graphics/opengl/program.hpp"
#include "graphics/opengl/vertex_array.hpp"
#include "maths/transform.hpp"
#include "resources/material.hpp"
#include "resources/model.hpp"
#include "resources/primitive.hpp"
#include <glad/gl.h>
#include <array>
#include <glm/ext/matrix_transform.hpp>
#include <glm/mat4x4.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <utility>
#include <vector>
namespace kuiper
{
namespace gl
{
static constexpr std::size_t MAX_NUM_LIGHTS = 100;
static constexpr std::size_t LIGHT_SSBO_INIT_SIZE = 1000;
static constexpr std::uint32_t NUM_CLUSTERS_X = 12;
static constexpr std::uint32_t NUM_CLUSTERS_Y = 12;
static constexpr std::uint32_t NUM_CLUSTERS_Z = 24;
static constexpr std::uint32_t NUM_CLUSTERS = NUM_CLUSTERS_X * NUM_CLUSTERS_Y * NUM_CLUSTERS_Z;
static constexpr std::uint32_t LOCAL_SIZE = 128;
static constexpr float NEAR_Z_DEPTH = 0.1f;
static constexpr float FAR_Z_DEPTH = std::numeric_limits<std::uint16_t>::max();
struct gl_state {
// Vertex & element buffers
vertex_array::s_ptr vao {nullptr};
buffer::s_ptr vertex_buffer {nullptr};
buffer::s_ptr index_buffer {nullptr};
// Indirect & model matrix buffer
buffer::s_ptr indirect_buffer {nullptr};
buffer::s_ptr matrix_buffer {nullptr};
// Shaders
program::s_ptr pbr_shader {nullptr};
program::s_ptr clustering_shader {nullptr};
program::s_ptr light_culling_shader {nullptr};
// Lighting buffers
buffer::s_ptr cluster_buffer {nullptr};
buffer::s_ptr lights_buffer {nullptr};
// Configuration
std::uint32_t max_texture_units {32};
std::vector<std::int32_t> texture_unit_slots {};
};
struct renderer_metrics {
std::size_t n_vertices {};
std::size_t n_indices {};
std::size_t n_draw_cmds {};
std::size_t n_matrices {};
std::size_t n_materials {};
std::size_t n_lights {};
};
class renderer {
public:
using vertex_t = vertex;
using index_t = std::uint32_t;
struct draw_elements_indirect_cmd {
std::uint32_t count {0};
std::uint32_t instance_count {0};
std::uint32_t first_index {0};
std::uint32_t base_vertex {0};
std::uint32_t base_instance {0};
};
struct alignas(4) render_matrices {
glm::mat4 model_matrix;
glm::mat3 normal_matrix;
};
struct alignas(16) cluster {
glm::vec4 min_point;
glm::vec4 max_point;
std::uint32_t count;
std::uint32_t light_indices[MAX_NUM_LIGHTS];
};
struct alignas(16) point_light {
glm::vec4 position;
glm::vec4 colour;
float intensity;
float radius;
};
private:
using vertex_storage_t = std::vector<vertex_t>;
using index_storage_t = std::vector<index_t>;
using draw_cmd_storage_t = std::vector<draw_elements_indirect_cmd>;
using matrix_storage_t = std::vector<render_matrices>;
using material_storage_t = std::vector<resource::material>;
using light_storage_t = std::vector<point_light>;
public:
renderer(gl_state&& state)
: m_state(std::move(state)) {}
~renderer() = default;
renderer(const renderer&) = delete;
renderer& operator=(const renderer&) = delete;
renderer(renderer&&) noexcept = delete;
renderer& operator=(renderer&&) noexcept = delete;
static renderer init();
void render(const framebuffer::s_ptr& framebuffer,
const glm::vec3& cam_pos,
const component::camera_props& cam_props) noexcept;
void flush() noexcept;
renderer_metrics metrics() const noexcept;
void draw_model(const resource::model& model, const maths::transform_euler& transform);
void draw_mesh(const resource::mesh& mesh,
const std::vector<resource::material>& materials,
const glm::mat4& transform);
void draw_primitive(const resource::primitive& primitive,
const resource::material& material,
const maths::transform_euler& transform);
void draw_primitive(const resource::primitive& primitive,
const resource::material& material,
const glm::mat4& transform);
void add_light(const glm::vec3& pos, const component::point_light& light) noexcept;
private:
void update_and_upload() noexcept;
private:
gl_state m_state {};
vertex_storage_t m_vertices {};
index_storage_t m_indices {};
draw_cmd_storage_t m_draw_cmds {};
matrix_storage_t m_matrices {};
material_storage_t m_materials {};
light_storage_t m_lights {};
};
} // namespace gl
} // namespace kuiper

View File

@@ -0,0 +1,79 @@
#pragma once
#include <glm/vec2.hpp>
#include <cstddef>
#include <cstdint>
#include <expected>
#include <filesystem>
#include <string_view>
#include <vector>
namespace kuiper
{
namespace resource
{
struct image_error {
std::string_view message;
};
class image {
public:
image(std::uint32_t width,
std::uint32_t height,
std::uint32_t n_channels,
const std::uint8_t* data,
std::size_t len)
: m_width(width), m_height(height), m_channels(n_channels), m_data(data, data + len) {}
image() = default;
~image() = default;
image(const image& other) = default;
image& operator=(const image& other) = default;
image(image&& other) noexcept = default;
image& operator=(image&& other) noexcept = default;
static image make_blank(std::uint32_t width, std::uint32_t height, std::uint32_t n_channels) noexcept;
static std::expected<image, image_error> from_path(const std::filesystem::path& path,
std::uint8_t num_channels = 4) noexcept;
static std::expected<image, image_error> from_memory(const std::uint8_t* data,
std::size_t len,
std::uint8_t num_channels = 4) noexcept;
std::uint32_t width() const noexcept {
return m_width;
}
std::uint32_t height() const noexcept {
return m_height;
}
glm::uvec2 dimensions() const noexcept {
return {m_width, m_height};
}
std::uint32_t channels() const noexcept {
return m_channels;
}
const std::vector<std::uint8_t>& pixel_data() const noexcept {
return m_data;
}
std::vector<std::uint8_t>& pixel_data_mut() noexcept {
return m_data;
}
void resize(std::uint32_t width, std::uint32_t height) noexcept;
private:
std::uint32_t m_width {0};
std::uint32_t m_height {0};
std::uint32_t m_channels {0};
std::vector<std::uint8_t> m_data {};
};
} // namespace resource
} // namespace kuiper

View File

@@ -0,0 +1,20 @@
#pragma once
#include "graphics/opengl/texture.hpp"
#include "graphics/opengl/texture_array.hpp"
#include <glm/vec4.hpp>
namespace kuiper::resource
{
struct material {
glm::vec4 base_colour_factor {0.0f, 0.0f, 0.0f, 1.0f};
float metallic_factor {1.0f};
float roughness_factor {1.0f};
// 0: Albedo, 1: Normal, 2: Metallic-Roughness
gl::texture_array::s_ptr textures {nullptr};
};
} // namespace kuiper::resource

View File

@@ -0,0 +1,55 @@
#pragma once
#include "maths/transform.hpp"
#include "resources/primitive.hpp"
#include <glm/mat4x4.hpp>
#include <initializer_list>
#include <vector>
namespace kuiper::resource
{
class mesh {
public:
using primitive_storage = std::vector<primitive>;
using submesh_storage = std::vector<mesh>;
mesh(const glm::mat4& t)
: m_transform(t) {}
mesh(const glm::mat4& t, std::initializer_list<primitive> primitives)
: m_transform(t) {
for (const auto& p : primitives) {
m_primitives.push_back(p);
}
}
void add_primitive(const primitive& prim) {
return m_primitives.push_back(prim);
}
void add_submesh(const class mesh& mesh) {
return m_submeshes.push_back(mesh);
}
const glm::mat4& transform() const noexcept {
return m_transform;
}
const primitive_storage& primitives() const noexcept {
return m_primitives;
}
const submesh_storage& submeshes() const noexcept {
return m_submeshes;
}
private:
primitive_storage m_primitives {};
submesh_storage m_submeshes {};
glm::mat4 m_transform {};
};
} // namespace kuiper::resource

View File

@@ -0,0 +1,56 @@
#pragma once
#include "errors/errors.hpp"
#include "resources/material.hpp"
#include "resources/mesh.hpp"
#include <fastgltf/core.hpp>
#include <expected>
#include <filesystem>
#include <vector>
namespace kuiper::resource
{
class model {
public:
using mesh_storage = std::vector<mesh>;
using material_storage = std::vector<material>;
model() = default;
model(std::initializer_list<mesh> meshes) {
for (const auto& m : meshes) {
m_meshes.push_back(m);
}
}
void push_mesh(const mesh& m) {
return m_meshes.push_back(m);
}
void push_material(const material& m) {
return m_materials.push_back(m);
}
const mesh_storage& meshes() const noexcept {
return m_meshes;
}
const material_storage& materials() const noexcept {
return m_materials;
}
/// Construct a `model` from a path to a glTF file
static std::expected<model, kuiper::error> from_gltf_path(const std::filesystem::path& gltf_path);
/// Construct a `model` from a path to a glTF file, prefer this function to the above as fastgltf's `Parser` should
/// ideally be re-used (only on one thread, though)
static std::expected<model, kuiper::error> from_gltf_path(const std::filesystem::path& gltf_path,
fastgltf::Parser& gltf_parser);
private:
mesh_storage m_meshes;
material_storage m_materials;
};
} // namespace kuiper::resource

View File

@@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <glm/vec2.hpp>
#include <glm/vec3.hpp>
#include <glm/vec4.hpp>
#include <cstdint>
#include <vector>
namespace kuiper
{
struct vertex {
glm::vec3 position; // (x, y, z), float
glm::vec3 normal; // (x, y, z), float
glm::vec4 tangent; // (x, y, z, w), float
glm::vec2 uv; // (x, y) float
};
enum class primitive_type : std::uint8_t {
none = 0,
triangles
};
} // namespace kuiper
namespace kuiper::resource
{
struct primitive {
primitive_type type {primitive_type::none};
std::vector<vertex> vertices {};
std::vector<std::uint32_t> indices {};
std::size_t material_idx {};
};
} // namespace kuiper::resource

View File

@@ -0,0 +1,13 @@
#pragma once
#include <filesystem>
namespace kuiper::resource
{
class index {
public:
static index from_file(const std::filesystem::path& index_path) noexcept;
};
} // namespace kuiper::resource

View File

@@ -0,0 +1,96 @@
#pragma once
#include "graphics/opengl/shader.hpp"
#include "graphics/opengl/texture.hpp"
#include "resources/material.hpp"
#include "resources/model.hpp"
#include "utils/xoshiro256plusplus.hpp"
#include <absl/container/flat_hash_map.h>
#include <concepts>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
namespace kuiper
{
using resource_id_t = std::uint32_t;
enum class resource_type : std::uint8_t {
none = 0,
texture,
shader,
model
};
struct resource_metadata {
resource_id_t id {0};
resource_type type {resource_type::none};
std::string name {};
};
class resource_manager {
public:
using metadata_map_t = absl::flat_hash_map<resource_id_t, resource_metadata>;
using texture_map_t = absl::flat_hash_map<resource_id_t, gl::texture::s_ptr>;
using shader_map_t = absl::flat_hash_map<resource_id_t, gl::shader::s_ptr>;
using model_map_t = absl::flat_hash_map<resource_id_t, resource::model>;
using material_map_t = absl::flat_hash_map<resource_id_t, resource::material>;
template<typename T>
resource_metadata add(T&& resource, std::string_view name = {}) {
const auto new_id = static_cast<resource_id_t>(m_prng.next());
resource_type type = resource_type::none;
if constexpr (std::same_as<T, resource::model>) {
type = resource_type::model;
m_models.insert({new_id, std::forward<T>(resource)});
} else {
static_assert(!"non-exhaustive if constexpr");
}
resource_metadata metadata {new_id, type, std::string(name)};
m_metadata.insert({new_id, metadata});
return metadata;
}
template<typename T>
std::optional<T> get(resource_id_t id) {
if constexpr (std::same_as<T, resource::model>) {
if (m_models.contains(id)) {
return m_models.at(id);
} else {
return std::nullopt;
}
} else {
static_assert(!"non-exhaustive if constexpr");
}
return std::nullopt;
}
std::optional<resource_metadata> import_from_path(const std::filesystem::path& path);
const metadata_map_t& metadata() const noexcept {
return m_metadata;
}
const model_map_t& models() const noexcept {
return m_models;
}
private:
random::xoshiro256pp m_prng {random::xoshiro256pp::from_random_device()};
metadata_map_t m_metadata {};
texture_map_t m_textures {};
shader_map_t m_shaders {};
model_map_t m_models {};
material_map_t m_materials {};
};
} // namespace kuiper

74
include/utils/arc.hpp Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include <atomic>
#include <cstddef>
#include <utility>
namespace kuiper
{
/// Atomically reference counted type
template<typename T>
class arc {
public:
/// Argument-forwarding constructor
template<typename... Args>
explicit arc(Args&&... args)
: m_inner(std::forward<Args>(args)...) {}
/// Default constructor (no args.)
arc()
: m_inner() {}
/// Destructor
~arc() {
if (m_ref_count.load() > 0)
return;
}
/// Copy constructor
arc(const arc& other)
: m_inner(other.m_inner), m_ref_count(other.m_ref_count.load() + 1) {}
/// Copy assignment constructor
arc& operator=(const arc& other) {
m_inner = other.m_inner;
m_ref_count = other.m_ref_count.load() + 1;
return *this;
}
/// Move constructor
arc(arc&& other) noexcept
: m_inner(std::move(other.m_inner)), m_ref_count(other.m_ref_count.load()) {};
/// Move assignment constructor
arc& operator=(arc&& other) noexcept {
*this = std::move(other);
return *this;
};
// Inner type access
/// Reference to the type contained in the `arc`
T& inner() noexcept {
return m_inner;
}
/// Read-only reference to the type contained in the `arc`
const T& const_inner() const noexcept {
return m_inner;
}
// Reference count
std::size_t ref_count() const noexcept {
return m_ref_count.load();
}
private:
T m_inner;
std::atomic_size_t m_ref_count = 0;
};
} // namespace kuiper

25
include/utils/assert.hpp Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include <cstdlib>
#include <print>
#include <stacktrace>
#include <string>
namespace kuiper
{
#define KUIPER_ASSERT(cond) \
if (!(cond)) { \
std::println(stderr, "Assertion failed at: {}", __PRETTY_FUNCTION__); \
std::abort(); \
}
#define KUIPER_ASSERT_STACKTRACE(cond) \
if (!(cond)) { \
const auto trace = std::stacktrace::current(); \
std::println(stderr, "Assertion failed at: {}", __PRETTY_FUNCTION__); \
std::println(stderr, "{}", std::to_string(trace)); \
std::abort(); \
}
} // namespace kuiper

35
include/utils/bytes.hpp Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <array>
#include <bit>
#include <concepts>
#include <cstdint>
namespace kuiper
{
namespace utils
{
namespace bits
{
template<typename T>
requires std::integral<T>
inline T set_bit_n(T value, std::uint8_t bit_n) {
return value | (static_cast<T>(0b00000001) << (bit_n - 1));
}
template<typename T>
requires std::integral<T>
inline T unset_bit_n(T value, std::uint8_t bit_n) {
return value & ~(static_cast<T>(0b00000001) << (bit_n - 1));
}
} // namespace bits
namespace bytes
{
template<typename T>
inline constexpr std::array<std::uint8_t, sizeof(T)> as_byte_array(T value) {
return std::bit_cast<std::array<std::uint8_t, sizeof(T)>, T>(value);
}
} // namespace bytes
} // namespace utils
} // namespace kuiper

17
include/utils/colours.hpp Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <glm/vec4.hpp>
namespace kuiper
{
namespace colours
{
constexpr const glm::vec4 middle_grey(0.737254901961f, 0.737254901961f, 0.737254901961f, 1.0f);
constexpr const glm::vec4 dark_grey(0.1337f, 0.1337f, 0.1337f, 1.0f);
constexpr const glm::vec4 very_dark_grey(0.05f, 0.05f, 0.05f, 1.0f);
constexpr const glm::vec4 black(0.0f, 0.0f, 0.0f, 1.0f);
constexpr const glm::vec4 nothing(0.0f, 0.0f, 0.0f, 0.0f);
} // namespace colours
} // namespace kuiper

View File

@@ -0,0 +1,15 @@
#pragma once
namespace kuiper
{
namespace constants
{
constexpr const char* kuiper_engine_str = "Kuiper Engine";
constexpr const char8_t* kuiper_engine_u8str = u8"Kuiper Engine";
constexpr const char* author_str = "Adam Macdonald";
constexpr const char8_t* author_u8str = u8"Adam Macdonald";
} // namespace constants
} // namespace kuiper

73
include/utils/rc.hpp Normal file
View File

@@ -0,0 +1,73 @@
#pragma once
#include <cstddef>
#include <utility>
namespace kuiper
{
/// Reference counted type
template<typename T>
class rc {
public:
/// Argument-forwarding constructor
template<typename... Args>
explicit rc(Args&&... args)
: m_inner(std::forward<Args>(args)...) {}
/// Default constructor (no args.)
rc()
: m_inner() {}
/// Destructor
~rc() {
if (m_ref_count > 0)
return;
}
/// Copy constructor
rc(const rc& other)
: m_inner(other.m_inner), m_ref_count(other.m_ref_count + 1) {}
/// Copy assignment constructor
rc& operator=(const rc& other) {
m_inner = other.m_inner;
m_ref_count = other.m_ref_count + 1;
return *this;
}
/// Move constructor
rc(rc&& other) noexcept
: m_inner(std::move(other.m_inner)), m_ref_count(other.m_ref_count) {};
/// Move assignment constructor
rc& operator=(rc&& other) noexcept {
*this = std::move(other);
return *this;
};
// Inner type access
/// Reference to the type contained in the `rc`
T& inner() noexcept {
return m_inner;
}
/// Read-only reference to the type contained in the `rc`
const T& const_inner() const noexcept {
return m_inner;
}
// Reference count
std::size_t ref_count() const noexcept {
return m_ref_count;
}
private:
T m_inner;
std::size_t m_ref_count = 0;
};
} // namespace kuiper

39
include/utils/uuid.hpp Normal file
View File

@@ -0,0 +1,39 @@
#pragma once
#include "random/random.hpp"
#include <cstddef> // For size_t
#include <cstdint> // For uint64_t
namespace kuiper
{
class UUID {
public:
UUID()
: m_uuid(Random::Instance().next_u64()) {}
UUID(const std::uint64_t uuid)
: m_uuid(uuid) {}
UUID(const UUID&) = default;
operator std::uint64_t() const {
return m_uuid;
}
private:
std::uint64_t m_uuid = 0;
};
} // namespace kuiper
namespace std
{
template<typename T> struct hash;
template<> struct hash<kuiper::UUID> {
std::size_t operator()(const kuiper::UUID& uuid) const {
// SURELY there won't be too many collisions with 2^64 possible combinations :^)
return static_cast<uint64_t>(uuid);
}
};
} // namespace std

View File

@@ -0,0 +1,148 @@
#pragma once
#include <array>
#include <cstdint>
#include <random>
namespace kuiper::random
{
class xoshiro256pp {
public:
xoshiro256pp(const std::array<std::uint64_t, 4>& state)
: s(state) {}
static xoshiro256pp from_random_device() {
std::random_device rd {};
static_assert(sizeof(std::random_device::result_type) == 4);
const std::uint64_t a = static_cast<std::uint64_t>(rd()) << 32 | static_cast<std::uint64_t>(rd());
const std::uint64_t b = static_cast<std::uint64_t>(rd()) << 32 | static_cast<std::uint64_t>(rd());
const std::uint64_t c = static_cast<std::uint64_t>(rd()) << 32 | static_cast<std::uint64_t>(rd());
const std::uint64_t d = static_cast<std::uint64_t>(rd()) << 32 | static_cast<std::uint64_t>(rd());
return {{{a, b, c, d}}};
}
public:
std::uint64_t next() noexcept {
const std::uint64_t result = rotl(s[0] + s[3], 23) + s[0];
const std::uint64_t t = s[1] << 17;
s[2] ^= s[0];
s[3] ^= s[1];
s[1] ^= s[2];
s[0] ^= s[3];
s[2] ^= t;
s[3] = rotl(s[3], 45);
return result;
}
std::uint64_t operator()(void) noexcept {
return next();
}
private:
/* This is the jump function for the generator. It is equivalent
to 2^128 calls to next(); it can be used to generate 2^128
non-overlapping subsequences for parallel computations. */
void jump() noexcept {
static constexpr std::array<std::uint64_t, 4> JUMP = {
0x180ec6d33cfd0aba, 0xd5a61266f0c9392c, 0xa9582618e03fc9aa, 0x39abdc4529b1661c};
std::uint64_t s0 = 0;
std::uint64_t s1 = 0;
std::uint64_t s2 = 0;
std::uint64_t s3 = 0;
for (int i = 0; i < JUMP.size() / sizeof(std::uint64_t); i++)
for (int b = 0; b < 64; b++) {
if (JUMP[i] & UINT64_C(1) << b) {
s0 ^= s[0];
s1 ^= s[1];
s2 ^= s[2];
s3 ^= s[3];
}
next();
}
s[0] = s0;
s[1] = s1;
s[2] = s2;
s[3] = s3;
}
/* This is the long-jump function for the generator. It is equivalent to
2^192 calls to next(); it can be used to generate 2^64 starting points,
from each of which jump() will generate 2^64 non-overlapping
subsequences for parallel distributed computations. */
void long_jump() {
static constexpr std::array<std::uint64_t, 4> LONG_JUMP = {
0x76e15d3efefdcbbf, 0xc5004e441c522fb3, 0x77710069854ee241, 0x39109bb02acbe635};
std::uint64_t s0 = 0;
std::uint64_t s1 = 0;
std::uint64_t s2 = 0;
std::uint64_t s3 = 0;
for (int i = 0; i < LONG_JUMP.size() / sizeof(std::uint64_t); i++)
for (int b = 0; b < 64; b++) {
if (LONG_JUMP[i] & UINT64_C(1) << b) {
s0 ^= s[0];
s1 ^= s[1];
s2 ^= s[2];
s3 ^= s[3];
}
next();
}
s[0] = s0;
s[1] = s1;
s[2] = s2;
s[3] = s3;
}
inline std::uint64_t rotl(const std::uint64_t x, int k) {
return (x << k) | (x >> (64 - k));
}
private:
std::array<std::uint64_t, 4> s {};
};
// Original license & preface:
/* Written in 2019 by David Blackman and Sebastiano Vigna (vigna@acm.org)
To the extent possible under law, the author has dedicated all copyright
and related and neighboring rights to this software to the public domain
worldwide.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
/* This is xoshiro256++ 1.0, one of our all-purpose, rock-solid generators.
It has excellent (sub-ns) speed, a state (256 bits) that is large
enough for any parallel application, and it passes all tests we are
aware of.
For generating just floating-point numbers, xoshiro256+ is even faster.
The state must be seeded so that it is not everywhere zero. If you have
a 64-bit seed, we suggest to seed a splitmix64 generator and use its
output to fill s. */
} // namespace kuiper::random

View File

@@ -0,0 +1,76 @@
#pragma once
#include <GLFW/glfw3.h>
#include <glm/vec2.hpp>
#include <array>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <vector>
namespace kuiper
{
using action_fn_t = std::function<void()>;
enum action_type : std::uint8_t {
none = 0,
escape,
};
constexpr std::size_t max_actions = 1 << (sizeof(action_type) * 8);
struct key_state {
int state = GLFW_RELEASE;
};
struct mouse_state {
glm::vec2 pos;
glm::vec2 delta_pos;
};
class input_system {
public:
input_system() {
// Default engine action keymappings
m_action_keymap[GLFW_KEY_ESCAPE] = action_type::escape;
return;
}
~input_system() = default;
// Keyboard
void set_key_state(int key, int state);
int get_key_state(int key) const;
bool is_key_pressed(int key) const;
bool is_key_released(int key) const;
bool is_key_held(int key) const;
bool is_key_down(int key) const;
// Mouse
void set_mouse_pos(const glm::vec2& pos);
void set_mouse_delta(const glm::vec2& pos);
/// Get mouse position (top-left corner is the origin)
glm::vec2 get_mouse_pos() const;
/// Get change in mouse position since last frame (top-left corner is the origin)
glm::vec2 get_mouse_delta() const;
// Actions
// TODO: user can add a new custom action of their own
void subscribe_to_action(action_type type, const action_fn_t& callback);
private:
std::array<key_state, GLFW_KEY_LAST> m_key_states {};
std::array<action_type, GLFW_KEY_LAST> m_action_keymap {};
std::array<std::vector<action_fn_t>, max_actions> m_action_listeners {};
mouse_state m_mouse_state {};
glm::vec2 m_old_mouse_pos {};
};
} // namespace kuiper

40
include/window/window.hpp Normal file
View File

@@ -0,0 +1,40 @@
#pragma once
#include "graphics/opengl/framebuffer.hpp"
#include <glm/vec2.hpp>
#include <cstdint>
#include <memory>
#include <string_view>
namespace kuiper
{
class window {
public:
virtual ~window() = default;
virtual void* get_native_handle() const noexcept = 0;
virtual void set_title(std::u8string_view title) noexcept = 0;
virtual void poll_events() noexcept = 0;
virtual void swap_buffers() noexcept = 0;
virtual void set_swap_interval(std::uint32_t interval) noexcept = 0;
virtual void bind_context() noexcept = 0;
virtual gl::framebuffer::s_ptr back_buffer() const noexcept = 0;
virtual bool should_close() const noexcept = 0;
virtual bool is_minimised() const noexcept = 0;
virtual bool is_cursor_locked() const noexcept = 0;
virtual void lock_cursor() noexcept = 0;
virtual void unlock_cursor() noexcept = 0;
virtual void enable_raw_input() noexcept = 0;
virtual void disable_raw_input() noexcept = 0;
virtual glm::uvec2 get_size() const noexcept = 0;
virtual glm::uvec2 get_framebuffer_size() const noexcept = 0; // should this go in render context?
virtual glm::vec2 get_content_scale() const noexcept = 0;
};
} // namespace kuiper