Merge pull request 'C++20 Port' (#5) from cpp-port into main
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				continuous-integration/drone Build is failing
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	continuous-integration/drone Build is failing
				
			Reviewed-on: #5
This commit is contained in:
		
						commit
						82d9fa85b8
					
				| @ -3,7 +3,7 @@ name: continuous integration | |||||||
| 
 | 
 | ||||||
| steps: | steps: | ||||||
| - name: build & test | - name: build & test | ||||||
|   image: euantorano/zig:0.9.1 |   image: ubuntu:jammy | ||||||
|   commands: |   commands: | ||||||
|   - zig build test |   - sudo apt install -y gcc python3.10 | ||||||
|   - $(find zig-cache -name test) main.zig |   - python3.10 ./build.py | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,2 +1,3 @@ | |||||||
| **/zig-out/ | cache | ||||||
| **/zig-cache/ | runtime | ||||||
|  | runtime.exe | ||||||
|  | |||||||
							
								
								
									
										16
									
								
								.vscode/c_cpp_properties.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.vscode/c_cpp_properties.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | |||||||
|  | { | ||||||
|  | 	"configurations": [ | ||||||
|  | 		{ | ||||||
|  | 			"name": "Linux", | ||||||
|  | 			"includePath": [ | ||||||
|  | 				"${workspaceFolder}/src" | ||||||
|  | 			], | ||||||
|  | 			"defines": [], | ||||||
|  | 			"compilerPath": "/usr/bin/gcc", | ||||||
|  | 			"cStandard": "gnu17", | ||||||
|  | 			"cppStandard": "gnu++20", | ||||||
|  | 			"intelliSenseMode": "linux-gcc-x64" | ||||||
|  | 		} | ||||||
|  | 	], | ||||||
|  | 	"version": 4 | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										21
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							| @ -2,23 +2,20 @@ | |||||||
| 	"version": "0.2.0", | 	"version": "0.2.0", | ||||||
| 	"configurations": [ | 	"configurations": [ | ||||||
| 		{ | 		{ | ||||||
| 			"name": "Build", | 			"name": "Runtime", | ||||||
| 			"type": "gdb", | 			"type": "gdb", | ||||||
| 			"request": "launch", | 			"request": "launch", | ||||||
| 			"target": "${workspaceFolder}/zig-out/bin/ona", | 			"target": "./runtime", | ||||||
| 			"cwd": "${workspaceRoot}", | 			"cwd": "${workspaceRoot}", | ||||||
| 			"valuesFormatting": "parseText", | 			"valuesFormatting": "parseText" | ||||||
| 			"preLaunchTask": "Build", |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"name": "Test", | 			"name": "Build Script", | ||||||
| 			"type": "gdb", | 			"type": "python", | ||||||
| 			"request": "launch", | 			"request": "launch", | ||||||
| 			"target": "${workspaceFolder}/zig-cache/o/b57ef32c79a05339fbe4a8eb648ff6df/test", | 			"program": "./build.py", | ||||||
| 			"arguments": "main.zig", | 			"console": "integratedTerminal", | ||||||
| 			"cwd": "${workspaceRoot}", | 			"justMyCode": true | ||||||
| 			"valuesFormatting": "parseText", | 		} | ||||||
| 			"preLaunchTask": "Build Test", |  | ||||||
| 		}, |  | ||||||
| 	] | 	] | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							
							
						
						
									
										21
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										
										
										Executable file → Normal file
									
								
							| @ -1,17 +1,8 @@ | |||||||
| { | { | ||||||
|     "editor.rulers": [100], |     "files.associations": { | ||||||
| 
 |         "type_traits": "cpp", | ||||||
|     "files.exclude":{ |         "cassert": "cpp", | ||||||
|         "**/.git": true, |         "cstddef": "cpp", | ||||||
|         "**/.svn": true, |         "string_view": "cpp" | ||||||
|         "**/.hg": true, |     } | ||||||
|         "**/CVS": true, |  | ||||||
|         "**/.DS_Store": true, |  | ||||||
|         "**/Thumbs.db": true, |  | ||||||
|         "**/zig-cache": true, |  | ||||||
|         "**/zig-out": true, |  | ||||||
|     }, |  | ||||||
| 
 |  | ||||||
|     "git.detectSubmodulesLimit": 0, |  | ||||||
|     "git.ignoreSubmodules": true, |  | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										60
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										60
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @ -1,60 +0,0 @@ | |||||||
| { |  | ||||||
| 	"version": "2.0.0", |  | ||||||
| 
 |  | ||||||
| 	"tasks": [ |  | ||||||
| 		{ |  | ||||||
| 			"label": "Build", |  | ||||||
| 			"type": "shell", |  | ||||||
| 			"command": "zig build", |  | ||||||
| 
 |  | ||||||
| 			"group": { |  | ||||||
| 				"kind": "build", |  | ||||||
| 				"isDefault": true |  | ||||||
| 			}, |  | ||||||
| 
 |  | ||||||
| 			"presentation": { |  | ||||||
| 				"echo": true, |  | ||||||
| 				"reveal": "always", |  | ||||||
| 				"focus": true, |  | ||||||
| 				"panel": "shared", |  | ||||||
| 				"showReuseMessage": true, |  | ||||||
| 				"clear": true, |  | ||||||
| 				"revealProblems": "onProblem", |  | ||||||
| 			}, |  | ||||||
| 
 |  | ||||||
| 			"problemMatcher": { |  | ||||||
| 				"source": "gcc", |  | ||||||
| 				"owner": "cpptools", |  | ||||||
| 
 |  | ||||||
| 				"fileLocation": [ |  | ||||||
| 					"autoDetect", |  | ||||||
| 					"${cwd}", |  | ||||||
| 				], |  | ||||||
| 
 |  | ||||||
| 				"pattern": { |  | ||||||
| 					"regexp": "^(.*?):(\\d+):(\\d*):?\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", |  | ||||||
| 					"file": 1, |  | ||||||
| 					"line": 2, |  | ||||||
| 					"column": 3, |  | ||||||
| 					"severity": 4, |  | ||||||
| 					"message": 5, |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"label": "Test", |  | ||||||
| 			"type": "shell", |  | ||||||
| 			"command": "$(find zig-cache -name test) src/main.zig", |  | ||||||
| 			"group": { |  | ||||||
| 				"kind": "test", |  | ||||||
| 				"isDefault": true |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"label": "Build Test", |  | ||||||
| 			"type": "shell", |  | ||||||
| 			"command": "zig build test", |  | ||||||
| 			"group": "test" |  | ||||||
| 		}, |  | ||||||
| 	], |  | ||||||
| } |  | ||||||
							
								
								
									
										36
									
								
								build.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										36
									
								
								build.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,36 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import subprocess | ||||||
|  | 
 | ||||||
|  | source_path = "./source/" | ||||||
|  | cache_path = "./cache/" | ||||||
|  | 
 | ||||||
|  | if not(os.path.exists(cache_path)): | ||||||
|  | 	os.mkdir(cache_path) | ||||||
|  | 
 | ||||||
|  | compile_command = f"clang++ -g -std=c++20 -fno-exceptions -fmodules -fprebuilt-module-path=./cache" | ||||||
|  | object_file_paths = [] | ||||||
|  | 
 | ||||||
|  | def compile_module(source_file_path, module_identifier) -> None: | ||||||
|  | 	output_path = os.path.join(cache_path, module_identifier) | ||||||
|  | 
 | ||||||
|  | 	subprocess.run(f"{compile_command} -Xclang -emit-module-interface -c {source_file_path} -o {output_path}.pcm", shell=True, check=True) | ||||||
|  | 	subprocess.run(f"{compile_command} -c {source_file_path} -o {output_path}.o", shell=True, check=True) | ||||||
|  | 	object_file_paths.append(f"{output_path}.o") | ||||||
|  | 
 | ||||||
|  | def compile_package(root_module_name: str) -> None: | ||||||
|  | 	root_module_source_path = os.path.join(source_path, root_module_name) | ||||||
|  | 
 | ||||||
|  | 	compile_module(f"{root_module_source_path}.cpp", root_module_name) | ||||||
|  | 
 | ||||||
|  | 	if os.path.isdir(root_module_source_path): | ||||||
|  | 		for file_name in os.listdir(root_module_source_path): | ||||||
|  | 			compile_module(os.path.join(root_module_source_path, file_name), f"{root_module_name}.{os.path.splitext(file_name)[0]}") | ||||||
|  | 
 | ||||||
|  | compile_package("coral") | ||||||
|  | compile_package("oar") | ||||||
|  | compile_package("app") | ||||||
|  | compile_package("kym") | ||||||
|  | compile_package("runtime") | ||||||
|  | subprocess.run(f"{compile_command} {' '.join(object_file_paths)} -o ./runtime -lSDL2", shell=True, check=True) | ||||||
							
								
								
									
										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); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										6
									
								
								config.kym
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								config.kym
									
									
									
									
									
										Normal file
									
								
							| @ -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, |  | ||||||
| } |  | ||||||
							
								
								
									
										73
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								readme.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,73 @@ | |||||||
|  | # Ona | ||||||
|  | 
 | ||||||
|  | ## Table of Contents | ||||||
|  | 
 | ||||||
|  | 1. [Overview](#overview) | ||||||
|  | 1. [Goals](#goals) | ||||||
|  | 1. [Technical Details](#technical-details) | ||||||
|  |     1. [Requirements](#requirements) | ||||||
|  |     1. [Building](#building) | ||||||
|  |     1. [Project Structure](#project-structure) | ||||||
|  |         1. [No Headers](#no-headers) | ||||||
|  |         1. [All Code is Equal](#all-code-is-equal) | ||||||
|  | 
 | ||||||
|  | ## Overview | ||||||
|  | 
 | ||||||
|  | Ona is a straightforward game engine with the aim of staying reasonably lightweight through its | ||||||
|  | 
 | ||||||
|  | Ona is also the Catalan word for "wave". | ||||||
|  | 
 | ||||||
|  | ## Goals | ||||||
|  | 
 | ||||||
|  | * Fully-featured two-dimensional raster and vector-derived rendering capabilities. | ||||||
|  | 
 | ||||||
|  | * Support major computer gaming ecosystems; Namely Microsoft Windows, SteamOS, and GNU Linux systems running on X11 or Wayland. | ||||||
|  | 
 | ||||||
|  | * Avoid shipping external dependencies beyond the executible itself. | ||||||
|  | 
 | ||||||
|  | * Be lightweight in base engine memory usage and disk size. | ||||||
|  | 
 | ||||||
|  | * Provide a simple scene graph system that translates its graph nodes into a cache-friendly representation at runtime. | ||||||
|  | 
 | ||||||
|  | * Provide an execution-speed optimized scripting interface through a Lua-inspired language named "Kym", with features like first-class support for common mathematic types used in rendering. | ||||||
|  | 
 | ||||||
|  | * One data serialization and configuration system to rule them all backed by the Kym scripting language. | ||||||
|  | 
 | ||||||
|  | * Opt-in overhead via a native C plug-in interface that allows for systems-level programmers to easily extend engine-level functionality and scripting language library tools. | ||||||
|  | 
 | ||||||
|  | ## Technical Details | ||||||
|  | 
 | ||||||
|  | ### Requirements | ||||||
|  | 
 | ||||||
|  | Ona currently depends the following third-party tools to build it: | ||||||
|  | 
 | ||||||
|  |   * Clang / LLVM toolchain with full C++20 support or above. | ||||||
|  |   * Python interpreter version 3.10 or above. | ||||||
|  | 
 | ||||||
|  | Additionally, Ona depends on the following third-party system-wide dependencies: | ||||||
|  | 
 | ||||||
|  |   * SDL2 version 2.0.20 or above. | ||||||
|  | 
 | ||||||
|  | As the project evolves, dependencies on libraries external to the project codebase will be minimized or removed outright to meet the goals of the project as closely as possible. | ||||||
|  | 
 | ||||||
|  | ### Building | ||||||
|  | 
 | ||||||
|  | Once all third-party tools and system-wide dependencies are satisfied, navigate to the root project folder and run the `./build.py` Python build script. | ||||||
|  | 
 | ||||||
|  | By default, the build script will build the engine runtime, required for running games built with Ona, in release-debug mode. | ||||||
|  | 
 | ||||||
|  | ### Project Structure | ||||||
|  | 
 | ||||||
|  | As Ona uses C++20, it is able to make use of the new modules language feature. While this brings with it a number of drawbacks, like a lack of widescale vendor adoption, it also provides some key benefits. | ||||||
|  | 
 | ||||||
|  | #### No Headers | ||||||
|  | 
 | ||||||
|  | All first-party code in the project is free of headers. Code is grouped in a module and package dichotomy, where each `.cpp` file in the root source directory represents the common package of a module grouping. | ||||||
|  | 
 | ||||||
|  | Subdirectories then build further abstractions atop these common module files. For example, the `core.cpp` source file contains many common memory manipulation and floating point mathematics utilities, which are made use of in `core/image.cpp` for modifying CPU-bound pixel data. | ||||||
|  | 
 | ||||||
|  | #### All Code is Equal | ||||||
|  | 
 | ||||||
|  | Following on from no headers necessary, declarations, template metaprogramming, and definitions all go into the same place now. A typical Ona source file mixes all of these, traditionally separate, pieces of logic together in shared `.cpp` files. | ||||||
|  | 
 | ||||||
|  | Alongside the surface-level benefit of writing having fewer lines of code, this also means there is less work necessary to maintain the codebase at large and a smaller space to create duplication errors in. | ||||||
							
								
								
									
										327
									
								
								source/app.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								source/app.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,327 @@ | |||||||
|  | module; | ||||||
|  | 
 | ||||||
|  | #include <SDL2/SDL.h> | ||||||
|  | 
 | ||||||
|  | export module app; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | import coral.files; | ||||||
|  | import coral.image; | ||||||
|  | import coral.math; | ||||||
|  | 
 | ||||||
|  | import oar; | ||||||
|  | 
 | ||||||
|  | struct file_reader : public coral::file_reader { | ||||||
|  | 	enum class [[nodiscard]] close_result { | ||||||
|  | 		ok, | ||||||
|  | 		io_unavailable, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	enum class [[nodiscard]] open_result { | ||||||
|  | 		ok, | ||||||
|  | 		io_unavailable, | ||||||
|  | 		access_denied, | ||||||
|  | 		not_found, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	file_reader(coral::fs * fs) : rw_ops{nullptr} { | ||||||
|  | 		this->fs = fs; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	close_result close() { | ||||||
|  | 		if (::SDL_RWclose(this->rw_ops) != 0) return close_result::io_unavailable; | ||||||
|  | 
 | ||||||
|  | 		this->rw_ops = nullptr; | ||||||
|  | 
 | ||||||
|  | 		return close_result::ok; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bool is_open() const { | ||||||
|  | 		return this->rw_ops != nullptr; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	open_result open(coral::path const & file_path) { | ||||||
|  | 		if (this->is_open()) switch (this->close()) { | ||||||
|  | 			case close_result::ok: break; | ||||||
|  | 			case close_result::io_unavailable: return open_result::io_unavailable; | ||||||
|  | 			default: coral::unreachable(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		this->rw_ops = ::SDL_RWFromFile(reinterpret_cast<char const *>(this->path_buffer), "r"); | ||||||
|  | 
 | ||||||
|  | 		if (this->rw_ops == nullptr) return open_result::not_found; | ||||||
|  | 
 | ||||||
|  | 		return open_result::ok; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & buffer) override { | ||||||
|  | 		if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		coral::usize const bytes_read{::SDL_RWread(this->rw_ops, buffer.pointer, sizeof(uint8_t), buffer.length)}; | ||||||
|  | 
 | ||||||
|  | 		if ((bytes_read == 0) && (::SDL_GetError() != nullptr)) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		return bytes_read; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override { | ||||||
|  | 		if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		// TODO: Fix cast.
 | ||||||
|  | 		coral::i64 const byte_position{ | ||||||
|  | 			::SDL_RWseek(this->rw_ops, static_cast<coral::i64>(offset), RW_SEEK_SET)}; | ||||||
|  | 
 | ||||||
|  | 		if (byte_position == -1) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		return static_cast<coral::u64>(byte_position); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	coral::expected<coral::u64, coral::io_error> tell() override { | ||||||
|  | 		if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		coral::i64 const byte_position{::SDL_RWseek(this->rw_ops, 0, RW_SEEK_SET)}; | ||||||
|  | 
 | ||||||
|  | 		if (byte_position == -1) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 		return static_cast<coral::u64>(byte_position); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private: | ||||||
|  | 	static constexpr coral::usize path_max{4096}; | ||||||
|  | 
 | ||||||
|  | 	coral::u8 path_buffer[path_max]; | ||||||
|  | 
 | ||||||
|  | 	coral::fs * fs; | ||||||
|  | 
 | ||||||
|  | 	::SDL_RWops * rw_ops; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct base_directory : public coral::fs { | ||||||
|  | 	base_directory() : directory_path{} { | ||||||
|  | 		char * const path{::SDL_GetBasePath()}; | ||||||
|  | 
 | ||||||
|  | 		if (path == nullptr) return; | ||||||
|  | 
 | ||||||
|  | 		coral::usize path_length{0}; | ||||||
|  | 
 | ||||||
|  | 		while (path[path_length] != 0) path_length += 1; | ||||||
|  | 
 | ||||||
|  | 		if (path_length == 0) { | ||||||
|  | 			::SDL_free(path); | ||||||
|  | 
 | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		this->directory_path = {path, path_length}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	~base_directory() override { | ||||||
|  | 		::SDL_free(this->directory_path.begin()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	void read_file(coral::path const & file_path, coral::callable<void(coral::file_reader &)> const & then) override { | ||||||
|  | 		if (this->directory_path.length == 0) return; | ||||||
|  | 
 | ||||||
|  | 		file_reader reader{this}; | ||||||
|  | 
 | ||||||
|  | 		if (reader.open(file_path) != file_reader::open_result::ok) return; | ||||||
|  | 
 | ||||||
|  | 		then(reader); | ||||||
|  | 
 | ||||||
|  | 		if (reader.close() != file_reader::close_result::ok) return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	void write_file(coral::path const & file_path, coral::callable<void(coral::file_writer &)> const & then) override { | ||||||
|  | 		// Directory is read-only.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	protected: | ||||||
|  | 	coral::slice<char> directory_path; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct user_directory : public coral::fs { | ||||||
|  | 	user_directory(coral::path const & title) : directory_path{} { | ||||||
|  | 		char * const path{::SDL_GetPrefPath("ona", title.begin())}; | ||||||
|  | 
 | ||||||
|  | 		if (path == nullptr) return; | ||||||
|  | 
 | ||||||
|  | 		coral::usize path_length{0}; | ||||||
|  | 
 | ||||||
|  | 		while (path[path_length] != 0) path_length += 1; | ||||||
|  | 
 | ||||||
|  | 		if (path_length == 0) { | ||||||
|  | 			::SDL_free(path); | ||||||
|  | 
 | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		this->directory_path = {path, path_length}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	~user_directory() override { | ||||||
|  | 		::SDL_free(this->directory_path.begin()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	void read_file(coral::path const & file_path, coral::callable<void(coral::file_reader &)> const & then) override { | ||||||
|  | 		if (this->directory_path.length == 0) return; | ||||||
|  | 
 | ||||||
|  | 		file_reader reader{this}; | ||||||
|  | 
 | ||||||
|  | 		if (reader.open(file_path) != file_reader::open_result::ok) return; | ||||||
|  | 
 | ||||||
|  | 		then(reader); | ||||||
|  | 
 | ||||||
|  | 		if (reader.close() != file_reader::close_result::ok) return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	void write_file(coral::path const & file_path, coral::callable<void(coral::file_writer &)> const & then) override { | ||||||
|  | 		// Directory is read-only.
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	protected: | ||||||
|  | 	coral::slice<char> directory_path; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export namespace app { | ||||||
|  | 	enum class log_level { | ||||||
|  | 		notice, | ||||||
|  | 		warning, | ||||||
|  | 		error, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct system { | ||||||
|  | 		system(coral::path const & title) : res{&base, "base_directory.oar"}, user{title} {} | ||||||
|  | 
 | ||||||
|  | 		coral::fs & base_fs() { | ||||||
|  | 			return this->base; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		bool poll() { | ||||||
|  | 			while (::SDL_PollEvent(&this->sdl_event) != 0) { | ||||||
|  | 				switch (this->sdl_event.type) { | ||||||
|  | 					case SDL_QUIT: return false; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::fs & res_fs() { | ||||||
|  | 			return this->res; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void log(app::log_level level, coral::slice<char const> const & message) { | ||||||
|  | 			coral::i32 const length{static_cast<coral::i32>( | ||||||
|  | 				coral::min(message.length, static_cast<size_t>(coral::i32_max)))}; | ||||||
|  | 
 | ||||||
|  | 			::SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, | ||||||
|  | 				SDL_LOG_PRIORITY_INFO, "%.*s", length, message.pointer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::allocator & thread_safe_allocator() { | ||||||
|  | 			return this->allocator; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::fs & user_fs() { | ||||||
|  | 			return this->user; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		::SDL_Event sdl_event; | ||||||
|  | 
 | ||||||
|  | 		struct : public coral::allocator { | ||||||
|  | 			coral::u8 * reallocate(coral::u8 * maybe_allocation, coral::usize requested_size) override { | ||||||
|  | 				return reinterpret_cast<coral::u8 *>(::SDL_malloc(requested_size)); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			void deallocate(void * allocation) override { | ||||||
|  | 				::SDL_free(allocation); | ||||||
|  | 			} | ||||||
|  | 		} allocator; | ||||||
|  | 
 | ||||||
|  | 		base_directory base; | ||||||
|  | 
 | ||||||
|  | 		user_directory user; | ||||||
|  | 
 | ||||||
|  | 		oar::archive res; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct graphics { | ||||||
|  | 		enum class [[nodiscard]] show_result { | ||||||
|  | 			ok, | ||||||
|  | 			out_of_memory, | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		struct canvas { | ||||||
|  | 			coral::color background_color; | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		graphics(coral::path const & title) { | ||||||
|  | 			this->retitle(title); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void present() { | ||||||
|  | 			if (this->sdl_renderer != nullptr) { | ||||||
|  | 				::SDL_RenderPresent(this->sdl_renderer); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void render(canvas & source_canvas) { | ||||||
|  | 			if (this->sdl_renderer != nullptr) { | ||||||
|  | 				SDL_SetRenderDrawColor(this->sdl_renderer, source_canvas.background_color.to_r8(), | ||||||
|  | 					source_canvas.background_color.to_g8(), source_canvas.background_color.to_b8(), | ||||||
|  | 						source_canvas.background_color.to_a8()); | ||||||
|  | 
 | ||||||
|  | 				SDL_RenderClear(this->sdl_renderer); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void retitle(coral::path const & title) { | ||||||
|  | 			this->title = title; | ||||||
|  | 
 | ||||||
|  | 			if (this->sdl_window != nullptr) | ||||||
|  | 				::SDL_SetWindowTitle(this->sdl_window, this->title.begin()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		show_result show(coral::u16 physical_width, coral::u16 physical_height) { | ||||||
|  | 			if (this->sdl_window == nullptr) { | ||||||
|  | 				constexpr int sdl_windowpos = SDL_WINDOWPOS_UNDEFINED; | ||||||
|  | 				constexpr coral::u32 sdl_windowflags = 0; | ||||||
|  | 
 | ||||||
|  | 				this->sdl_window = ::SDL_CreateWindow(this->title.begin(), sdl_windowpos, | ||||||
|  | 					sdl_windowpos, static_cast<int>(physical_width), static_cast<int>(physical_height), | ||||||
|  | 						sdl_windowflags); | ||||||
|  | 
 | ||||||
|  | 				if (this->sdl_window == nullptr) return show_result::out_of_memory; | ||||||
|  | 			} else { | ||||||
|  | 				::SDL_ShowWindow(this->sdl_window); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (this->sdl_renderer == nullptr) { | ||||||
|  | 				constexpr coral::u32 sdl_rendererflags = 0; | ||||||
|  | 
 | ||||||
|  | 				this->sdl_renderer = ::SDL_CreateRenderer(this->sdl_window, -1, sdl_rendererflags); | ||||||
|  | 
 | ||||||
|  | 				if (this->sdl_renderer == nullptr) return show_result::out_of_memory; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return show_result::ok; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		coral::path title; | ||||||
|  | 
 | ||||||
|  | 		::SDL_Window * sdl_window = nullptr; | ||||||
|  | 
 | ||||||
|  | 		::SDL_Renderer * sdl_renderer = nullptr; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	using graphical_runnable = coral::callable<int(system &, graphics &)>; | ||||||
|  | 
 | ||||||
|  | 	int display(coral::path const & title, graphical_runnable const & run) { | ||||||
|  | 		system app_system{title}; | ||||||
|  | 		graphics app_graphics{title}; | ||||||
|  | 
 | ||||||
|  | 		return run(app_system, app_graphics); | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										611
									
								
								source/coral.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										611
									
								
								source/coral.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,611 @@ | |||||||
|  | module; | ||||||
|  | 
 | ||||||
|  | #include <cstdint> | ||||||
|  | #include <cstddef> | ||||||
|  | 
 | ||||||
|  | export module coral; | ||||||
|  | 
 | ||||||
|  | // Runtime utilities.
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Triggers safety-checked behavior in debug mode. | ||||||
|  | 	 * | ||||||
|  | 	 * In release mode, the compiler can use this function as a marker to optimize out safety- | ||||||
|  | 	 * checked logic branches that should never be executed. | ||||||
|  | 	 */ | ||||||
|  | 	[[noreturn]] void unreachable() { | ||||||
|  | 		__builtin_unreachable(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Concrete and interface types.
 | ||||||
|  | export namespace coral { | ||||||
|  | 	using usize = size_t; | ||||||
|  | 
 | ||||||
|  | 	using size = __ssize_t; | ||||||
|  | 
 | ||||||
|  | 	using u8 = uint8_t; | ||||||
|  | 
 | ||||||
|  | 	usize const u8_max = 0xff; | ||||||
|  | 
 | ||||||
|  | 	using i8 = uint8_t; | ||||||
|  | 
 | ||||||
|  | 	using u16 = uint16_t; | ||||||
|  | 
 | ||||||
|  | 	usize const u16_max = 0xffff; | ||||||
|  | 
 | ||||||
|  | 	using i16 = uint16_t; | ||||||
|  | 
 | ||||||
|  | 	using u32 = uint32_t; | ||||||
|  | 
 | ||||||
|  | 	using i32 = int32_t; | ||||||
|  | 
 | ||||||
|  | 	usize const i32_max = 0xffffffff; | ||||||
|  | 
 | ||||||
|  | 	using u64 = uint64_t; | ||||||
|  | 
 | ||||||
|  | 	using i64 = int64_t; | ||||||
|  | 
 | ||||||
|  | 	using f32 = float; | ||||||
|  | 
 | ||||||
|  | 	using f64 = double; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Base type for runtime-pluggable memory allocation strategies used by the core library. | ||||||
|  | 	 */ | ||||||
|  | 	struct allocator { | ||||||
|  | 		virtual ~allocator() {}; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * If `allocation` is `nullptr`, the allocator will attempt to allocate a new memory block | ||||||
|  | 		 * of `requested_size` bytes. Otherwise, the allocator will attempt to reallocate | ||||||
|  | 		 * `allocation` to be `request_size` bytes in size. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned address will point to a dynamically allocated buffer of `requested_size` if | ||||||
|  | 		 * the operation was successful, otherwise `nullptr`. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to | ||||||
|  | 		 * program exit. This may be achieved through either [deallocate] or implementation- | ||||||
|  | 		 * specific allocator functionality. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: Attempting to pass a non-`nullptr` `allocation` address not allocated by the | ||||||
|  | 		 * allocator *will* result in erroneous implementation-behavior. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: After invocation, `allocation` should be considered an invalid memory address. | ||||||
|  | 		 */ | ||||||
|  | 		[[nodiscard]] virtual u8 * reallocate(u8 * allocation, usize requested_size) = 0; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * If `allocation` points to a non-`nullptr` address, the allocator will deallocate it. | ||||||
|  | 		 * Otherwise, the function has no side-effects. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* that attempting to pass a non-`nullptr` `allocation` address not allocated by the | ||||||
|  | 		 * allocator *will* result in erroneous implementation-behavior. | ||||||
|  | 		 */ | ||||||
|  | 		virtual void deallocate(void * allocation) = 0; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Length-signed pointer type that describes how many elements of `type` it references, | ||||||
|  | 	 * providing a type-safe wrapper for passing arrays and zero-terminated strings to functions. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename type> struct slice { | ||||||
|  | 		/**
 | ||||||
|  | 		 * Number of `type` elements referenced. | ||||||
|  | 		 */ | ||||||
|  | 		usize length; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Base element address referenced. | ||||||
|  | 		 */ | ||||||
|  | 		type * pointer; | ||||||
|  | 
 | ||||||
|  | 		constexpr slice() { | ||||||
|  | 			this->length = 0; | ||||||
|  | 			this->pointer = nullptr; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		constexpr slice(char const *&& zstring) { | ||||||
|  | 			this->pointer = zstring; | ||||||
|  | 			this->length = 0; | ||||||
|  | 
 | ||||||
|  | 			while (zstring[length] != 0) this->length += 1; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		constexpr slice(type * slice_pointer, usize slice_length) { | ||||||
|  | 			this->pointer = slice_pointer; | ||||||
|  | 			this->length = slice_length; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		constexpr slice(type * slice_begin, type * slice_end) { | ||||||
|  | 			this->pointer = slice_begin; | ||||||
|  | 			this->length = static_cast<usize>(slice_end - slice_begin); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		template<usize array_size> constexpr slice(type(&array)[array_size]) { | ||||||
|  | 			this->pointer = array; | ||||||
|  | 			this->length = array_size; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Reinterprets the data referenced as a series of bytes. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned view is constant to protect against inadvertant memory corruption. | ||||||
|  | 		 */ | ||||||
|  | 		slice<u8 const> as_bytes() const { | ||||||
|  | 			return {reinterpret_cast<u8 const *>(this->pointer), this->length * sizeof(type)}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Reinterprets the data referenced as a series of chars. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned view is constant to protect against inadvertant memory corruption. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* the returned value has no guarantees about the validity of any specific character | ||||||
|  | 		 * encoding set. | ||||||
|  | 		 */ | ||||||
|  | 		slice<char const> as_chars() const { | ||||||
|  | 			return {reinterpret_cast<char const *>(this->pointer), this->length * sizeof(type)}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the base pointer of the slice. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr type * begin() const { | ||||||
|  | 			return this->pointer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the tail pointer of the slice. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr type * end() const { | ||||||
|  | 			return this->pointer + this->length; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a new slice with the base-pointer offset by `index` elements and a length of | ||||||
|  | 		 * `range` elements from `index`. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* that attempting to slice with an `index` or `range` outside of the existing slice | ||||||
|  | 		 * bounds will result in safety-checked behavior. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr slice sliced(usize index, usize range) const { | ||||||
|  | 			if ((this->length <= index) || ((range + index) > this->length)) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return {this->pointer + index, range - index}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		operator slice<type const>() const { | ||||||
|  | 			return (*reinterpret_cast<slice<type const> const *>(this)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		constexpr type & operator[](usize index) const { | ||||||
|  | 			if (this->length <= index) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return this->pointer[index]; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Math functions.
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns the maximum value between `a` and `b`. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename scalar> constexpr scalar max(scalar const & a, scalar const & b) { | ||||||
|  | 		return (a > b) ? a : b; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns the minimum value between `a` and `b`. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename scalar> constexpr scalar min(scalar const & a, scalar const & b) { | ||||||
|  | 		return (a < b) ? a : b; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns `value` clamped between the range of `min_value` and `max_value` (inclusive). | ||||||
|  | 	 */ | ||||||
|  | 	template<typename scalar> constexpr scalar clamp(scalar const & value, scalar const & min_value, scalar const & max_value) { | ||||||
|  | 		return max(min_value, min(max_value, value)); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns `value` rounded to the nearest whole number. | ||||||
|  | 	 */ | ||||||
|  | 	f32 round32(f32 value) { | ||||||
|  | 		return __builtin_roundf(value); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Allocates and initializes a type of `requested_size` in `buffer`, returning its base pointer. As | ||||||
|  |  * a result of accepting a pre-allocated buffer, invocation does not allocate any dynamic memory. | ||||||
|  |  * | ||||||
|  |  * *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked | ||||||
|  |  * behavior. | ||||||
|  |  */ | ||||||
|  | export void * operator new(coral::usize requested_size, coral::slice<coral::u8> const & buffer) { | ||||||
|  | 	if (buffer.length < requested_size) coral::unreachable(); | ||||||
|  | 
 | ||||||
|  | 	return buffer.pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Allocates and initializes a series of types at `requested_size` in `buffer`, returning the base | ||||||
|  |  * pointer. As a result of accepting a pre-allocated buffer, invocation does not allocate any | ||||||
|  |  * dynamic memory. | ||||||
|  |  * | ||||||
|  |  * *Note*: passing an `buffer` smaller than `requested_size` will result in safety-checked | ||||||
|  |  * behavior. | ||||||
|  |  */ | ||||||
|  | export void * operator new[](coral::usize requested_size, coral::slice<coral::u8> const & buffer) { | ||||||
|  | 	if (buffer.length < requested_size) coral::unreachable(); | ||||||
|  | 
 | ||||||
|  | 	return buffer.pointer; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Attempts to allocate and initialize a type of `requested_size` using `allocator`. | ||||||
|  |  * | ||||||
|  |  * *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program | ||||||
|  |  * exit. This may be achieved through either [coral::allocator::deallocate] or implementation- | ||||||
|  |  * specific allocator functionality. | ||||||
|  |  */ | ||||||
|  | export [[nodiscard]] void * operator new(coral::usize requested_size, coral::allocator & allocator) { | ||||||
|  | 	return allocator.reallocate(nullptr, requested_size); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /**
 | ||||||
|  |  * Attempts to allocate and initialize a series of types of `requested_size` using `allocator`. | ||||||
|  |  * | ||||||
|  |  * *Note*: If the returned address is a non-`nullptr`, it should be deallocated prior to program | ||||||
|  |  * exit. This may be achieved through either [coral::allocator::deallocate] or implementation- | ||||||
|  |  * specific allocator functionality. | ||||||
|  |  */ | ||||||
|  | export [[nodiscard]] void * operator new[](coral::usize requested_size, coral::allocator & allocator) { | ||||||
|  | 	return allocator.reallocate(nullptr, requested_size); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Wrapper types.
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Monadic container for a single-`element` value or nothing. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename element> struct [[nodiscard]] optional { | ||||||
|  | 		optional() : buffer{0} {} | ||||||
|  | 
 | ||||||
|  | 		optional(element const & value) : buffer{0} { | ||||||
|  | 			(*reinterpret_cast<element *>(this->buffer)) = value; | ||||||
|  | 			this->buffer[sizeof(element)] = 1; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		optional(optional const & that) : buffer{0} { | ||||||
|  | 			if (that.has_value()) { | ||||||
|  | 				(*reinterpret_cast<element *>(this->buffer)) = that.value(); | ||||||
|  | 				this->buffer[sizeof(element)] = 1; | ||||||
|  | 			} else { | ||||||
|  | 				this->buffer[sizeof(element)] = 0; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns `true` if the optional contains a value, otherwise `false`. | ||||||
|  | 		 */ | ||||||
|  | 		bool has_value() const { | ||||||
|  | 			return this->buffer[sizeof(element)] == 1; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the contained value or `fallback` if the optional is empty. | ||||||
|  | 		 */ | ||||||
|  | 		element const & value_or(element const & fallback) const { | ||||||
|  | 			return this->has_value() ? *reinterpret_cast<element const *>(this->buffer) : fallback; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a reference to the contained value. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the value of an empty optional will trigger safety-checked | ||||||
|  | 		 * behavior. | ||||||
|  | 		 */ | ||||||
|  | 		element & value() { | ||||||
|  | 			if (!this->has_value()) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return *reinterpret_cast<element *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the contained value. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the value of an empty optional will trigger safety-checked | ||||||
|  | 		 * behavior. | ||||||
|  | 		 */ | ||||||
|  | 		element const & value() const { | ||||||
|  | 			return *reinterpret_cast<element const *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		u8 buffer[sizeof(element) + 1]; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Monadic container for a descriminating union of either `value_element` or `error_element`. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename value_element, typename error_element> struct [[nodiscard]] expected { | ||||||
|  | 		expected(value_element const & value) : buffer{0} { | ||||||
|  | 			(*reinterpret_cast<value_element *>(this->buffer)) = value; | ||||||
|  | 			this->buffer[buffer_size] = 1; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		expected(error_element const & error) : buffer{0} { | ||||||
|  | 			(*reinterpret_cast<error_element *>(this->buffer)) = error; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns `true` if the optional contains a value, otherwise `false` if it holds an error. | ||||||
|  | 		 */ | ||||||
|  | 		bool is_ok() const { | ||||||
|  | 			return this->buffer[buffer_size]; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a reference to the contained value. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the value of an erroneous expected will trigger safety- | ||||||
|  | 		 * checked behavior. | ||||||
|  | 		 */ | ||||||
|  | 		value_element & value() { | ||||||
|  | 			if (!this->is_ok()) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return *reinterpret_cast<value_element *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the contained value. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the value of an erroneous expected will trigger safety- | ||||||
|  | 		 * checked behavior. | ||||||
|  | 		 */ | ||||||
|  | 		value_element const & value() const { | ||||||
|  | 			if (!this->is_ok()) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return *reinterpret_cast<value_element const *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a reference to the contained error. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the error of a non-erroneous expected will trigger safety- | ||||||
|  | 		 * checked behavior. | ||||||
|  | 		 */ | ||||||
|  | 		error_element & error() { | ||||||
|  | 			if (this->is_ok()) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return *reinterpret_cast<error_element *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the contained error. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: attempting to access the error of a non-erroneous expected will trigger safety- | ||||||
|  | 		 * checked behavior. | ||||||
|  | 		 */ | ||||||
|  | 		error_element const & error() const { | ||||||
|  | 			if (this->is_ok()) unreachable(); | ||||||
|  | 
 | ||||||
|  | 			return *reinterpret_cast<error_element const *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		static constexpr usize buffer_size = max(sizeof(value_element), sizeof(error_element)); | ||||||
|  | 
 | ||||||
|  | 		u8 buffer[buffer_size + 1]; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	template<typename> struct callable; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Type-erasing wrapper for functor types that have a call operator with a return value | ||||||
|  | 	 * matching `return_value` and arguments matching `argument_values`. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename return_value, typename... argument_values> struct callable<return_value(argument_values...)> { | ||||||
|  | 		using function = return_value(*)(argument_values...); | ||||||
|  | 
 | ||||||
|  | 		callable(function callable_function) { | ||||||
|  | 			this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value { | ||||||
|  | 				return (*reinterpret_cast<function const *>(userdata))(arguments...); | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			new (this->capture) function{callable_function}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		callable(callable const &) = delete; | ||||||
|  | 
 | ||||||
|  | 		template<typename functor> callable(functor const & callable_functor) { | ||||||
|  | 			this->dispatcher = [](u8 const * userdata, argument_values... arguments) -> return_value { | ||||||
|  | 				return (*reinterpret_cast<functor const*>(userdata))(arguments...); | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			new (this->capture) functor{callable_functor}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return_value operator()(argument_values const &... arguments) const { | ||||||
|  | 			return this->dispatcher(this->capture, arguments...); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		static constexpr usize capture_size = 24; | ||||||
|  | 
 | ||||||
|  | 		return_value(* dispatcher)(u8 const * userdata, argument_values... arguments); | ||||||
|  | 
 | ||||||
|  | 		u8 capture[capture_size]; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Errors that may occur while executing an opaque I/O operation via the `readable` and | ||||||
|  | 	 * `writable` type aliases. | ||||||
|  | 	 */ | ||||||
|  | 	enum class io_error { | ||||||
|  | 		unavailable, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct reader { | ||||||
|  | 		virtual expected<usize, io_error> read(slice<u8> const & buffer) = 0; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct writer { | ||||||
|  | 		virtual expected<usize, io_error> write(slice<u8 const> const & buffer) = 0; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Input/output operations.
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Compares `a` and `b`, returning the difference between them or `0` if they are identical. | ||||||
|  | 	 */ | ||||||
|  | 	constexpr size compare(slice<u8 const> const & a, slice<u8 const> const & b) { | ||||||
|  | 		usize const range = min(a.length, b.length); | ||||||
|  | 
 | ||||||
|  | 		for (usize index = 0; index < range; index += 1) { | ||||||
|  | 			size const difference = static_cast<size>(a[index]) - static_cast<size>(b[index]); | ||||||
|  | 
 | ||||||
|  | 			if (difference != 0) return difference; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return static_cast<size>(a.length) - static_cast<size>(b.length); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Copies the contents of `origin` into `target`. | ||||||
|  | 	 * | ||||||
|  | 	 * *Note*: safety-checked behavior is triggered if `target` is smaller than `origin`. | ||||||
|  | 	 */ | ||||||
|  | 	void copy(slice<u8> const & target, slice<u8 const> const & origin) { | ||||||
|  | 		if (target.length < origin.length) unreachable(); | ||||||
|  | 
 | ||||||
|  | 		for (usize i = 0; i < origin.length; i += 1) target[i] = origin[i]; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Zeroes the contents of `target`. | ||||||
|  | 	 */ | ||||||
|  | 	void zero(slice<u8> const & target) { | ||||||
|  | 		for (usize i = 0; i < target.length; i += 1) target[i] = 0; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Tests the equality of `a` against `b`, returning `true` if they contain identical bytes, | ||||||
|  | 	 * otherwise `false`. | ||||||
|  | 	 */ | ||||||
|  | 	constexpr bool equals(slice<u8 const> const & a, slice<u8 const> const & b) { | ||||||
|  | 		if (a.length != b.length) return false; | ||||||
|  | 
 | ||||||
|  | 		for (size_t i = 0; i < a.length; i += 1) if (a[i] != b[i]) return false; | ||||||
|  | 
 | ||||||
|  | 		return true; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns a hash code generated from the values in `bytes`. | ||||||
|  | 	 * | ||||||
|  | 	 * *Note:* the returned hash code is not guaranteed to be unique. | ||||||
|  | 	 */ | ||||||
|  | 	constexpr usize hash(slice<u8 const> const & bytes) { | ||||||
|  | 		usize hash_code = 5381; | ||||||
|  | 
 | ||||||
|  | 		for (u8 const byte : bytes) hash_code = ((hash_code << 5) + hash_code) + byte; | ||||||
|  | 
 | ||||||
|  | 		return hash_code; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Swaps the values of `element` in `a` and `b` around using copy semantics. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename element> constexpr void swap(element & a, element & b) { | ||||||
|  | 		element const temp = a; | ||||||
|  | 		a = b; | ||||||
|  | 		b = temp; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Streams the data from `input` to `output`, using `buffer` as temporary transfer space. | ||||||
|  | 	 * | ||||||
|  | 	 * The returned [expected] can be used to introspect if `input` or `output` encountered any | ||||||
|  | 	 * issues during streaming, otherwise it will contain the number of bytes streamed. | ||||||
|  | 	 * | ||||||
|  | 	 * *Note*: if `buffer` has a length of `0`, no data will be streamed as there is nowhere to | ||||||
|  | 	 * temporarily place data during streaming. | ||||||
|  | 	 */ | ||||||
|  | 	expected<usize, io_error> stream(writer & output, reader & input, slice<u8> const & buffer) { | ||||||
|  | 		usize total_bytes_written = 0; | ||||||
|  | 		expected bytes_read = input.read(buffer); | ||||||
|  | 
 | ||||||
|  | 		if (!bytes_read.is_ok()) return bytes_read.error(); | ||||||
|  | 
 | ||||||
|  | 		usize read = bytes_read.value(); | ||||||
|  | 
 | ||||||
|  | 		while (read != 0) { | ||||||
|  | 			expected const bytes_written = output.write(buffer.sliced(0, read)); | ||||||
|  | 
 | ||||||
|  | 			if (!bytes_written.is_ok()) return bytes_read.error(); | ||||||
|  | 
 | ||||||
|  | 			total_bytes_written += bytes_written.value(); | ||||||
|  | 			bytes_read = input.read(buffer); | ||||||
|  | 
 | ||||||
|  | 			if (!bytes_read.is_ok()) return bytes_read.error(); | ||||||
|  | 
 | ||||||
|  | 			read = bytes_read.value(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return total_bytes_written; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Attempts to format and print `value` as an unsigned integer out to `output`. | ||||||
|  | 	 * | ||||||
|  | 	 * The returned [expected] can be used to introspect if `output` encountered any issues during | ||||||
|  | 	 * printing, otherwise it will contain the number of characters used to print `value` as text. | ||||||
|  | 	 */ | ||||||
|  | 	expected<usize, io_error> print_unsigned(writer & output, u64 value) { | ||||||
|  | 		if (value == 0) return output.write(slice{"0"}.as_bytes()); | ||||||
|  | 
 | ||||||
|  | 		u8 buffer[20]{0}; | ||||||
|  | 		usize buffer_count{0}; | ||||||
|  | 
 | ||||||
|  | 		while (value != 0) { | ||||||
|  | 			constexpr usize radix{10}; | ||||||
|  | 
 | ||||||
|  | 			buffer[buffer_count] = static_cast<u8>((value % radix) + '0'); | ||||||
|  | 			value = (value / radix); | ||||||
|  | 			buffer_count += 1; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		usize const half_buffer_count{buffer_count / 2}; | ||||||
|  | 
 | ||||||
|  | 		for (usize i = 0; i < half_buffer_count; i += 1) | ||||||
|  | 			swap(buffer[i], buffer[buffer_count - i - 1]); | ||||||
|  | 
 | ||||||
|  | 		return output.write(slice{buffer, buffer_count}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Returns a reference to a shared [allocator] which will always return `nullptr` on calls to | ||||||
|  | 	 * [allocator::reallocate]. | ||||||
|  | 	 */ | ||||||
|  | 	allocator & null_allocator() { | ||||||
|  | 		static struct : public allocator { | ||||||
|  | 			u8 * reallocate(u8 * maybe_allocation, usize requested_size) override { | ||||||
|  | 				if (maybe_allocation != nullptr) unreachable(); | ||||||
|  | 
 | ||||||
|  | 				return nullptr; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			void deallocate(void * allocation) override { | ||||||
|  | 				if (allocation != nullptr) unreachable(); | ||||||
|  | 			} | ||||||
|  | 		} a; | ||||||
|  | 
 | ||||||
|  | 		return a; | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										139
									
								
								source/coral/files.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								source/coral/files.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,139 @@ | |||||||
|  | export module coral.files; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | 
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Platform-generalized identifier for a resource in a [file_store]. | ||||||
|  | 	 */ | ||||||
|  | 	struct path { | ||||||
|  | 		/**
 | ||||||
|  | 		 * Maximum path length. | ||||||
|  | 		 */ | ||||||
|  | 		static usize const max = u8_max; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Common path component separator. | ||||||
|  | 		 */ | ||||||
|  | 		static char const seperator = '/'; | ||||||
|  | 
 | ||||||
|  | 		constexpr path() : buffer{0} { | ||||||
|  | 			this->buffer[max] = max; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		template<usize text_size> constexpr path(char const(&text)[text_size]) : path{} { | ||||||
|  | 			static_assert(text_size <= max); | ||||||
|  | 
 | ||||||
|  | 			for (usize i = 0; i < text_size; i += 1) this->buffer[i] = text[i]; | ||||||
|  | 
 | ||||||
|  | 			this->buffer[max] = max - text_size; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a weak reference to the [path] as a [slice]. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr slice<char const> as_slice() const { | ||||||
|  | 			return {this->buffer, this->byte_size()}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the base pointer of the path name. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note*: the returned buffer pointer is guaranteed to end with a zero terminator. | ||||||
|  | 		 */ | ||||||
|  | 		char const * begin() const { | ||||||
|  | 			return reinterpret_cast<char const *>(this->buffer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the number of bytes composing the path. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr usize byte_size() const { | ||||||
|  | 			return max - this->buffer[max]; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Compares the path to `that`, returning the difference between the two paths or `0` if | ||||||
|  | 		 * they are identical. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr size compare(path const & that) const { | ||||||
|  | 			return coral::compare(this->as_slice().as_bytes(), that.as_slice().as_bytes()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the tail pointer of the path name. | ||||||
|  | 		 */ | ||||||
|  | 		char const * end() const { | ||||||
|  | 			return this->buffer + this->byte_size(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Tests the path against `that` for equality, returning `true` if they are identical, | ||||||
|  | 		 * otherwise `false`. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr bool equals(path const & that) const { | ||||||
|  | 			return coral::equals(this->as_slice().as_bytes(), that.as_slice().as_bytes()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the path hash code. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note:* the returned hash code is not guaranteed to be unique. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr u64 hash() const { | ||||||
|  | 			return coral::hash(this->as_slice().as_bytes()); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns a new [path] composed of the current path joined with `text`. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note:* should the new path exceed [max] bytes in size, an empty [path] is returned instead. | ||||||
|  | 		 */ | ||||||
|  | 		constexpr path joined(slice<char const> const & text) const { | ||||||
|  | 			if (text.length > this->buffer[max]) return path{}; | ||||||
|  | 
 | ||||||
|  | 			path joined_path = *this; | ||||||
|  | 
 | ||||||
|  | 			for (char const c : text) { | ||||||
|  | 				joined_path.buffer[joined_path.byte_size()] = c; | ||||||
|  | 				joined_path.buffer[max] -= 1; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return joined_path; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		char buffer[max + 1]; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct file_reader : public reader { | ||||||
|  | 		virtual expected<u64, io_error> seek(u64 offset) = 0; | ||||||
|  | 
 | ||||||
|  | 		virtual expected<u64, io_error> tell() = 0; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct file_writer : public writer {}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Platform-generalized file system interface. | ||||||
|  | 	 */ | ||||||
|  | 	struct fs { | ||||||
|  | 		virtual ~fs() {}; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to read the file in the file system located at `file_path` relative, calling | ||||||
|  | 		 * `then` if it was successfully opened for reading. | ||||||
|  | 		 * | ||||||
|  | 		 * Once `then` returns, access to the file is closed automatically. | ||||||
|  | 		 */ | ||||||
|  | 		virtual void read_file(path const & file_path, callable<void(file_reader &)> const & then) = 0; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to write the file in the file system located at `file_path` relative, calling | ||||||
|  | 		 * `then` if it was successfully opened for writing. | ||||||
|  | 		 * | ||||||
|  | 		 * Once `then` returns, access to the file is closed automatically. | ||||||
|  | 		 */ | ||||||
|  | 		virtual void write_file(path const & file_path, callable<void(file_writer &)> const & then) = 0; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								source/coral/image.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								source/coral/image.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | export module coral.image; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | 
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * All-purpose color value for red, green, blue, alpha channel-encoded values. | ||||||
|  | 	 */ | ||||||
|  | 	struct color { | ||||||
|  | 		/**
 | ||||||
|  | 		 * Red channel. | ||||||
|  | 		 */ | ||||||
|  | 		float r; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Green channel. | ||||||
|  | 		 */ | ||||||
|  | 		float g; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Blue channel. | ||||||
|  | 		 */ | ||||||
|  | 		float b; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Alpha channel. | ||||||
|  | 		 */ | ||||||
|  | 		float a; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Red channel represented in an 8-bit unsigned value. | ||||||
|  | 		 */ | ||||||
|  | 		u8 to_r8() const { | ||||||
|  | 			return static_cast<u8>(round32(clamp(this->r, 0.0f, 1.0f) * u8_max)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Green channel represented in an 8-bit unsigned value. | ||||||
|  | 		 */ | ||||||
|  | 		u8 to_g8() const { | ||||||
|  | 			return static_cast<u8>(round32(clamp(this->g, 0.0f, 1.0f) * u8_max)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Blue channel represented in an 8-bit unsigned value. | ||||||
|  | 		 */ | ||||||
|  | 		u8 to_b8() const { | ||||||
|  | 			return static_cast<u8>(round32(clamp(this->b, 0.0f, 1.0f) * u8_max)); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Alpha channel represented in an 8-bit unsigned value. | ||||||
|  | 		 */ | ||||||
|  | 		u8 to_a8() const { | ||||||
|  | 			return static_cast<u8>(round32(clamp(this->a, 0.0f, 1.0f) * u8_max)); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								source/coral/math.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								source/coral/math.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | |||||||
|  | export module coral.math; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | 
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Two-component vector type backed by 32-bit floating point values. | ||||||
|  | 	 */ | ||||||
|  | 	struct vector2 { | ||||||
|  | 		/**
 | ||||||
|  | 		 * "X" axis spatial component. | ||||||
|  | 		 */ | ||||||
|  | 		f32 x; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * "Y" axis spatial component. | ||||||
|  | 		 */ | ||||||
|  | 		f32 y; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Three-component vector type backed by 32-bit floating point values. | ||||||
|  | 	 */ | ||||||
|  | 	struct vector3 { | ||||||
|  | 		/**
 | ||||||
|  | 		 * "X" axis spatial component. | ||||||
|  | 		 */ | ||||||
|  | 		f32 x; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * "Y" axis spatial component. | ||||||
|  | 		 */ | ||||||
|  | 		f32 y; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * "Z" axis spatial component. | ||||||
|  | 		 */ | ||||||
|  | 		f32 z; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										219
									
								
								source/coral/sequence.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								source/coral/sequence.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | |||||||
|  | export module coral.sequence; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | 
 | ||||||
|  | export namespace coral { | ||||||
|  | 	/**
 | ||||||
|  | 	 * Result codes used by [sequence]-derived types when they are appended to in any way. | ||||||
|  | 	 * | ||||||
|  | 	 * [append_result::ok] indicates that an append operation was successful. | ||||||
|  | 	 * | ||||||
|  | 	 * [append_result::out_of_memory] alerts that the memory required to perform the append | ||||||
|  | 	 * operation failed. | ||||||
|  | 	 */ | ||||||
|  | 	enum class [[nodiscard]] append_result { | ||||||
|  | 		ok, | ||||||
|  | 		out_of_memory, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Base type for all sequence-like types. | ||||||
|  | 	 * | ||||||
|  | 	 * Sequences are any data structure which owns a linear, non-unique set of elements which may | ||||||
|  | 	 * be queried and/or mutated. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename element> struct sequence { | ||||||
|  | 		virtual ~sequence() {}; | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to append `source_elements` to the sequence. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned [append_result] indicates whether the operation was successful or not. | ||||||
|  | 		 * | ||||||
|  | 		 * If the returned [append_result] is anything but [append_result::ok], the [sequence] will | ||||||
|  | 		 * be left in an implementation-defined state. | ||||||
|  | 		 */ | ||||||
|  | 		virtual append_result append(slice<element const> const & source_elements) = 0; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Last-in-first-out linear sequence of `element` values. | ||||||
|  | 	 * | ||||||
|  | 	 * [stack] types will default to using an inline array of `init_capacity` at first. After all | ||||||
|  | 	 * local storage has been exhausted, the [stack] will switch to a dynamic buffer. Because of | ||||||
|  | 	 * this, it is recommended to use larger `init_capacity` values for data which has a known or | ||||||
|  | 	 * approximate upper bound at compile-time. Otherwise, the `init_capacity` value may be left at | ||||||
|  | 	 * its default. | ||||||
|  | 	 */ | ||||||
|  | 	template<typename element, usize init_capacity = 1> struct stack : public sequence<element> { | ||||||
|  | 		stack(allocator * dynamic_allocator) : local_buffer{0} { | ||||||
|  | 			this->dynamic_allocator = dynamic_allocator; | ||||||
|  | 			this->filled = 0; | ||||||
|  | 			this->elements = this->local_buffer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		~stack() override { | ||||||
|  | 			if (this->is_dynamic()) this->dynamic_allocator->deallocate(this->elements.pointer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to append `source_elements` to the top of the stack. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned [append_result] indicates whether the operation was successful or not. | ||||||
|  | 		 * | ||||||
|  | 		 * If the returned [append_result] is anything but [append_result::ok], the stack will be | ||||||
|  | 		 * be left in an empty but valid state. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* that [push] is recommended when appending singular values. | ||||||
|  | 		 */ | ||||||
|  | 		append_result append(slice<element const> const & source_elements) override { | ||||||
|  | 			usize const updated_fill = this->filled + source_elements.length; | ||||||
|  | 
 | ||||||
|  | 			if (updated_fill >= this->elements.length) { | ||||||
|  | 				append_result const result = this->reserve(updated_fill); | ||||||
|  | 
 | ||||||
|  | 				if (result != append_result::ok) return result; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			for (usize i = 0; i < source_elements.length; i += 1) | ||||||
|  | 				this->elements[this->filled + i] = source_elements[i]; | ||||||
|  | 
 | ||||||
|  | 			this->filled = updated_fill; | ||||||
|  | 
 | ||||||
|  | 			return append_result::ok; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the beginning of the elements as a mutable pointer. | ||||||
|  | 		 */ | ||||||
|  | 		element * begin() { | ||||||
|  | 			return this->elements.pointer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the beginning of the elements as a const pointer. | ||||||
|  | 		 */ | ||||||
|  | 		element const * begin() const { | ||||||
|  | 			return this->elements.pointer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the ending of the elements as a mutable pointer. | ||||||
|  | 		 */ | ||||||
|  | 		element * end() { | ||||||
|  | 			return this->elements.pointer + this->filled; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns the ending of the elements as a const pointer. | ||||||
|  | 		 */ | ||||||
|  | 		element const * end() const { | ||||||
|  | 			return this->elements.pointer + this->filled; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Returns `true` if the stack is backed by dynamic memory, otherwise `false`. | ||||||
|  | 		 */ | ||||||
|  | 		bool is_dynamic() const { | ||||||
|  | 			return this->elements.pointer != this->local_buffer; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to append `source_element` to the top of the stack. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned [append_result] indicates whether the operation was successful or not. | ||||||
|  | 		 * | ||||||
|  | 		 * If the returned [append_result] is anything but [append_result::ok], the stack will be | ||||||
|  | 		 * be left in an empty but valid state. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* that [append] is recommended when appending many values at once. | ||||||
|  | 		 */ | ||||||
|  | 		append_result push(element const & source_element) { | ||||||
|  | 			if (this->filled == this->elements.length) { | ||||||
|  | 				append_result const result = this->reserve(this->elements.length); | ||||||
|  | 
 | ||||||
|  | 				if (result != append_result::ok) return result; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			this->elements[this->filled] = source_element; | ||||||
|  | 			this->filled += 1; | ||||||
|  | 
 | ||||||
|  | 			return append_result::ok; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/**
 | ||||||
|  | 		 * Attempts to reserve `capacity` number of elements additional space on the stack, forcing | ||||||
|  | 		 * it to use dynamic memory _even_ if it hasn't exhausted the local buffer yet. | ||||||
|  | 		 * | ||||||
|  | 		 * The returned [append_result] indicates whether the operation was successful or not. | ||||||
|  | 		 * | ||||||
|  | 		 * If the returned [append_result] is anything but [append_result::ok], the stack will be | ||||||
|  | 		 * be left in an empty but valid state. | ||||||
|  | 		 * | ||||||
|  | 		 * *Note* that manual invocation is not recommended if the [stack] has a large | ||||||
|  | 		 * `initial_capacity` argument. | ||||||
|  | 		 */ | ||||||
|  | 		append_result reserve(usize capacity) { | ||||||
|  | 			usize const requested_capacity = this->filled + capacity; | ||||||
|  | 
 | ||||||
|  | 			if (this->is_dynamic()) { | ||||||
|  | 				// Grow dynamic buffer (bailing out if failed).
 | ||||||
|  | 				u8 * const buffer = this->dynamic_allocator->reallocate( | ||||||
|  | 					reinterpret_cast<u8 *>(this->elements.pointer), | ||||||
|  | 						sizeof(element) * requested_capacity); | ||||||
|  | 
 | ||||||
|  | 				if (buffer == nullptr) { | ||||||
|  | 					this->elements = {}; | ||||||
|  | 
 | ||||||
|  | 					return append_result::out_of_memory; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				this->elements = {reinterpret_cast<element *>(buffer), requested_capacity}; | ||||||
|  | 			} else { | ||||||
|  | 				usize const buffer_size = sizeof(element) * requested_capacity; | ||||||
|  | 				u8 * const buffer = this->dynamic_allocator->reallocate(nullptr, buffer_size); | ||||||
|  | 
 | ||||||
|  | 				if (buffer == nullptr) { | ||||||
|  | 					this->elements = {}; | ||||||
|  | 
 | ||||||
|  | 					return append_result::out_of_memory; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				copy({buffer, buffer_size}, this->elements.as_bytes()); | ||||||
|  | 
 | ||||||
|  | 				this->elements = {reinterpret_cast<element *>(buffer), requested_capacity}; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return append_result::ok; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		allocator * dynamic_allocator; | ||||||
|  | 
 | ||||||
|  | 		usize filled; | ||||||
|  | 
 | ||||||
|  | 		slice<element> elements; | ||||||
|  | 
 | ||||||
|  | 		element local_buffer[init_capacity]; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	/**
 | ||||||
|  | 	 * Writable type for appending data to a [sequence] containing [u8] values. | ||||||
|  | 	 */ | ||||||
|  | 	struct sequence_writer : public writer { | ||||||
|  | 		sequence_writer(sequence<u8> * output_sequence) { | ||||||
|  | 			this->output_sequence = output_sequence; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		expected<usize, io_error> write(slice<u8 const> const & buffer) { | ||||||
|  | 			switch (output_sequence->append(buffer)) { | ||||||
|  | 				case append_result::ok: return buffer.length; | ||||||
|  | 				case append_result::out_of_memory: return io_error::unavailable; | ||||||
|  | 				default: unreachable(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		sequence<u8> * output_sequence; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										49
									
								
								source/kym.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								source/kym.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | export module kym; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | import coral.math; | ||||||
|  | 
 | ||||||
|  | export namespace kym { | ||||||
|  | 	enum class value_type { | ||||||
|  | 		nil, | ||||||
|  | 		boolean, | ||||||
|  | 		integer, | ||||||
|  | 		scalar, | ||||||
|  | 		vector2, | ||||||
|  | 		vector3, | ||||||
|  | 		object, | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct value { | ||||||
|  | 		value_type type; | ||||||
|  | 
 | ||||||
|  | 		union { | ||||||
|  | 			bool boolean; | ||||||
|  | 
 | ||||||
|  | 			coral::i64 integer; | ||||||
|  | 
 | ||||||
|  | 			coral::f64 scalar; | ||||||
|  | 
 | ||||||
|  | 			coral::vector2 vector2; | ||||||
|  | 
 | ||||||
|  | 			coral::vector3 vector3; | ||||||
|  | 
 | ||||||
|  | 			void * object; | ||||||
|  | 		} as; | ||||||
|  | 
 | ||||||
|  | 		coral::optional<coral::u16> as_u16() const { | ||||||
|  | 			if ((this->type == value_type::integer) && | ||||||
|  | 				(this->as.integer >= 0) && (this->as.integer <= coral::u16_max)) { | ||||||
|  | 
 | ||||||
|  | 				return static_cast<coral::u16>(this->as.integer); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return {}; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	value nil = { | ||||||
|  | 		.type = value_type::nil, | ||||||
|  | 		.as = {.object = nullptr}, | ||||||
|  | 	}; | ||||||
|  | }; | ||||||
							
								
								
									
										182
									
								
								source/kym/environment.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								source/kym/environment.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,182 @@ | |||||||
|  | export module kym.environment; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | import coral.sequence; | ||||||
|  | 
 | ||||||
|  | import kym; | ||||||
|  | 
 | ||||||
|  | using loggable = coral::callable<void(coral::slice<char const> const &)>; | ||||||
|  | 
 | ||||||
|  | export namespace kym { | ||||||
|  | 	struct vm; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | enum class token_kind { | ||||||
|  | 	end, | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct token { | ||||||
|  | 	coral::slice<char const> text; | ||||||
|  | 
 | ||||||
|  | 	token_kind kind; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct tokenizer { | ||||||
|  | 	coral::slice<char const> source; | ||||||
|  | 
 | ||||||
|  | 	tokenizer(coral::slice<char const> const & source) : source{source} {} | ||||||
|  | 
 | ||||||
|  | 	token next() { | ||||||
|  | 		coral::usize cursor = 0; | ||||||
|  | 
 | ||||||
|  | 		while (cursor < source.length) { | ||||||
|  | 
 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return token{ | ||||||
|  | 			.kind = token_kind::end, | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | struct bytecode { | ||||||
|  | 	bytecode(coral::allocator * allocator) : error_message_buffer{allocator} { | ||||||
|  | 
 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	bool compile(tokenizer bytecode_tokenizer, loggable const & log_error) { | ||||||
|  | 		for (;;) { | ||||||
|  | 			token const initial_token = bytecode_tokenizer.next(); | ||||||
|  | 
 | ||||||
|  | 			switch (initial_token.kind) { | ||||||
|  | 				case token_kind::end: return true; | ||||||
|  | 
 | ||||||
|  | 				default: coral::unreachable(); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	kym::value execute(kym::vm & vm, coral::slice<kym::value const> const & arguments) { | ||||||
|  | 		return kym::nil; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private: | ||||||
|  | 	coral::stack<char> error_message_buffer; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export namespace kym { | ||||||
|  | 	value default_call(vm & owning_vm, void * userdata, coral::slice<value const> const & arguments) { | ||||||
|  | 		return nil; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	coral::expected<coral::usize, coral::io_error> default_stringify(vm & owning_vm, void * userdata, coral::writer & output) { | ||||||
|  | 		return output.write(coral::slice{"[object]"}.as_bytes()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	struct bound_object { | ||||||
|  | 		void * userdata; | ||||||
|  | 
 | ||||||
|  | 		struct { | ||||||
|  | 			void(*cleanup)(vm &, void *); | ||||||
|  | 
 | ||||||
|  | 			value(*call)(vm &, void *, coral::slice<value const> const &); | ||||||
|  | 
 | ||||||
|  | 			coral::expected<coral::usize, coral::io_error>(*stringify)(vm &, void *, coral::writer &); | ||||||
|  | 		} behavior; | ||||||
|  | 
 | ||||||
|  | 		bound_object(vm * owning_vm) : userdata{nullptr}, owning_vm{owning_vm}, behavior{ | ||||||
|  | 			.cleanup = [](vm & owning_vm, void * userdata) {}, | ||||||
|  | 			.call = default_call, | ||||||
|  | 			.stringify = default_stringify, | ||||||
|  | 		} {} | ||||||
|  | 
 | ||||||
|  | 		void cleanup() { | ||||||
|  | 			this->behavior.cleanup(*this->owning_vm, this->userdata); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		value call(coral::slice<value const> const & arguments) { | ||||||
|  | 			return this->behavior.call(*this->owning_vm, this->userdata, arguments); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::expected<coral::usize, coral::io_error> stringify(coral::writer & output) { | ||||||
|  | 			return this->behavior.stringify(*this->owning_vm, this->userdata, output); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		value get_field(coral::slice<char const> const & field_name) { | ||||||
|  | 			return nil; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		vm * owning_vm; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct vm { | ||||||
|  | 		struct init_options { | ||||||
|  | 			coral::u16 datastack_size; | ||||||
|  | 
 | ||||||
|  | 			coral::u16 callstack_size; | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		vm(coral::allocator * allocator, auto log) : allocator{allocator}, log{log}, data_stack{} {} | ||||||
|  | 
 | ||||||
|  | 		~vm() { | ||||||
|  | 			if (this->data_stack.pointer != nullptr) | ||||||
|  | 				this->allocator->deallocate(this->data_stack.pointer); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		bool init(init_options const & options) { | ||||||
|  | 			coral::u8 * const data_stack_buffer = this->allocator->reallocate(reinterpret_cast | ||||||
|  | 				<coral::u8 *>(this->data_stack.pointer), options.datastack_size * sizeof(value)); | ||||||
|  | 
 | ||||||
|  | 			if (data_stack_buffer == nullptr) return false; | ||||||
|  | 
 | ||||||
|  | 			this->data_stack = { | ||||||
|  | 				reinterpret_cast<value * >(data_stack_buffer), options.datastack_size}; | ||||||
|  | 
 | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		value compile(coral::slice<char const> const & source) { | ||||||
|  | 			bytecode * source_bytecode = new (*this->allocator) bytecode{allocator}; | ||||||
|  | 
 | ||||||
|  | 			if (source_bytecode == nullptr) return nil; | ||||||
|  | 
 | ||||||
|  | 			if (!source_bytecode->compile(tokenizer{source}, [&](coral::slice<char const> error_message) { | ||||||
|  | 				this->log(error_message); | ||||||
|  | 			})) { | ||||||
|  | 				this->allocator->deallocate(source_bytecode); | ||||||
|  | 
 | ||||||
|  | 				return nil; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return this->new_object([this, source_bytecode](bound_object & object) { | ||||||
|  | 				object.userdata = source_bytecode; | ||||||
|  | 
 | ||||||
|  | 				object.behavior.cleanup = [](vm & owning_vm, void * userdata) { | ||||||
|  | 					owning_vm.allocator->deallocate(userdata); | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				object.behavior.call = [](vm & owning_vm, void * userdata, coral::slice<value const> const & arguments) -> value { | ||||||
|  | 					return reinterpret_cast<bytecode *>(userdata)->execute(owning_vm, arguments); | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				this->allocator->deallocate(source_bytecode); | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		value new_object(coral::callable<void(bound_object &)> const & then) { | ||||||
|  | 			return nil; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void with_object(value object_value, coral::callable<void(bound_object &)> const & then) { | ||||||
|  | 
 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		coral::slice<value> data_stack; | ||||||
|  | 
 | ||||||
|  | 		loggable log; | ||||||
|  | 
 | ||||||
|  | 		coral::allocator * allocator; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										202
									
								
								source/oar.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								source/oar.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,202 @@ | |||||||
|  | export module oar; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | import coral.files; | ||||||
|  | 
 | ||||||
|  | constexpr coral::usize signature_length{4}; | ||||||
|  | 
 | ||||||
|  | constexpr coral::usize signature_version_length{1}; | ||||||
|  | 
 | ||||||
|  | constexpr coral::usize signature_identifier_length{signature_length - signature_version_length}; | ||||||
|  | 
 | ||||||
|  | constexpr coral::u8 signature_magic[signature_length]{'o', 'a', 'r', 0}; | ||||||
|  | 
 | ||||||
|  | struct header { | ||||||
|  | 	coral::u8 signature_magic[signature_length]; | ||||||
|  | 
 | ||||||
|  | 	coral::u32 entry_count; | ||||||
|  | 
 | ||||||
|  | 	coral::u8 padding[504]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static_assert(sizeof(header) == 512); | ||||||
|  | 
 | ||||||
|  | struct entry { | ||||||
|  | 	coral::path path; | ||||||
|  | 
 | ||||||
|  | 	coral::u64 data_offset; | ||||||
|  | 
 | ||||||
|  | 	coral::u64 data_length; | ||||||
|  | 
 | ||||||
|  | 	coral::u8 padding[240]; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | static_assert(sizeof(entry) == 512); | ||||||
|  | 
 | ||||||
|  | export namespace oar { | ||||||
|  | 	struct archive_file_reader : public coral::file_reader { | ||||||
|  | 		enum class [[nodiscard]] close_result { | ||||||
|  | 			ok, | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		enum class [[nodiscard]] open_result { | ||||||
|  | 			ok, | ||||||
|  | 			io_unavailable, | ||||||
|  | 			archive_invalid, | ||||||
|  | 			archive_unsupported, | ||||||
|  | 			not_found, | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		archive_file_reader(coral::file_reader * archive_reader) { | ||||||
|  | 			this->archive_reader = archive_reader; | ||||||
|  | 			this->data_offset = 0; | ||||||
|  | 			this->data_length = 0; | ||||||
|  | 			this->data_cursor = 0; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		close_result close() { | ||||||
|  | 			return close_result::ok; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		bool is_open() const { | ||||||
|  | 			return this->data_offset >= sizeof(header); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		open_result open(coral::path const & file_path) { | ||||||
|  | 			if (this->is_open()) switch (this->close()) { | ||||||
|  | 				case close_result::ok: break; | ||||||
|  | 				default: coral::unreachable(); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if (!this->archive_reader->seek(0).is_ok()) return open_result::io_unavailable; | ||||||
|  | 
 | ||||||
|  | 			constexpr coral::usize header_size = sizeof(header); | ||||||
|  | 			coral::u8 archive_header_buffer[header_size]{0}; | ||||||
|  | 
 | ||||||
|  | 			{ | ||||||
|  | 				coral::expected const read_bytes{archive_reader->read(archive_header_buffer)}; | ||||||
|  | 
 | ||||||
|  | 				if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size)) | ||||||
|  | 					return open_result::archive_invalid; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			header const * const archive_header{reinterpret_cast<header const *>(archive_header_buffer)}; | ||||||
|  | 
 | ||||||
|  | 			if (!coral::equals(coral::slice{archive_header->signature_magic, | ||||||
|  | 				signature_identifier_length}, coral::slice{signature_magic, | ||||||
|  | 				signature_identifier_length})) return open_result::archive_invalid; | ||||||
|  | 
 | ||||||
|  | 			if (archive_header->signature_magic[signature_identifier_length] != | ||||||
|  | 				signature_magic[signature_identifier_length]) return open_result::archive_unsupported; | ||||||
|  | 
 | ||||||
|  | 			// Read file table.
 | ||||||
|  | 			coral::u64 head{0}; | ||||||
|  | 			coral::u64 tail{archive_header->entry_count - 1}; | ||||||
|  | 			constexpr coral::usize entry_size{sizeof(entry)}; | ||||||
|  | 			coral::u8 file_entry_buffer[entry_size]{0}; | ||||||
|  | 
 | ||||||
|  | 			while (head <= tail) { | ||||||
|  | 				coral::u64 const midpoint{head + ((tail - head) / 2)}; | ||||||
|  | 
 | ||||||
|  | 				if (!archive_reader->seek(header_size + (entry_size * midpoint)).is_ok()) | ||||||
|  | 					return open_result::archive_invalid; | ||||||
|  | 
 | ||||||
|  | 				{ | ||||||
|  | 					coral::expected const read_bytes{archive_reader->read(file_entry_buffer)}; | ||||||
|  | 
 | ||||||
|  | 					if ((!read_bytes.is_ok()) || (read_bytes.value() != header_size)) | ||||||
|  | 						return open_result::archive_invalid; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				entry const * const archive_entry{reinterpret_cast<entry const *>(file_entry_buffer)}; | ||||||
|  | 				coral::size const comparison{file_path.compare(archive_entry->path)}; | ||||||
|  | 
 | ||||||
|  | 				if (comparison == 0) { | ||||||
|  | 					this->data_offset = archive_entry->data_offset; | ||||||
|  | 					this->data_length = archive_entry->data_length; | ||||||
|  | 					this->data_cursor = archive_entry->data_offset; | ||||||
|  | 
 | ||||||
|  | 					return open_result::ok; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if (comparison > 0) { | ||||||
|  | 					head = (midpoint + 1); | ||||||
|  | 				} else { | ||||||
|  | 					tail = (midpoint - 1); | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return open_result::not_found; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::expected<coral::usize, coral::io_error> read(coral::slice<coral::u8> const & buffer) override { | ||||||
|  | 			if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 			if (this->archive_reader->seek(this->data_offset + this->data_cursor).is_ok()) | ||||||
|  | 				return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 			coral::expected const bytes_read{this->archive_reader->read(buffer.sliced(0, | ||||||
|  | 				coral::min(buffer.length, static_cast<coral::usize>(( | ||||||
|  | 					this->data_offset + this->data_length) - this->data_cursor))))}; | ||||||
|  | 
 | ||||||
|  | 			if (!bytes_read.is_ok()) this->data_cursor += bytes_read.value(); | ||||||
|  | 
 | ||||||
|  | 			return bytes_read; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::expected<coral::u64, coral::io_error> seek(coral::u64 offset) override { | ||||||
|  | 			if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 			return coral::io_error::unavailable; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		coral::expected<coral::u64, coral::io_error> tell() override { | ||||||
|  | 			if (!this->is_open()) return coral::io_error::unavailable; | ||||||
|  | 
 | ||||||
|  | 			return this->data_cursor; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		coral::file_reader * archive_reader; | ||||||
|  | 
 | ||||||
|  | 		coral::u64 data_offset; | ||||||
|  | 
 | ||||||
|  | 		coral::u64 data_length; | ||||||
|  | 
 | ||||||
|  | 		coral::u64 data_cursor; | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	struct archive : public coral::fs { | ||||||
|  | 		archive(coral::fs * backing_fs, coral::path const & archive_path) { | ||||||
|  | 			this->backing_fs = backing_fs; | ||||||
|  | 			this->archive_path = archive_path; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void read_file(coral::path const & file_path, | ||||||
|  | 			coral::callable<void(coral::file_reader &)> const & then) override { | ||||||
|  | 
 | ||||||
|  | 			if ((this->backing_fs == nullptr) || (this->archive_path.byte_size() == 0)) return; | ||||||
|  | 
 | ||||||
|  | 			this->backing_fs->read_file(this->archive_path, [&](coral::file_reader & archive_reader) { | ||||||
|  | 				archive_file_reader file_reader{&archive_reader}; | ||||||
|  | 
 | ||||||
|  | 				if (file_reader.open(file_path) != archive_file_reader::open_result::ok) return; | ||||||
|  | 
 | ||||||
|  | 				then(file_reader); | ||||||
|  | 
 | ||||||
|  | 				if (file_reader.close() != archive_file_reader::close_result::ok) return; | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		void write_file(coral::path const & file_path, | ||||||
|  | 			coral::callable<void(coral::file_writer &)> const & then) override { | ||||||
|  | 
 | ||||||
|  | 			// Read-only file system.
 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		private: | ||||||
|  | 		coral::fs * backing_fs; | ||||||
|  | 
 | ||||||
|  | 		coral::path archive_path; | ||||||
|  | 	}; | ||||||
|  | } | ||||||
							
								
								
									
										97
									
								
								source/runtime.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								source/runtime.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | export module runtime; | ||||||
|  | 
 | ||||||
|  | import app; | ||||||
|  | 
 | ||||||
|  | import coral; | ||||||
|  | import coral.files; | ||||||
|  | import coral.math; | ||||||
|  | import coral.sequence; | ||||||
|  | 
 | ||||||
|  | import kym; | ||||||
|  | import kym.environment; | ||||||
|  | 
 | ||||||
|  | extern "C" int main(int argc, char const * const * argv) { | ||||||
|  | 	return app::display("Ona Runtime", [](app::system & system, app::graphics & graphics) -> int { | ||||||
|  | 		constexpr coral::path config_path{"config.kym"}; | ||||||
|  | 		bool is_config_loaded{false}; | ||||||
|  | 
 | ||||||
|  | 		system.res_fs().read_file(config_path, [&](coral::reader & file) { | ||||||
|  | 			coral::allocator * const allocator{&system.thread_safe_allocator()}; | ||||||
|  | 
 | ||||||
|  | 			kym::vm vm{allocator, [&system](coral::slice<char const> const & error_message) { | ||||||
|  | 				system.log(app::log_level::error, error_message); | ||||||
|  | 			}}; | ||||||
|  | 
 | ||||||
|  | 			if (!vm.init({.datastack_size = 64, .callstack_size = 64})) { | ||||||
|  | 				system.log(app::log_level::error, "failed to allocate memory for config vm"); | ||||||
|  | 
 | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			coral::stack<coral::u8> script_source{allocator}; | ||||||
|  | 
 | ||||||
|  | 			{ | ||||||
|  | 				coral::u8 stream_buffer[1024]{0}; | ||||||
|  | 				coral::sequence_writer script_writer{&script_source}; | ||||||
|  | 
 | ||||||
|  | 				if (!coral::stream(script_writer, file, stream_buffer).is_ok()) return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			vm.with_object(vm.compile(coral::slice{script_source.begin(), script_source.end()}.as_chars()), [&](kym::bound_object & script) { | ||||||
|  | 				vm.with_object(script.call({}), [&](kym::bound_object & config) { | ||||||
|  | 					coral::u16 const width{config.get_field("width").as_u16().value_or(0)}; | ||||||
|  | 
 | ||||||
|  | 					if (width == 0) return system.log(app::log_level::error, | ||||||
|  | 						"failed to decode `width` property of config"); | ||||||
|  | 
 | ||||||
|  | 					coral::u16 const height{config.get_field("height").as_u16().value_or(0)}; | ||||||
|  | 
 | ||||||
|  | 					if (height == 0) return system.log(app::log_level::error, | ||||||
|  | 						"failed to decode `height` property of config"); | ||||||
|  | 
 | ||||||
|  | 					if (graphics.show(width, height) != app::graphics::show_result::ok) | ||||||
|  | 						return system.log(app::log_level::error, "failed to initialize window"); | ||||||
|  | 
 | ||||||
|  | 					vm.with_object(config.get_field("title"), [&](kym::bound_object & title) { | ||||||
|  | 						coral::stack<coral::u8, 128> title_buffer{&system.thread_safe_allocator()}; | ||||||
|  | 						coral::sequence_writer title_writer{&title_buffer}; | ||||||
|  | 
 | ||||||
|  | 						if (!title.stringify(title_writer).is_ok()) { | ||||||
|  | 							system.log(app::log_level::error, | ||||||
|  | 								"failed to decode `title` property of config"); | ||||||
|  | 
 | ||||||
|  | 							return; | ||||||
|  | 						} | ||||||
|  | 
 | ||||||
|  | 						is_config_loaded = true; | ||||||
|  | 					}); | ||||||
|  | 				}); | ||||||
|  | 			}); | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		if (!is_config_loaded) { | ||||||
|  | 			coral::stack<coral::u8> error_message{&system.thread_safe_allocator()}; | ||||||
|  | 
 | ||||||
|  | 			{ | ||||||
|  | 				coral::sequence_writer error_writer{&error_message}; | ||||||
|  | 
 | ||||||
|  | 				if (!error_writer.write(coral::slice{"failed to load "}.as_bytes()).is_ok()) | ||||||
|  | 					return coral::u8_max; | ||||||
|  | 
 | ||||||
|  | 				if (!error_writer.write(config_path.as_slice().as_bytes()).is_ok()) | ||||||
|  | 					return coral::u8_max; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			return coral::u8_max; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// app::canvas canvas_2d();
 | ||||||
|  | 
 | ||||||
|  | 		while (system.poll()) { | ||||||
|  | 			// canvas_2d.render(graphics);
 | ||||||
|  | 			graphics.present(); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return 0; | ||||||
|  | 	}); | ||||||
|  | } | ||||||
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user