C++20 Port #5
|
@ -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
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
**/zig-out/
|
cache
|
||||||
**/zig-cache/
|
runtime
|
||||||
|
runtime.exe
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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",
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
|
@ -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)
|
34
build.zig
34
build.zig
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
return {
|
||||||
|
title = "Demo",
|
||||||
|
width = 640,
|
||||||
|
height = 480,
|
||||||
|
}
|
8
ona.lua
8
ona.lua
|
@ -1,8 +0,0 @@
|
||||||
|
|
||||||
return {
|
|
||||||
name = "Ona",
|
|
||||||
initial_width = 1280,
|
|
||||||
initial_height = 800,
|
|
||||||
|
|
||||||
initial_scene = nil,
|
|
||||||
}
|
|
|
@ -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.
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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},
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
148
src/io.zig
148
src/io.zig
|
@ -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));
|
|
||||||
}
|
|
50
src/main.zig
50
src/main.zig
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
87
src/mem.zig
87
src/mem.zig
|
@ -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);
|
|
||||||
}
|
|
117
src/stack.zig
117
src/stack.zig
|
@ -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);
|
|
||||||
}
|
|
642
src/sys.zig
642
src/sys.zig
|
@ -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));
|
|
||||||
}
|
|
Loading…
Reference in New Issue