Add asset, resource system, mesh formats, color, SDL3 display and example game
Asset directory with CMake add_asset (extension dispatched), plain file asset access, resource/resource manager with async load thread and text loader, URI utilities, mesh format helpers, generated color header, and example game hooks wired through the engine. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
65c4a9c051
commit
cad41e456b
47 files changed
+2333
-5
No files matched your search
@@ -11,6 +11,10 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
|||||||
# Directories
|
# Directories
|
||||||
set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}")
|
set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}")
|
||||||
set(DUSK_SOURCES_DIR "${DUSK_ROOT_DIR}/src")
|
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)
|
project(DuskCore VERSION 1.0.0 LANGUAGES C)
|
||||||
|
|
||||||
@@ -65,9 +69,25 @@ if(DUSK_BACKTRACE)
|
|||||||
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
|
||||||
endif()
|
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
|
# Core engine code
|
||||||
add_subdirectory(${DUSK_SOURCES_DIR}/dusk)
|
add_subdirectory(${DUSK_SOURCES_DIR}/dusk)
|
||||||
|
|
||||||
|
# Assets, copied into <build>/assets
|
||||||
|
include(${DUSK_ROOT_DIR}/cmake/asset/asset.cmake)
|
||||||
|
# Separate binary dir, so <build>/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
|
# Platform code
|
||||||
foreach(DIR ${DUSK_PLATFORM_DIRS})
|
foreach(DIR ${DUSK_PLATFORM_DIRS})
|
||||||
add_subdirectory(${DUSK_SOURCES_DIR}/${DIR})
|
add_subdirectory(${DUSK_SOURCES_DIR}/${DIR})
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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_<extension> (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 <build>/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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -13,6 +13,9 @@ option(DUSK_BUILD_TESTS "Enable tests." OFF)
|
|||||||
option(DUSK_NETWORK "Enable network support." ON)
|
option(DUSK_NETWORK "Enable network support." ON)
|
||||||
|
|
||||||
# Game metadata
|
# 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_NAME "Dusk" CACHE STRING "Game display name.")
|
||||||
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder.")
|
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder.")
|
||||||
if(NOT DEFINED DUSK_VERSION)
|
if(NOT DEFINED DUSK_VERSION)
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Modules
|
# Modules
|
||||||
|
add_subdirectory(asset)
|
||||||
add_subdirectory(assert)
|
add_subdirectory(assert)
|
||||||
add_subdirectory(console)
|
add_subdirectory(console)
|
||||||
add_subdirectory(display)
|
add_subdirectory(display)
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
add_subdirectory(error)
|
add_subdirectory(error)
|
||||||
add_subdirectory(perf)
|
add_subdirectory(perf)
|
||||||
|
add_subdirectory(resource)
|
||||||
add_subdirectory(time)
|
add_subdirectory(time)
|
||||||
add_subdirectory(thread)
|
add_subdirectory(thread)
|
||||||
add_subdirectory(util)
|
add_subdirectory(util)
|
||||||
@@ -283,8 +283,25 @@
|
|||||||
|
|
||||||
#endif
|
#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) \
|
#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
|
// EOF
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -7,3 +7,15 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
display.c
|
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)
|
||||||
@@ -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
|
||||||
|
@@ -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
|
||||||
|
)
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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);
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
#include "perf/perf.h"
|
#include "perf/perf.h"
|
||||||
#include "time/time.h"
|
#include "time/time.h"
|
||||||
#include "display/display.h"
|
#include "display/display.h"
|
||||||
|
#include "game/game.h"
|
||||||
|
#include "resource/resourcemanager.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "console/console.h"
|
#include "console/console.h"
|
||||||
|
|
||||||
@@ -27,7 +30,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
|
|||||||
perfInit();
|
perfInit();
|
||||||
timeInit();
|
timeInit();
|
||||||
consoleInit();
|
consoleInit();
|
||||||
|
errorChain(assetInit());
|
||||||
|
errorChain(resourceManagerInit());
|
||||||
errorChain(displayInit());
|
errorChain(displayInit());
|
||||||
|
errorChain(gameInit());
|
||||||
|
|
||||||
consolePrint("Engine initialized");
|
consolePrint("Engine initialized");
|
||||||
|
|
||||||
@@ -44,6 +50,8 @@ errorret_t engineUpdate(void) {
|
|||||||
// Order here is important.
|
// Order here is important.
|
||||||
timeUpdate();
|
timeUpdate();
|
||||||
consoleUpdate();
|
consoleUpdate();
|
||||||
|
errorChain(resourceManagerUpdate());
|
||||||
|
errorChain(gameUpdate());
|
||||||
errorChain(displayUpdate());
|
errorChain(displayUpdate());
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
@@ -54,7 +62,10 @@ void engineExit(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
errorret_t engineDispose(void) {
|
errorret_t engineDispose(void) {
|
||||||
|
errorChain(gameDispose());
|
||||||
errorChain(displayDispose());
|
errorChain(displayDispose());
|
||||||
|
errorChain(resourceManagerDispose());
|
||||||
|
errorChain(assetDispose());
|
||||||
consoleDispose();
|
consoleDispose();
|
||||||
perfDispose();
|
perfDispose();
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -9,6 +9,10 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|
||||||
|
#ifdef DUSK_THREAD_PTHREAD
|
||||||
|
#include <time.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
void threadInit(thread_t *thread, const threadcallback_t callback) {
|
void threadInit(thread_t *thread, const threadcallback_t callback) {
|
||||||
assertNotNull(thread, "Thread cannot be NULL.");
|
assertNotNull(thread, "Thread cannot be NULL.");
|
||||||
assertNotNull(callback, "Thread callback cannot be NULL.");
|
assertNotNull(callback, "Thread callback cannot be NULL.");
|
||||||
@@ -115,3 +119,12 @@ bool_t threadShouldStop(thread_t *thread) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
||||||
|
}
|
||||||
@@ -95,3 +95,10 @@ bool_t threadShouldStop(thread_t *thread);
|
|||||||
*/
|
*/
|
||||||
void * threadHandler(thread_t *thread);
|
void * threadHandler(thread_t *thread);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleeps the calling thread.
|
||||||
|
*
|
||||||
|
* @param milliseconds How long to sleep for.
|
||||||
|
*/
|
||||||
|
void threadSleepMs(const uint32_t milliseconds);
|
||||||
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
math.c
|
math.c
|
||||||
sort.c
|
sort.c
|
||||||
ref.c
|
ref.c
|
||||||
|
uri.c
|
||||||
)
|
)
|
||||||
+2
-2
@@ -27,16 +27,16 @@ void refInit(
|
|||||||
|
|
||||||
void refLock(ref_t *ref) {
|
void refLock(ref_t *ref) {
|
||||||
assertNotNull(ref, "Ref cannot be NULL.");
|
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++;
|
ref->count++;
|
||||||
if(ref->onLock != NULL) ref->onLock(ref);
|
if(ref->onLock != NULL) ref->onLock(ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t refUnlock(ref_t *ref) {
|
bool_t refUnlock(ref_t *ref) {
|
||||||
assertNotNull(ref, "Ref cannot be NULL.");
|
assertNotNull(ref, "Ref cannot be NULL.");
|
||||||
assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count.");
|
|
||||||
|
|
||||||
ref->count--;
|
ref->count--;
|
||||||
|
assertTrue(ref->count >= 0, "Ref count cannot be negative.");
|
||||||
|
|
||||||
if(ref->count > 0) {
|
if(ref->count > 0) {
|
||||||
if(ref->onUnlock != NULL) ref->onUnlock(ref);
|
if(ref->onUnlock != NULL) ref->onUnlock(ref);
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ typedef struct ref_s ref_t;
|
|||||||
typedef void (*refcallback_t)(ref_t *ref);
|
typedef void (*refcallback_t)(ref_t *ref);
|
||||||
|
|
||||||
typedef struct ref_s {
|
typedef struct ref_s {
|
||||||
uint32_t count;
|
int32_t count;
|
||||||
void *data;
|
void *data;
|
||||||
refcallback_t onLock;
|
refcallback_t onLock;
|
||||||
refcallback_t onUnlock;
|
refcallback_t onUnlock;
|
||||||
|
|||||||
@@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -18,3 +18,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
|||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Threads::Threads m)
|
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Threads::Threads m)
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
|
||||||
|
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 <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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))
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user