Port codebase to C++20

This commit is contained in:
kayomn 2023-02-18 03:34:40 +00:00
parent 3a0da39b3e
commit d5b4281d36
25 changed files with 1122 additions and 1178 deletions

View File

@ -3,7 +3,7 @@ name: continuous integration
steps:
- name: build & test
image: euantorano/zig:0.9.1
image: ubuntu:jammy
commands:
- zig build test
- $(find zig-cache -name test) main.zig
- sudo apt install -y gcc python3.10
- python3.10 ./build.py

5
.gitignore vendored
View File

@ -1,2 +1,3 @@
**/zig-out/
**/zig-cache/
cache
runtime
runtime.exe

16
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/src"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu17",
"cppStandard": "gnu++20",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}

21
.vscode/launch.json vendored Executable file → Normal file
View File

@ -2,23 +2,20 @@
"version": "0.2.0",
"configurations": [
{
"name": "Build",
"name": "Runtime",
"type": "gdb",
"request": "launch",
"target": "${workspaceFolder}/zig-out/bin/ona",
"target": "./runtime",
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"preLaunchTask": "Build",
"valuesFormatting": "parseText"
},
{
"name": "Test",
"type": "gdb",
"name": "Build Script",
"type": "python",
"request": "launch",
"target": "${workspaceFolder}/zig-cache/o/b57ef32c79a05339fbe4a8eb648ff6df/test",
"arguments": "main.zig",
"cwd": "${workspaceRoot}",
"valuesFormatting": "parseText",
"preLaunchTask": "Build Test",
},
"program": "./build.py",
"console": "integratedTerminal",
"justMyCode": true
}
]
}

21
.vscode/settings.json vendored Executable file → Normal file
View File

@ -1,17 +1,8 @@
{
"editor.rulers": [100],
"files.exclude":{
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"**/zig-cache": true,
"**/zig-out": true,
},
"git.detectSubmodulesLimit": 0,
"git.ignoreSubmodules": true,
"files.associations": {
"type_traits": "cpp",
"cassert": "cpp",
"cstddef": "cpp",
"string_view": "cpp"
}
}

60
.vscode/tasks.json vendored
View File

@ -1,60 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"command": "zig build",
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
"revealProblems": "onProblem",
},
"problemMatcher": {
"source": "gcc",
"owner": "cpptools",
"fileLocation": [
"autoDetect",
"${cwd}",
],
"pattern": {
"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5,
}
}
},
{
"label": "Test",
"type": "shell",
"command": "$(find zig-cache -name test) src/main.zig",
"group": {
"kind": "test",
"isDefault": true
},
},
{
"label": "Build Test",
"type": "shell",
"command": "zig build test",
"group": "test"
},
],
}

35
build.py Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/env python3
import os
import subprocess
source_path = "./source/"
cache_path = "./cache/"
if not(os.path.exists(cache_path)):
os.mkdir(cache_path)
compile_command = f"clang++ -std=c++20 -fno-exceptions -fmodules -fprebuilt-module-path=./cache"
object_file_paths = []
def compile_module(source_file_path, module_identifier) -> None:
output_path = os.path.join(cache_path, module_identifier)
subprocess.run(f"{compile_command} -Xclang -emit-module-interface -c {source_file_path} -o {output_path}.pcm", shell=True, check=True)
subprocess.run(f"{compile_command} -c {source_file_path} -o {output_path}.o", shell=True, check=True)
object_file_paths.append(f"{output_path}.o")
def compile_package(root_module_name: str) -> None:
root_module_source_path = os.path.join(source_path, root_module_name)
compile_module(f"{root_module_source_path}.cpp", root_module_name)
if os.path.isdir(root_module_source_path):
for file_name in os.listdir(root_module_source_path):
compile_module(os.path.join(root_module_source_path, file_name), f"{root_module_name}.{os.path.splitext(file_name)[0]}")
compile_package("core")
compile_package("app")
compile_package("kym")
compile_package("runtime")
subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True)

View File

@ -1,34 +0,0 @@
const std = @import("std");
pub fn build(builder: *std.build.Builder) void {
const target = builder.standardTargetOptions(.{});
const mode = builder.standardReleaseOptions();
// Ona executable.
{
const ona_exe = builder.addExecutable("ona", "./src/main.zig");
ona_exe.setTarget(target);
ona_exe.setBuildMode(mode);
ona_exe.install();
ona_exe.addIncludeDir("./ext");
ona_exe.linkSystemLibrary("SDL2");
const run_cmd = ona_exe.run();
run_cmd.step.dependOn(builder.getInstallStep());
if (builder.args) |args| run_cmd.addArgs(args);
builder.step("run", "Run Ona application").dependOn(&run_cmd.step);
}
// Ona tests.
{
const ona_tests = builder.addTestExe("test", "./src/main.zig");
ona_tests.setTarget(target);
ona_tests.setBuildMode(mode);
builder.step("test", "Run Ona unit tests").dependOn(&ona_tests.step);
}
}

6
config.kym Normal file
View File

@ -0,0 +1,6 @@
return {
title = "Demo",
width = 640,
height = 480,
}

View File

@ -1,8 +0,0 @@
return {
name = "Ona",
initial_width = 1280,
initial_height = 800,
initial_scene = nil,
}

73
readme.md Normal file
View File

@ -0,0 +1,73 @@
# Ona
## Table of Contents
1. [Overview](#overview)
1. [Goals](#goals)
1. [Technical Details](#technical-details)
1. [Requirements](#requirements)
1. [Building](#building)
1. [Project Structure](#project-structure)
1. [No Headers](#no-headers)
1. [All Code is Equal](#all-code-is-equal)
## Overview
Ona is a straightforward game engine with the aim of staying reasonably lightweight through its
Ona is also the Catalan word for "wave".
## Goals
* Fully-featured two-dimensional raster and vector-derived rendering capabilities.
* Support major computer gaming ecosystems; Namely Microsoft Windows, SteamOS, and GNU Linux systems running on X11 or Wayland.
* Avoid shipping external dependencies beyond the executible itself.
* Be lightweight in base engine memory usage and disk size.
* Provide a simple scene graph system that translates its graph nodes into a cache-friendly representation at runtime.
* Provide an execution-speed optimized scripting interface through a Lua-inspired language named "Kym", with features like first-class support for common mathematic types used in rendering.
* One data serialization and configuration system to rule them all backed by the Kym scripting language.
* Opt-in overhead via a native C plug-in interface that allows for systems-level programmers to easily extend engine-level functionality and scripting language library tools.
## Technical Details
### Requirements
Ona currently depends the following third-party tools to build it:
* GNU C++ compiler with full C++20 support or above.
* Python interpreter version 3.10 or above.
Additionally, Ona depends on the following third-party system-wide dependencies:
* SDL2 version 2.0.20 or above.
As the project evolves, dependencies on libraries external to the project codebase will be minimized or removed outright to meet the goals of the project as closely as possible.
### Building
Once all third-party tools and system-wide dependencies are satisfied, navigate to the root project folder and run the `./build.py` Python build script.
By default, the build script will build the engine runtime, required for running games built with Ona, in release-debug mode.
### Project Structure
As Ona uses C++20, it is able to make use of the new modules language feature. While this brings with it a number of drawbacks, like a lack of widescale vendor adoption, it also provides some key benefits.
#### No Headers
All first-party code in the project is free of headers. Code is grouped in a module and package dichotomy, where each `.cpp` file in the root source directory represents the common package of a module grouping.
Subdirectories then build further abstractions atop these common module files. For example, the `core.cpp` source file contains many common memory manipulation and floating point mathematics utilities, which are made use of in `core/image.cpp` for modifying CPU-bound pixel data.
#### All Code is Equal
Following on from no headers necessary, declarations, template metaprogramming, and definitions all go into the same place now. A typical Ona source file mixes all of these, traditionally separate, pieces of logic together in shared `.cpp` files.
Alongside the surface-level benefit of writing having fewer lines of code, this also means there is less work necessary to maintain the codebase at large and a smaller space to create duplication errors in.

95
source/app.cpp Normal file
View File

@ -0,0 +1,95 @@
export module app;
import core;
import core.image;
import core.math;
export namespace app {
struct path {
static constexpr core::usize max = 0xff;
static constexpr char seperator = '/';
core::u8 buffer[max + 1];
static constexpr path empty() {
path empty_path = {0};
empty_path.buffer[max] = max;
return empty_path;
}
core::slice<char const> as_slice() const {
return {reinterpret_cast<char const *>(this->buffer), this->size()};
}
constexpr core::usize size() const {
return max - this->buffer[max];
}
core::i16 compare(path const & that) {
return 0;
}
bool equals(path const & that) {
return core::equals(this->as_slice(), that.as_slice());
}
constexpr path joined(core::slice<char const> const & text) const {
if (text.length > this->buffer[max]) return empty();
path joined_path = *this;
for (char const c : text) {
joined_path.buffer[joined_path.size()] = c;
joined_path.buffer[max] -= 1;
}
return joined_path;
}
core::u64 hash() {
return 0;
}
};
struct file_store {
virtual void read_file(app::path const & file_path, core::callable<void(core::readable const &)> const & then) = 0;
};
enum class log_level {
notice,
warning,
error,
};
struct system {
virtual bool poll() = 0;
virtual file_store & bundle() = 0;
virtual void log(log_level level, core::slice<char const> const & message) = 0;
virtual core::allocator & thread_safe_allocator() = 0;
};
struct graphics {
enum class show_error {
none,
out_of_memory,
};
struct canvas {
core::color background_color;
};
virtual void render(canvas & source_canvas) = 0;
virtual void present() = 0;
virtual show_error show(core::u16 physical_width, core::u16 physical_height) = 0;
virtual void retitle(core::slice<const char> const & updated_title) = 0;
};
}

162
source/app/sdl.cpp Normal file
View File

@ -0,0 +1,162 @@
module;
#include <SDL2/SDL.h>
export module app.sdl;
import app;
import core;
import core.image;
import core.sequence;
import core.math;
struct bundled_file_store : public app::file_store {
void read_file(app::path const & file_path, core::callable<void(core::readable const &)> const & then) override {
// Path is guaranteed to never be greater than 512 characters long (file_path is max 256 and prefix is 2).
core::stack<char, 512> path_buffer{&core::null_allocator()};
if (path_buffer.append("./").has_value()) core::unreachable();
// File path is guaranteed to be null-terminated.
if (path_buffer.append(file_path.as_slice()).has_value()) core::unreachable();
SDL_RWops * rw_ops = ::SDL_RWFromFile(path_buffer.as_slice().pointer, "r");
if (rw_ops == nullptr) return;
then([rw_ops](core::slice<uint8_t> const & buffer) -> size_t {
return ::SDL_RWread(rw_ops, buffer.pointer, sizeof(uint8_t), buffer.length);
});
::SDL_RWclose(rw_ops);
}
};
struct sdl_allocator : public core::allocator {
core::u8 * reallocate(core::u8 * maybe_allocation, core::usize requested_size) override {
return reinterpret_cast<core::u8 *>(::SDL_malloc(requested_size));
}
void deallocate(void * allocation) override {
::SDL_free(allocation);
}
};
struct sdl_system : public app::system {
private:
::SDL_Event sdl_event;
sdl_allocator allocator;
bundled_file_store bundled_store;
public:
sdl_system() :
sdl_event{0},
allocator{} {}
bool poll() override {
while (::SDL_PollEvent(&this->sdl_event) != 0) {
switch (this->sdl_event.type) {
case SDL_QUIT: return false;
}
}
return true;
}
app::file_store & bundle() override {
return this->bundled_store;
}
void log(app::log_level level, core::slice<char const> const & message) override {
core::i32 const length = static_cast<core::i32>(
core::min(message.length, static_cast<size_t>(core::i32_max)));
::SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer);
}
core::allocator & thread_safe_allocator() override {
return this->allocator;
}
};
struct sdl_graphics : public app::graphics {
static constexpr core::usize title_maximum = 128;
core::u32 title_length;
char title_buffer[title_maximum];
::SDL_Window * sdl_window = nullptr;
::SDL_Renderer * sdl_renderer = nullptr;
sdl_graphics(core::slice<char const> const & title) {
this->retitle(title);
}
void render(canvas & source_canvas) override {
if (this->sdl_renderer != nullptr) {
SDL_SetRenderDrawColor(this->sdl_renderer, source_canvas.background_color.to_r8(),
source_canvas.background_color.to_g8(), source_canvas.background_color.to_b8(),
source_canvas.background_color.to_a8());
SDL_RenderClear(this->sdl_renderer);
}
}
void retitle(core::slice<char const> const & title) override {
this->title_length = core::min(title.length, title_maximum - 1);
for (core::usize i = 0; i < this->title_length; i += 1) title_buffer[i] = title[i];
for (core::usize i = this->title_length; i < title_maximum; i += 1) title_buffer[i] = 0;
if (this->sdl_window != nullptr) ::SDL_SetWindowTitle(this->sdl_window, title_buffer);
}
app::graphics::show_error show(core::u16 physical_width, core::u16 physical_height) override {
if (this->sdl_window == nullptr) {
constexpr int sdl_windowpos = SDL_WINDOWPOS_UNDEFINED;
constexpr core::u32 sdl_windowflags = 0;
this->sdl_window = ::SDL_CreateWindow(title_buffer, sdl_windowpos, sdl_windowpos,
static_cast<int>(physical_width), static_cast<int>(physical_height),
sdl_windowflags);
if (this->sdl_window == nullptr) return show_error::out_of_memory;
} else {
::SDL_ShowWindow(this->sdl_window);
}
if (this->sdl_renderer == nullptr) {
constexpr core::u32 sdl_rendererflags = 0;
this->sdl_renderer = ::SDL_CreateRenderer(this->sdl_window, -1, sdl_rendererflags);
if (this->sdl_renderer == nullptr) return show_error::out_of_memory;
}
return show_error::none;
}
void present() override {
if (this->sdl_renderer != nullptr) {
::SDL_RenderPresent(this->sdl_renderer);
}
}
};
export namespace app {
int display(core::slice<char const> const & title,
core::callable<int(app::system &, app::graphics &)> const & run) {
sdl_system system;
sdl_graphics graphics(title);
return run(system, graphics);
}
}

272
source/core.cpp Normal file
View File

@ -0,0 +1,272 @@
module;
#include <cstdint>
#include <cstddef>
export module core;
export namespace core {
using usize = size_t;
using size = __ssize_t;
using u8 = uint8_t;
usize const u8_max = 0xff;
using i8 = uint8_t;
using u16 = uint16_t;
usize const u16_max = 0xffff;
using i16 = uint16_t;
using u32 = uint32_t;
using i32 = uint32_t;
usize const i32_max = 0xffffffff;
using u64 = uint32_t;
using i64 = uint32_t;
using f32 = float;
using f64 = double;
struct allocator {
allocator() = default;
allocator(allocator const &) = delete;
virtual ~allocator() {};
virtual u8 * reallocate(u8 * maybe_allocation, usize requested_size) = 0;
virtual void deallocate(void * allocation) = 0;
};
}
export void * operator new(core::usize requested_size, core::u8 * allocation_placement) {
return allocation_placement;
}
export void * operator new[](core::usize requested_size, core::u8 * allocation_placement) {
return allocation_placement;
}
export void * operator new(core::usize requested_size, core::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
export void * operator new[](core::usize requested_size, core::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
export namespace core {
[[noreturn]] void unreachable() {
__builtin_unreachable();
}
template<typename type> struct slice {
usize length;
type * pointer;
constexpr slice() : length{0}, pointer{nullptr} {
}
constexpr slice(char const *&& zstring) : length{0}, pointer{zstring} {
while (zstring[length] != 0) this->length += 1;
}
constexpr slice(type * slice_pointer, usize slice_length) : length{slice_length}, pointer{slice_pointer} {
}
template<usize array_size> constexpr slice(type(&array)[array_size]) : length{array_size}, pointer{array} {
}
slice<u8 const> as_bytes() const {
return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)};
}
slice<char const> as_chars() const {
return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)};
}
operator slice<type const>() const {
return (*reinterpret_cast<slice<type const> const *>(this));
}
type & operator[](usize index) const {
return this->pointer[index];
}
slice<type> after(usize index) const {
return {this->pointer + index, this->length - index};
}
slice<type> until(usize index) const {
return {this->pointer, index};
}
slice<type> between(usize a, usize b) const {
return {this->pointer + a, b};
}
constexpr type * begin() const {
return this->pointer;
}
constexpr type * end() const {
return this->pointer + this->length;
}
};
template<typename element> struct [[nodiscard]] optional {
optional() : buffer{0} {
}
optional(element const & value) : buffer{0} {
(*reinterpret_cast<element *>(this->buffer)) = value;
this->buffer[sizeof(element)] = 1;
}
optional(optional const & that) : buffer{0} {
if (that.has_value()) {
(*reinterpret_cast<element *>(this->buffer)) = that.value();
this->buffer[sizeof(element)] = 1;
} else {
this->buffer[sizeof(element)] = 0;
}
}
bool has_value() const {
return this->buffer[sizeof(element)] == 1;
}
element const & value_or(element const & fallback) const {
return this->has_value() ? *reinterpret_cast<element const *>(this->buffer) : fallback;
}
element & value() {
return *reinterpret_cast<element *>(this->buffer);
}
element const & value() const {
return *reinterpret_cast<element const *>(this->buffer);
}
private:
u8 buffer[sizeof(element) + 1];
};
template<typename> struct callable;
template<typename return_type, typename... argument_types> struct callable<return_type(argument_types...)> {
using function = return_type(*)(argument_types...);
callable(function callable_function) : dispatcher(dispatch_function) {
new (this->capture) function{callable_function};
}
callable(callable const &) = delete;
template<typename functor> callable(functor const & callable_functor) : dispatcher(dispatch_functor<functor>) {
new (this->capture) functor{callable_functor};
}
return_type operator()(argument_types const &... arguments) const {
return this->dispatcher(this->capture, arguments...);
}
private:
static constexpr usize capture_size = 24;
return_type(* dispatcher)(u8 const * userdata, argument_types... arguments);
u8 capture[capture_size];
static return_type dispatch_function(u8 const * userdata, argument_types... arguments) {
return (*reinterpret_cast<function const *>(userdata))(arguments...);
}
template<typename functor_type> static return_type dispatch_functor(u8 const * userdata, argument_types... arguments) {
return (*reinterpret_cast<functor_type const*>(userdata))(arguments...);
}
};
allocator & null_allocator() {
static struct : public allocator {
u8 * reallocate(u8 * maybe_allocation, usize requested_size) override {
if (maybe_allocation != nullptr) unreachable();
return nullptr;
}
void deallocate(void * allocation) override {
if (allocation != nullptr) unreachable();
}
} a;
return a;
}
using readable = callable<optional<usize>(slice<u8> const &)>;
using writable = callable<optional<usize>(slice<u8 const> const &)>;
template<typename element_type> bool equals(slice<element_type const> const & a, slice<element_type const> const & b) {
if (a.length != b.length) return false;
for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false;
return true;
}
optional<usize> stream(writable const & output, readable const & input, slice<u8> const & buffer) {
usize written = 0;
optional maybe_read = input(buffer);
if (!maybe_read.has_value()) return {};
usize read = maybe_read.value();
while (read != 0) {
optional const maybe_written = output(buffer.until(read));
if (!maybe_written.has_value()) return {};
written += maybe_written.value();
maybe_read = input(buffer);
if (!maybe_read.has_value()) return {};
read = maybe_read.value();
}
return written;
}
template<typename scalar> scalar max(scalar const & a, scalar const & b) {
return (a > b) ? a : b;
}
template<typename scalar> scalar min(scalar const & a, scalar const & b) {
return (a < b) ? a : b;
}
template<typename scalar> scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) {
return max(min_value, min(max_value, value));
}
f32 round32(f32 value) {
return __builtin_roundf(value);
}
}

31
source/core/image.cpp Normal file
View File

@ -0,0 +1,31 @@
export module core.image;
import core;
export namespace core {
struct color {
float r;
float g;
float b;
float a;
u8 to_r8() const {
return static_cast<u8>(round32(clamp(this->r, 0.0f, 1.0f) * u8_max));
}
u8 to_g8() const {
return static_cast<u8>(round32(clamp(this->g, 0.0f, 1.0f) * u8_max));
}
u8 to_b8() const {
return static_cast<u8>(round32(clamp(this->b, 0.0f, 1.0f) * u8_max));
}
u8 to_a8() const {
return static_cast<u8>(round32(clamp(this->a, 0.0f, 1.0f) * u8_max));
}
};
}

19
source/core/math.cpp Normal file
View File

@ -0,0 +1,19 @@
export module core.math;
import core;
export namespace core {
struct vector2 {
core::f32 x;
core::f32 y;
};
struct vector3 {
core::f32 x;
core::f32 y;
core::f32 z;
};
}

118
source/core/sequence.cpp Normal file
View File

@ -0,0 +1,118 @@
export module core.sequence;
import core;
export namespace core {
enum class reserve_error {
out_of_memory,
};
template<typename element> struct sequence {
sequence() = default;
sequence(sequence const &) = delete;
virtual ~sequence() {};
virtual slice<element const> as_slice() const = 0;
virtual optional<reserve_error> append(slice<element const> const & appended_elements) = 0;
};
template<typename element, usize initial_capacity = 1> struct stack : public sequence<element> {
stack(allocator * buffer_allocator) : filled{0},
buffer{0}, buffer_allocator{buffer_allocator} {
this->elements = this->buffer;
}
~stack() override {
if (this->elements.pointer != this->buffer)
this->buffer_allocator->deallocate(this->elements.pointer);
}
slice<element const> as_slice() const override {
return this->elements;
}
optional<reserve_error> push(element const & pushed_element) {
if (this->filled == this->elements.length) {
optional const maybe_error = this->reserve(this->elements.length);
if (maybe_error.has_value()) return maybe_error;
}
this->elements[this->filled] = pushed_element;
this->filled += 1;
return {};
}
optional<reserve_error> append(slice<element const> const & pushed_elements) override {
usize const updated_fill = this->filled + pushed_elements.length;
if (updated_fill >= this->elements.length) {
optional const maybe_error = this->reserve(max(this->elements.length, updated_fill));
if (maybe_error.has_value()) return maybe_error;
}
for (usize i = 0; i < pushed_elements.length; i += 1)
this->elements[this->filled + i] = pushed_elements[i];
this->filled = updated_fill;
return {};
}
optional<reserve_error> reserve(usize capacity) {
usize const requested_capacity = this->elements.length + capacity;
if (this->elements.pointer == this->buffer) {
u8 * const maybe_allocation = this->buffer_allocator->
reallocate(nullptr, sizeof(element) * requested_capacity);
if (maybe_allocation == nullptr) {
this->elements = {};
return reserve_error::out_of_memory;
}
this->elements = {reinterpret_cast<element *>(maybe_allocation), requested_capacity};
} else {
u8 * const maybe_allocation = this->buffer_allocator->reallocate(
reinterpret_cast<u8 *>(this->elements.pointer),
sizeof(element) * requested_capacity);
if (maybe_allocation == nullptr) {
this->elements = {};
return reserve_error::out_of_memory;
}
this->elements = {reinterpret_cast<element *>(maybe_allocation), requested_capacity};
}
return {};
}
private:
allocator * buffer_allocator;
usize filled;
slice<element> elements;
element buffer[initial_capacity];
};
struct sequence_writer : public writable {
sequence_writer(sequence<u8> * target_sequence) : writable{[target_sequence](slice<u8 const> const & buffer) -> optional<usize> {
optional const maybe_error = target_sequence->append(buffer);
if (maybe_error.has_value()) return {};
return buffer.length;
}} {}
};
}

49
source/kym.cpp Normal file
View File

@ -0,0 +1,49 @@
export module kym;
import core;
import core.math;
export namespace kym {
enum class value_type {
nil,
boolean,
integer,
scalar,
vector2,
vector3,
object,
};
struct value {
value_type type;
union {
bool boolean;
core::i64 integer;
core::f64 scalar;
core::vector2 vector2;
core::vector3 vector3;
void * object;
} as;
core::optional<core::u16> as_u16() const {
if ((this->type == value_type::integer) &&
(this->as.integer >= 0) && (this->as.integer <= core::u16_max)) {
return static_cast<core::u16>(this->as.integer);
}
return {};
}
};
value nil = {
.type = value_type::nil,
.as = {.object = nullptr},
};
};

144
source/kym/environment.cpp Normal file
View File

@ -0,0 +1,144 @@
export module kym.environment;
import core;
import core.sequence;
import kym;
export namespace kym {
struct vm;
}
enum class token_kind {
end,
};
struct token {
core::slice<char const> text;
token_kind kind;
};
using tokenizable = core::callable<token()>;
struct bytecode {
bytecode(core::allocator * allocator) : error_message_buffer{allocator} {
}
bool compile(tokenizable const & bytecode_tokenizable) {
for (;;) {
token const initial_token = bytecode_tokenizable();
switch (initial_token.kind) {
case token_kind::end: return true;
default: core::unreachable();
}
}
}
kym::value execute(kym::vm & vm, core::slice<kym::value> const & arguments) {
return kym::nil;
}
core::slice<char const> error_message() const {
return this->error_message_buffer.as_slice();
}
private:
core::stack<char> error_message_buffer;
};
export namespace kym {
struct bound_object {
core::callable<void()> cleanup;
core::callable<value(core::slice<value>)> call;
value get_field(core::slice<char const> const & field_name) {
return nil;
}
bool is_string() {
return false;
}
core::usize stringify(core::writable const & writable) {
return 0;
}
};
struct vm {
struct init_options {
core::u16 datastack_size;
core::u16 callstack_size;
};
vm(core::allocator * allocator, auto log) : allocator{allocator}, log{log}, data_stack{} {}
~vm() {
if (this->data_stack.pointer != nullptr)
this->allocator->deallocate(this->data_stack.pointer);
}
bool init(init_options const & options) {
core::u8 * const data_stack_buffer = this->allocator->reallocate(reinterpret_cast
<core::u8 *>(this->data_stack.pointer), options.datastack_size * sizeof(value));
if (data_stack_buffer == nullptr) return false;
this->data_stack = {
reinterpret_cast<value * >(data_stack_buffer), options.datastack_size};
return true;
}
value compile(core::slice<char const> const & source) {
bytecode * source_bytecode = new (*this->allocator) bytecode{allocator};
if (source_bytecode == nullptr) return nil;
core::usize cursor = 0;
if (source_bytecode->compile([]() {
return token{
};
})) {
this->log(source_bytecode->error_message());
this->allocator->deallocate(source_bytecode);
return nil;
}
return this->new_object([this, source_bytecode](bound_object & object) {
object.cleanup = [this, source_bytecode]() {
this->allocator->deallocate(source_bytecode);
};
object.call = [this, source_bytecode](core::slice<value> const & arguments) {
return source_bytecode->execute(*this, arguments);
};
this->allocator->deallocate(source_bytecode);
});
}
value new_object(core::callable<void(bound_object &)> const & then) {
return nil;
}
void with_object(value object_value, core::callable<void(bound_object &)> const & then) {
}
private:
core::slice<value> data_stack;
core::callable<void(core::slice<char const>)> log;
core::allocator * allocator;
};
}

81
source/runtime.cpp Normal file
View File

@ -0,0 +1,81 @@
export module runtime;
import app;
import app.sdl;
import core;
import core.math;
import core.sequence;
import kym;
import kym.environment;
extern "C" int main(int argc, char const * const * argv) {
return app::display("Ona Runtime", [](app::system & system, app::graphics & graphics) -> int {
constexpr app::path config_path = app::path::empty().joined("config.kym");
bool is_config_loaded = false;
system.bundle().read_file(config_path, [&](core::readable const & config_readable) {
kym::vm vm{&system.thread_safe_allocator(), [&system](core::slice<char const> const & error_message) {
system.log(app::log_level::error, error_message);
}};
if (!vm.init({
.datastack_size = 64,
.callstack_size = 64,
})) {
system.log(app::log_level::error, "failed to allocate memory for config vm");
return;
}
core::stack<core::u8> config_source{&system.thread_safe_allocator()};
core::u8 config_source_stream_buffer[1024] = {};
if (!core::stream(core::sequence_writer{&config_source},
config_readable, config_source_stream_buffer).has_value()) return;
vm.with_object(vm.compile(config_source.as_slice().as_chars()), [&](kym::bound_object & config_script) {
vm.with_object(config_script.call({}), [&](kym::bound_object & config) {
core::u16 const width = config.get_field("width").as_u16().value_or(0);
if (width == 0) return system.log(
app::log_level::error, "failed to decode `width` property of config");
core::u16 const height = config.get_field("height").as_u16().value_or(0);
if (height == 0) return system.log(
app::log_level::error, "failed to decode `height` property of config");
graphics.show(width, height);
vm.with_object(config.get_field("title"), [&](kym::bound_object & title) {
core::stack<core::u8, 128> title_buffer{&system.thread_safe_allocator()};
if (!title.is_string()) return system.log(
app::log_level::error, "failed to decode `title` property of config");
title.stringify(core::sequence_writer(&title_buffer));
is_config_loaded = true;
});
});
});
});
if (!is_config_loaded) {
system.log(app::log_level::error, "failed to load config");
return core::u8_max;
}
// app::canvas canvas_2d();
while (system.poll()) {
// canvas_2d.render(graphics);
graphics.present();
}
return 0;
});
}

View File

@ -1,148 +0,0 @@
const stack = @import("./stack.zig");
const std = @import("std");
///
/// Opaque interface to a "writable" resource, such as a block device, memory buffer, or network
/// socket.
///
pub const Writer = struct {
context: *anyopaque,
writeContext: fn (*anyopaque, []const u8) usize,
///
/// Radices supported by [writeInt].
///
pub const Radix = enum {
binary,
tinary,
quaternary,
quinary,
senary,
septenary,
octal,
nonary,
decimal,
undecimal,
duodecimal,
tridecimal,
tetradecimal,
pentadecimal,
hexadecimal,
};
///
/// Wraps and returns a reference to `write_context` of type `WriteContext` and its associated
/// `writeContext` writing operation in a [Writer].
///
pub fn wrap(
comptime WriteContext: type,
write_context: *WriteContext,
comptime writeContext: fn (*WriteContext, []const u8) usize
) Writer {
return .{
.context = write_context,
.writeContext = struct {
fn write(context: *anyopaque, buffer: []const u8) usize {
return writeContext(@ptrCast(*WriteContext,
@alignCast(@alignOf(WriteContext), context)), buffer);
}
}.write,
};
}
///
/// Attempts to write `buffer` to `writer`, returning the number of bytes from `buffer` that
/// were successfully written.
///
pub fn write(writer: Writer, buffer: []const u8) usize {
return writer.writeContext(writer.context, buffer);
}
///
/// Writes the singular `byte` to `writer`, returning `true` if it was successfully written,
/// otherwise `false`.
///
pub fn writeByte(writer: Writer, byte: u8) bool {
return (writer.writeContext(writer.context,
@ptrCast([*]const u8, &byte)[0 .. 1]) != 0);
}
///
/// Writes `value` as a ASCII / UTF-8 encoded integer to `writer`, returning `true` if the full
/// sequence was successfully written, otherwise `false`.
///
/// The `radix` argument identifies which base system to encode `value` as, with `10` being
/// decimal, `16` being hexadecimal, `8` being octal`, so on and so forth.
///
pub fn writeInt(writer: Writer, radix: Radix, value: anytype) bool {
const Int = @TypeOf(value);
const type_info = @typeInfo(Int);
switch (type_info) {
.Int => {
if (value == 0) return writer.writeByte('0');
// TODO: Unhardcode this as it will break with large ints.
var buffer = std.mem.zeroes([28]u8);
var buffer_count = @as(usize, 0);
var n1 = value;
if ((type_info.Int.signedness == .signed) and (value < 0)) {
// Negative value.
n1 = -value;
buffer[0] = '-';
buffer_count += 1;
}
while (n1 != 0) {
const base = @enumToInt(radix);
buffer[buffer_count] = @intCast(u8, (n1 % base) + '0');
n1 = (n1 / base);
buffer_count += 1;
}
for (buffer[0 .. (buffer_count / 2)]) |_, i|
std.mem.swap(u8, &buffer[i], &buffer[buffer_count - i - 1]);
return (writer.write(buffer[0 .. buffer_count]) == buffer_count);
},
// Cast comptime int into known-size integer and try again.
.ComptimeInt => return writer.
writeInt(radix, @intCast(std.math.IntFittingRange(value, value), value)),
else => @compileError("value must be of type int"),
}
}
};
///
/// Writer that silently throws consumed data away and never fails.
///
/// This is commonly used for testing or redirected otherwise unwanted output data that can't not be
/// sent somewhere for whatever reason.
///
pub const null_writer = Writer{
.context = undefined,
.writeContext = struct {
fn write(_: *anyopaque, buffer: []const u8) usize {
return buffer.len;
}
}.write,
};
test {
const testing = std.testing;
{
const sequence = "foo";
try testing.expectEqual(null_writer.write(sequence), sequence.len);
}
try testing.expect(null_writer.writeByte(0));
try testing.expect(null_writer.writeInt(.decimal, 420));
}

View File

@ -1,50 +0,0 @@
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const io = @import("./io.zig");
const stack = @import("./stack.zig");
const std = @import("std");
const sys = @import("./sys.zig");
///
/// Entry point.
///
pub fn main() anyerror!void {
return sys.runGraphics(anyerror, run);
}
test {
_ = io;
_ = stack;
_ = std;
_ = sys;
}
fn run(event_loop: *sys.EventLoop, graphics: *sys.GraphicsContext) anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
{
const file_access = try event_loop.open(.readonly,
try sys.FileSystem.data.joinedPath(&.{"data", "ona.lua"}));
defer event_loop.close(file_access);
const file_size = try file_access.size(event_loop);
const allocator = gpa.allocator();
const buffer = try allocator.alloc(u8, file_size);
defer allocator.free(buffer);
if ((try event_loop.readFile(file_access, buffer)) != file_size)
return error.ScriptLoadFailure;
event_loop.log(.debug, buffer);
}
while (graphics.poll()) |_| {
graphics.present();
}
}

View File

@ -1,87 +0,0 @@
const std = @import("std");
///
/// State machine for lazily computing all components of [Spliterator.source] that match the pattern
/// in [Spliterator.delimiter].
///
pub fn Spliterator(comptime Element: type) type {
return struct {
source: []const Element,
delimiter: []const Element,
const Self = @This();
///
/// Returns `true` if there is more data to be processed, otherwise `false`.
///
pub fn hasNext(self: Self) bool {
return (self.source.len != 0);
}
///
/// Iterates on `self` and returns the next view of [Spliterator.source] that matches
/// [Spliterator.delimiter], or `null` if there is no more data to be processed.
///
pub fn next(self: *Self) ?[]const Element {
if (!self.hasNext()) return null;
if (std.mem.indexOfPos(Element, self.source, 0, self.delimiter)) |index| {
defer self.source = self.source[(index + self.delimiter.len) .. self.source.len];
return self.source[0 .. index];
}
defer self.source = self.source[self.source.len .. self.source.len];
return self.source;
}
};
}
test {
const testing = std.testing;
// Single-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "single.character.separated.hello.world",
.delimiter = ".",
};
const components = [_][]const u8{"single", "character", "separated", "hello", "world"};
var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(std.mem.eql(u8, split, components[index]));
}
}
// Multi-character delimiter.
{
var spliterator = Spliterator(u8){
.source = "finding a needle in a needle stack",
.delimiter = "needle",
};
const components = [_][]const u8{"finding a ", " in a ", " stack"};
var index = @as(usize, 0);
while (spliterator.next()) |split| : (index += 1) {
try testing.expect(std.mem.eql(u8, split, components[index]));
}
}
}
///
/// Searches the slice of `Data` referenced by `data` for the first instance of `sought_datum`,
/// returning its index or `null` if it could not be found.
///
pub fn findFirst(comptime Data: type, data: []const Data, sought_datum: Data) ?usize {
for (data) |datum, index| if (datum == sought_datum) return index;
return null;
}
test {
try std.testing.expectEqual(findFirst(u8, "1234567890", '7'), 6);
}

View File

@ -1,117 +0,0 @@
const io = @import("./io.zig");
const std = @import("std");
pub fn Fixed(comptime Element: type) type {
return struct {
filled: usize = 0,
buffer: []Element,
const Self = @This();
///
/// Wraps `self` and returns it in a [io.Writer] value.
///
/// Note that this will raise a compilation error if [Element] is not `u8`.
///
pub fn writer(self: *Self) io.Writer {
if (Element != u8) @compileError("Cannot coerce fixed stack of type " ++
@typeName(Element) ++ " into a Writer");
return io.Writer.wrap(Self, self, struct {
fn write(stack: *Self, buffer: []const u8) usize {
stack.pushAll(buffer) catch |err| switch (err) {
error.Overflow => return 0,
};
return buffer.len;
}
}.write);
}
///
/// Clears all elements from `self`.
///
pub fn clear(self: *Self) void {
self.filled = 0;
}
///
/// Counts and returns the number of pushed elements in `self`.
///
pub fn count(self: Self) usize {
return self.filled;
}
///
/// Attempts to pop the tail-end of `self`, returning the element value or `null` if the
/// stack is empty.
///
pub fn pop(self: *Self) ?Element {
if (self.filled == 0) return null;
self.filled -= 1;
return self.buffer[self.filled];
}
///
/// Attempts to push `element` into `self`, returning a [FixedPushError] if it failed.
///
pub fn push(self: *Self, element: Element) FixedPushError!void {
if (self.filled == self.buffer.len) return error.Overflow;
self.buffer[self.filled] = element;
self.filled += 1;
}
///
/// Attempts to push all of `elements` into `self`, returning a [FixedPushError] if it
/// failed.
///
pub fn pushAll(self: *Self, elements: []const u8) FixedPushError!void {
const filled = (self.filled + elements.len);
if (filled > self.buffer.len) return error.Overflow;
std.mem.copy(u8, self.buffer[self.filled ..], elements);
self.filled = filled;
}
};
}
///
/// Potential errors that may occur while trying to push one or more elements into a stack of a
/// known maximum size.
///
/// [FinitePushError.Overflow] is returned if the stack does not have sufficient capacity to hold a
/// given set of elements.
///
pub const FixedPushError = error {
Overflow,
};
test {
const testing = std.testing;
var buffer = std.mem.zeroes([4]u8);
var stack = Fixed(u8){.buffer = &buffer};
try testing.expectEqual(stack.count(), 0);
try testing.expectEqual(stack.pop(), null);
try stack.push(69);
try testing.expectEqual(stack.count(), 1);
try testing.expectEqual(stack.pop(), 69);
try stack.pushAll(&.{42, 10, 95, 0});
try testing.expectEqual(stack.count(), 4);
try testing.expectError(FixedPushError.Overflow, stack.push(1));
try testing.expectError(FixedPushError.Overflow, stack.pushAll(&.{1, 11, 11}));
stack.clear();
try testing.expectEqual(stack.count(), 0);
const writer = stack.writer();
try testing.expectEqual(writer.write(&.{0, 0, 0, 0}), 4);
try testing.expectEqual(writer.writeByte(0), false);
}

View File

@ -1,642 +0,0 @@
const ext = @cImport({
@cInclude("SDL2/SDL.h");
});
const io = @import("./io.zig");
const mem = @import("./mem.zig");
const stack = @import("./stack.zig");
const std = @import("std");
///
/// A thread-safe platform abstraction over multiplexing system I/O processing and event handling.
///
pub const EventLoop = opaque {
///
/// Linked list of messages chained together to be processed by the internal file system message
/// processor of an [EventLoop].
///
const FileSystemMessage = struct {
next: ?*FileSystemMessage = null,
frame: anyframe,
request: union(enum) {
exit,
close: struct {
file_access: *FileAccess,
},
log: struct {
message: []const u8,
kind: LogKind,
},
open: struct {
mode: OpenMode,
file_system_path: *const FileSystem.Path,
result: OpenError!*FileAccess = error.NotFound,
},
read_file: struct {
file_access: *FileAccess,
buffer: []const u8,
result: FileError!usize = error.Inaccessible,
},
seek_file: struct {
file_access: *FileAccess,
origin: SeekOrigin,
offset: usize,
result: FileError!void = error.Inaccessible,
},
tell_file: struct {
file_access: *FileAccess,
result: FileError!usize = error.Inaccessible,
},
},
};
///
/// Internal state of the event loop hidden from the API consumer.
///
const Implementation = struct {
user_prefix: []const u8,
file_system_semaphore: *ext.SDL_sem,
file_system_mutex: *ext.SDL_mutex,
file_system_thread: *ext.SDL_Thread,
file_system_messages: ?*FileSystemMessage = null,
///
/// Casts `event_loop` to a [Implementation] reference.
///
/// *Note* that if `event_loop` does not have the same alignment as [Implementation],
/// safety-checked undefined behavior will occur.
///
fn cast(event_loop: *EventLoop) *Implementation {
return @ptrCast(*Implementation, @alignCast(@alignOf(Implementation), event_loop));
}
};
///
/// [LogKind.info] represents a log message which is purely informative and does not indicate
/// any kind of issue.
///
/// [LogKind.debug] represents a log message which is purely for debugging purposes and will
/// only occurs in debug builds.
///
/// [LogKind.warning] represents a log message which is a warning about a issue that does not
/// break anything important but is not ideal.
///
pub const LogKind = enum(c_int) {
info = ext.SDL_LOG_PRIORITY_INFO,
debug = ext.SDL_LOG_PRIORITY_DEBUG,
warning = ext.SDL_LOG_PRIORITY_WARN,
};
///
/// [OpenError.NotFound] is a catch-all for when a file could not be located to be opened. This
/// may be as simple as it doesn't exist or the because the underlying file-system will not /
/// cannot give access to it at this time.
///
pub const OpenError = error {
NotFound,
};
///
/// [OpenMode.readonly] indicates that an existing file is opened in a read-only state,
/// disallowing write access.
///
/// [OpenMode.overwrite] indicates that an empty file has been created or an existing file has
/// been completely overwritten into.
///
/// [OpenMode.append] indicates that an existing file that has been opened for reading from and
/// writing to on the end of existing data.
///
pub const OpenMode = enum {
readonly,
overwrite,
append,
};
///
/// [SeekOrigin.head] indicates that a seek operation will seek from the offset origin of the
/// file beginning, or "head".
///
/// [SeekOrigin.tail] indicates that a seek operation will seek from the offset origin of the
/// file end, or "tail".
///
/// [SeekOrigin.cursor] indicates that a seek operation will seek from the current position of
/// the file cursor.
///
pub const SeekOrigin = enum {
head,
tail,
cursor,
};
///
/// Closes access to the file referenced by `file_access` via `event_loop`.
///
/// *Note* that nothing happens to `file_access` if it is already closed.
///
pub fn close(event_loop: *EventLoop, file_access: *FileAccess) void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.close = .{.file_access = file_access}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
}
///
/// Enqueues `message` to the file system message processor to be processed at a later, non-
/// deterministic point.
///
fn enqueueFileSystemMessage(event_loop: *EventLoop, message: *FileSystemMessage) void {
const implementation = Implementation.cast(event_loop);
// TODO: Error check this.
_ = ext.SDL_LockMutex(implementation.file_system_mutex);
if (implementation.file_system_messages) |messages| {
messages.next = message;
} else {
implementation.file_system_messages = message;
}
// TODO: Error check these.
_ = ext.SDL_UnlockMutex(implementation.file_system_mutex);
_ = ext.SDL_SemPost(implementation.file_system_semaphore);
}
///
/// Writes `message` to the application log with `kind` via `event_loop`.
///
/// *Note* that `message` is not guaranteed to be partly, wholely, or at all written.
///
pub fn log(event_loop: *EventLoop, kind: LogKind, message: []const u8) void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.log = .{
.message = message,
.kind = kind,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
}
///
/// Attempts to open access to a file referenced at `file_system_path` using `mode` as the way
/// to open it via `event_loop`.
///
/// A [FileAccess] pointer is returned referencing the opened file or a [OpenError] if the file
/// could not be opened.
///
/// *Note* that all files are opened in "binary-mode", or Unix-mode. There are no conversions
/// applied when data is accessed from a file.
///
pub fn open(event_loop: *EventLoop, mode: OpenMode,
file_system_path: FileSystem.Path) OpenError!*FileAccess {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.open = .{
.mode = mode,
.file_system_path = &file_system_path,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.open.result;
}
///
/// [FileSystemMessage] processing function used by a dedicated worker thread, where `data` is
/// a type-erased reference to a [EventLoop].
///
/// The processor returns `0` if it exited normally or any other value if an erroneous exit
/// occured.
///
fn processFileSystemMessages(data: ?*anyopaque) callconv(.C) c_int {
const implementation = Implementation.cast(@ptrCast(*EventLoop, data orelse unreachable));
while (true) {
while (implementation.file_system_messages) |messages| {
switch (messages.request) {
.exit => return 0,
.log => |*log_request| ext.SDL_LogMessage(ext.SDL_LOG_CATEGORY_APPLICATION,
@enumToInt(log_request.priority), log_request.message),
.open => |*open_request| {
switch (open_request.path.file_system) {
.data => {
// TODO: Implement
open_request.result = error.NotFound;
},
.user => {
var path_buffer = std.mem.zeroes([4096]u8);
var path = stack.Fixed(u8){.buffer = path_buffer[0 .. ]};
path.pushAll(implementation.user_prefix) catch {
open_request.result = error.BadFileSystem;
continue;
};
if (!open_request.path.write(path.writer())) {
open_request.result = error.NotFound;
continue;
}
if (ext.SDL_RWFromFile(&path_buffer, switch (open_request.mode) {
.readonly => "rb",
.overwrite => "wb",
.append => "ab",
})) |rw_ops| {
open_request.result = @ptrCast(*FileAccess, rw_ops);
} else {
open_request.result = error.NotFound;
}
},
}
},
.close => |*close_request| {
// TODO: Use this result somehow.
_ = ext.SDL_RWclose(@ptrCast(*ext.SDL_RWops, @alignCast(
@alignOf(ext.SDL_RWops), close_request.file_access)));
},
.read_file => |read_request| {
// TODO: Implement.
_ = read_request;
},
.seek_file => |seek_request| {
// TODO: Implement.
_ = seek_request;
},
.tell_file => |tell_request| {
// TODO: Implement.
_ = tell_request;
},
}
resume messages.frame;
implementation.file_system_messages = messages.next;
}
// TODO: Error check this.
_ = ext.SDL_SemWait(implementation.file_system_semaphore);
}
}
///
/// Attempts to read the contents of the file referenced by `file_access` at the current file
/// cursor position into `buffer`.
///
/// The number of bytes that could be read / fitted into `buffer` is returned or a [FileError]
/// if the file failed to be read.
///
pub fn readFile(event_loop: *EventLoop, file_access: *FileAccess,
buffer: []const u8) FileError!usize {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.read_file = .{
.file_access = file_access,
.buffer = buffer,
}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.read_file.result;
}
///
/// Attempts to tell the current file cursor position for the file referenced by `file_access`.
///
/// Returns the number of bytes into the file that the cursor is relative to its beginning or a
/// [FileError] if the file failed to be queried.
///
pub fn queryFile(event_loop: *EventLoop, file_access: *FileAccess) FileError!usize {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{.tell_file = .{.file_access = file_access}},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.tell_file.result;
}
///
/// Attempts to seek the file cursor through the file referenced by `file_access` from `origin`
/// to `offset` via `event_loop`, returning a [FileError] if the file failed to be sought.
///
pub fn seekFile(event_loop: *EventLoop, file_access: *FileAccess,
origin: SeekOrigin, offset: usize) FileError!void {
var file_system_message = FileSystemMessage{
.frame = @frame(),
.request = .{
.seek_file = .{
.file_access = file_access,
.origin = origin,
.offset = offset,
},
},
};
suspend event_loop.enqueueFileSystemMessage(&file_system_message);
return file_system_message.request.seek_file.result;
}
};
///
/// File-system agnostic abstraction for manipulating a file.
///
pub const FileAccess = opaque {
///
/// Scans the number of bytes in the file referenced by `file_access` via `event_loop`, returing
/// its byte size or a [FileError] if it failed.
///
pub fn size(file_access: *FileAccess, event_loop: *EventLoop) FileError!usize {
// Save cursor to return to it later.
const origin_cursor = try event_loop.queryFile(file_access);
try event_loop.seekFile(file_access, .tail, 0);
const ending_cursor = try event_loop.queryFile(file_access);
// Return to original cursor.
try event_loop.seekFile(file_access, .head, origin_cursor);
return ending_cursor;
}
};
///
/// With files typically being backed by a block device, they can produce a variety of errors -
/// from physical to virtual errors - these are all encapsulated by the API as general
/// [Error.Inaccessible] errors.
///
pub const FileError = error {
Inaccessible,
};
///
/// Platform-agnostic mechanism for working with an abstraction of the underlying file-system(s)
/// available to the application in a sandboxed environment.
///
pub const FileSystem = enum {
data,
user,
///
/// Platform-agnostic mechanism for referencing files and directories on a [FileSystem].
///
pub const Path = struct {
file_system: FileSystem,
length: u16,
buffer: [max]u8,
///
/// Returns `true` if the length of `path` is empty, otherwise `false`.
///
pub fn isEmpty(path: Path) bool {
return (path.length == 0);
}
///
/// Returns `true` if `this` is equal to `that`, otherwise `false`.
///
pub fn equals(this: Path, that: Path) bool {
return std.mem.eql(u8, this.buffer[0 .. this.length], that.buffer[0 .. that.length]);
}
///
/// The maximum possible byte-length of a [Path].
///
/// Note that paths are encoded using UTF-8, meaning that a character may be bigger than one
/// byte. Because of this, it is not safe to asume that a path may hold [max] individual
/// characters.
///
pub const max = 1000;
///
///
///
pub fn write(path: Path, writer: io.Writer) bool {
return (writer.write(path.buffer[0 .. path.length]) == path.length);
}
};
///
/// [PathError.TooLong] occurs when creating a path that is greater than the maximum size **in
/// bytes**.
///
pub const PathError = error {
TooLong,
};
///
/// Creates and returns a [Path] value in the file system to the location specified by the
/// joining of the `sequences` path values.
///
pub fn joinedPath(file_system: FileSystem, sequences: []const []const u8) PathError!Path {
var path = Path{
.file_system = file_system,
.buffer = std.mem.zeroes([Path.max]u8),
.length = 0,
};
for (sequences) |sequence| if (sequence.len != 0) {
var components = mem.Spliterator(u8){
.source = sequence,
.delimiter = "/",
};
while (components.next()) |component| if (component.len != 0) {
for (component) |byte| {
if (path.length == Path.max) return error.TooLong;
path.buffer[path.length] = byte;
path.length += 1;
}
if (path.length == Path.max) return error.TooLong;
path.buffer[path.length] = '/';
path.length += 1;
};
};
return path;
}
};
///
///
///
pub const GraphicsContext = opaque {
///
///
///
pub const Event = struct {
keys_up: Keys = std.mem.zeroes(Keys),
keys_down: Keys = std.mem.zeroes(Keys),
keys_held: Keys = std.mem.zeroes(Keys),
const Keys = [256]bool;
};
const Implementation = struct {
event: Event,
};
///
///
///
pub fn poll(graphics_context: *GraphicsContext) ?*const Event {
_ = graphics_context;
return null;
}
///
///
///
pub fn present(graphics_context: *GraphicsContext) void {
// TODO: Implement;
_ = graphics_context;
}
};
///
///
///
pub fn GraphicsRunner(comptime Errors: type) type {
return fn (*EventLoop, *GraphicsContext) Errors!void;
}
///
///
///
pub fn runGraphics(comptime Errors: anytype, run: GraphicsRunner(Errors)) Errors!void {
if (ext.SDL_Init(ext.SDL_INIT_EVERYTHING) != 0) {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to initialize runtime");
return error.InitFailure;
}
defer ext.SDL_Quit();
const pref_path = create_pref_path: {
const path = ext.SDL_GetPrefPath("ona", "ona") orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to load user path");
return error.InitFailure;
};
break: create_pref_path path[0 .. std.mem.len(path)];
};
defer ext.SDL_free(pref_path.ptr);
const window = create_window: {
const pos = ext.SDL_WINDOWPOS_UNDEFINED;
var flags = @as(u32, 0);
break: create_window ext.SDL_CreateWindow("Ona", pos, pos, 640, 480, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create window");
return error.InitFailure;
};
};
defer ext.SDL_DestroyWindow(window);
const renderer = create_renderer: {
var flags = @as(u32, 0);
break: create_renderer ext.SDL_CreateRenderer(window, -1, flags) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION, "Failed to create renderer");
return error.InitFailure;
};
};
defer ext.SDL_DestroyRenderer(renderer);
var event_loop = EventLoop.Implementation{
.file_system_semaphore = ext.SDL_CreateSemaphore(0) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work scheduler");
return error.InitFailure;
},
.file_system_mutex = ext.SDL_CreateMutex() orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work lock");
return error.InitFailure;
},
.file_system_thread = unreachable,
.user_prefix = pref_path,
};
event_loop.file_system_thread = ext.SDL_CreateThread(
EventLoop.processFileSystemMessages, "File System Worker", &event_loop) orelse {
ext.SDL_LogCritical(ext.SDL_LOG_CATEGORY_APPLICATION,
"Failed to create file-system work processor");
return error.InitFailure;
};
defer {
ext.SDL_DestroyThread(event_loop.file_system_thread);
ext.SDL_DestroySemaphore(event_loop.file_system_mutex);
ext.SDL_DestroySemaphore(event_loop.file_system_semaphore);
}
var graphics_context = GraphicsContext.Implementation{
.event = .{
},
};
var message = EventLoop.FileSystemMessage{
.frame = @frame(),
.request = .exit,
};
@ptrCast(*EventLoop, event_loop).enqueueFileSystemMessage(&message);
var status = @as(c_int, 0);
ext.SDL_WaitThread(event_loop.file_system_thread, &status);
if (status != 0) {
// TODO: Error check this.
}
return run(@ptrCast(*EventLoop, &event_loop), @ptrCast(*GraphicsContext, &graphics_context));
}