diff --git a/CMakeLists.txt b/CMakeLists.txt index b553a978..a44c89c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,10 @@ set(CMAKE_C_STANDARD_REQUIRED ON) # Directories set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}") set(DUSK_SOURCES_DIR "${DUSK_ROOT_DIR}/src") +set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets") +set(DUSK_BUILT_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets") +set(DUSK_GENERATED_HEADERS_DIR "${CMAKE_BINARY_DIR}/generated") +file(MAKE_DIRECTORY ${DUSK_GENERATED_HEADERS_DIR}) project(DuskCore VERSION 1.0.0 LANGUAGES C) @@ -65,9 +69,25 @@ if(DUSK_BACKTRACE) target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic) endif() +# Tools (run during the build to generate headers) +add_subdirectory(tools) +target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + ${DUSK_GENERATED_HEADERS_DIR} +) + # Core engine code add_subdirectory(${DUSK_SOURCES_DIR}/dusk) +# Assets, copied into /assets +include(${DUSK_ROOT_DIR}/cmake/asset/asset.cmake) +# Separate binary dir, so /assets only contains built assets. +add_subdirectory(${DUSK_ROOT_DIR}/assets ${CMAKE_BINARY_DIR}/assets_cmake) +dusk_assets_finalize(DuskAssets) +add_dependencies(${DUSK_LIBRARY_TARGET_NAME} DuskAssets) + +# Game code +add_subdirectory(${DUSK_SOURCES_DIR}/${DUSK_GAME}) + # Platform code foreach(DIR ${DUSK_PLATFORM_DIRS}) add_subdirectory(${DUSK_SOURCES_DIR}/${DIR}) diff --git a/assets/CMakeLists.txt b/assets/CMakeLists.txt new file mode 100644 index 00000000..c59434a8 --- /dev/null +++ b/assets/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_asset(cube.obj) diff --git a/assets/cube.obj b/assets/cube.obj new file mode 100644 index 00000000..278c4fa2 --- /dev/null +++ b/assets/cube.obj @@ -0,0 +1,42 @@ +# Unit cube, centered on the origin, with per face normals and UVs. +o Cube + +v -0.5 -0.5 -0.5 +v 0.5 -0.5 -0.5 +v 0.5 0.5 -0.5 +v -0.5 0.5 -0.5 +v -0.5 -0.5 0.5 +v 0.5 -0.5 0.5 +v 0.5 0.5 0.5 +v -0.5 0.5 0.5 + +vt 0.0 0.0 +vt 1.0 0.0 +vt 1.0 1.0 +vt 0.0 1.0 + +vn 0.0 0.0 -1.0 +vn 0.0 0.0 1.0 +vn -1.0 0.0 0.0 +vn 1.0 0.0 0.0 +vn 0.0 -1.0 0.0 +vn 0.0 1.0 0.0 + +# Back (-Z) +f 2/1/1 1/2/1 4/3/1 +f 2/1/1 4/3/1 3/4/1 +# Front (+Z) +f 5/1/2 6/2/2 7/3/2 +f 5/1/2 7/3/2 8/4/2 +# Left (-X) +f 1/1/3 5/2/3 8/3/3 +f 1/1/3 8/3/3 4/4/3 +# Right (+X) +f 6/1/4 2/2/4 3/3/4 +f 6/1/4 3/3/4 7/4/4 +# Bottom (-Y) +f 1/1/5 2/2/5 6/3/5 +f 1/1/5 6/3/5 5/4/5 +# Top (+Y) +f 8/1/6 7/2/6 3/3/6 +f 8/1/6 3/3/6 4/4/6 diff --git a/cmake/asset/asset.cmake b/cmake/asset/asset.cmake new file mode 100644 index 00000000..e2913275 --- /dev/null +++ b/cmake/asset/asset.cmake @@ -0,0 +1,65 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Asset building. Include this from the root CMakeLists.txt. +# +# Assets are registered with add_asset() and built by dusk_assets_finalize(). +# Each asset is dispatched on its file extension to a handler function named +# dusk_asset_ (e.g. dusk_asset_obj), defined in its own file in this +# directory. Extensions without a handler fall back to a plain copy. + +include(${CMAKE_CURRENT_LIST_DIR}/copy.cmake) +include(${CMAKE_CURRENT_LIST_DIR}/obj.cmake) + +# Registers an asset to be built into the project. The path is relative to the +# calling CMakeLists.txt, and is kept relative to the root assets directory in +# the output, so assets/models/a.obj ends up at /assets/models/a.obj. +# +# This only records the asset. The build rules are created by +# dusk_assets_finalize() from the root CMakeLists.txt, because a custom command +# output can only be used by targets in the directory it was defined in. +function(add_asset ASSET_PATH) + set(ASSET_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${ASSET_PATH}") + if(NOT EXISTS "${ASSET_SOURCE}") + message(FATAL_ERROR "add_asset: ${ASSET_SOURCE} does not exist") + endif() + + set_property(GLOBAL APPEND PROPERTY DUSK_ASSET_SOURCES "${ASSET_SOURCE}") +endfunction() + +# Determines the lowercase extension of an asset, without the leading dot. +function(dusk_asset_extension ASSET_SOURCE OUT_VAR) + get_filename_component(ASSET_EXT "${ASSET_SOURCE}" LAST_EXT) + string(TOLOWER "${ASSET_EXT}" ASSET_EXT) + string(REGEX REPLACE "^\\." "" ASSET_EXT "${ASSET_EXT}") + set(${OUT_VAR} "${ASSET_EXT}" PARENT_SCOPE) +endfunction() + +# Creates the TARGET_NAME target, which builds every asset registered with +# add_asset(). Call once from the root CMakeLists.txt after adding the assets. +function(dusk_assets_finalize TARGET_NAME) + get_property(ASSET_SOURCES GLOBAL PROPERTY DUSK_ASSET_SOURCES) + + set(ASSET_OUTPUTS "") + foreach(ASSET_SOURCE IN LISTS ASSET_SOURCES) + file(RELATIVE_PATH ASSET_RELATIVE "${DUSK_ASSETS_DIR}" "${ASSET_SOURCE}") + dusk_asset_extension("${ASSET_SOURCE}" ASSET_EXT) + + # Dispatch on the extension, falling back to a plain copy. + set(ASSET_OUTPUT "") + if(COMMAND dusk_asset_${ASSET_EXT}) + cmake_language(CALL dusk_asset_${ASSET_EXT} + "${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT + ) + else() + message(STATUS "Asset ${ASSET_RELATIVE}: no handler for '.${ASSET_EXT}', copying") + dusk_asset_copy("${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT) + endif() + + list(APPEND ASSET_OUTPUTS "${ASSET_OUTPUT}") + endforeach() + + add_custom_target(${TARGET_NAME} ALL DEPENDS ${ASSET_OUTPUTS}) +endfunction() diff --git a/cmake/asset/copy.cmake b/cmake/asset/copy.cmake new file mode 100644 index 00000000..2fddb5bb --- /dev/null +++ b/cmake/asset/copy.cmake @@ -0,0 +1,22 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Handler that copies an asset into the built assets directory as is. Every +# handler has the signature (SOURCE RELATIVE OUT_VAR) and must set OUT_VAR, in +# the caller's scope, to the file it produces. +function(dusk_asset_copy ASSET_SOURCE ASSET_RELATIVE OUT_VAR) + set(ASSET_OUTPUT "${DUSK_BUILT_ASSETS_DIR}/${ASSET_RELATIVE}") + + add_custom_command( + OUTPUT "${ASSET_OUTPUT}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${ASSET_SOURCE}" "${ASSET_OUTPUT}" + DEPENDS "${ASSET_SOURCE}" + COMMENT "Copying asset ${ASSET_RELATIVE}" + VERBATIM + ) + + set(${OUT_VAR} "${ASSET_OUTPUT}" PARENT_SCOPE) +endfunction() diff --git a/cmake/asset/obj.cmake b/cmake/asset/obj.cmake new file mode 100644 index 00000000..a0b01b35 --- /dev/null +++ b/cmake/asset/obj.cmake @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +# Handler for .obj (Wavefront mesh) assets. Currently just copies the file. +function(dusk_asset_obj ASSET_SOURCE ASSET_RELATIVE OUT_VAR) + dusk_asset_copy("${ASSET_SOURCE}" "${ASSET_RELATIVE}" ASSET_OUTPUT) + set(${OUT_VAR} "${ASSET_OUTPUT}" PARENT_SCOPE) +endfunction() diff --git a/cmake/option.cmake b/cmake/option.cmake index 2d75973e..0c3ff363 100644 --- a/cmake/option.cmake +++ b/cmake/option.cmake @@ -13,6 +13,9 @@ option(DUSK_BUILD_TESTS "Enable tests." OFF) option(DUSK_NETWORK "Enable network support." ON) # Game metadata +set(DUSK_GAME "duskexample" CACHE STRING + "Directory under src/ containing the game to build (provides game/game.c)." +) set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name.") set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder.") if(NOT DEFINED DUSK_VERSION) diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index b8beba4b..1875e69d 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -9,12 +9,14 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ) # Modules +add_subdirectory(asset) add_subdirectory(assert) add_subdirectory(console) add_subdirectory(display) add_subdirectory(engine) add_subdirectory(error) add_subdirectory(perf) +add_subdirectory(resource) add_subdirectory(time) add_subdirectory(thread) add_subdirectory(util) diff --git a/src/dusk/assert/assert.h b/src/dusk/assert/assert.h index 0040065d..35e62d4d 100644 --- a/src/dusk/assert/assert.h +++ b/src/dusk/assert/assert.h @@ -283,8 +283,25 @@ #endif -// Static Assertions +// Static Assertions. These are compile time only, so they are never faked. + +/** + * Asserts a constant expression at compile time. Never faked, even when + * DUSK_ASSERTIONS_FAKED is set. + * + * @param x Constant expression that must be true. + * @param message Message to show on failure, must be a string literal. + */ +#define assertStatic(x, message) \ + _Static_assert(x, message) + +/** + * Asserts the size of a struct at compile time. + * + * @param struct The struct type to check. + * @param size The size in bytes the struct must be. + */ #define assertStructSize(struct, size) \ - _Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size) + assertStatic(sizeof(struct) == size, "Size of " #struct " must be " #size) // EOF diff --git a/src/dusk/asset/CMakeLists.txt b/src/dusk/asset/CMakeLists.txt new file mode 100644 index 00000000..9832f8d5 --- /dev/null +++ b/src/dusk/asset/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + asset.c + assetfile.c +) diff --git a/src/dusk/asset/asset.c b/src/dusk/asset/asset.c new file mode 100644 index 00000000..9fb267d1 --- /dev/null +++ b/src/dusk/asset/asset.c @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "asset/asset.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +asset_t ASSET; + +errorret_t assetInit(void) { + memoryZero(&ASSET, sizeof(asset_t)); + + // The platform must set ASSET.baseDirectory or throw an error. + errorChain(assetInitPlatform()); + assertStrLenMin(ASSET.baseDirectory, 1, "Asset directory empty without error."); + + errorOk(); +} + +errorret_t assetGetPath( + const char_t *filename, + char_t *out, + const size_t outSize +) { + assertNotNull(filename, "Filename cannot be NULL."); + assertNotNull(out, "Output cannot be NULL."); + + if(filename[0] == '\0') { + errorThrow("Asset filename cannot be empty."); + } + if(filename[0] == '/' || stringIncludesString(filename, "..")) { + errorThrow("Invalid asset filename: %s", filename); + } + + int32_t len = stringFormat(out, outSize, "%s/%s", ASSET.baseDirectory, filename); + if(len < 0 || (size_t)len >= outSize) { + errorThrow("Asset path too long: %s", filename); + } + + errorOk(); +} + +bool_t assetFileExists(const char_t *filename) { + char_t path[ASSET_PATH_MAX]; + errorret_t ret = assetGetPath(filename, path, sizeof(path)); + if(errorIsNotOk(ret)) { + errorCatch(ret); + return false; + } + + FILE *file = fopen(path, "rb"); + if(file == NULL) return false; + fclose(file); + return true; +} + +errorret_t assetDispose(void) { + errorChain(assetDisposePlatform()); + memoryZero(&ASSET, sizeof(asset_t)); + errorOk(); +} diff --git a/src/dusk/asset/asset.h b/src/dusk/asset/asset.h new file mode 100644 index 00000000..f3f043f0 --- /dev/null +++ b/src/dusk/asset/asset.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "asset/assetplatform.h" +#include "asset/assetfile.h" + +#ifndef assetInitPlatform + #error "Platform must define assetInitPlatform function." +#endif +#ifndef assetDisposePlatform + #error "Platform must define assetDisposePlatform function." +#endif + +#define ASSET_BASE_DIRECTORY_MAX 256 + +typedef struct { + // Directory that every asset filename is resolved relative to. Set by the + // platform in assetInitPlatform. + char_t baseDirectory[ASSET_BASE_DIRECTORY_MAX]; +} asset_t; + +extern asset_t ASSET; + +/** + * Initializes the asset system. The platform locates the assets and sets + * ASSET.baseDirectory, or throws an error if it cannot. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t assetInit(void); + +/** + * Builds the full path of an asset on disk. + * + * @param filename Asset filename, relative to the asset directory. + * @param out Buffer to write the null terminated path into. + * @param outSize Size of the buffer in bytes. + * @return An error if the filename is invalid or the path does not fit. + */ +errorret_t assetGetPath( + const char_t *filename, + char_t *out, + const size_t outSize +); + +/** + * Checks whether an asset exists. + * + * @param filename Asset filename, relative to the asset directory. + * @return True if the asset exists and can be opened for reading. + */ +bool_t assetFileExists(const char_t *filename); + +/** + * Disposes of the asset system. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t assetDispose(void); diff --git a/src/dusk/asset/assetfile.c b/src/dusk/asset/assetfile.c new file mode 100644 index 00000000..e60baeb6 --- /dev/null +++ b/src/dusk/asset/assetfile.c @@ -0,0 +1,158 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "asset/asset.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "util/math.h" + +errorret_t assetFileInit(assetfile_t *file, const char_t *filename) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(filename, "Filename cannot be NULL."); + assertStrLenMax(filename, ASSET_FILE_NAME_MAX, "Filename too long."); + + memoryZero(file, sizeof(assetfile_t)); + stringCopy(file->filename, filename, ASSET_FILE_NAME_MAX); + + // Open temporarily to find the size. + char_t path[ASSET_PATH_MAX]; + errorChain(assetGetPath(filename, path, sizeof(path))); + + FILE *handle = fopen(path, "rb"); + if(handle == NULL) { + errorThrow("Failed to open asset file: %s", filename); + } + if(fseek(handle, 0, SEEK_END) != 0) { + fclose(handle); + errorThrow("Failed to seek asset file: %s", filename); + } + long size = ftell(handle); + fclose(handle); + if(size < 0) { + errorThrow("Failed to get size of asset file: %s", filename); + } + + file->size = (size_t)size; + errorOk(); +} + +errorret_t assetFileOpen(assetfile_t *file) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNull(file->file, "Asset file already open."); + + char_t path[ASSET_PATH_MAX]; + errorChain(assetGetPath(file->filename, path, sizeof(path))); + + file->file = fopen(path, "rb"); + if(file->file == NULL) { + errorThrow("Failed to open asset file: %s", file->filename); + } + file->position = 0; + errorOk(); +} + +errorret_t assetFileRewind(assetfile_t *file) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(file->file, "Asset file must be opened before rewinding."); + + if(fseek(file->file, 0, SEEK_SET) != 0) { + errorThrow("Failed to rewind asset file: %s", file->filename); + } + file->position = 0; + errorOk(); +} + +errorret_t assetFileRead( + assetfile_t *file, + void *buffer, + const size_t bufferSize +) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(file->file, "Asset file must be opened before reading."); + + // No buffer, skip the bytes. + if(buffer == NULL) { + size_t skipped = 0; + uint8_t temp[256]; + while(skipped < bufferSize) { + size_t chunk = mathMin(bufferSize - skipped, sizeof(temp)); + size_t got = fread(temp, 1, chunk, file->file); + skipped += got; + if(got < chunk) break; + } + file->position += skipped; + file->lastRead = skipped; + errorOk(); + } + + size_t got = fread(buffer, 1, bufferSize, file->file); + if(got < bufferSize && ferror(file->file)) { + errorThrow("Failed to read from asset file: %s", file->filename); + } + file->position += got; + file->lastRead = got; + errorOk(); +} + +errorret_t assetFileClose(assetfile_t *file) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(file->file, "Asset file must be opened before closing."); + + int result = fclose(file->file); + file->file = NULL; + file->position = 0; + if(result != 0) { + errorThrow("Failed to close asset file: %s", file->filename); + } + errorOk(); +} + +errorret_t assetFileDispose(assetfile_t *file) { + assertNotNull(file, "Asset file cannot be NULL."); + if(file->file != NULL) { + errorChain(assetFileClose(file)); + } + memoryZero(file, sizeof(assetfile_t)); + errorOk(); +} + +errorret_t assetFileReadEntire( + assetfile_t *file, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(file, "Asset file cannot be NULL."); + assertNotNull(outBuffer, "outBuffer cannot be NULL."); + assertNotNull(outSize, "outSize cannot be NULL."); + assertNull(file->file, "Asset file must be closed before reading entire."); + + if(file->size == 0) { + errorThrow("Asset file is empty: %s", file->filename); + } + + errorChain(assetFileOpen(file)); + + uint8_t *buffer = (uint8_t *)memoryAllocate(file->size); + errorret_t ret = assetFileRead(file, buffer, file->size); + if(errorIsOk(ret) && file->lastRead != file->size) { + ret = errorThrowImpl( + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Failed to read entire asset file: %s", file->filename + ); + } + if(errorIsNotOk(ret)) { + memoryFree(buffer); + assetFileClose(file); + errorChain(ret); + } + + errorChain(assetFileClose(file)); + *outBuffer = buffer; + *outSize = file->size; + errorOk(); +} diff --git a/src/dusk/asset/assetfile.h b/src/dusk/asset/assetfile.h new file mode 100644 index 00000000..34fbc063 --- /dev/null +++ b/src/dusk/asset/assetfile.h @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +#define ASSET_FILE_NAME_MAX 128 + +// Full on disk path, base directory plus filename. +#define ASSET_PATH_MAX 512 + +typedef struct { + char_t filename[ASSET_FILE_NAME_MAX]; + FILE *file; + size_t size; + size_t position; + size_t lastRead; +} assetfile_t; + +/** + * Initializes an asset file, checking it exists and reading its size. Does not + * open it. + * + * @param file The asset file to initialize. + * @param filename Asset filename, relative to the asset directory. + * @return An error if the file does not exist or cannot be read. + */ +errorret_t assetFileInit(assetfile_t *file, const char_t *filename); + +/** + * Opens an asset file for reading, positioned at the start. + * + * @param file The asset file to open. Must not already be open. + * @return An error if the file cannot be opened. + */ +errorret_t assetFileOpen(assetfile_t *file); + +/** + * Moves an open asset file back to its start. + * + * @param file The asset file to rewind. Must be open. + * @return An error if the file cannot be rewound. + */ +errorret_t assetFileRewind(assetfile_t *file); + +/** + * Reads bytes from an open asset file. The number of bytes actually read is + * stored in file->lastRead, which is less than bufferSize at the end of the + * file. + * + * @param file The asset file to read from. Must be open. + * @param buffer Buffer to read into. If NULL the bytes are skipped. + * @param bufferSize Number of bytes to read. + * @return An error if reading fails. + */ +errorret_t assetFileRead( + assetfile_t *file, + void *buffer, + const size_t bufferSize +); + +/** + * Closes an open asset file. + * + * @param file The asset file to close. Must be open. + * @return An error if the file cannot be closed. + */ +errorret_t assetFileClose(assetfile_t *file); + +/** + * Disposes an asset file, closing it first if it is still open. + * + * @param file The asset file to dispose. + * @return An error if the file cannot be closed. + */ +errorret_t assetFileDispose(assetfile_t *file); + +/** + * Reads a whole asset file into a newly allocated buffer. The file is opened + * and closed by this call, and must not already be open. + * + * @param file The asset file to read, already initialized. + * @param outBuffer Output buffer, allocated with memoryAllocate. The caller + * must memoryFree it. + * @param outSize Output size of the buffer in bytes. + * @return An error if the file cannot be read in full. + */ +errorret_t assetFileReadEntire( + assetfile_t *file, + uint8_t **outBuffer, + size_t *outSize +); diff --git a/src/dusk/display/CMakeLists.txt b/src/dusk/display/CMakeLists.txt index 466356d5..4cf5ec4a 100644 --- a/src/dusk/display/CMakeLists.txt +++ b/src/dusk/display/CMakeLists.txt @@ -7,3 +7,15 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC display.c ) + +# Subdirectories +add_subdirectory(mesh) + +# Color definitions +dusk_run_python( + dusk_color_defs + tools.color + --csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv + --output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h +) +add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_color_defs) diff --git a/src/dusk/display/color.csv b/src/dusk/display/color.csv new file mode 100644 index 00000000..62c2e1bc --- /dev/null +++ b/src/dusk/display/color.csv @@ -0,0 +1,23 @@ +name,r,g,b,a +black,0,0,0,1 +white,1,1,1,1 +red,1,0,0,1 +green,0,1,0,1 +blue,0,0,1,1 +yellow,1,1,0,1 +cyan,0,1,1,1 +magenta,1,0,1,1 +transparent,0,0,0,0 +transparent_white,1,1,1,0 +transparent_black,0,0,0,0 +gray,0.5,0.5,0.5,1 +light_gray,0.75,0.75,0.75,1 +dark_gray,0.25,0.25,0.25,1 +orange,1,0.65,0,1 +purple,0.5,0,0.5,1 +brown,0.6,0.4,0.2,1 +pink,1,0.75,0.8,1 +lime,0.75,1,0,1 +navy,0,0,0.5,1 +teal,0,0.5,0.5,1 +cornflower_blue,0.39,0.58,0.93,1 \ No newline at end of file diff --git a/src/dusk/display/mesh/CMakeLists.txt b/src/dusk/display/mesh/CMakeLists.txt new file mode 100644 index 00000000..ad5fb368 --- /dev/null +++ b/src/dusk/display/mesh/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + meshformat.c +) diff --git a/src/dusk/display/mesh/mesh.h b/src/dusk/display/mesh/mesh.h new file mode 100644 index 00000000..123fa56f --- /dev/null +++ b/src/dusk/display/mesh/mesh.h @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/display.h" +#include "display/color.h" +#include "display/mesh/meshformat.h" +#include "display/mesh/meshplatform.h" + +#ifndef meshInitPlatform + #error "meshInitPlatform must be defined" +#endif +#ifndef meshDrawPlatform + #error "meshDrawPlatform must be defined" +#endif +#ifndef meshGetFormatPlatform + #error "meshGetFormatPlatform must be defined" +#endif +#ifndef meshGetVertexCountPlatform + #error "meshGetVertexCountPlatform must be defined" +#endif +#ifndef meshDisposePlatform + #error "meshDisposePlatform must be defined" +#endif + +typedef meshprimitivetypeplatform_t meshprimitivetype_t; +typedef meshplatform_t mesh_t; + +/** + * Initializes a mesh. + * + * @param mesh The mesh to initialize. + * @param primitiveType The primitive type (e.g. triangles). + * @param format The MESH_FORMAT_* flags describing each vertex's attributes. + * @param vertexCount The number of vertices in the mesh. + * @param vertices Interleaved vertex data, laid out as described by format + * (see meshFormatStride / meshFormatOffset and meshvertex.h). + * @return An error indicating success or failure. + */ +errorret_t meshInit( + mesh_t *mesh, + const meshprimitivetype_t primitiveType, + const meshformat_t format, + const int32_t vertexCount, + const void *vertices +); + +/** + * Instructs the mesh to flush the vertices to the GPU. This is surprisingly + * only really necessary on modern devices, as we tend to let older devices + * read the vertices from the main memory directly. + * + * @param mesh Mesh to flush the vertices for. + * @param vertexOffset Start vertex to flush. + * @param vertexCount Count of vertices to flush, set to -1 for all. + * @return Error state. + */ +errorret_t meshFlush( + mesh_t *mesh, + const int32_t vertexOffset, + const int32_t vertexCount +); + +/** + * Draws a mesh. + * + * @param mesh The mesh to draw. + * @param vertexOffset The offset in the vertex array to start drawing from. + * @param vertexCount The number of vertices to draw. If -1, draws all vertices. + * @return An error indicating success or failure. + */ +errorret_t meshDraw( + const mesh_t *mesh, + const int32_t vertexOffset, + const int32_t vertexCount +); + +/** + * Gets the axis-aligned bounding box of a mesh. + * + * @param mesh The mesh to get the bounds of. + * @param outMin Output parameter for the minimum corner of the bounding box. + * @param outMax Output parameter for the maximum corner of the bounding box. + */ +void meshGetBounds( + const mesh_t *mesh, + float_t outMin[MESH_VERTEX_POSITION_SIZE], + float_t outMax[MESH_VERTEX_POSITION_SIZE] +); + +/** + * Gets the vertex format of a mesh. + * + * @param mesh The mesh to get the format from. + * @return The MESH_FORMAT_* flags the mesh was created with. + */ +meshformat_t meshGetFormat(const mesh_t *mesh); + +/** + * Gets the vertex count of a mesh. + * + * @param mesh The mesh to get the vertex count from. + * @return The vertex count of the mesh. + */ +int32_t meshGetVertexCount(const mesh_t *mesh); + +/** + * Disposes a mesh. + * + * @param mesh The mesh to dispose. + * @return An error indicating success or failure. + */ +errorret_t meshDispose(mesh_t *mesh); diff --git a/src/dusk/display/mesh/meshformat.c b/src/dusk/display/mesh/meshformat.c new file mode 100644 index 00000000..45758d81 --- /dev/null +++ b/src/dusk/display/mesh/meshformat.c @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "display/mesh/meshformat.h" +#include "assert/assert.h" + +#define MESH_FORMAT_ALL ( \ + MESH_FORMAT_POSITION | MESH_FORMAT_NORMAL | \ + MESH_FORMAT_COLOR | MESH_FORMAT_UV \ +) + +bool_t meshFormatIsValid(const meshformat_t format) { + if((format & ~MESH_FORMAT_ALL) != 0) return false; + return (format & MESH_FORMAT_POSITION) != 0; +} + +bool_t meshFormatHas(const meshformat_t format, const meshformat_t flags) { + return (format & flags) == flags; +} + +size_t meshFormatAttributeSize(const meshformat_t attribute) { + switch(attribute) { + case MESH_FORMAT_POSITION: + return sizeof(float_t) * MESH_VERTEX_POSITION_SIZE; + + case MESH_FORMAT_NORMAL: + return sizeof(float_t) * MESH_VERTEX_NORMAL_SIZE; + + case MESH_FORMAT_COLOR: + return sizeof(uint8_t) * MESH_VERTEX_COLOR_SIZE; + + case MESH_FORMAT_UV: + return sizeof(float_t) * MESH_VERTEX_UV_SIZE; + + default: + assertUnreachable("Mesh attribute must be exactly one format flag."); + return 0; + } +} + +size_t meshFormatStride(const meshformat_t format) { + assertTrue(meshFormatIsValid(format), "Mesh format is not valid."); + + size_t stride = 0; + for(meshformat_t flag = 1; flag <= MESH_FORMAT_ALL; flag <<= 1) { + if(meshFormatHas(format, flag)) stride += meshFormatAttributeSize(flag); + } + return stride; +} + +size_t meshFormatOffset( + const meshformat_t format, + const meshformat_t attribute +) { + assertTrue(meshFormatIsValid(format), "Mesh format is not valid."); + assertTrue( + meshFormatHas(format, attribute), + "Mesh format does not contain the requested attribute." + ); + + // Attributes are interleaved in flag order, so sum everything before it. + size_t offset = 0; + for(meshformat_t flag = 1; flag < attribute; flag <<= 1) { + if(meshFormatHas(format, flag)) offset += meshFormatAttributeSize(flag); + } + return offset; +} diff --git a/src/dusk/display/mesh/meshformat.h b/src/dusk/display/mesh/meshformat.h new file mode 100644 index 00000000..f303dadf --- /dev/null +++ b/src/dusk/display/mesh/meshformat.h @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +/** + * Describes which attributes each vertex of a mesh carries. Combine the + * MESH_FORMAT_* flags. Position is mandatory. + */ +typedef uint8_t meshformat_t; + +#define MESH_FORMAT_POSITION 0x01 +#define MESH_FORMAT_NORMAL 0x02 +#define MESH_FORMAT_COLOR 0x04 +#define MESH_FORMAT_UV 0x08 + +// Common combinations. +#define MESH_FORMAT_P (MESH_FORMAT_POSITION) +#define MESH_FORMAT_PC (MESH_FORMAT_POSITION | MESH_FORMAT_COLOR) +#define MESH_FORMAT_PU (MESH_FORMAT_POSITION | MESH_FORMAT_UV) +#define MESH_FORMAT_PCU \ + (MESH_FORMAT_POSITION | MESH_FORMAT_COLOR | MESH_FORMAT_UV) +#define MESH_FORMAT_PN (MESH_FORMAT_POSITION | MESH_FORMAT_NORMAL) +#define MESH_FORMAT_PNU \ + (MESH_FORMAT_POSITION | MESH_FORMAT_NORMAL | MESH_FORMAT_UV) +#define MESH_FORMAT_PNC \ + (MESH_FORMAT_POSITION | MESH_FORMAT_NORMAL | MESH_FORMAT_COLOR) +#define MESH_FORMAT_PNCU ( \ + MESH_FORMAT_POSITION | MESH_FORMAT_NORMAL | \ + MESH_FORMAT_COLOR | MESH_FORMAT_UV \ +) + +// Component counts and sizes of each attribute within a vertex. +#define MESH_VERTEX_POSITION_SIZE 3 +#define MESH_VERTEX_NORMAL_SIZE 3 +#define MESH_VERTEX_COLOR_SIZE 4 +#define MESH_VERTEX_UV_SIZE 2 + +/** + * Checks that a format is valid, i.e. has a position and no unknown bits. + * + * @param format The format to check. + * @return True if the format is valid. + */ +bool_t meshFormatIsValid(const meshformat_t format); + +/** + * Checks whether a format contains every attribute in a set of flags. + * + * @param format The format to check. + * @param flags One or more MESH_FORMAT_* flags that must all be present. + * @return True if all of the flags are present in the format. + */ +bool_t meshFormatHas(const meshformat_t format, const meshformat_t flags); + +/** + * Gets the size in bytes of a single attribute. + * + * @param attribute Exactly one MESH_FORMAT_* flag. + * @return The size in bytes of that attribute. + */ +size_t meshFormatAttributeSize(const meshformat_t attribute); + +/** + * Gets the size in bytes of one whole vertex of the given format. Attributes + * are interleaved in the order position, normal, color, uv. + * + * @param format The format to get the stride of. + * @return The size in bytes of one vertex. + */ +size_t meshFormatStride(const meshformat_t format); + +/** + * Gets the byte offset of an attribute within a vertex of the given format. + * + * @param format The format of the vertex. + * @param attribute Exactly one MESH_FORMAT_* flag, which must be in format. + * @return The byte offset of the attribute from the start of the vertex. + */ +size_t meshFormatOffset( + const meshformat_t format, + const meshformat_t attribute +); diff --git a/src/dusk/display/mesh/meshvertex.h b/src/dusk/display/mesh/meshvertex.h new file mode 100644 index 00000000..8a732b28 --- /dev/null +++ b/src/dusk/display/mesh/meshvertex.h @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "display/mesh/meshformat.h" +#include "assert/assert.h" + +/** + * Typed helpers matching each mesh format's interleaved layout (position, + * normal, color, uv). The format flags and meshFormatStride() are the source + * of truth; these exist for building meshes in C, and are size checked below. + */ + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; +} meshvertexp_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + uint8_t color[MESH_VERTEX_COLOR_SIZE]; +} meshvertexpc_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + float_t uv[MESH_VERTEX_UV_SIZE]; +} meshvertexpu_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + uint8_t color[MESH_VERTEX_COLOR_SIZE]; + float_t uv[MESH_VERTEX_UV_SIZE]; +} meshvertexpcu_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + float_t normal[MESH_VERTEX_NORMAL_SIZE]; +} meshvertexpn_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + float_t normal[MESH_VERTEX_NORMAL_SIZE]; + float_t uv[MESH_VERTEX_UV_SIZE]; +} meshvertexpnu_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + float_t normal[MESH_VERTEX_NORMAL_SIZE]; + uint8_t color[MESH_VERTEX_COLOR_SIZE]; +} meshvertexpnc_t; + +typedef struct { + float_t pos[MESH_VERTEX_POSITION_SIZE]; + float_t normal[MESH_VERTEX_NORMAL_SIZE]; + uint8_t color[MESH_VERTEX_COLOR_SIZE]; + float_t uv[MESH_VERTEX_UV_SIZE]; +} meshvertexpncu_t; + +assertStructSize(meshvertexp_t, 12); +assertStructSize(meshvertexpc_t, 16); +assertStructSize(meshvertexpu_t, 20); +assertStructSize(meshvertexpcu_t, 24); +assertStructSize(meshvertexpn_t, 24); +assertStructSize(meshvertexpnu_t, 32); +assertStructSize(meshvertexpnc_t, 28); +assertStructSize(meshvertexpncu_t, 36); diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 806c5086..47bd279c 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -10,6 +10,9 @@ #include "perf/perf.h" #include "time/time.h" #include "display/display.h" +#include "game/game.h" +#include "resource/resourcemanager.h" +#include "asset/asset.h" #include "assert/assert.h" #include "console/console.h" @@ -27,7 +30,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { perfInit(); timeInit(); consoleInit(); + errorChain(assetInit()); + errorChain(resourceManagerInit()); errorChain(displayInit()); + errorChain(gameInit()); consolePrint("Engine initialized"); @@ -44,6 +50,8 @@ errorret_t engineUpdate(void) { // Order here is important. timeUpdate(); consoleUpdate(); + errorChain(resourceManagerUpdate()); + errorChain(gameUpdate()); errorChain(displayUpdate()); errorOk(); @@ -54,7 +62,10 @@ void engineExit(void) { } errorret_t engineDispose(void) { + errorChain(gameDispose()); errorChain(displayDispose()); + errorChain(resourceManagerDispose()); + errorChain(assetDispose()); consoleDispose(); perfDispose(); diff --git a/src/dusk/game/game.h b/src/dusk/game/game.h new file mode 100644 index 00000000..3e854e00 --- /dev/null +++ b/src/dusk/game/game.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Initializes the game. Implemented by the game project being built (e.g. + * duskexample), not by the engine. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t gameInit(void); + +/** + * Updates the game, called once per frame. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t gameUpdate(void); + +/** + * Disposes of the game. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t gameDispose(void); diff --git a/src/dusk/resource/CMakeLists.txt b/src/dusk/resource/CMakeLists.txt new file mode 100644 index 00000000..30ae27f0 --- /dev/null +++ b/src/dusk/resource/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + resource.c + resourcemanager.c +) + +add_subdirectory(loader) diff --git a/src/dusk/resource/loader/CMakeLists.txt b/src/dusk/resource/loader/CMakeLists.txt new file mode 100644 index 00000000..84d7b596 --- /dev/null +++ b/src/dusk/resource/loader/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + resourcetext.c +) diff --git a/src/dusk/resource/loader/resourcetext.c b/src/dusk/resource/loader/resourcetext.c new file mode 100644 index 00000000..a60f5933 --- /dev/null +++ b/src/dusk/resource/loader/resourcetext.c @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "resource/loader/resourcetext.h" +#include "assert/assert.h" +#include "util/memory.h" + +errorret_t resourceTextLoadAsync(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertTrue( + resource->type == RESOURCE_TYPE_TXT || + resource->type == RESOURCE_TYPE_OBJ, + "Resource is not loaded as text" + ); + assertNull(resource->data.text, "Text already loaded"); + + uint8_t *buffer; + size_t size; + errorChain(resourceReadSource(resource, 1, &buffer, &size)); + + resource->data.text = (char_t *)buffer; + errorOk(); +} + +errorret_t resourceTextLoadSync(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertTrue( + resource->type == RESOURCE_TYPE_TXT || + resource->type == RESOURCE_TYPE_OBJ, + "Resource is not loaded as text" + ); + assertNotNull(resource->data.text, "Text was not loaded"); + errorOk(); +} + +errorret_t resourceTextDispose(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertTrue( + resource->type == RESOURCE_TYPE_TXT || + resource->type == RESOURCE_TYPE_OBJ, + "Resource is not loaded as text" + ); + + if(resource->data.text != NULL) { + memoryFree(resource->data.text); + resource->data.text = NULL; + } + errorOk(); +} diff --git a/src/dusk/resource/loader/resourcetext.h b/src/dusk/resource/loader/resourcetext.h new file mode 100644 index 00000000..51acb786 --- /dev/null +++ b/src/dusk/resource/loader/resourcetext.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "resource/resource.h" + +/** + * Async stage of loading a text resource. Reads the bytes from the resource's + * source and stores them as a null terminated string in resource->data.text. + * + * @param resource The resource to load. + * @return An error if the data cannot be read. + */ +errorret_t resourceTextLoadAsync(resource_t *resource); + +/** + * Sync stage of loading a text resource. Nothing is left to do on the main + * thread for text. + * + * @param resource The resource to load. + * @return An errorret_t indicating success or failure. + */ +errorret_t resourceTextLoadSync(resource_t *resource); + +/** + * Frees the text of a loaded text resource. + * + * @param resource The resource to dispose. + * @return An errorret_t indicating success or failure. + */ +errorret_t resourceTextDispose(resource_t *resource); diff --git a/src/dusk/resource/resource.c b/src/dusk/resource/resource.c new file mode 100644 index 00000000..88bc4110 --- /dev/null +++ b/src/dusk/resource/resource.c @@ -0,0 +1,253 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "resource.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" +#include "thread/thread.h" +#include "asset/asset.h" +#include "resource/resourcemanager.h" +#include "resource/loader/resourcetext.h" + +const resourcetypecallbacks_t RESOURCE_TYPE_CALLBACKS[RESOURCE_TYPE_COUNT] = { + [RESOURCE_TYPE_NULL] = { NULL, NULL, NULL }, + [RESOURCE_TYPE_TXT] = { + resourceTextLoadAsync, resourceTextLoadSync, resourceTextDispose + }, + // TODO: No mesh parsing yet, so .obj is kept as its raw text. + [RESOURCE_TYPE_OBJ] = { + resourceTextLoadAsync, resourceTextLoadSync, resourceTextDispose + }, +}; + +// File extension (without the dot) of each resource type. +const resourcetypeextension_t RESOURCE_TYPE_EXTENSIONS[] = { + { "txt", RESOURCE_TYPE_TXT }, + { "obj", RESOURCE_TYPE_OBJ }, +}; +const size_t RESOURCE_TYPE_EXTENSION_COUNT = ( + sizeof(RESOURCE_TYPE_EXTENSIONS) / sizeof(RESOURCE_TYPE_EXTENSIONS[0]) +); + +void resourceInit(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + memoryZero(resource, sizeof(resource_t)); + refInit(&resource->ref, resource, NULL, NULL, NULL); +} + +resourceloader_t resourceLoaderFromProtocol(const char_t *protocol) { + assertNotNull(protocol, "Protocol cannot be NULL"); + + if(stringEquals(protocol, URI_PROTOCOL_ASSET)) { + return RESOURCE_LOADER_ASSET; + } + if( + stringEquals(protocol, URI_PROTOCOL_HTTP) || + stringEquals(protocol, URI_PROTOCOL_HTTPS) + ) { + return RESOURCE_LOADER_HTTP; + } + + return RESOURCE_LOADER_NULL; +} + +void resourceSetURI(resource_t *resource, const uri_t *uri) { + assertNotNull(resource, "Resource cannot be NULL"); + assertNotNull(uri, "URI cannot be NULL"); + assertTrue(uriIsValid(uri), "URI is not valid"); + + resource->uri = *uri; + + resource->loader = resourceLoaderFromProtocol(resource->uri.protocol); + assertTrue( + resource->loader != RESOURCE_LOADER_NULL, + "URI protocol has no resource loader" + ); + + resource->type = resourceTypeFromPath(resource->uri.path); +} + +void resourceSetURIString(resource_t *resource, const char_t *uriString) { + assertNotNull(resource, "Resource cannot be NULL"); + assertNotNull(uriString, "URI string cannot be NULL"); + + uri_t uri = uriFromString(uriString); + resourceSetURI(resource, &uri); +} + +resourcetype_t resourceTypeFromPath(const char_t *path) { + assertNotNull(path, "Path cannot be NULL"); + + // The extension is whatever follows the last '.' in the filename. + const char_t *dot = stringFindLastChar(path, '.'); + const char_t *slash = stringFindLastChar(path, '/'); + if(dot == NULL || (slash != NULL && dot < slash)) return RESOURCE_TYPE_NULL; + const char_t *extension = dot + 1; + + const resourcetypeextension_t *entry = RESOURCE_TYPE_EXTENSIONS; + const resourcetypeextension_t *end = ( + RESOURCE_TYPE_EXTENSIONS + RESOURCE_TYPE_EXTENSION_COUNT + ); + do { + if(stringCompareInsensitive(extension, entry->extension) == 0) { + return entry->type; + } + entry++; + } while(entry < end); + + return RESOURCE_TYPE_NULL; +} + +void resourceSetState(resource_t *resource, const resourcestate_t state) { + assertNotNull(resource, "Resource cannot be NULL"); + + threadMutexLock(&RESOURCE_MANAGER.mutex); + resource->state = state; + threadMutexUnlock(&RESOURCE_MANAGER.mutex); +} + +void resourceLoad(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertTrue( + resource->type != RESOURCE_TYPE_NULL, + "Resource cannot be loaded with a NULL type" + ); + assertTrue( + resource->loader != RESOURCE_LOADER_NULL, + "Resource cannot be loaded with a NULL loader" + ); + assertTrue( + resource->state == RESOURCE_STATE_INITIAL, + "Resource must be in INITIAL state to load" + ); + + resourceSetState(resource, RESOURCE_STATE_PENDING_ASYNC); +} + +errorret_t resourceLoadAsync(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertNotMainThread("Async loading must not run on the main thread"); + assertTrue( + resource->state == RESOURCE_STATE_LOADING_ASYNC, + "Resource must be in LOADING_ASYNC state" + ); + + const resourcetypecallbacks_t *callbacks = ( + &RESOURCE_TYPE_CALLBACKS[resource->type] + ); + assertNotNull(callbacks->loadAsync, "Resource type has no async loader"); + + errorret_t ret = callbacks->loadAsync(resource); + if(errorIsNotOk(ret)) { + resourceSetState(resource, RESOURCE_STATE_FAILED); + errorChain(ret); + } + + resourceSetState(resource, RESOURCE_STATE_PENDING_SYNC); + errorOk(); +} + +errorret_t resourceLoadSync(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertIsMainThread("Sync loading must run on the main thread"); + assertTrue( + resource->state == RESOURCE_STATE_LOADING_SYNC, + "Resource must be in LOADING_SYNC state" + ); + + const resourcetypecallbacks_t *callbacks = ( + &RESOURCE_TYPE_CALLBACKS[resource->type] + ); + assertNotNull(callbacks->loadSync, "Resource type has no sync loader"); + + errorret_t ret = callbacks->loadSync(resource); + if(errorIsNotOk(ret)) { + resourceSetState(resource, RESOURCE_STATE_FAILED); + errorChain(ret); + } + + resourceSetState(resource, RESOURCE_STATE_LOADED); + errorOk(); +} + +errorret_t resourceReadSource( + resource_t *resource, + const size_t extraBytes, + uint8_t **outBuffer, + size_t *outSize +) { + assertNotNull(resource, "Resource cannot be NULL"); + assertNotNull(outBuffer, "outBuffer cannot be NULL"); + assertNotNull(outSize, "outSize cannot be NULL"); + + switch(resource->loader) { + case RESOURCE_LOADER_ASSET: { + // "asset:///dir/file.txt" is the asset "dir/file.txt" + const char_t *filename = resource->uri.path; + if(filename[0] == '/') filename++; + if(filename[0] == '\0') { + errorThrow("Asset URI has no path: %s", resource->uri.host); + } + + assetfile_t file; + errorChain(assetFileInit(&file, filename)); + if(file.size == 0) { + errorThrow("Resource is empty: %s", filename); + } + + uint8_t *buffer = (uint8_t *)memoryAllocate(file.size + extraBytes); + errorret_t ret = assetFileOpen(&file); + if(errorIsOk(ret)) ret = assetFileRead(&file, buffer, file.size); + if(errorIsOk(ret) && file.lastRead != file.size) { + ret = errorThrowImpl( + &ERROR_STATE, ERROR_NOT_OK, __FILE__, __func__, __LINE__, + "Failed to read entire resource: %s", filename + ); + } + if(file.file != NULL) assetFileClose(&file); + if(errorIsNotOk(ret)) { + memoryFree(buffer); + errorChain(ret); + } + + if(extraBytes > 0) memoryZero(buffer + file.size, extraBytes); + *outBuffer = buffer; + *outSize = file.size; + errorOk(); + } + + case RESOURCE_LOADER_HTTP: + errorThrow("HTTP resources are not supported yet"); + + default: + errorThrow("Resource has no loader"); + } +} + +errorret_t resourceDispose(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + assertIsMainThread("Resources must be disposed on the main thread"); + + const resourcetypecallbacks_t *callbacks = ( + &RESOURCE_TYPE_CALLBACKS[resource->type] + ); + if(callbacks->dispose != NULL) errorChain(callbacks->dispose(resource)); + + resourceInit(resource); + errorOk(); +} + +void resourceLock(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + refLock(&resource->ref); +} + +bool_t resourceUnlock(resource_t *resource) { + assertNotNull(resource, "Resource cannot be NULL"); + return refUnlock(&resource->ref); +} diff --git a/src/dusk/resource/resource.h b/src/dusk/resource/resource.h new file mode 100644 index 00000000..9c9b0d02 --- /dev/null +++ b/src/dusk/resource/resource.h @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "util/uri.h" +#include "util/ref.h" +#include "error/error.h" + +typedef enum { + RESOURCE_TYPE_NULL, + + RESOURCE_TYPE_TXT, + RESOURCE_TYPE_OBJ, + + RESOURCE_TYPE_COUNT, +} resourcetype_t; + +typedef enum { + RESOURCE_STATE_INITIAL, + + // Queued for the resource manager thread to run resourceLoadAsync. + RESOURCE_STATE_PENDING_ASYNC, + RESOURCE_STATE_LOADING_ASYNC, + + // Async stage done, waiting for the main thread to run resourceLoadSync. + RESOURCE_STATE_PENDING_SYNC, + RESOURCE_STATE_LOADING_SYNC, + + RESOURCE_STATE_LOADED, + RESOURCE_STATE_FAILED, + RESOURCE_STATE_UNLOADING, +} resourcestate_t; + +typedef union { + char_t *text; + // mesh_t mesh; + // meshvertice_t vertices; +} resourcedata_t; + +typedef enum { + RESOURCE_LOADER_NULL, + RESOURCE_LOADER_ASSET, + RESOURCE_LOADER_HTTP, + RESOURCE_LOADER_COUNT, +} resourceloader_t; + +typedef struct resource_s resource_t; + +struct resource_s { + uri_t uri; + ref_t ref; + resourcetype_t type; + resourcestate_t state; + resourcedata_t data; + resourceloader_t loader; +}; + +/** + * Callbacks a resource type provides to load and dispose its data. + */ +typedef struct { + /** Runs on the resource manager thread. Reads and parses the data. */ + errorret_t (*loadAsync)(resource_t *resource); + + /** Runs on the main thread. Finishes loading, e.g. GPU uploads. */ + errorret_t (*loadSync)(resource_t *resource); + + /** Runs on the main thread. Frees the data of a loaded resource. */ + errorret_t (*dispose)(resource_t *resource); +} resourcetypecallbacks_t; + +/** + * Maps a file extension to the resource type it loads as. + */ +typedef struct { + const char_t *extension; + resourcetype_t type; +} resourcetypeextension_t; + +extern const resourcetypeextension_t RESOURCE_TYPE_EXTENSIONS[]; +extern const size_t RESOURCE_TYPE_EXTENSION_COUNT; + +extern const resourcetypecallbacks_t RESOURCE_TYPE_CALLBACKS[ + RESOURCE_TYPE_COUNT +]; + +/** + * Initializes a resource structure. Sets it to a NULL state. + * + * @param resource The resource to initialize. + */ +void resourceInit(resource_t *resource); + +/** + * Locks a resource, incrementing its reference count. Every lock must be + * matched by a resourceUnlock. + * + * @param resource The resource to lock. + */ +void resourceLock(resource_t *resource); + +/** + * Unlocks a resource, decrementing its reference count. The count cannot go + * negative. + * + * @param resource The resource to unlock. + * @return True if this was the last lock and the resource is now unreferenced. + */ +bool_t resourceUnlock(resource_t *resource); + +/** + * Determines which loader handles a URI protocol: "asset" is read from the + * asset archive, and "http" / "https" are fetched over the network. + * + * @param protocol The URI protocol, e.g. "asset" (see URI_PROTOCOL_*). + * @return The loader for the protocol, or RESOURCE_LOADER_NULL if there isn't + * one. + */ +resourceloader_t resourceLoaderFromProtocol(const char_t *protocol); + +/** + * Sets the URI of a resource, and determines its loader from the URI's + * protocol and its type from the extension of the URI's path. The protocol must + * be one that has a loader. + * + * @param resource The resource to set the URI of. + * @param uri The URI to set, which must be valid. + */ +void resourceSetURI(resource_t *resource, const uri_t *uri); + +/** + * Shorthand for resourceSetURI that parses the URI from a string first. + * + * @param resource The resource to set the URI of. + * @param uriString The URI string, e.g. "asset:///models/cube.obj". + */ +void resourceSetURIString(resource_t *resource, const char_t *uriString); + +/** + * Determines the resource type from the extension of a path, e.g. ".txt". + * Matching is case insensitive. + * + * @param path The path to inspect, e.g. "/docs/readme.txt". + * @return The type, or RESOURCE_TYPE_NULL if the extension is not supported. + */ +resourcetype_t resourceTypeFromPath(const char_t *path); + +/** + * Sets the state of a resource. Safe to call from any thread, as the resource + * manager thread and the main thread both change states. + * + * @param resource The resource to change. + * @param state The state to set. + */ +void resourceSetState(resource_t *resource, const resourcestate_t state); + +/** + * Starts loading a resource. It is queued for the resource manager thread, + * and the resource manager finishes it on the main thread. Requires the URI to + * have been set so the type and loader are known. + * + * @param resource The resource to load, in the INITIAL state. + */ +void resourceLoad(resource_t *resource); + +/** + * Runs the async stage of loading a resource, on the resource manager thread. + * Moves the resource to PENDING_SYNC, or to FAILED if there is an error. + * + * @param resource The resource, in the LOADING_ASYNC state. + * @return An error if the load failed. + */ +errorret_t resourceLoadAsync(resource_t *resource); + +/** + * Runs the sync stage of loading a resource, on the main thread. Moves the + * resource to LOADED, or to FAILED if there is an error. + * + * @param resource The resource, in the LOADING_SYNC state. + * @return An error if the load failed. + */ +errorret_t resourceLoadSync(resource_t *resource); + +/** + * Reads the raw bytes of a resource from its source, chosen by the resource's + * loader (asset archive, network). Called by the type loaders. + * + * @param resource The resource to read, with its URI set. + * @param extraBytes Zeroed bytes to add after the data, e.g. 1 for a string. + * @param outBuffer Output buffer, allocated with memoryAllocate. The caller + * must memoryFree it. + * @param outSize Output size of the data in bytes, excluding extraBytes. + * @return An error if the data cannot be read. + */ +errorret_t resourceReadSource( + resource_t *resource, + const size_t extraBytes, + uint8_t **outBuffer, + size_t *outSize +); + +/** + * Frees the data of a loaded resource and returns it to the INITIAL state. + * + * @param resource The resource to dispose. + * @return An error if the type's dispose fails. + */ +errorret_t resourceDispose(resource_t *resource); diff --git a/src/dusk/resource/resourcemanager.c b/src/dusk/resource/resourcemanager.c new file mode 100644 index 00000000..9e9218c1 --- /dev/null +++ b/src/dusk/resource/resourcemanager.c @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "resourcemanager.h" +#include "assert/assert.h" +#include "util/memory.h" + +resourcemanager_t RESOURCE_MANAGER; + +errorret_t resourceManagerInit(void) { + memoryZero(&RESOURCE_MANAGER, sizeof(resourcemanager_t)); + + // do/while walking a pointer, the count is a compile time constant > 0. + assertStatic( + RESOURCE_MANAGER_RESOURCE_COUNT_MAX > 0, + "Resource manager needs at least one resource slot" + ); + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + do { + resourceInit(resource); + resource++; + } while(resource < end); + + threadMutexInit(&RESOURCE_MANAGER.mutex); + threadInit(&RESOURCE_MANAGER.loadThread, resourceManagerLoadThread); + threadStart(&RESOURCE_MANAGER.loadThread); + + errorOk(); +} + +void resourceManagerLoadThread(thread_t *thread) { + assertNotMainThread("Resource load thread must not be the main thread"); + + while(!threadShouldStop(thread)) { + // Claim the next resource waiting for its async stage. + resource_t *next = NULL; + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + + threadMutexLock(&RESOURCE_MANAGER.mutex); + do { + if(resource->state == RESOURCE_STATE_PENDING_ASYNC) { + resource->state = RESOURCE_STATE_LOADING_ASYNC; + next = resource; + break; + } + resource++; + } while(resource < end); + threadMutexUnlock(&RESOURCE_MANAGER.mutex); + + if(next == NULL) { + threadSleepMs(1); + continue; + } + + // resourceLoadAsync sets the final state itself, we just report errors. + errorret_t ret = resourceLoadAsync(next); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } +} + +errorret_t resourceManagerUpdate(void) { + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + + do { + // Claim it if its async stage has finished. + bool_t ready = false; + threadMutexLock(&RESOURCE_MANAGER.mutex); + if(resource->state == RESOURCE_STATE_PENDING_SYNC) { + resource->state = RESOURCE_STATE_LOADING_SYNC; + ready = true; + } + threadMutexUnlock(&RESOURCE_MANAGER.mutex); + + // resourceLoadSync sets the final state itself, we just report errors. + if(ready) { + errorret_t ret = resourceLoadSync(resource); + if(errorIsNotOk(ret)) errorCatch(errorPrint(ret)); + } + + resource++; + } while(resource < end); + + errorOk(); +} + +errorret_t resourceManagerDispose(void) { + // Stop loading before touching any resource. + threadStop(&RESOURCE_MANAGER.loadThread); + + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + do { + if(resource->state == RESOURCE_STATE_LOADED) { + errorChain(resourceDispose(resource)); + } + resource++; + } while(resource < end); + + threadMutexDispose(&RESOURCE_MANAGER.mutex); + memoryZero(&RESOURCE_MANAGER, sizeof(resourcemanager_t)); + errorOk(); +} + +resource_t * resourceManagerGetByURI(const uri_t *uri) { + assertNotNull(uri, "URI cannot be NULL"); + assertTrue(uriIsValid(uri), "URI is not valid"); + assertIsMainThread("Resources must be requested from the main thread"); + + // A slot is in use once it has a URI. + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + do { + if( + resource->uri.protocol[0] != '\0' && + uriEquals(&resource->uri, uri) + ) { + return resource; + } + resource++; + } while(resource < end); + + return NULL; +} + +resource_t * resourceManagerGetByURIString(const char_t *uriString) { + assertNotNull(uriString, "URI string cannot be NULL"); + + uri_t uri = uriFromString(uriString); + return resourceManagerGetByURI(&uri); +} + +resource_t * resourceManagerGetOrCreateByURI(const uri_t *uri) { + resource_t *existing = resourceManagerGetByURI(uri); + if(existing != NULL) return existing; + + // Not requested yet, take the first free slot. + resource_t *resource = RESOURCE_MANAGER.resources; + const resource_t *end = resource + RESOURCE_MANAGER_RESOURCE_COUNT_MAX; + do { + if(resource->uri.protocol[0] == '\0') { + resourceSetURI(resource, uri); + return resource; + } + resource++; + } while(resource < end); + + assertUnreachable("No free resource slots"); + return NULL; +} + +resource_t * resourceManagerGetOrCreateByURIString(const char_t *uriString) { + assertNotNull(uriString, "URI string cannot be NULL"); + + uri_t uri = uriFromString(uriString); + return resourceManagerGetOrCreateByURI(&uri); +} + +resource_t * resourceManagerLock(const char_t *uriString) { + resource_t *resource = resourceManagerGetOrCreateByURIString(uriString); + resourceLock(resource); + return resource; +} + +bool_t resourceManagerUnlock(const char_t *uriString) { + resource_t *resource = resourceManagerGetByURIString(uriString); + assertNotNull(resource, "Cannot unlock a resource that was never locked"); + return resourceUnlock(resource); +} diff --git a/src/dusk/resource/resourcemanager.h b/src/dusk/resource/resourcemanager.h new file mode 100644 index 00000000..b5e5066e --- /dev/null +++ b/src/dusk/resource/resourcemanager.h @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "resource/resource.h" +#include "thread/thread.h" + +// Maximum number of resources the manager can hold at once. +#ifndef RESOURCE_MANAGER_RESOURCE_COUNT_MAX + #define RESOURCE_MANAGER_RESOURCE_COUNT_MAX 64 +#endif + +typedef struct { + resource_t resources[RESOURCE_MANAGER_RESOURCE_COUNT_MAX]; + + // Guards resource states, which the load thread and main thread both change. + threadmutex_t mutex; + + // Runs the async stage of loading resources. + thread_t loadThread; +} resourcemanager_t; + +extern resourcemanager_t RESOURCE_MANAGER; + +/** + * Initializes the resource manager, setting every resource slot to an + * initial, unused state. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t resourceManagerInit(void); + +/** + * Body of the resource manager's load thread. Runs the async stage of every + * resource queued by resourceLoad. + * + * @param thread The thread this runs on. + */ +void resourceManagerLoadThread(thread_t *thread); + +/** + * Updates the resource manager, called once per frame. Runs the sync stage of + * every resource whose async stage has finished. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t resourceManagerUpdate(void); + +/** + * Disposes of the resource manager. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t resourceManagerDispose(void); + +/** + * Finds the resource that was requested for a URI. Does not create one, see + * resourceManagerGetOrCreateByURI for that. Main thread only. + * + * @param uri The URI of the resource, which must be valid. + * @return The resource for the URI, or NULL if there is not one. + */ +resource_t * resourceManagerGetByURI(const uri_t *uri); + +/** + * Shorthand for resourceManagerGetByURI that parses the URI from a string. + * + * @param uriString The URI string, e.g. "asset:///models/cube.obj". + * @return The resource for the URI, or NULL if there is not one. + */ +resource_t * resourceManagerGetByURIString(const char_t *uriString); + +/** + * Gets the resource for a URI, which is the existing resource if the URI has + * already been requested, otherwise a free slot that is set up for the URI. + * It is not locked and does not start loading, the caller does that with + * resourceLock and resourceLoad. Main thread only. + * + * @param uri The URI of the resource, which must be valid. + * @return The resource for the URI. Asserts if there are no free slots. + */ +resource_t * resourceManagerGetOrCreateByURI(const uri_t *uri); + +/** + * Shorthand for resourceManagerGetOrCreateByURI that parses the URI from a + * string. + * + * @param uriString The URI string, e.g. "asset:///models/cube.obj". + * @return The resource for the URI. Asserts if there are no free slots. + */ +resource_t * resourceManagerGetOrCreateByURIString(const char_t *uriString); + +/** + * Locks the resource for a URI string, creating it first if it has not been + * requested yet. Shorthand for resourceManagerGetOrCreateByURIString followed + * by resourceLock. It does not start loading, call resourceLoad for that. Every + * lock must be matched by a resourceManagerUnlock. Main thread only. + * + * @param uriString The URI string, e.g. "asset:///models/cube.obj". + * @return The locked resource. Asserts if there are no free slots. + */ +resource_t * resourceManagerLock(const char_t *uriString); + +/** + * Unlocks the resource for a URI string, which must have been locked with + * resourceManagerLock. Shorthand for resourceManagerGetByURIString followed by + * resourceUnlock. Main thread only. + * + * @param uriString The URI string that was locked. + * @return True if this was the last lock and the resource is now unreferenced. + */ +bool_t resourceManagerUnlock(const char_t *uriString); diff --git a/src/dusk/thread/thread.c b/src/dusk/thread/thread.c index f0d09195..33c7e086 100644 --- a/src/dusk/thread/thread.c +++ b/src/dusk/thread/thread.c @@ -9,6 +9,10 @@ #include "util/memory.h" #include "assert/assert.h" +#ifdef DUSK_THREAD_PTHREAD + #include +#endif + void threadInit(thread_t *thread, const threadcallback_t callback) { assertNotNull(thread, "Thread cannot be NULL."); assertNotNull(callback, "Thread callback cannot be NULL."); @@ -114,4 +118,13 @@ bool_t threadShouldStop(thread_t *thread) { threadMutexUnlock(&thread->stateMutex); return NULL; } -#endif \ No newline at end of file +#endif + +void threadSleepMs(const uint32_t milliseconds) { + #ifdef DUSK_THREAD_PTHREAD + struct timespec ts; + ts.tv_sec = milliseconds / 1000; + ts.tv_nsec = (long)(milliseconds % 1000) * 1000000L; + nanosleep(&ts, NULL); + #endif +} diff --git a/src/dusk/thread/thread.h b/src/dusk/thread/thread.h index 046747f5..ef0c91e3 100644 --- a/src/dusk/thread/thread.h +++ b/src/dusk/thread/thread.h @@ -94,4 +94,11 @@ bool_t threadShouldStop(thread_t *thread); * @param thread Pointer to the thread structure to handle. */ void * threadHandler(thread_t *thread); -#endif \ No newline at end of file +#endif + +/** + * Sleeps the calling thread. + * + * @param milliseconds How long to sleep for. + */ +void threadSleepMs(const uint32_t milliseconds); diff --git a/src/dusk/util/CMakeLists.txt b/src/dusk/util/CMakeLists.txt index 62fb7902..741eb4a3 100644 --- a/src/dusk/util/CMakeLists.txt +++ b/src/dusk/util/CMakeLists.txt @@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} math.c sort.c ref.c + uri.c ) \ No newline at end of file diff --git a/src/dusk/util/ref.c b/src/dusk/util/ref.c index faf84a9c..f77332e6 100644 --- a/src/dusk/util/ref.c +++ b/src/dusk/util/ref.c @@ -27,16 +27,16 @@ void refInit( void refLock(ref_t *ref) { assertNotNull(ref, "Ref cannot be NULL."); - assertTrue(ref->count >= 0, "Cannot lock a ref with negative count."); + assertTrue(ref->count >= 0, "Ref count cannot be negative."); ref->count++; if(ref->onLock != NULL) ref->onLock(ref); } bool_t refUnlock(ref_t *ref) { assertNotNull(ref, "Ref cannot be NULL."); - assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count."); ref->count--; + assertTrue(ref->count >= 0, "Ref count cannot be negative."); if(ref->count > 0) { if(ref->onUnlock != NULL) ref->onUnlock(ref); diff --git a/src/dusk/util/ref.h b/src/dusk/util/ref.h index e2c41af5..144c515c 100644 --- a/src/dusk/util/ref.h +++ b/src/dusk/util/ref.h @@ -12,7 +12,7 @@ typedef struct ref_s ref_t; typedef void (*refcallback_t)(ref_t *ref); typedef struct ref_s { - uint32_t count; + int32_t count; void *data; refcallback_t onLock; refcallback_t onUnlock; diff --git a/src/dusk/util/uri.c b/src/dusk/util/uri.c new file mode 100644 index 00000000..0854e31b --- /dev/null +++ b/src/dusk/util/uri.c @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uri.h" +#include "assert/assert.h" +#include "util/memory.h" +#include "util/string.h" + +uri_t uriFromString(const char_t *uriString) { + assertNotNull(uriString, "URI string cannot be NULL"); + assertStrLenMin(uriString, 1, "URI string cannot be empty"); + assertStrLenMax(uriString, URI_STRING_SIZE_MAX, "URI string is too long"); + + uri_t out; + memoryZero(&out, sizeof(uri_t)); + + // Protocol, everything before "://" + const char_t *protocolEnd = uriString; + while( + *protocolEnd != '\0' && + !( + protocolEnd[0] == ':' && protocolEnd[1] == '/' && protocolEnd[2] == '/' + ) + ) { + protocolEnd++; + } + assertTrue( + *protocolEnd != '\0', + "URI string must contain a protocol (e.g. 'http://')" + ); + + size_t protocolLength = (size_t)(protocolEnd - uriString); + assertTrue(protocolLength > 0, "URI protocol cannot be empty"); + assertTrue(protocolLength < URI_PROTOCOL_SIZE, "URI protocol is too long"); + memoryCopy(out.protocol, uriString, protocolLength); + out.protocol[protocolLength] = '\0'; + + // Host, everything after "://" up to the next '/' (or the end) + const char_t *hostStart = protocolEnd + 3; + const char_t *hostEnd = hostStart; + while(*hostEnd != '\0' && *hostEnd != '/') hostEnd++; + + size_t hostLength = (size_t)(hostEnd - hostStart); + assertTrue(hostLength < URI_HOST_SIZE, "URI host is too long"); + if(hostLength > 0) memoryCopy(out.host, hostStart, hostLength); + out.host[hostLength] = '\0'; + + // Path, the remainder including its leading '/', empty if there is none + size_t pathLength = strlen(hostEnd); + assertTrue(pathLength < URI_PATH_SIZE, "URI path is too long"); + if(pathLength > 0) memoryCopy(out.path, hostEnd, pathLength); + out.path[pathLength] = '\0'; + + return out; +} + +uri_t uriCreate(const char_t *protocol, const char_t *host, const char_t *path) { + assertNotNull(protocol, "URI protocol cannot be NULL"); + assertNotNull(host, "URI host cannot be NULL"); + assertNotNull(path, "URI path cannot be NULL"); + assertStrLenMin(protocol, 1, "URI protocol cannot be empty"); + assertStrLenMax(protocol, URI_PROTOCOL_SIZE, "URI protocol is too long"); + assertStrLenMax(host, URI_HOST_SIZE, "URI host is too long"); + assertStrLenMax(path, URI_PATH_SIZE, "URI path is too long"); + + uri_t out; + memoryZero(&out, sizeof(uri_t)); + stringCopy(out.protocol, protocol, URI_PROTOCOL_SIZE); + stringCopy(out.host, host, URI_HOST_SIZE); + stringCopy(out.path, path, URI_PATH_SIZE); + return out; +} + +bool_t uriIsValid(const uri_t *uri) { + if(uri == NULL) return false; + + // Every part must be terminated inside its buffer. + if(memchr(uri->protocol, '\0', URI_PROTOCOL_SIZE) == NULL) return false; + if(memchr(uri->host, '\0', URI_HOST_SIZE) == NULL) return false; + if(memchr(uri->path, '\0', URI_PATH_SIZE) == NULL) return false; + + // Protocol, e.g. "https" + if(uri->protocol[0] == '\0') return false; + if(!isalpha((unsigned char)uri->protocol[0])) return false; + for(const char_t *c = uri->protocol; *c != '\0'; c++) { + if( + !isalnum((unsigned char)*c) && *c != '+' && *c != '-' && *c != '.' + ) return false; + } + + // Host + for(const char_t *c = uri->host; *c != '\0'; c++) { + if(*c == '/' || stringIsWhitespace(*c)) return false; + } + + // Path + if(uri->path[0] != '\0' && uri->path[0] != '/') return false; + for(const char_t *c = uri->path; *c != '\0'; c++) { + if(stringIsWhitespace(*c)) return false; + } + + // Network protocols need somewhere to connect to. + if( + stringEquals(uri->protocol, URI_PROTOCOL_HTTP) || + stringEquals(uri->protocol, URI_PROTOCOL_HTTPS) + ) { + if(uri->host[0] == '\0') return false; + } + + return true; +} + +bool_t uriEquals(const uri_t *a, const uri_t *b) { + assertNotNull(a, "First URI cannot be NULL"); + assertNotNull(b, "Second URI cannot be NULL"); + + return ( + stringEquals(a->protocol, b->protocol) && + stringEquals(a->host, b->host) && + stringEquals(a->path, b->path) + ); +} diff --git a/src/dusk/util/uri.h b/src/dusk/util/uri.h new file mode 100644 index 00000000..9c4f724e --- /dev/null +++ b/src/dusk/util/uri.h @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "dusk.h" + +#define URI_PROTOCOL_ASSET "asset" +#define URI_PROTOCOL_HTTP "http" +#define URI_PROTOCOL_HTTPS "https" + +#define URI_PROTOCOL_SIZE 6 +#define URI_HOST_SIZE 64 +#define URI_PATH_SIZE 256 + +// e.g. "https://google.com/path/to/thing" +#define URI_STRING_SIZE_MAX ( \ + URI_PROTOCOL_SIZE + 3 + \ + URI_HOST_SIZE + 1 + \ + URI_PATH_SIZE + 1 \ +) + +typedef struct { + char_t protocol[URI_PROTOCOL_SIZE]; + char_t host[URI_HOST_SIZE]; + char_t path[URI_PATH_SIZE]; +} uri_t; + +/** + * Parses a URI string into its components. + * + * @param uriString The URI string to parse. + * @return The parsed URI structure. + */ +uri_t uriFromString(const char_t *uriString); + +/** + * Creates a URI from the given components. + * + * @param protocol The protocol (e.g. "http", "https", "asset"). + * @param host The host (e.g. "example.com"). + * @param path The path (e.g. "/path/to/resource"). + * @return The created URI. + */ +uri_t uriCreate(const char_t *protocol, const char_t *host, const char_t *path); + +/** + * Checks whether a URI is well formed. Never asserts, so it is safe to use on + * untrusted or partially built URIs. A URI is valid when: + * - Every part is null terminated within its buffer. + * - The protocol is non-empty, starts with a letter, and only contains + * letters, digits, '+', '-' or '.'. + * - The host contains no '/' and no whitespace. + * - The path is empty or starts with '/', and contains no whitespace. + * - http and https URIs have a host. + * + * @param uri The URI to check. + * @return True if the URI is valid, false otherwise. + */ +bool_t uriIsValid(const uri_t *uri); + +/** + * Checks whether two URIs are the same. The protocol, host and path must all + * match exactly. + * + * @param a The first URI. + * @param b The second URI. + * @return True if the URIs are equal, false otherwise. + */ +bool_t uriEquals(const uri_t *a, const uri_t *b); diff --git a/src/duskexample/CMakeLists.txt b/src/duskexample/CMakeLists.txt new file mode 100644 index 00000000..6b0416d6 --- /dev/null +++ b/src/duskexample/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +target_sources(${DUSK_LIBRARY_TARGET_NAME} + PUBLIC + game/game.c +) diff --git a/src/duskexample/game/game.c b/src/duskexample/game/game.c new file mode 100644 index 00000000..ee7000a4 --- /dev/null +++ b/src/duskexample/game/game.c @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "game/game.h" +#include "console/console.h" +#include "resource/resourcemanager.h" +#include "thread/thread.h" + +errorret_t gameInit(void) { + consolePrint("Game initialized"); + + // Test: load the cube.obj asset as plain text and print it. + resource_t *cube = resourceManagerLock("asset:///cube.obj"); + resourceLoad(cube); + + // Block until the load thread and the main thread have both finished it. + while( + cube->state != RESOURCE_STATE_LOADED && + cube->state != RESOURCE_STATE_FAILED + ) { + errorChain(resourceManagerUpdate()); + threadSleepMs(1); + } + if(cube->state == RESOURCE_STATE_FAILED) { + errorThrow("Failed to load cube.obj"); + } + + printf("%s\n", cube->data.text); + errorOk(); +} + +errorret_t gameUpdate(void) { + errorOk(); +} + +errorret_t gameDispose(void) { + resourceManagerUnlock("asset:///cube.obj"); + consolePrint("Game disposed"); + errorOk(); +} diff --git a/src/dusklinux/CMakeLists.txt b/src/dusklinux/CMakeLists.txt index 1c9f0a51..08172730 100644 --- a/src/dusklinux/CMakeLists.txt +++ b/src/dusklinux/CMakeLists.txt @@ -18,3 +18,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC find_package(Threads REQUIRED) target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Threads::Threads m) + +target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC + asset/assetlinux.c +) diff --git a/src/dusklinux/asset/assetlinux.c b/src/dusklinux/asset/assetlinux.c new file mode 100644 index 00000000..7446609a --- /dev/null +++ b/src/dusklinux/asset/assetlinux.c @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "asset/assetlinux.h" +#include "asset/asset.h" +#include "util/string.h" +#include +#include + +errorret_t assetInitLinux(void) { + // Directory of the running executable. + char_t exe[ASSET_BASE_DIRECTORY_MAX]; + ssize_t len = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if(len <= 0) { + errorThrow("Failed to find the executable path."); + } + exe[len] = '\0'; + + char_t *slash = stringFindLastChar(exe, '/'); + if(slash == NULL) { + errorThrow("Executable path has no directory: %s", exe); + } + *slash = '\0'; + + int32_t written = stringFormat( + ASSET.baseDirectory, ASSET_BASE_DIRECTORY_MAX, "%s/assets", exe + ); + if(written < 0 || written >= ASSET_BASE_DIRECTORY_MAX) { + errorThrow("Asset directory path too long."); + } + + struct stat info; + if(stat(ASSET.baseDirectory, &info) != 0 || !S_ISDIR(info.st_mode)) { + errorThrow("Asset directory not found: %s", ASSET.baseDirectory); + } + + errorOk(); +} + +errorret_t assetDisposeLinux(void) { + errorOk(); +} diff --git a/src/dusklinux/asset/assetlinux.h b/src/dusklinux/asset/assetlinux.h new file mode 100644 index 00000000..6a4987e4 --- /dev/null +++ b/src/dusklinux/asset/assetlinux.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Finds the assets directory, which is the "assets" directory next to the + * running executable, and sets ASSET.baseDirectory to it. + * + * @return An error if the assets directory cannot be found. + */ +errorret_t assetInitLinux(void); + +/** + * Disposes of the Linux asset platform. + * + * @return An errorret_t indicating success or failure. + */ +errorret_t assetDisposeLinux(void); diff --git a/src/dusklinux/asset/assetplatform.h b/src/dusklinux/asset/assetplatform.h new file mode 100644 index 00000000..6126b4fc --- /dev/null +++ b/src/dusklinux/asset/assetplatform.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "asset/assetlinux.h" + +#define assetInitPlatform assetInitLinux +#define assetDisposePlatform assetDisposeLinux diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 00000000..fa3f544a --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +add_subdirectory(run_python) \ No newline at end of file diff --git a/tools/color.py b/tools/color.py new file mode 100644 index 00000000..b0e17dee --- /dev/null +++ b/tools/color.py @@ -0,0 +1,70 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +import argparse +import csv +import os + +parser = argparse.ArgumentParser(description="Color CSV to .h defines") +parser.add_argument("--csv", required=True, help="Path to color CSV file") +parser.add_argument("--output", required=True, help="Path to output .h file") +args = parser.parse_args() + +# Load colors +colors = {} +with open(args.csv, newline="", encoding="utf-8") as f: + reader = csv.DictReader(f) + if "name" not in reader.fieldnames: + raise ValueError("CSV must have a 'name' column") + for row in reader: + name = row["name"] + r, g, b = row["r"], row["g"], row["b"] + a = row["a"] if "a" in row and row["a"] != "" else "1.0" + for ch in (r, g, b, a): + if not (0.0 <= float(ch) <= 1.0): + raise ValueError(f"Color channel {ch!r} for '{name}' out of range [0.0, 1.0]") + colors[name] = (r, g, b, a) + +# Build output +out = [ + "#pragma once", + '#include "dusk.h"', + "", + "typedef float_t colorchannelf_t;", + "typedef uint8_t colorchannel8_t;", + "", + "typedef struct { colorchannelf_t r, g, b; } color3f_t;", + "typedef struct { colorchannelf_t r, g, b, a; } color4f_t;", + "typedef struct { colorchannel8_t r, g, b; } color3b_t;", + "typedef struct { colorchannel8_t r, g, b, a; } color4b_t;", + "typedef color4b_t color_t;", + "", + "#define color3f(r, g, b) ((color3f_t){ r, g, b })", + "#define color4f(r, g, b, a) ((color4f_t){ r, g, b, a })", + "#define color3b(r, g, b) ((color3b_t){ r, g, b })", + "#define color4b(r, g, b, a) ((color4b_t){ r, g, b, a })", + "#define color(r, g, b, a) color4b(r, g, b, a)", + "#define colorHex(hex) color4b(((hex >> 24) & 0xFF), ((hex >> 16) & 0xFF), ((hex >> 8) & 0xFF), (hex & 0xFF))", + "", +] + +for name, (r, g, b, a) in colors.items(): + r8, g8, b8, a8 = (int(float(ch) * 255) for ch in (r, g, b, a)) + macro = "COLOR_" + name.upper() + camel = "".join(p[0].upper() + p[1:].lower() for p in name.split("_")) + + out += [ + f"// {name}", + f"#define {macro}_4B color4b({r8}, {g8}, {b8}, {a8})", + f"#define {macro}_3B color3b({r8}, {g8}, {b8})", + f"#define {macro}_3F color3f({float(r)}f, {float(g)}f, {float(b)}f)", + f"#define {macro}_4F color4f({float(r)}f, {float(g)}f, {float(b)}f, {float(a)}f)", + f"#define {macro} {macro}_4B", + "", + ] + +os.makedirs(os.path.dirname(args.output), exist_ok=True) +with open(args.output, "w", encoding="utf-8") as f: + f.write("\n".join(out)) diff --git a/tools/run_python/CMakeLists.txt b/tools/run_python/CMakeLists.txt new file mode 100644 index 00000000..abd8808a --- /dev/null +++ b/tools/run_python/CMakeLists.txt @@ -0,0 +1,14 @@ +# Copyright (c) 2026 Dominic Masters +# +# This software is released under the MIT License. +# https://opensource.org/licenses/MIT + +function(dusk_run_python CMAKE_TARGET_NAME PYTHON_MODULE) + find_package(Python3 COMPONENTS Interpreter REQUIRED) + + add_custom_target(${CMAKE_TARGET_NAME} ALL + WORKING_DIRECTORY ${DUSK_ROOT_DIR} + COMMAND + ${Python3_EXECUTABLE} -m ${PYTHON_MODULE} ${ARGN} + ) +endfunction()