Merge pull request 'C++20 Port' (#5) from cpp-port into main
continuous-integration/drone Build is failing Details

Reviewed-on: #5
This commit is contained in:
kayomn 2023-02-20 02:33:45 +01:00
commit 82d9fa85b8
26 changed files with 2076 additions and 1178 deletions

View File

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

5
.gitignore vendored
View File

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

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

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

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"
},
],
}

36
build.py Executable file
View File

@ -0,0 +1,36 @@
#!/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++ -g -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("coral")
compile_package("oar")
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:
* Clang / LLVM toolchain 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.

327
source/app.cpp Normal file
View File

@ -0,0 +1,327 @@
module;
#include <SDL2/SDL.h>
export module app;
import coral;
import coral.files;
import coral.image;
import coral.math;
import oar;
struct file_reader : public coral::file_reader {
enum class [[nodiscard]] close_result {
ok,
io_unavailable,
};
enum class [[nodiscard]] open_result {
ok,
io_unavailable,
access_denied,
not_found,
};
file_reader(coral::fs * fs) : rw_ops{nullptr} {
this->fs = fs;
}
close_result close() {
if (::SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable;
this->rw_ops = nullptr;
return close_result::ok;
}
bool is_open() const {
return this->rw_ops != nullptr;
}
open_result open(coral::path const & file_path) {
if (this->is_open()) switch (this->close()) {
case close_result::ok: break;
case close_result::io_unavailable: return open_result::io_unavailable;
default: coral::unreachable();
}
this->rw_ops = ::SDL_RWFromFile(reinterpret_cast<char const *>(this->path_buffer), "r");
if (this->rw_ops == nullptr) return open_result::not_found;
return open_result::ok;
}
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & buffer) override {
if (!this->is_open()) return coral::io_error::unavailable;
coral::usize const bytes_read{::SDL_RWread(this->rw_ops, buffer.pointer, sizeof(uint8_t), buffer.length)};
if ((bytes_read == 0) && (::SDL_GetError() != nullptr)) return coral::io_error::unavailable;
return bytes_read;
}
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
if (!this->is_open()) return coral::io_error::unavailable;
// TODO: Fix cast.
coral::i64 const byte_position{
::SDL_RWseek(this->rw_ops, static_cast<coral::i64>(offset), RW_SEEK_SET)};
if (byte_position == -1) return coral::io_error::unavailable;
return static_cast<coral::u64>(byte_position);
}
coral::expected<coral::u64, coral::io_error> tell() override {
if (!this->is_open()) return coral::io_error::unavailable;
coral::i64 const byte_position{::SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)};
if (byte_position == -1) return coral::io_error::unavailable;
return static_cast<coral::u64>(byte_position);
}
private:
static constexpr coral::usize path_max{4096};
coral::u8 path_buffer[path_max];
coral::fs * fs;
::SDL_RWops * rw_ops;
};
struct base_directory : public coral::fs {
base_directory() : directory_path{} {
char * const path{::SDL_GetBasePath()};
if (path == nullptr) return;
coral::usize path_length{0};
while (path[path_length] != 0) path_length += 1;
if (path_length == 0) {
::SDL_free(path);
return;
}
this->directory_path = {path, path_length};
}
~base_directory() override {
::SDL_free(this->directory_path.begin());
}
void read_file(coral::path const & file_path, coral::callable<void(coral::file_reader &)> const & then) override {
if (this->directory_path.length == 0) return;
file_reader reader{this};
if (reader.open(file_path) != file_reader::open_result::ok) return;
then(reader);
if (reader.close() != file_reader::close_result::ok) return;
}
void write_file(coral::path const & file_path, coral::callable<void(coral::file_writer &)> const & then) override {
// Directory is read-only.
}
protected:
coral::slice<char> directory_path;
};
struct user_directory : public coral::fs {
user_directory(coral::path const & title) : directory_path{} {
char * const path{::SDL_GetPrefPath("ona", title.begin())};
if (path == nullptr) return;
coral::usize path_length{0};
while (path[path_length] != 0) path_length += 1;
if (path_length == 0) {
::SDL_free(path);
return;
}
this->directory_path = {path, path_length};
}
~user_directory() override {
::SDL_free(this->directory_path.begin());
}
void read_file(coral::path const & file_path, coral::callable<void(coral::file_reader &)> const & then) override {
if (this->directory_path.length == 0) return;
file_reader reader{this};
if (reader.open(file_path) != file_reader::open_result::ok) return;
then(reader);
if (reader.close() != file_reader::close_result::ok) return;
}
void write_file(coral::path const & file_path, coral::callable<void(coral::file_writer &)> const & then) override {
// Directory is read-only.
}
protected:
coral::slice<char> directory_path;
};
export namespace app {
enum class log_level {
notice,
warning,
error,
};
struct system {
system(coral::path const & title) : res{&base, "base_directory.oar"}, user{title} {}
coral::fs & base_fs() {
return this->base;
}
bool poll() {
while (::SDL_PollEvent(&this->sdl_event) != 0) {
switch (this->sdl_event.type) {
case SDL_QUIT: return false;
}
}
return true;
}
coral::fs & res_fs() {
return this->res;
}
void log(app::log_level level, coral::slice<char const> const & message) {
coral::i32 const length{static_cast<coral::i32>(
coral::min(message.length, static_cast<size_t>(coral::i32_max)))};
::SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer);
}
coral::allocator & thread_safe_allocator() {
return this->allocator;
}
coral::fs & user_fs() {
return this->user;
}
private:
::SDL_Event sdl_event;
struct : public coral::allocator {
coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override {
return reinterpret_cast<coral::u8 *>(::SDL_malloc(requested_size));
}
void deallocate(void * allocation) override {
::SDL_free(allocation);
}
} allocator;
base_directory base;
user_directory user;
oar::archive res;
};
struct graphics {
enum class [[nodiscard]] show_result {
ok,
out_of_memory,
};
struct canvas {
coral::color background_color;
};
graphics(coral::path const & title) {
this->retitle(title);
}
void present() {
if (this->sdl_renderer != nullptr) {
::SDL_RenderPresent(this->sdl_renderer);
}
}
void render(canvas & source_canvas) {
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(coral::path const & title) {
this->title = title;
if (this->sdl_window != nullptr)
::SDL_SetWindowTitle(this->sdl_window, this->title.begin());
}
show_result show(coral::u16 physical_width, coral::u16 physical_height) {
if (this->sdl_window == nullptr) {
constexpr int sdl_windowpos = SDL_WINDOWPOS_UNDEFINED;
constexpr coral::u32 sdl_windowflags = 0;
this->sdl_window = ::SDL_CreateWindow(this->title.begin(), sdl_windowpos,
sdl_windowpos, static_cast<int>(physical_width), static_cast<int>(physical_height),
sdl_windowflags);
if (this->sdl_window == nullptr) return show_result::out_of_memory;
} else {
::SDL_ShowWindow(this->sdl_window);
}
if (this->sdl_renderer == nullptr) {
constexpr coral::u32 sdl_rendererflags = 0;
this->sdl_renderer = ::SDL_CreateRenderer(this->sdl_window, -1, sdl_rendererflags);
if (this->sdl_renderer == nullptr) return show_result::out_of_memory;
}
return show_result::ok;
}
private:
coral::path title;
::SDL_Window * sdl_window = nullptr;
::SDL_Renderer * sdl_renderer = nullptr;
};
using graphical_runnable = coral::callable<int(system &, graphics &)>;
int display(coral::path const & title, graphical_runnable const & run) {
system app_system{title};
graphics app_graphics{title};
return run(app_system, app_graphics);
}
}

611
source/coral.cpp Normal file
View File

@ -0,0 +1,611 @@
module;
#include <cstdint>
#include <cstddef>
export module coral;
// Runtime utilities.
export namespace coral {
/**
* Triggers safety-checked behavior in debug mode.
*
* In release mode, the compiler can use this function as a marker to optimize out safety-
* checked logic branches that should never be executed.
*/
[[noreturn]] void unreachable() {
__builtin_unreachable();
}
}
// Concrete and interface types.
export namespace coral {
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 = int32_t;
usize const i32_max = 0xffffffff;
using u64 = uint64_t;
using i64 = int64_t;
using f32 = float;
using f64 = double;
/**
* Base type for runtime-pluggable memory allocation strategies used by the core library.
*/
struct allocator {
virtual ~allocator() {};
/**
* If `allocation` is `nullptr`, the allocator will attempt to allocate a new memory block
* of `requested_size` bytes. Otherwise, the allocator will attempt to reallocate
* `allocation` to be `request_size` bytes in size.
*
* The returned address will point to a dynamically allocated buffer of `requested_size` if
* the operation was successful, otherwise `nullptr`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to
* program exit. This may be achieved through either [deallocate] or implementation-
* specific allocator functionality.
*
* *Note*: Attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*
* *Note*: After invocation, `allocation` should be considered an invalid memory address.
*/
[[nodiscard]] virtual u8 * reallocate(u8 * allocation, usize requested_size) = 0;
/**
* If `allocation` points to a non-`nullptr` address, the allocator will deallocate it.
* Otherwise, the function has no side-effects.
*
* *Note* that attempting to pass a non-`nullptr` `allocation` address not allocated by the
* allocator *will* result in erroneous implementation-behavior.
*/
virtual void deallocate(void * allocation) = 0;
};
/**
* Length-signed pointer type that describes how many elements of `type` it references,
* providing a type-safe wrapper for passing arrays and zero-terminated strings to functions.
*/
template<typename type> struct slice {
/**
* Number of `type` elements referenced.
*/
usize length;
/**
* Base element address referenced.
*/
type * pointer;
constexpr slice() {
this->length = 0;
this->pointer = nullptr;
}
constexpr slice(char const *&& zstring) {
this->pointer = zstring;
this->length = 0;
while (zstring[length] != 0) this->length += 1;
}
constexpr slice(type * slice_pointer, usize slice_length) {
this->pointer = slice_pointer;
this->length = slice_length;
}
constexpr slice(type * slice_begin, type * slice_end) {
this->pointer = slice_begin;
this->length = static_cast<usize>(slice_end - slice_begin);
}
template<usize array_size> constexpr slice(type(&array)[array_size]) {
this->pointer = array;
this->length = array_size;
}
/**
* Reinterprets the data referenced as a series of bytes.
*
* The returned view is constant to protect against inadvertant memory corruption.
*/
slice<u8 const> as_bytes() const {
return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Reinterprets the data referenced as a series of chars.
*
* The returned view is constant to protect against inadvertant memory corruption.
*
* *Note* the returned value has no guarantees about the validity of any specific character
* encoding set.
*/
slice<char const> as_chars() const {
return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)};
}
/**
* Returns the base pointer of the slice.
*/
constexpr type * begin() const {
return this->pointer;
}
/**
* Returns the tail pointer of the slice.
*/
constexpr type * end() const {
return this->pointer + this->length;
}
/**
* Returns a new slice with the base-pointer offset by `index` elements and a length of
* `range` elements from `index`.
*
* *Note* that attempting to slice with an `index` or `range` outside of the existing slice
* bounds will result in safety-checked behavior.
*/
constexpr slice sliced(usize index, usize range) const {
if ((this->length <= index) || ((range + index) > this->length)) unreachable();
return {this->pointer + index, range - index};
}
operator slice<type const>() const {
return (*reinterpret_cast<slice<type const> const *>(this));
}
constexpr type & operator[](usize index) const {
if (this->length <= index) unreachable();
return this->pointer[index];
}
};
}
// Math functions.
export namespace coral {
/**
* Returns the maximum value between `a` and `b`.
*/
template<typename scalar> constexpr scalar max(scalar const & a, scalar const & b) {
return (a > b) ? a : b;
}
/**
* Returns the minimum value between `a` and `b`.
*/
template<typename scalar> constexpr scalar min(scalar const & a, scalar const & b) {
return (a < b) ? a : b;
}
/**
* Returns `value` clamped between the range of `min_value` and `max_value` (inclusive).
*/
template<typename scalar> constexpr scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) {
return max(min_value, min(max_value, value));
}
/**
* Returns `value` rounded to the nearest whole number.
*/
f32 round32(f32 value) {
return __builtin_roundf(value);
}
}
/**
* Allocates and initializes a type of `requested_size` in `buffer`, returning its base pointer. As
* a result of accepting a pre-allocated buffer, invocation does not allocate any dynamic memory.
*
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked
* behavior.
*/
export void * operator new(coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
if (buffer.length < requested_size) coral::unreachable();
return buffer.pointer;
}
/**
* Allocates and initializes a series of types at `requested_size` in `buffer`, returning the base
* pointer. As a result of accepting a pre-allocated buffer, invocation does not allocate any
* dynamic memory.
*
* *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked
* behavior.
*/
export void * operator new[](coral::usize requested_size, coral::slice<coral::u8> const & buffer) {
if (buffer.length < requested_size) coral::unreachable();
return buffer.pointer;
}
/**
* Attempts to allocate and initialize a type of `requested_size` using `allocator`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program
* exit. This may be achieved through either [coral::allocator::deallocate] or implementation-
* specific allocator functionality.
*/
export [[nodiscard]] void * operator new(coral::usize requested_size, coral::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
/**
* Attempts to allocate and initialize a series of types of `requested_size` using `allocator`.
*
* *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program
* exit. This may be achieved through either [coral::allocator::deallocate] or implementation-
* specific allocator functionality.
*/
export [[nodiscard]] void * operator new[](coral::usize requested_size, coral::allocator & allocator) {
return allocator.reallocate(nullptr, requested_size);
}
// Wrapper types.
export namespace coral {
/**
* Monadic container for a single-`element` value or nothing.
*/
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;
}
}
/**
* Returns `true` if the optional contains a value, otherwise `false`.
*/
bool has_value() const {
return this->buffer[sizeof(element)] == 1;
}
/**
* Returns the contained value or `fallback` if the optional is empty.
*/
element const & value_or(element const & fallback) const {
return this->has_value() ? *reinterpret_cast<element const *>(this->buffer) : fallback;
}
/**
* Returns a reference to the contained value.
*
* *Note*: attempting to access the value of an empty optional will trigger safety-checked
* behavior.
*/
element & value() {
if (!this->has_value()) unreachable();
return *reinterpret_cast<element *>(this->buffer);
}
/**
* Returns the contained value.
*
* *Note*: attempting to access the value of an empty optional will trigger safety-checked
* behavior.
*/
element const & value() const {
return *reinterpret_cast<element const *>(this->buffer);
}
private:
u8 buffer[sizeof(element) + 1];
};
/**
* Monadic container for a descriminating union of either `value_element` or `error_element`.
*/
template<typename value_element, typename error_element> struct [[nodiscard]] expected {
expected(value_element const & value) : buffer{0} {
(*reinterpret_cast<value_element *>(this->buffer)) = value;
this->buffer[buffer_size] = 1;
}
expected(error_element const & error) : buffer{0} {
(*reinterpret_cast<error_element *>(this->buffer)) = error;
}
/**
* Returns `true` if the optional contains a value, otherwise `false` if it holds an error.
*/
bool is_ok() const {
return this->buffer[buffer_size];
}
/**
* Returns a reference to the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element & value() {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element *>(this->buffer);
}
/**
* Returns the contained value.
*
* *Note*: attempting to access the value of an erroneous expected will trigger safety-
* checked behavior.
*/
value_element const & value() const {
if (!this->is_ok()) unreachable();
return *reinterpret_cast<value_element const *>(this->buffer);
}
/**
* Returns a reference to the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element & error() {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element *>(this->buffer);
}
/**
* Returns the contained error.
*
* *Note*: attempting to access the error of a non-erroneous expected will trigger safety-
* checked behavior.
*/
error_element const & error() const {
if (this->is_ok()) unreachable();
return *reinterpret_cast<error_element const *>(this->buffer);
}
private:
static constexpr usize buffer_size = max(sizeof(value_element), sizeof(error_element));
u8 buffer[buffer_size + 1];
};
template<typename> struct callable;
/**
* Type-erasing wrapper for functor types that have a call operator with a return value
* matching `return_value` and arguments matching `argument_values`.
*/
template<typename return_value, typename... argument_values> struct callable<return_value(argument_values...)> {
using function = return_value(*)(argument_values...);
callable(function callable_function) {
this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value {
return (*reinterpret_cast<function const *>(userdata))(arguments...);
};
new (this->capture) function{callable_function};
}
callable(callable const &) = delete;
template<typename functor> callable(functor const & callable_functor) {
this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value {
return (*reinterpret_cast<functor const*>(userdata))(arguments...);
};
new (this->capture) functor{callable_functor};
}
return_value operator()(argument_values const &... arguments) const {
return this->dispatcher(this->capture, arguments...);
}
private:
static constexpr usize capture_size = 24;
return_value(* dispatcher)(u8 const * userdata, argument_values... arguments);
u8 capture[capture_size];
};
/**
* Errors that may occur while executing an opaque I/O operation via the `readable` and
* `writable` type aliases.
*/
enum class io_error {
unavailable,
};
struct reader {
virtual expected<usize, io_error> read(slice<u8> const & buffer) = 0;
};
struct writer {
virtual expected<usize, io_error> write(slice<u8 const> const & buffer) = 0;
};
}
// Input/output operations.
export namespace coral {
/**
* Compares `a` and `b`, returning the difference between them or `0` if they are identical.
*/
constexpr size compare(slice<u8 const> const & a, slice<u8 const> const & b) {
usize const range = min(a.length, b.length);
for (usize index = 0; index < range; index += 1) {
size const difference = static_cast<size>(a[index]) - static_cast<size>(b[index]);
if (difference != 0) return difference;
}
return static_cast<size>(a.length) - static_cast<size>(b.length);
}
/**
* Copies the contents of `origin` into `target`.
*
* *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`.
*/
void copy(slice<u8> const & target, slice<u8 const> const & origin) {
if (target.length < origin.length) unreachable();
for (usize i = 0; i < origin.length; i += 1) target[i] = origin[i];
}
/**
* Zeroes the contents of `target`.
*/
void zero(slice<u8> const & target) {
for (usize i = 0; i < target.length; i += 1) target[i] = 0;
}
/**
* Tests the equality of `a` against `b`, returning `true` if they contain identical bytes,
* otherwise `false`.
*/
constexpr bool equals(slice<u8 const> const & a, slice<u8 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;
}
/**
* Returns a hash code generated from the values in `bytes`.
*
* *Note:* the returned hash code is not guaranteed to be unique.
*/
constexpr usize hash(slice<u8 const> const & bytes) {
usize hash_code = 5381;
for (u8 const byte : bytes) hash_code = ((hash_code << 5) + hash_code) + byte;
return hash_code;
}
/**
* Swaps the values of `element` in `a` and `b` around using copy semantics.
*/
template<typename element> constexpr void swap(element & a, element & b) {
element const temp = a;
a = b;
b = temp;
}
/**
* Streams the data from `input` to `output`, using `buffer` as temporary transfer space.
*
* The returned [expected] can be used to introspect if `input` or `output` encountered any
* issues during streaming, otherwise it will contain the number of bytes streamed.
*
* *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to
* temporarily place data during streaming.
*/
expected<usize, io_error> stream(writer & output, reader & input, slice<u8> const & buffer) {
usize total_bytes_written = 0;
expected bytes_read = input.read(buffer);
if (!bytes_read.is_ok()) return bytes_read.error();
usize read = bytes_read.value();
while (read != 0) {
expected const bytes_written = output.write(buffer.sliced(0, read));
if (!bytes_written.is_ok()) return bytes_read.error();
total_bytes_written += bytes_written.value();
bytes_read = input.read(buffer);
if (!bytes_read.is_ok()) return bytes_read.error();
read = bytes_read.value();
}
return total_bytes_written;
}
/**
* Attempts to format and print `value` as an unsigned integer out to `output`.
*
* The returned [expected] can be used to introspect if `output` encountered any issues during
* printing, otherwise it will contain the number of characters used to print `value` as text.
*/
expected<usize, io_error> print_unsigned(writer & output, u64 value) {
if (value == 0) return output.write(slice{"0"}.as_bytes());
u8 buffer[20]{0};
usize buffer_count{0};
while (value != 0) {
constexpr usize radix{10};
buffer[buffer_count] = static_cast<u8>((value % radix) + '0');
value = (value / radix);
buffer_count += 1;
}
usize const half_buffer_count{buffer_count / 2};
for (usize i = 0; i < half_buffer_count; i += 1)
swap(buffer[i], buffer[buffer_count - i - 1]);
return output.write(slice{buffer, buffer_count});
}
/**
* Returns a reference to a shared [allocator] which will always return `nullptr` on calls to
* [allocator::reallocate].
*/
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;
}
}

139
source/coral/files.cpp Normal file
View File

@ -0,0 +1,139 @@
export module coral.files;
import coral;
export namespace coral {
/**
* Platform-generalized identifier for a resource in a [file_store].
*/
struct path {
/**
* Maximum path length.
*/
static usize const max = u8_max;
/**
* Common path component separator.
*/
static char const seperator = '/';
constexpr path() : buffer{0} {
this->buffer[max] = max;
}
template<usize text_size> constexpr path(char const(&text)[text_size]) : path{} {
static_assert(text_size <= max);
for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i];
this->buffer[max] = max - text_size;
}
/**
* Returns a weak reference to the [path] as a [slice].
*/
constexpr slice<char const> as_slice() const {
return {this->buffer, this->byte_size()};
}
/**
* Returns the base pointer of the path name.
*
* *Note*: the returned buffer pointer is guaranteed to end with a zero terminator.
*/
char const * begin() const {
return reinterpret_cast<char const *>(this->buffer);
}
/**
* Returns the number of bytes composing the path.
*/
constexpr usize byte_size() const {
return max - this->buffer[max];
}
/**
* Compares the path to `that`, returning the difference between the two paths or `0` if
* they are identical.
*/
constexpr size compare(path const & that) const {
return coral::compare(this->as_slice().as_bytes(), that.as_slice().as_bytes());
}
/**
* Returns the tail pointer of the path name.
*/
char const * end() const {
return this->buffer + this->byte_size();
}
/**
* Tests the path against `that` for equality, returning `true` if they are identical,
* otherwise `false`.
*/
constexpr bool equals(path const & that) const {
return coral::equals(this->as_slice().as_bytes(), that.as_slice().as_bytes());
}
/**
* Returns the path hash code.
*
* *Note:* the returned hash code is not guaranteed to be unique.
*/
constexpr u64 hash() const {
return coral::hash(this->as_slice().as_bytes());
}
/**
* Returns a new [path] composed of the current path joined with `text`.
*
* *Note:* should the new path exceed [max] bytes in size, an empty [path] is returned instead.
*/
constexpr path joined(slice<char const> const & text) const {
if (text.length > this->buffer[max]) return path{};
path joined_path = *this;
for (char const c : text) {
joined_path.buffer[joined_path.byte_size()] = c;
joined_path.buffer[max] -= 1;
}
return joined_path;
}
private:
char buffer[max + 1];
};
struct file_reader : public reader {
virtual expected<u64, io_error> seek(u64 offset) = 0;
virtual expected<u64, io_error> tell() = 0;
};
struct file_writer : public writer {};
/**
* Platform-generalized file system interface.
*/
struct fs {
virtual ~fs() {};
/**
* Attempts to read the file in the file system located at `file_path` relative, calling
* `then` if it was successfully opened for reading.
*
* Once `then` returns, access to the file is closed automatically.
*/
virtual void read_file(path const & file_path, callable<void(file_reader &)> const & then) = 0;
/**
* Attempts to write the file in the file system located at `file_path` relative, calling
* `then` if it was successfully opened for writing.
*
* Once `then` returns, access to the file is closed automatically.
*/
virtual void write_file(path const & file_path, callable<void(file_writer &)> const & then) = 0;
};
}

58
source/coral/image.cpp Normal file
View File

@ -0,0 +1,58 @@
export module coral.image;
import coral;
export namespace coral {
/**
* All-purpose color value for red, green, blue, alpha channel-encoded values.
*/
struct color {
/**
* Red channel.
*/
float r;
/**
* Green channel.
*/
float g;
/**
* Blue channel.
*/
float b;
/**
* Alpha channel.
*/
float a;
/**
* Red channel represented in an 8-bit unsigned value.
*/
u8 to_r8() const {
return static_cast<u8>(round32(clamp(this->r, 0.0f, 1.0f) * u8_max));
}
/**
* Green channel represented in an 8-bit unsigned value.
*/
u8 to_g8() const {
return static_cast<u8>(round32(clamp(this->g, 0.0f, 1.0f) * u8_max));
}
/**
* Blue channel represented in an 8-bit unsigned value.
*/
u8 to_b8() const {
return static_cast<u8>(round32(clamp(this->b, 0.0f, 1.0f) * u8_max));
}
/**
* Alpha channel represented in an 8-bit unsigned value.
*/
u8 to_a8() const {
return static_cast<u8>(round32(clamp(this->a, 0.0f, 1.0f) * u8_max));
}
};
}

40
source/coral/math.cpp Normal file
View File

@ -0,0 +1,40 @@
export module coral.math;
import coral;
export namespace coral {
/**
* Two-component vector type backed by 32-bit floating point values.
*/
struct vector2 {
/**
* "X" axis spatial component.
*/
f32 x;
/**
* "Y" axis spatial component.
*/
f32 y;
};
/**
* Three-component vector type backed by 32-bit floating point values.
*/
struct vector3 {
/**
* "X" axis spatial component.
*/
f32 x;
/**
* "Y" axis spatial component.
*/
f32 y;
/**
* "Z" axis spatial component.
*/
f32 z;
};
}

219
source/coral/sequence.cpp Normal file
View File

@ -0,0 +1,219 @@
export module coral.sequence;
import coral;
export namespace coral {
/**
* Result codes used by [sequence]-derived types when they are appended to in any way.
*
* [append_result::ok] indicates that an append operation was successful.
*
* [append_result::out_of_memory] alerts that the memory required to perform the append
* operation failed.
*/
enum class [[nodiscard]] append_result {
ok,
out_of_memory,
};
/**
* Base type for all sequence-like types.
*
* Sequences are any data structure which owns a linear, non-unique set of elements which may
* be queried and/or mutated.
*/
template<typename element> struct sequence {
virtual ~sequence() {};
/**
* Attempts to append `source_elements` to the sequence.
*
* The returned [append_result] indicates whether the operation was successful or not.
*
* If the returned [append_result] is anything but [append_result::ok], the [sequence] will
* be left in an implementation-defined state.
*/
virtual append_result append(slice<element const> const & source_elements) = 0;
};
/**
* Last-in-first-out linear sequence of `element` values.
*
* [stack] types will default to using an inline array of `init_capacity` at first. After all
* local storage has been exhausted, the [stack] will switch to a dynamic buffer. Because of
* this, it is recommended to use larger `init_capacity` values for data which has a known or
* approximate upper bound at compile-time. Otherwise, the `init_capacity` value may be left at
* its default.
*/
template<typename element, usize init_capacity = 1> struct stack : public sequence<element> {
stack(allocator * dynamic_allocator) : local_buffer{0} {
this->dynamic_allocator = dynamic_allocator;
this->filled = 0;
this->elements = this->local_buffer;
}
~stack() override {
if (this->is_dynamic()) this->dynamic_allocator->deallocate(this->elements.pointer);
}
/**
* Attempts to append `source_elements` to the top of the stack.
*
* The returned [append_result] indicates whether the operation was successful or not.
*
* If the returned [append_result] is anything but [append_result::ok], the stack will be
* be left in an empty but valid state.
*
* *Note* that [push] is recommended when appending singular values.
*/
append_result append(slice<element const> const & source_elements) override {
usize const updated_fill = this->filled + source_elements.length;
if (updated_fill >= this->elements.length) {
append_result const result = this->reserve(updated_fill);
if (result != append_result::ok) return result;
}
for (usize i = 0; i < source_elements.length; i += 1)
this->elements[this->filled + i] = source_elements[i];
this->filled = updated_fill;
return append_result::ok;
}
/**
* Returns the beginning of the elements as a mutable pointer.
*/
element * begin() {
return this->elements.pointer;
}
/**
* Returns the beginning of the elements as a const pointer.
*/
element const * begin() const {
return this->elements.pointer;
}
/**
* Returns the ending of the elements as a mutable pointer.
*/
element * end() {
return this->elements.pointer + this->filled;
}
/**
* Returns the ending of the elements as a const pointer.
*/
element const * end() const {
return this->elements.pointer + this->filled;
}
/**
* Returns `true` if the stack is backed by dynamic memory, otherwise `false`.
*/
bool is_dynamic() const {
return this->elements.pointer != this->local_buffer;
}
/**
* Attempts to append `source_element` to the top of the stack.
*
* The returned [append_result] indicates whether the operation was successful or not.
*
* If the returned [append_result] is anything but [append_result::ok], the stack will be
* be left in an empty but valid state.
*
* *Note* that [append] is recommended when appending many values at once.
*/
append_result push(element const & source_element) {
if (this->filled == this->elements.length) {
append_result const result = this->reserve(this->elements.length);
if (result != append_result::ok) return result;
}
this->elements[this->filled] = source_element;
this->filled += 1;
return append_result::ok;
}
/**
* Attempts to reserve `capacity` number of elements additional space on the stack, forcing
* it to use dynamic memory _even_ if it hasn't exhausted the local buffer yet.
*
* The returned [append_result] indicates whether the operation was successful or not.
*
* If the returned [append_result] is anything but [append_result::ok], the stack will be
* be left in an empty but valid state.
*
* *Note* that manual invocation is not recommended if the [stack] has a large
* `initial_capacity` argument.
*/
append_result reserve(usize capacity) {
usize const requested_capacity = this->filled + capacity;
if (this->is_dynamic()) {
// Grow dynamic buffer (bailing out if failed).
u8 * const buffer = this->dynamic_allocator->reallocate(
reinterpret_cast<u8 *>(this->elements.pointer),
sizeof(element) * requested_capacity);
if (buffer == nullptr) {
this->elements = {};
return append_result::out_of_memory;
}
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
} else {
usize const buffer_size = sizeof(element) * requested_capacity;
u8 * const buffer = this->dynamic_allocator->reallocate(nullptr, buffer_size);
if (buffer == nullptr) {
this->elements = {};
return append_result::out_of_memory;
}
copy({buffer, buffer_size}, this->elements.as_bytes());
this->elements = {reinterpret_cast<element *>(buffer), requested_capacity};
}
return append_result::ok;
}
private:
allocator * dynamic_allocator;
usize filled;
slice<element> elements;
element local_buffer[init_capacity];
};
/**
* Writable type for appending data to a [sequence] containing [u8] values.
*/
struct sequence_writer : public writer {
sequence_writer(sequence<u8> * output_sequence) {
this->output_sequence = output_sequence;
}
expected<usize, io_error> write(slice<u8 const> const & buffer) {
switch (output_sequence->append(buffer)) {
case append_result::ok: return buffer.length;
case append_result::out_of_memory: return io_error::unavailable;
default: unreachable();
}
}
private:
sequence<u8> * output_sequence;
};
}

49
source/kym.cpp Normal file
View File

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

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

@ -0,0 +1,182 @@
export module kym.environment;
import coral;
import coral.sequence;
import kym;
using loggable = coral::callable<void(coral::slice<char const> const &)>;
export namespace kym {
struct vm;
}
enum class token_kind {
end,
};
struct token {
coral::slice<char const> text;
token_kind kind;
};
struct tokenizer {
coral::slice<char const> source;
tokenizer(coral::slice<char const> const & source) : source{source} {}
token next() {
coral::usize cursor = 0;
while (cursor < source.length) {
}
return token{
.kind = token_kind::end,
};
}
};
struct bytecode {
bytecode(coral::allocator * allocator) : error_message_buffer{allocator} {
}
bool compile(tokenizer bytecode_tokenizer, loggable const & log_error) {
for (;;) {
token const initial_token = bytecode_tokenizer.next();
switch (initial_token.kind) {
case token_kind::end: return true;
default: coral::unreachable();
}
}
}
kym::value execute(kym::vm & vm, coral::slice<kym::value const> const & arguments) {
return kym::nil;
}
private:
coral::stack<char> error_message_buffer;
};
export namespace kym {
value default_call(vm & owning_vm, void * userdata, coral::slice<value const> const & arguments) {
return nil;
}
coral::expected<coral::usize, coral::io_error> default_stringify(vm & owning_vm, void * userdata, coral::writer & output) {
return output.write(coral::slice{"[object]"}.as_bytes());
}
struct bound_object {
void * userdata;
struct {
void(*cleanup)(vm &, void *);
value(*call)(vm &, void *, coral::slice<value const> const &);
coral::expected<coral::usize, coral::io_error>(*stringify)(vm &, void *, coral::writer &);
} behavior;
bound_object(vm * owning_vm) : userdata{nullptr}, owning_vm{owning_vm}, behavior{
.cleanup = [](vm & owning_vm, void * userdata) {},
.call = default_call,
.stringify = default_stringify,
} {}
void cleanup() {
this->behavior.cleanup(*this->owning_vm, this->userdata);
}
value call(coral::slice<value const> const & arguments) {
return this->behavior.call(*this->owning_vm, this->userdata, arguments);
}
coral::expected<coral::usize, coral::io_error> stringify(coral::writer & output) {
return this->behavior.stringify(*this->owning_vm, this->userdata, output);
}
value get_field(coral::slice<char const> const & field_name) {
return nil;
}
private:
vm * owning_vm;
};
struct vm {
struct init_options {
coral::u16 datastack_size;
coral::u16 callstack_size;
};
vm(coral::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) {
coral::u8 * const data_stack_buffer = this->allocator->reallocate(reinterpret_cast
<coral::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(coral::slice<char const> const & source) {
bytecode * source_bytecode = new (*this->allocator) bytecode{allocator};
if (source_bytecode == nullptr) return nil;
if (!source_bytecode->compile(tokenizer{source}, [&](coral::slice<char const> error_message) {
this->log(error_message);
})) {
this->allocator->deallocate(source_bytecode);
return nil;
}
return this->new_object([this, source_bytecode](bound_object & object) {
object.userdata = source_bytecode;
object.behavior.cleanup = [](vm & owning_vm, void * userdata) {
owning_vm.allocator->deallocate(userdata);
};
object.behavior.call = [](vm & owning_vm, void * userdata, coral::slice<value const> const & arguments) -> value {
return reinterpret_cast<bytecode *>(userdata)->execute(owning_vm, arguments);
};
this->allocator->deallocate(source_bytecode);
});
}
value new_object(coral::callable<void(bound_object &)> const & then) {
return nil;
}
void with_object(value object_value, coral::callable<void(bound_object &)> const & then) {
}
private:
coral::slice<value> data_stack;
loggable log;
coral::allocator * allocator;
};
}

202
source/oar.cpp Normal file
View File

@ -0,0 +1,202 @@
export module oar;
import coral;
import coral.files;
constexpr coral::usize signature_length{4};
constexpr coral::usize signature_version_length{1};
constexpr coral::usize signature_identifier_length{signature_length - signature_version_length};
constexpr coral::u8 signature_magic[signature_length]{'o', 'a', 'r', 0};
struct header {
coral::u8 signature_magic[signature_length];
coral::u32 entry_count;
coral::u8 padding[504];
};
static_assert(sizeof(header) == 512);
struct entry {
coral::path path;
coral::u64 data_offset;
coral::u64 data_length;
coral::u8 padding[240];
};
static_assert(sizeof(entry) == 512);
export namespace oar {
struct archive_file_reader : public coral::file_reader {
enum class [[nodiscard]] close_result {
ok,
};
enum class [[nodiscard]] open_result {
ok,
io_unavailable,
archive_invalid,
archive_unsupported,
not_found,
};
archive_file_reader(coral::file_reader * archive_reader) {
this->archive_reader = archive_reader;
this->data_offset = 0;
this->data_length = 0;
this->data_cursor = 0;
}
close_result close() {
return close_result::ok;
}
bool is_open() const {
return this->data_offset >= sizeof(header);
}
open_result open(coral::path const & file_path) {
if (this->is_open()) switch (this->close()) {
case close_result::ok: break;
default: coral::unreachable();
}
if (!this->archive_reader->seek(0).is_ok()) return open_result::io_unavailable;
constexpr coral::usize header_size = sizeof(header);
coral::u8 archive_header_buffer[header_size]{0};
{
coral::expected const read_bytes{archive_reader->read(archive_header_buffer)};
if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size))
return open_result::archive_invalid;
}
header const * const archive_header{reinterpret_cast<header const *>(archive_header_buffer)};
if (!coral::equals(coral::slice{archive_header->signature_magic,
signature_identifier_length}, coral::slice{signature_magic,
signature_identifier_length})) return open_result::archive_invalid;
if (archive_header->signature_magic[signature_identifier_length] !=
signature_magic[signature_identifier_length]) return open_result::archive_unsupported;
// Read file table.
coral::u64 head{0};
coral::u64 tail{archive_header->entry_count - 1};
constexpr coral::usize entry_size{sizeof(entry)};
coral::u8 file_entry_buffer[entry_size]{0};
while (head <= tail) {
coral::u64 const midpoint{head + ((tail - head) / 2)};
if (!archive_reader->seek(header_size + (entry_size * midpoint)).is_ok())
return open_result::archive_invalid;
{
coral::expected const read_bytes{archive_reader->read(file_entry_buffer)};
if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size))
return open_result::archive_invalid;
}
entry const * const archive_entry{reinterpret_cast<entry const *>(file_entry_buffer)};
coral::size const comparison{file_path.compare(archive_entry->path)};
if (comparison == 0) {
this->data_offset = archive_entry->data_offset;
this->data_length = archive_entry->data_length;
this->data_cursor = archive_entry->data_offset;
return open_result::ok;
}
if (comparison > 0) {
head = (midpoint + 1);
} else {
tail = (midpoint - 1);
}
}
return open_result::not_found;
}
coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & buffer) override {
if (!this->is_open()) return coral::io_error::unavailable;
if (this->archive_reader->seek(this->data_offset + this->data_cursor).is_ok())
return coral::io_error::unavailable;
coral::expected const bytes_read{this->archive_reader->read(buffer.sliced(0,
coral::min(buffer.length, static_cast<coral::usize>((
this->data_offset + this->data_length) - this->data_cursor))))};
if (!bytes_read.is_ok()) this->data_cursor += bytes_read.value();
return bytes_read;
}
coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override {
if (!this->is_open()) return coral::io_error::unavailable;
return coral::io_error::unavailable;
}
coral::expected<coral::u64, coral::io_error> tell() override {
if (!this->is_open()) return coral::io_error::unavailable;
return this->data_cursor;
}
private:
coral::file_reader * archive_reader;
coral::u64 data_offset;
coral::u64 data_length;
coral::u64 data_cursor;
};
struct archive : public coral::fs {
archive(coral::fs * backing_fs, coral::path const & archive_path) {
this->backing_fs = backing_fs;
this->archive_path = archive_path;
}
void read_file(coral::path const & file_path,
coral::callable<void(coral::file_reader &)> const & then) override {
if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return;
this->backing_fs->read_file(this->archive_path, [&](coral::file_reader & archive_reader) {
archive_file_reader file_reader{&archive_reader};
if (file_reader.open(file_path) != archive_file_reader::open_result::ok) return;
then(file_reader);
if (file_reader.close() != archive_file_reader::close_result::ok) return;
});
}
void write_file(coral::path const & file_path,
coral::callable<void(coral::file_writer &)> const & then) override {
// Read-only file system.
}
private:
coral::fs * backing_fs;
coral::path archive_path;
};
}

97
source/runtime.cpp Normal file
View File

@ -0,0 +1,97 @@
export module runtime;
import app;
import coral;
import coral.files;
import coral.math;
import coral.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 coral::path config_path{"config.kym"};
bool is_config_loaded{false};
system.res_fs().read_file(config_path, [&](coral::reader & file) {
coral::allocator * const allocator{&system.thread_safe_allocator()};
kym::vm vm{allocator, [&system](coral::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;
}
coral::stack<coral::u8> script_source{allocator};
{
coral::u8 stream_buffer[1024]{0};
coral::sequence_writer script_writer{&script_source};
if (!coral::stream(script_writer, file, stream_buffer).is_ok()) return;
}
vm.with_object(vm.compile(coral::slice{script_source.begin(), script_source.end()}.as_chars()), [&](kym::bound_object & script) {
vm.with_object(script.call({}), [&](kym::bound_object & config) {
coral::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");
coral::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");
if (graphics.show(width, height) != app::graphics::show_result::ok)
return system.log(app::log_level::error, "failed to initialize window");
vm.with_object(config.get_field("title"), [&](kym::bound_object & title) {
coral::stack<coral::u8, 128> title_buffer{&system.thread_safe_allocator()};
coral::sequence_writer title_writer{&title_buffer};
if (!title.stringify(title_writer).is_ok()) {
system.log(app::log_level::error,
"failed to decode `title` property of config");
return;
}
is_config_loaded = true;
});
});
});
});
if (!is_config_loaded) {
coral::stack<coral::u8> error_message{&system.thread_safe_allocator()};
{
coral::sequence_writer error_writer{&error_message};
if (!error_writer.write(coral::slice{"failed to load "}.as_bytes()).is_ok())
return coral::u8_max;
if (!error_writer.write(config_path.as_slice().as_bytes()).is_ok())
return coral::u8_max;
}
return coral::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));
}