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
+2335
-7
No files matched your search
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
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 "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();
|
||||
|
||||
|
||||
@@ -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 "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.");
|
||||
@@ -114,4 +118,13 @@ bool_t threadShouldStop(thread_t *thread) {
|
||||
threadMutexUnlock(&thread->stateMutex);
|
||||
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
|
||||
}
|
||||
@@ -94,4 +94,11 @@ bool_t threadShouldStop(thread_t *thread);
|
||||
* @param thread Pointer to the thread structure to handle.
|
||||
*/
|
||||
void * threadHandler(thread_t *thread);
|
||||
#endif
|
||||
#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
|
||||
sort.c
|
||||
ref.c
|
||||
uri.c
|
||||
)
|
||||
+2
-2
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
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