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:
YourWishesandClaude Sonnet 5.5 committed 2026-11-07 20:12:40 -06:00
1 parent 65c4a9c051
commit cad41e456b
47 files changed
+2333 -5

No files matched your search

+20
View File
@@ -11,6 +11,10 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# Directories
set(DUSK_ROOT_DIR "${CMAKE_SOURCE_DIR}")
set(DUSK_SOURCES_DIR "${DUSK_ROOT_DIR}/src")
set(DUSK_ASSETS_DIR "${DUSK_ROOT_DIR}/assets")
set(DUSK_BUILT_ASSETS_DIR "${CMAKE_BINARY_DIR}/assets")
set(DUSK_GENERATED_HEADERS_DIR "${CMAKE_BINARY_DIR}/generated")
file(MAKE_DIRECTORY ${DUSK_GENERATED_HEADERS_DIR})
project(DuskCore VERSION 1.0.0 LANGUAGES C)
@@ -65,9 +69,25 @@ if(DUSK_BACKTRACE)
target_link_options(${DUSK_LIBRARY_TARGET_NAME} PUBLIC -rdynamic)
endif()
# Tools (run during the build to generate headers)
add_subdirectory(tools)
target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
${DUSK_GENERATED_HEADERS_DIR}
)
# Core engine code
add_subdirectory(${DUSK_SOURCES_DIR}/dusk)
# Assets, copied into <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
foreach(DIR ${DUSK_PLATFORM_DIRS})
add_subdirectory(${DUSK_SOURCES_DIR}/${DIR})
+6
View File
@@ -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)
+42
View File
@@ -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
+65
View File
@@ -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()
+22
View File
@@ -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()
+10
View File
@@ -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()
+3
View File
@@ -13,6 +13,9 @@ option(DUSK_BUILD_TESTS "Enable tests." OFF)
option(DUSK_NETWORK "Enable network support." ON)
# Game metadata
set(DUSK_GAME "duskexample" CACHE STRING
"Directory under src/ containing the game to build (provides game/game.c)."
)
set(DUSK_GAME_NAME "Dusk" CACHE STRING "Game display name.")
set(DUSK_GAME_AUTHOR "YourWishes" CACHE STRING "Game author / coder.")
if(NOT DEFINED DUSK_VERSION)
+2
View File
@@ -9,12 +9,14 @@ target_include_directories(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
)
# Modules
add_subdirectory(asset)
add_subdirectory(assert)
add_subdirectory(console)
add_subdirectory(display)
add_subdirectory(engine)
add_subdirectory(error)
add_subdirectory(perf)
add_subdirectory(resource)
add_subdirectory(time)
add_subdirectory(thread)
add_subdirectory(util)
+19 -2
View File
@@ -283,8 +283,25 @@
#endif
// Static Assertions
// Static Assertions. These are compile time only, so they are never faked.
/**
* Asserts a constant expression at compile time. Never faked, even when
* DUSK_ASSERTIONS_FAKED is set.
*
* @param x Constant expression that must be true.
* @param message Message to show on failure, must be a string literal.
*/
#define assertStatic(x, message) \
_Static_assert(x, message)
/**
* Asserts the size of a struct at compile time.
*
* @param struct The struct type to check.
* @param size The size in bytes the struct must be.
*/
#define assertStructSize(struct, size) \
_Static_assert(sizeof(struct) == size, "Size of " #struct " must be " #size)
assertStatic(sizeof(struct) == size, "Size of " #struct " must be " #size)
// EOF
+10
View File
@@ -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
)
+66
View File
@@ -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();
}
+65
View File
@@ -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);
+158
View File
@@ -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();
}
+96
View File
@@ -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
);
+12
View File
@@ -7,3 +7,15 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
display.c
)
# Subdirectories
add_subdirectory(mesh)
# Color definitions
dusk_run_python(
dusk_color_defs
tools.color
--csv ${CMAKE_CURRENT_SOURCE_DIR}/color.csv
--output ${DUSK_GENERATED_HEADERS_DIR}/display/color.h
)
add_dependencies(${DUSK_LIBRARY_TARGET_NAME} dusk_color_defs)
+23
View File
@@ -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
1 name r g b a
2 black 0 0 0 1
3 white 1 1 1 1
4 red 1 0 0 1
5 green 0 1 0 1
6 blue 0 0 1 1
7 yellow 1 1 0 1
8 cyan 0 1 1 1
9 magenta 1 0 1 1
10 transparent 0 0 0 0
11 transparent_white 1 1 1 0
12 transparent_black 0 0 0 0
13 gray 0.5 0.5 0.5 1
14 light_gray 0.75 0.75 0.75 1
15 dark_gray 0.25 0.25 0.25 1
16 orange 1 0.65 0 1
17 purple 0.5 0 0.5 1
18 brown 0.6 0.4 0.2 1
19 pink 1 0.75 0.8 1
20 lime 0.75 1 0 1
21 navy 0 0 0.5 1
22 teal 0 0.5 0.5 1
23 cornflower_blue 0.39 0.58 0.93 1
+9
View File
@@ -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
)
+117
View File
@@ -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);
+71
View File
@@ -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;
}
+88
View File
@@ -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
);
+69
View File
@@ -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);
+11
View File
@@ -10,6 +10,9 @@
#include "perf/perf.h"
#include "time/time.h"
#include "display/display.h"
#include "game/game.h"
#include "resource/resourcemanager.h"
#include "asset/asset.h"
#include "assert/assert.h"
#include "console/console.h"
@@ -27,7 +30,10 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
perfInit();
timeInit();
consoleInit();
errorChain(assetInit());
errorChain(resourceManagerInit());
errorChain(displayInit());
errorChain(gameInit());
consolePrint("Engine initialized");
@@ -44,6 +50,8 @@ errorret_t engineUpdate(void) {
// Order here is important.
timeUpdate();
consoleUpdate();
errorChain(resourceManagerUpdate());
errorChain(gameUpdate());
errorChain(displayUpdate());
errorOk();
@@ -54,7 +62,10 @@ void engineExit(void) {
}
errorret_t engineDispose(void) {
errorChain(gameDispose());
errorChain(displayDispose());
errorChain(resourceManagerDispose());
errorChain(assetDispose());
consoleDispose();
perfDispose();
+31
View File
@@ -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);
+12
View File
@@ -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)
+9
View File
@@ -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
)
+53
View File
@@ -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();
}
+35
View File
@@ -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);
+253
View File
@@ -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);
}
+212
View File
@@ -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);
+175
View File
@@ -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);
}
+117
View File
@@ -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);
+13
View File
@@ -9,6 +9,10 @@
#include "util/memory.h"
#include "assert/assert.h"
#ifdef DUSK_THREAD_PTHREAD
#include <time.h>
#endif
void threadInit(thread_t *thread, const threadcallback_t callback) {
assertNotNull(thread, "Thread cannot be NULL.");
assertNotNull(callback, "Thread callback cannot be NULL.");
@@ -115,3 +119,12 @@ bool_t threadShouldStop(thread_t *thread) {
return NULL;
}
#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
}
+7
View File
@@ -95,3 +95,10 @@ bool_t threadShouldStop(thread_t *thread);
*/
void * threadHandler(thread_t *thread);
#endif
/**
* Sleeps the calling thread.
*
* @param milliseconds How long to sleep for.
*/
void threadSleepMs(const uint32_t milliseconds);
+1
View File
@@ -15,4 +15,5 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
math.c
sort.c
ref.c
uri.c
)
+2 -2
View File
@@ -27,16 +27,16 @@ void refInit(
void refLock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
assertTrue(ref->count >= 0, "Cannot lock a ref with negative count.");
assertTrue(ref->count >= 0, "Ref count cannot be negative.");
ref->count++;
if(ref->onLock != NULL) ref->onLock(ref);
}
bool_t refUnlock(ref_t *ref) {
assertNotNull(ref, "Ref cannot be NULL.");
assertTrue(ref->count >= 0, "Cannot unlock a ref with negative count.");
ref->count--;
assertTrue(ref->count >= 0, "Ref count cannot be negative.");
if(ref->count > 0) {
if(ref->onUnlock != NULL) ref->onUnlock(ref);
+1 -1
View File
@@ -12,7 +12,7 @@ typedef struct ref_s ref_t;
typedef void (*refcallback_t)(ref_t *ref);
typedef struct ref_s {
uint32_t count;
int32_t count;
void *data;
refcallback_t onLock;
refcallback_t onUnlock;
+126
View File
@@ -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)
);
}
+73
View File
@@ -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);
+9
View File
@@ -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
)
+44
View File
@@ -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();
}
+4
View File
@@ -18,3 +18,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
find_package(Threads REQUIRED)
target_link_libraries(${DUSK_LIBRARY_TARGET_NAME} PUBLIC Threads::Threads m)
target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC
asset/assetlinux.c
)
+46
View File
@@ -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();
}
+24
View File
@@ -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);
+12
View File
@@ -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
+6
View File
@@ -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)
+70
View File
@@ -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))
+14
View File
@@ -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()