From d5b4281d3639a941a458c1f9d718110d76bf9efa Mon Sep 17 00:00:00 2001 From: kayomn Date: Sat, 18 Feb 2023 03:34:40 +0000 Subject: [PATCH] Port codebase to C++20 --- .drone.yml | 6 +- .gitignore | 5 +- .vscode/c_cpp_properties.json | 16 + .vscode/launch.json | 21 +- .vscode/settings.json | 21 +- .vscode/tasks.json | 60 ---- build.py | 35 ++ build.zig | 34 -- config.kym | 6 + ona.lua | 8 - readme.md | 73 ++++ source/app.cpp | 95 +++++ source/app/sdl.cpp | 162 +++++++++ source/core.cpp | 272 ++++++++++++++ source/core/image.cpp | 31 ++ source/core/math.cpp | 19 + source/core/sequence.cpp | 118 +++++++ source/kym.cpp | 49 +++ source/kym/environment.cpp | 144 ++++++++ source/runtime.cpp | 81 +++++ src/io.zig | 148 -------- src/main.zig | 50 --- src/mem.zig | 87 ----- src/stack.zig | 117 ------- src/sys.zig | 642 ---------------------------------- 25 files changed, 1122 insertions(+), 1178 deletions(-) create mode 100644 .vscode/c_cpp_properties.json mode change 100755 => 100644 .vscode/launch.json mode change 100755 => 100644 .vscode/settings.json delete mode 100755 .vscode/tasks.json create mode 100755 build.py delete mode 100644 build.zig create mode 100644 config.kym delete mode 100644 ona.lua create mode 100644 readme.md create mode 100644 source/app.cpp create mode 100644 source/app/sdl.cpp create mode 100644 source/core.cpp create mode 100644 source/core/image.cpp create mode 100644 source/core/math.cpp create mode 100644 source/core/sequence.cpp create mode 100644 source/kym.cpp create mode 100644 source/kym/environment.cpp create mode 100644 source/runtime.cpp delete mode 100644 src/io.zig delete mode 100644 src/main.zig delete mode 100644 src/mem.zig delete mode 100755 src/stack.zig delete mode 100644 src/sys.zig diff --git a/.drone.yml b/.drone.yml index f9941e1..5249bc7 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,7 +3,7 @@ name: continuous integration steps: - name: build & test - image: euantorano/zig:0.9.1 + image: ubuntu:jammy commands: - - zig build test - - $(find zig-cache -name test) main.zig + - sudo apt install -y gcc python3.10 + - python3.10 ./build.py diff --git a/.gitignore b/.gitignore index 4c14adf..fa7dadd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -**/zig-out/ -**/zig-cache/ +cache +runtime +runtime.exe diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..1fabea9 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -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 +} diff --git a/.vscode/launch.json b/.vscode/launch.json old mode 100755 new mode 100644 index a37980f..7b90bde --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,23 +2,20 @@ "version": "0.2.0", "configurations": [ { - "name": "Build", + "name": "Runtime", "type": "gdb", "request": "launch", - "target": "${workspaceFolder}/zig-out/bin/ona", + "target": "./runtime", "cwd": "${workspaceRoot}", - "valuesFormatting": "parseText", - "preLaunchTask": "Build", + "valuesFormatting": "parseText" }, { - "name": "Test", - "type": "gdb", + "name": "Build Script", + "type": "python", "request": "launch", - "target": "${workspaceFolder}/zig-cache/o/b57ef32c79a05339fbe4a8eb648ff6df/test", - "arguments": "main.zig", - "cwd": "${workspaceRoot}", - "valuesFormatting": "parseText", - "preLaunchTask": "Build Test", - }, + "program": "./build.py", + "console": "integratedTerminal", + "justMyCode": true + } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100755 new mode 100644 index 4beb35f..15daf29 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,17 +1,8 @@ { - "editor.rulers": [100], - - "files.exclude":{ - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/zig-cache": true, - "**/zig-out": true, - }, - - "git.detectSubmodulesLimit": 0, - "git.ignoreSubmodules": true, + "files.associations": { + "type_traits": "cpp", + "cassert": "cpp", + "cstddef": "cpp", + "string_view": "cpp" + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100755 index 016ab6a..0000000 --- a/.vscode/tasks.json +++ /dev/null @@ -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" - }, - ], -} diff --git a/build.py b/build.py new file mode 100755 index 0000000..41c3cb1 --- /dev/null +++ b/build.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 + +import os +import subprocess + +source_path = "./source/" +cache_path = "./cache/" + +if not(os.path.exists(cache_path)): + os.mkdir(cache_path) + +compile_command = f"clang++ -std=c++20 -fno-exceptions -fmodules -fprebuilt-module-path=./cache" +object_file_paths = [] + +def compile_module(source_file_path, module_identifier) -> None: + output_path = os.path.join(cache_path, module_identifier) + + subprocess.run(f"{compile_command} -Xclang -emit-module-interface -c {source_file_path} -o {output_path}.pcm", shell=True, check=True) + subprocess.run(f"{compile_command} -c {source_file_path} -o {output_path}.o", shell=True, check=True) + object_file_paths.append(f"{output_path}.o") + +def compile_package(root_module_name: str) -> None: + root_module_source_path = os.path.join(source_path, root_module_name) + + compile_module(f"{root_module_source_path}.cpp", root_module_name) + + if os.path.isdir(root_module_source_path): + for file_name in os.listdir(root_module_source_path): + compile_module(os.path.join(root_module_source_path, file_name), f"{root_module_name}.{os.path.splitext(file_name)[0]}") + +compile_package("core") +compile_package("app") +compile_package("kym") +compile_package("runtime") +subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True) diff --git a/build.zig b/build.zig deleted file mode 100644 index 7eef4a8..0000000 --- a/build.zig +++ /dev/null @@ -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); - } -} diff --git a/config.kym b/config.kym new file mode 100644 index 0000000..187e4f2 --- /dev/null +++ b/config.kym @@ -0,0 +1,6 @@ + +return { + title = "Demo", + width = 640, + height = 480, +} diff --git a/ona.lua b/ona.lua deleted file mode 100644 index 286067e..0000000 --- a/ona.lua +++ /dev/null @@ -1,8 +0,0 @@ - -return { - name = "Ona", - initial_width = 1280, - initial_height = 800, - - initial_scene = nil, -} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ce4dcbb --- /dev/null +++ b/readme.md @@ -0,0 +1,73 @@ +# Ona + +## Table of Contents + +1. [Overview](#overview) +1. [Goals](#goals) +1. [Technical Details](#technical-details) + 1. [Requirements](#requirements) + 1. [Building](#building) + 1. [Project Structure](#project-structure) + 1. [No Headers](#no-headers) + 1. [All Code is Equal](#all-code-is-equal) + +## Overview + +Ona is a straightforward game engine with the aim of staying reasonably lightweight through its + +Ona is also the Catalan word for "wave". + +## Goals + +* Fully-featured two-dimensional raster and vector-derived rendering capabilities. + +* Support major computer gaming ecosystems; Namely Microsoft Windows, SteamOS, and GNU Linux systems running on X11 or Wayland. + +* Avoid shipping external dependencies beyond the executible itself. + +* Be lightweight in base engine memory usage and disk size. + +* Provide a simple scene graph system that translates its graph nodes into a cache-friendly representation at runtime. + +* Provide an execution-speed optimized scripting interface through a Lua-inspired language named "Kym", with features like first-class support for common mathematic types used in rendering. + +* One data serialization and configuration system to rule them all backed by the Kym scripting language. + +* Opt-in overhead via a native C plug-in interface that allows for systems-level programmers to easily extend engine-level functionality and scripting language library tools. + +## Technical Details + +### Requirements + +Ona currently depends the following third-party tools to build it: + + * GNU C++ compiler with full C++20 support or above. + * Python interpreter version 3.10 or above. + +Additionally, Ona depends on the following third-party system-wide dependencies: + + * SDL2 version 2.0.20 or above. + +As the project evolves, dependencies on libraries external to the project codebase will be minimized or removed outright to meet the goals of the project as closely as possible. + +### Building + +Once all third-party tools and system-wide dependencies are satisfied, navigate to the root project folder and run the `./build.py` Python build script. + +By default, the build script will build the engine runtime, required for running games built with Ona, in release-debug mode. + +### Project Structure + +As Ona uses C++20, it is able to make use of the new modules language feature. While this brings with it a number of drawbacks, like a lack of widescale vendor adoption, it also provides some key benefits. + +#### No Headers + +All first-party code in the project is free of headers. Code is grouped in a module and package dichotomy, where each `.cpp` file in the root source directory represents the common package of a module grouping. + +Subdirectories then build further abstractions atop these common module files. For example, the `core.cpp` source file contains many common memory manipulation and floating point mathematics utilities, which are made use of in `core/image.cpp` for modifying CPU-bound pixel data. + +#### All Code is Equal + +Following on from no headers necessary, declarations, template metaprogramming, and definitions all go into the same place now. A typical Ona source file mixes all of these, traditionally separate, pieces of logic together in shared `.cpp` files. + +Alongside the surface-level benefit of writing having fewer lines of code, this also means there is less work necessary to maintain the codebase at large and a smaller space to create duplication errors in. diff --git a/source/app.cpp b/source/app.cpp new file mode 100644 index 0000000..5a4a1b3 --- /dev/null +++ b/source/app.cpp @@ -0,0 +1,95 @@ +export module app; + +import core; +import core.image; +import core.math; + +export namespace app { + struct path { + static constexpr core::usize max = 0xff; + + static constexpr char seperator = '/'; + + core::u8 buffer[max + 1]; + + static constexpr path empty() { + path empty_path = {0}; + + empty_path.buffer[max] = max; + + return empty_path; + } + + core::slice as_slice() const { + return {reinterpret_cast(this->buffer), this->size()}; + } + + constexpr core::usize size() const { + return max - this->buffer[max]; + } + + core::i16 compare(path const & that) { + return 0; + } + + bool equals(path const & that) { + return core::equals(this->as_slice(), that.as_slice()); + } + + constexpr path joined(core::slice const & text) const { + if (text.length > this->buffer[max]) return empty(); + + path joined_path = *this; + + for (char const c : text) { + joined_path.buffer[joined_path.size()] = c; + joined_path.buffer[max] -= 1; + } + + return joined_path; + } + + core::u64 hash() { + return 0; + } + }; + + struct file_store { + virtual void read_file(app::path const & file_path, core::callable const & then) = 0; + }; + + enum class log_level { + notice, + warning, + error, + }; + + struct system { + virtual bool poll() = 0; + + virtual file_store & bundle() = 0; + + virtual void log(log_level level, core::slice const & message) = 0; + + virtual core::allocator & thread_safe_allocator() = 0; + }; + + struct graphics { + enum class show_error { + none, + out_of_memory, + }; + + struct canvas { + core::color background_color; + }; + + virtual void render(canvas & source_canvas) = 0; + + virtual void present() = 0; + + virtual show_error show(core::u16 physical_width, core::u16 physical_height) = 0; + + virtual void retitle(core::slice const & updated_title) = 0; + }; +} diff --git a/source/app/sdl.cpp b/source/app/sdl.cpp new file mode 100644 index 0000000..ce1825c --- /dev/null +++ b/source/app/sdl.cpp @@ -0,0 +1,162 @@ +module; + +#include + +export module app.sdl; + +import app; + +import core; +import core.image; +import core.sequence; +import core.math; + +struct bundled_file_store : public app::file_store { + void read_file(app::path const & file_path, core::callable const & then) override { + // Path is guaranteed to never be greater than 512 characters long (file_path is max 256 and prefix is 2). + core::stack path_buffer{&core::null_allocator()}; + + if (path_buffer.append("./").has_value()) core::unreachable(); + + // File path is guaranteed to be null-terminated. + if (path_buffer.append(file_path.as_slice()).has_value()) core::unreachable(); + + SDL_RWops * rw_ops = ::SDL_RWFromFile(path_buffer.as_slice().pointer, "r"); + + if (rw_ops == nullptr) return; + + then([rw_ops](core::slice const & buffer) -> size_t { + return ::SDL_RWread(rw_ops, buffer.pointer, sizeof(uint8_t), buffer.length); + }); + + ::SDL_RWclose(rw_ops); + } +}; + +struct sdl_allocator : public core::allocator { + core::u8 * reallocate(core::u8 * maybe_allocation, core::usize requested_size) override { + return reinterpret_cast(::SDL_malloc(requested_size)); + } + + void deallocate(void * allocation) override { + ::SDL_free(allocation); + } +}; + +struct sdl_system : public app::system { + private: + ::SDL_Event sdl_event; + + sdl_allocator allocator; + + bundled_file_store bundled_store; + + public: + sdl_system() : + sdl_event{0}, + allocator{} {} + + bool poll() override { + while (::SDL_PollEvent(&this->sdl_event) != 0) { + switch (this->sdl_event.type) { + case SDL_QUIT: return false; + } + } + + return true; + } + + app::file_store & bundle() override { + return this->bundled_store; + } + + void log(app::log_level level, core::slice const & message) override { + core::i32 const length = static_cast( + core::min(message.length, static_cast(core::i32_max))); + + ::SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, + SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer); + } + + core::allocator & thread_safe_allocator() override { + return this->allocator; + } +}; + +struct sdl_graphics : public app::graphics { + static constexpr core::usize title_maximum = 128; + + core::u32 title_length; + + char title_buffer[title_maximum]; + + ::SDL_Window * sdl_window = nullptr; + + ::SDL_Renderer * sdl_renderer = nullptr; + + sdl_graphics(core::slice const & title) { + this->retitle(title); + } + + void render(canvas & source_canvas) override { + if (this->sdl_renderer != nullptr) { + SDL_SetRenderDrawColor(this->sdl_renderer, source_canvas.background_color.to_r8(), + source_canvas.background_color.to_g8(), source_canvas.background_color.to_b8(), + source_canvas.background_color.to_a8()); + + SDL_RenderClear(this->sdl_renderer); + } + } + + void retitle(core::slice const & title) override { + this->title_length = core::min(title.length, title_maximum - 1); + + for (core::usize i = 0; i < this->title_length; i += 1) title_buffer[i] = title[i]; + + for (core::usize i = this->title_length; i < title_maximum; i += 1) title_buffer[i] = 0; + + if (this->sdl_window != nullptr) ::SDL_SetWindowTitle(this->sdl_window, title_buffer); + } + + app::graphics::show_error show(core::u16 physical_width, core::u16 physical_height) override { + if (this->sdl_window == nullptr) { + constexpr int sdl_windowpos = SDL_WINDOWPOS_UNDEFINED; + constexpr core::u32 sdl_windowflags = 0; + + this->sdl_window = ::SDL_CreateWindow(title_buffer, sdl_windowpos, sdl_windowpos, + static_cast(physical_width), static_cast(physical_height), + sdl_windowflags); + + if (this->sdl_window == nullptr) return show_error::out_of_memory; + } else { + ::SDL_ShowWindow(this->sdl_window); + } + + if (this->sdl_renderer == nullptr) { + constexpr core::u32 sdl_rendererflags = 0; + + this->sdl_renderer = ::SDL_CreateRenderer(this->sdl_window, -1, sdl_rendererflags); + + if (this->sdl_renderer == nullptr) return show_error::out_of_memory; + } + + return show_error::none; + } + + void present() override { + if (this->sdl_renderer != nullptr) { + ::SDL_RenderPresent(this->sdl_renderer); + } + } +}; + +export namespace app { + int display(core::slice const & title, + core::callable const & run) { + + sdl_system system; + sdl_graphics graphics(title); + + return run(system, graphics); + } +} diff --git a/source/core.cpp b/source/core.cpp new file mode 100644 index 0000000..429717e --- /dev/null +++ b/source/core.cpp @@ -0,0 +1,272 @@ +module; + +#include +#include + +export module core; + +export namespace core { + using usize = size_t; + + using size = __ssize_t; + + using u8 = uint8_t; + + usize const u8_max = 0xff; + + using i8 = uint8_t; + + using u16 = uint16_t; + + usize const u16_max = 0xffff; + + using i16 = uint16_t; + + using u32 = uint32_t; + + using i32 = uint32_t; + + usize const i32_max = 0xffffffff; + + using u64 = uint32_t; + + using i64 = uint32_t; + + using f32 = float; + + using f64 = double; + + struct allocator { + allocator() = default; + + allocator(allocator const &) = delete; + + virtual ~allocator() {}; + + virtual u8 * reallocate(u8 * maybe_allocation, usize requested_size) = 0; + + virtual void deallocate(void * allocation) = 0; + }; +} + +export void * operator new(core::usize requested_size, core::u8 * allocation_placement) { + return allocation_placement; +} + +export void * operator new[](core::usize requested_size, core::u8 * allocation_placement) { + return allocation_placement; +} + +export void * operator new(core::usize requested_size, core::allocator & allocator) { + return allocator.reallocate(nullptr, requested_size); +} + +export void * operator new[](core::usize requested_size, core::allocator & allocator) { + return allocator.reallocate(nullptr, requested_size); +} + +export namespace core { + [[noreturn]] void unreachable() { + __builtin_unreachable(); + } + + template struct slice { + usize length; + + type * pointer; + + constexpr slice() : length{0}, pointer{nullptr} { + + } + + constexpr slice(char const *&& zstring) : length{0}, pointer{zstring} { + while (zstring[length] != 0) this->length += 1; + } + + constexpr slice(type * slice_pointer, usize slice_length) : length{slice_length}, pointer{slice_pointer} { + + } + + template constexpr slice(type(&array)[array_size]) : length{array_size}, pointer{array} { + + } + + slice as_bytes() const { + return {reinterpret_cast(this->pointer), this->length * sizeof(type)}; + } + + slice as_chars() const { + return {reinterpret_cast(this->pointer), this->length * sizeof(type)}; + } + + operator slice() const { + return (*reinterpret_cast const *>(this)); + } + + type & operator[](usize index) const { + return this->pointer[index]; + } + + slice after(usize index) const { + return {this->pointer + index, this->length - index}; + } + + slice until(usize index) const { + return {this->pointer, index}; + } + + slice between(usize a, usize b) const { + return {this->pointer + a, b}; + } + + constexpr type * begin() const { + return this->pointer; + } + + constexpr type * end() const { + return this->pointer + this->length; + } + }; + + template struct [[nodiscard]] optional { + optional() : buffer{0} { + + } + + optional(element const & value) : buffer{0} { + (*reinterpret_cast(this->buffer)) = value; + this->buffer[sizeof(element)] = 1; + } + + optional(optional const & that) : buffer{0} { + if (that.has_value()) { + (*reinterpret_cast(this->buffer)) = that.value(); + this->buffer[sizeof(element)] = 1; + } else { + this->buffer[sizeof(element)] = 0; + } + } + + bool has_value() const { + return this->buffer[sizeof(element)] == 1; + } + + element const & value_or(element const & fallback) const { + return this->has_value() ? *reinterpret_cast(this->buffer) : fallback; + } + + element & value() { + return *reinterpret_cast(this->buffer); + } + + element const & value() const { + return *reinterpret_cast(this->buffer); + } + + private: + u8 buffer[sizeof(element) + 1]; + }; + + template struct callable; + + template struct callable { + using function = return_type(*)(argument_types...); + + callable(function callable_function) : dispatcher(dispatch_function) { + new (this->capture) function{callable_function}; + } + + callable(callable const &) = delete; + + template callable(functor const & callable_functor) : dispatcher(dispatch_functor) { + new (this->capture) functor{callable_functor}; + } + + return_type operator()(argument_types const &... arguments) const { + return this->dispatcher(this->capture, arguments...); + } + + private: + static constexpr usize capture_size = 24; + + return_type(* dispatcher)(u8 const * userdata, argument_types... arguments); + + u8 capture[capture_size]; + + static return_type dispatch_function(u8 const * userdata, argument_types... arguments) { + return (*reinterpret_cast(userdata))(arguments...); + } + + template static return_type dispatch_functor(u8 const * userdata, argument_types... arguments) { + return (*reinterpret_cast(userdata))(arguments...); + } + }; + + allocator & null_allocator() { + static struct : public allocator { + u8 * reallocate(u8 * maybe_allocation, usize requested_size) override { + if (maybe_allocation != nullptr) unreachable(); + + return nullptr; + } + + void deallocate(void * allocation) override { + if (allocation != nullptr) unreachable(); + } + } a; + + return a; + } + + using readable = callable(slice const &)>; + + using writable = callable(slice const &)>; + + template bool equals(slice const & a, slice const & b) { + if (a.length != b.length) return false; + + for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false; + + return true; + } + + optional stream(writable const & output, readable const & input, slice const & buffer) { + usize written = 0; + optional maybe_read = input(buffer); + + if (!maybe_read.has_value()) return {}; + + usize read = maybe_read.value(); + + while (read != 0) { + optional const maybe_written = output(buffer.until(read)); + + if (!maybe_written.has_value()) return {}; + + written += maybe_written.value(); + maybe_read = input(buffer); + + if (!maybe_read.has_value()) return {}; + + read = maybe_read.value(); + } + + return written; + } + + template scalar max(scalar const & a, scalar const & b) { + return (a > b) ? a : b; + } + + template scalar min(scalar const & a, scalar const & b) { + return (a < b) ? a : b; + } + + template scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) { + return max(min_value, min(max_value, value)); + } + + f32 round32(f32 value) { + return __builtin_roundf(value); + } +} diff --git a/source/core/image.cpp b/source/core/image.cpp new file mode 100644 index 0000000..0a0a75f --- /dev/null +++ b/source/core/image.cpp @@ -0,0 +1,31 @@ +export module core.image; + +import core; + +export namespace core { + struct color { + float r; + + float g; + + float b; + + float a; + + u8 to_r8() const { + return static_cast(round32(clamp(this->r, 0.0f, 1.0f) * u8_max)); + } + + u8 to_g8() const { + return static_cast(round32(clamp(this->g, 0.0f, 1.0f) * u8_max)); + } + + u8 to_b8() const { + return static_cast(round32(clamp(this->b, 0.0f, 1.0f) * u8_max)); + } + + u8 to_a8() const { + return static_cast(round32(clamp(this->a, 0.0f, 1.0f) * u8_max)); + } + }; +} diff --git a/source/core/math.cpp b/source/core/math.cpp new file mode 100644 index 0000000..1cc49b5 --- /dev/null +++ b/source/core/math.cpp @@ -0,0 +1,19 @@ +export module core.math; + +import core; + +export namespace core { + struct vector2 { + core::f32 x; + + core::f32 y; + }; + + struct vector3 { + core::f32 x; + + core::f32 y; + + core::f32 z; + }; +} diff --git a/source/core/sequence.cpp b/source/core/sequence.cpp new file mode 100644 index 0000000..bcc0868 --- /dev/null +++ b/source/core/sequence.cpp @@ -0,0 +1,118 @@ +export module core.sequence; + +import core; + +export namespace core { + enum class reserve_error { + out_of_memory, + }; + + template struct sequence { + sequence() = default; + + sequence(sequence const &) = delete; + + virtual ~sequence() {}; + + virtual slice as_slice() const = 0; + + virtual optional append(slice const & appended_elements) = 0; + }; + + template struct stack : public sequence { + stack(allocator * buffer_allocator) : filled{0}, + buffer{0}, buffer_allocator{buffer_allocator} { + + this->elements = this->buffer; + } + + ~stack() override { + if (this->elements.pointer != this->buffer) + this->buffer_allocator->deallocate(this->elements.pointer); + } + + slice as_slice() const override { + return this->elements; + } + + optional push(element const & pushed_element) { + if (this->filled == this->elements.length) { + optional const maybe_error = this->reserve(this->elements.length); + + if (maybe_error.has_value()) return maybe_error; + } + + this->elements[this->filled] = pushed_element; + this->filled += 1; + + return {}; + } + + optional append(slice const & pushed_elements) override { + usize const updated_fill = this->filled + pushed_elements.length; + + if (updated_fill >= this->elements.length) { + optional const maybe_error = this->reserve(max(this->elements.length, updated_fill)); + + if (maybe_error.has_value()) return maybe_error; + } + + for (usize i = 0; i < pushed_elements.length; i += 1) + this->elements[this->filled + i] = pushed_elements[i]; + + this->filled = updated_fill; + + return {}; + } + + optional reserve(usize capacity) { + usize const requested_capacity = this->elements.length + capacity; + + if (this->elements.pointer == this->buffer) { + u8 * const maybe_allocation = this->buffer_allocator-> + reallocate(nullptr, sizeof(element) * requested_capacity); + + if (maybe_allocation == nullptr) { + this->elements = {}; + + return reserve_error::out_of_memory; + } + + this->elements = {reinterpret_cast(maybe_allocation), requested_capacity}; + } else { + u8 * const maybe_allocation = this->buffer_allocator->reallocate( + reinterpret_cast(this->elements.pointer), + sizeof(element) * requested_capacity); + + if (maybe_allocation == nullptr) { + this->elements = {}; + + return reserve_error::out_of_memory; + } + + this->elements = {reinterpret_cast(maybe_allocation), requested_capacity}; + } + + return {}; + } + + private: + allocator * buffer_allocator; + + usize filled; + + slice elements; + + element buffer[initial_capacity]; + }; + + struct sequence_writer : public writable { + sequence_writer(sequence * target_sequence) : writable{[target_sequence](slice const & buffer) -> optional { + optional const maybe_error = target_sequence->append(buffer); + + if (maybe_error.has_value()) return {}; + + return buffer.length; + }} {} + }; +} diff --git a/source/kym.cpp b/source/kym.cpp new file mode 100644 index 0000000..3ac4528 --- /dev/null +++ b/source/kym.cpp @@ -0,0 +1,49 @@ +export module kym; + +import core; +import core.math; + +export namespace kym { + enum class value_type { + nil, + boolean, + integer, + scalar, + vector2, + vector3, + object, + }; + + struct value { + value_type type; + + union { + bool boolean; + + core::i64 integer; + + core::f64 scalar; + + core::vector2 vector2; + + core::vector3 vector3; + + void * object; + } as; + + core::optional as_u16() const { + if ((this->type == value_type::integer) && + (this->as.integer >= 0) && (this->as.integer <= core::u16_max)) { + + return static_cast(this->as.integer); + } + + return {}; + } + }; + + value nil = { + .type = value_type::nil, + .as = {.object = nullptr}, + }; +}; diff --git a/source/kym/environment.cpp b/source/kym/environment.cpp new file mode 100644 index 0000000..dfd3b7f --- /dev/null +++ b/source/kym/environment.cpp @@ -0,0 +1,144 @@ +export module kym.environment; + +import core; +import core.sequence; + +import kym; + +export namespace kym { + struct vm; +} + +enum class token_kind { + end, +}; + +struct token { + core::slice text; + + token_kind kind; +}; + +using tokenizable = core::callable; + +struct bytecode { + bytecode(core::allocator * allocator) : error_message_buffer{allocator} { + + } + + bool compile(tokenizable const & bytecode_tokenizable) { + for (;;) { + token const initial_token = bytecode_tokenizable(); + + switch (initial_token.kind) { + case token_kind::end: return true; + + default: core::unreachable(); + } + } + } + + kym::value execute(kym::vm & vm, core::slice const & arguments) { + return kym::nil; + } + + core::slice error_message() const { + return this->error_message_buffer.as_slice(); + } + + private: + core::stack error_message_buffer; +}; + +export namespace kym { + struct bound_object { + core::callable cleanup; + + core::callable)> call; + + value get_field(core::slice const & field_name) { + return nil; + } + + bool is_string() { + return false; + } + + core::usize stringify(core::writable const & writable) { + return 0; + } + }; + + struct vm { + struct init_options { + core::u16 datastack_size; + + core::u16 callstack_size; + }; + + vm(core::allocator * allocator, auto log) : allocator{allocator}, log{log}, data_stack{} {} + + ~vm() { + if (this->data_stack.pointer != nullptr) + this->allocator->deallocate(this->data_stack.pointer); + } + + bool init(init_options const & options) { + core::u8 * const data_stack_buffer = this->allocator->reallocate(reinterpret_cast + (this->data_stack.pointer), options.datastack_size * sizeof(value)); + + if (data_stack_buffer == nullptr) return false; + + this->data_stack = { + reinterpret_cast(data_stack_buffer), options.datastack_size}; + + return true; + } + + value compile(core::slice const & source) { + bytecode * source_bytecode = new (*this->allocator) bytecode{allocator}; + + if (source_bytecode == nullptr) return nil; + + core::usize cursor = 0; + + if (source_bytecode->compile([]() { + return token{ + + }; + })) { + this->log(source_bytecode->error_message()); + this->allocator->deallocate(source_bytecode); + + return nil; + } + + return this->new_object([this, source_bytecode](bound_object & object) { + object.cleanup = [this, source_bytecode]() { + this->allocator->deallocate(source_bytecode); + }; + + object.call = [this, source_bytecode](core::slice const & arguments) { + return source_bytecode->execute(*this, arguments); + }; + + this->allocator->deallocate(source_bytecode); + }); + } + + value new_object(core::callable const & then) { + return nil; + } + + void with_object(value object_value, core::callable const & then) { + + } + + private: + core::slice data_stack; + + core::callable)> log; + + core::allocator * allocator; + }; +} diff --git a/source/runtime.cpp b/source/runtime.cpp new file mode 100644 index 0000000..a183d25 --- /dev/null +++ b/source/runtime.cpp @@ -0,0 +1,81 @@ +export module runtime; + +import app; +import app.sdl; + +import core; +import core.math; +import core.sequence; + +import kym; +import kym.environment; + +extern "C" int main(int argc, char const * const * argv) { + return app::display("Ona Runtime", [](app::system & system, app::graphics & graphics) -> int { + constexpr app::path config_path = app::path::empty().joined("config.kym"); + bool is_config_loaded = false; + + system.bundle().read_file(config_path, [&](core::readable const & config_readable) { + kym::vm vm{&system.thread_safe_allocator(), [&system](core::slice const & error_message) { + system.log(app::log_level::error, error_message); + }}; + + if (!vm.init({ + .datastack_size = 64, + .callstack_size = 64, + })) { + system.log(app::log_level::error, "failed to allocate memory for config vm"); + + return; + } + + core::stack config_source{&system.thread_safe_allocator()}; + core::u8 config_source_stream_buffer[1024] = {}; + + if (!core::stream(core::sequence_writer{&config_source}, + config_readable, config_source_stream_buffer).has_value()) return; + + vm.with_object(vm.compile(config_source.as_slice().as_chars()), [&](kym::bound_object & config_script) { + vm.with_object(config_script.call({}), [&](kym::bound_object & config) { + core::u16 const width = config.get_field("width").as_u16().value_or(0); + + if (width == 0) return system.log( + app::log_level::error, "failed to decode `width` property of config"); + + core::u16 const height = config.get_field("height").as_u16().value_or(0); + + if (height == 0) return system.log( + app::log_level::error, "failed to decode `height` property of config"); + + graphics.show(width, height); + + vm.with_object(config.get_field("title"), [&](kym::bound_object & title) { + core::stack title_buffer{&system.thread_safe_allocator()}; + + if (!title.is_string()) return system.log( + app::log_level::error, "failed to decode `title` property of config"); + + title.stringify(core::sequence_writer(&title_buffer)); + + is_config_loaded = true; + }); + }); + }); + }); + + if (!is_config_loaded) { + system.log(app::log_level::error, "failed to load config"); + + return core::u8_max; + } + + // app::canvas canvas_2d(); + + while (system.poll()) { + // canvas_2d.render(graphics); + graphics.present(); + } + + return 0; + }); +} diff --git a/src/io.zig b/src/io.zig deleted file mode 100644 index 58a3c38..0000000 --- a/src/io.zig +++ /dev/null @@ -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)); -} diff --git a/src/main.zig b/src/main.zig deleted file mode 100644 index 4108ff8..0000000 --- a/src/main.zig +++ /dev/null @@ -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(); - } -} diff --git a/src/mem.zig b/src/mem.zig deleted file mode 100644 index 3df40b9..0000000 --- a/src/mem.zig +++ /dev/null @@ -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); -} diff --git a/src/stack.zig b/src/stack.zig deleted file mode 100755 index 3f8284a..0000000 --- a/src/stack.zig +++ /dev/null @@ -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); -} diff --git a/src/sys.zig b/src/sys.zig deleted file mode 100644 index b880fef..0000000 --- a/src/sys.zig +++ /dev/null @@ -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)); -}