diff --git a/.gitignore b/.gitignore index 53ae41c..14284d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,7 @@ /subprojects/* !/subprojects/*.wrap -# assetc directories -/actemp/* -/data/* +/res/* /.cache/* /.vs/* diff --git a/assets/shader/test.pipeline b/assets/shader/test.pipeline index d1a0c81..bc11e0d 100644 --- a/assets/shader/test.pipeline +++ b/assets/shader/test.pipeline @@ -22,6 +22,17 @@ VSOutput VsMain(VSInput input, uint vertexIndex : SV_VertexID) { fragment { vk BEGIN +struct PSOutput { + float4 Color : SV_TARGET0; +}; +PSOutput PsMain(void) { + PSOutput output = (PSOutput)0; + output.Color[0] = 0; + output.Color[1] = 0; + output.Color[2] = 0; + output.Color[3] = 0; + return output; +} END } \ No newline at end of file diff --git a/meson.build b/meson.build index 47d65e6..40a5f5d 100644 --- a/meson.build +++ b/meson.build @@ -2,9 +2,13 @@ project('voyage', ['c', 'cpp'], default_options: ['buildtype=debug', 'b_sanitize=address', 'c_std=c17', + 'cpp_std=c++14', 'warning_level=3', 'werror=true', - 'b_vscrt=static_from_buildtype']) + 'b_vscrt=static_from_buildtype', + 'default_library=static', + 'b_rtti=false', + 'cpp_eh=none']) compiler = meson.get_compiler('c') buildtype = get_option('buildtype') @@ -16,13 +20,11 @@ if compiler.get_argument_syntax() == 'gcc' '-Wdouble-promotion', '-Wno-unused-function', '-Wno-unused-parameter'], language : ['c', 'cpp'] ) - add_project_arguments(['-fno-exceptions', '-fno-rtti'], language : 'cpp') elif compiler.get_argument_syntax() == 'msvc' add_project_arguments( ['/wd4146', '/wd4245', '/wd4100', '/D_CRT_SECURE_NO_WARNINGS'], language: ['c', 'cpp'] ) - add_project_arguments(['/EHsc', '/GR-'], language : 'cpp') if buildtype == 'debug' add_project_arguments(['/RTCsu'], language : ['c', 'cpp']) endif @@ -66,26 +68,6 @@ runtime_deps = [thread_dep, m_dep, windowing_dep] if get_option('build_asset_compiler') runtime_cargs += ['-DRT_BUILD_ASSET_COMPILER'] - # Shaderc for shaders - #if get_option('enable_vulkan_shader_compiler') - # shaderc_include = include_directories('contrib\\shaderc\\libshaderc\\include') - # shaderc_libdir = 'NONE' - # if host_machine.system() == 'windows' - # if buildtype == 'debug' or buildtype == 'debugoptimized' - # shaderc_libdir = meson.project_source_root() / 'contrib\\shaderc\\build-win\\libshaderc\\Debug' - # else - # shaderc_libdir = meson.project_source_root() / 'contrib\\shaderc\\build-win\\libshaderc\\Release' - # endif - # endif - # shaderc_dep = declare_dependency(link_args : ['-L'+shaderc_libdir, '-lshaderc_combined'], - # include_directories : shaderc_include) - # runtime_deps += shaderc_dep - # runtime_additional_sources += [ - # 'src/runtime/vulkan_shader_compiler.c' - # ] - # runtime_cargs += ['-DRT_BUILD_VULKAN_SHADER_COMPILER'] - #endif - # DXC for vulkan & directx shaders if get_option('enable_dxc_shader_compiler') # We package dxc binaries under contrib/dxc @@ -125,9 +107,10 @@ runtime_lib = library('rt', # Project Sources 'src/runtime/aio.h', 'src/runtime/app.h', - 'src/runtime/assets.h', 'src/runtime/buffer_manager.h', + 'src/runtime/compression.h', 'src/runtime/config.h', + 'src/runtime/ds.h', 'src/runtime/dynamic_libs.h', 'src/runtime/file_tab.h', 'src/runtime/fsutils.h', @@ -138,14 +121,17 @@ runtime_lib = library('rt', 'src/runtime/mem_arena.h', 'src/runtime/packages.h', 'src/runtime/renderer_api.h', + 'src/runtime/resources.h', 'src/runtime/runtime.h', 'src/runtime/threading.h', 'src/runtime/aio.c', 'src/runtime/app.c', - 'src/runtime/asset_manager.c', + 'src/runtime/assert.c', 'src/runtime/buffer_manager.c', + 'src/runtime/compression.c', 'src/runtime/config.c', + 'src/runtime/ds_minheap.c', 'src/runtime/dynamic_libs.c', 'src/runtime/error_report.c', 'src/runtime/file_tab.c', @@ -156,6 +142,8 @@ runtime_lib = library('rt', 'src/runtime/jobs.c', 'src/runtime/mem_arena.c', 'src/runtime/packages.c', + 'src/runtime/resource_manager.c', + 'src/runtime/sprint.c', 'src/runtime/text.c', 'src/runtime/threading_cond.c', 'src/runtime/threading_mutex.c', @@ -217,40 +205,6 @@ else engine_link_libs = [runtime_lib] endif -# Asset Compiler Tool -#executable('assetc', -# 'src/tools/assetc/assetmeta.h', -# 'src/tools/assetc/assetsettings.h', -# 'src/tools/assetc/dependency_tracking.h', -# 'src/tools/assetc/description_parser.h', -# 'src/tools/assetc/options.h', -# 'src/tools/assetc/packages.h', -# 'src/tools/assetc/processing.h', -# 'src/tools/assetc/processing_flags.h', -# 'src/tools/assetc/utils.h', -# -# 'src/tools/assetc/assetc.c', -# 'src/tools/assetc/assetmeta.c', -# 'src/tools/assetc/assetsettings.c', -# 'src/tools/assetc/dependency_tracking.c', -# 'src/tools/assetc/description_parser.c', -# 'src/tools/assetc/discovery.c', -# 'src/tools/assetc/packages.c', -# 'src/tools/assetc/pipeline_processor.c', -# 'src/tools/assetc/processor.c', -# 'src/tools/assetc/shader_processor.c', -# 'src/tools/assetc/uidtable.c', -# 'src/tools/assetc/utils.c', -# -# # Contrib sources -# 'contrib/xxhash/xxhash.c', -# 'contrib/lz4/lz4.c', -# include_directories : [incdir, shaderc_include], -# dependencies : [], -# link_with : engine_link_libs, -# link_args : ['-L'+shaderc_libdir, '-lshaderc_combined'], -# win_subsystem : 'console') - # Game executable('voyage', 'src/game/voyage.c', diff --git a/pch/rt_pch.h b/pch/rt_pch.h index b358cf8..cca9941 100644 --- a/pch/rt_pch.h +++ b/pch/rt_pch.h @@ -1,5 +1,8 @@ #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX #include +#include #endif /* C standard library*/ @@ -7,4 +10,7 @@ #include #include #include -#include \ No newline at end of file +#include + +/* Runtime */ +#include "runtime/runtime.h" \ No newline at end of file diff --git a/src/runtime/buffer_manager.c b/src/runtime/buffer_manager.c index 175fe2d..7bb7b8a 100644 --- a/src/runtime/buffer_manager.c +++ b/src/runtime/buffer_manager.c @@ -50,8 +50,8 @@ static rt_mutex *_guard; static size_t _block_count; RT_CVAR_I(rt_BufferMemoryBudget, - "The amount of memory to allocate for the buffer manager. Default: 512MB", - RT_MB(512)); + "The amount of memory to allocate for the buffer manager. Default: 1GB", + RT_GB(1)); extern rt_result InitBufferManager(void) { _guard = rtCreateMutex(); diff --git a/src/runtime/compression.c b/src/runtime/compression.c new file mode 100644 index 0000000..1eef6f7 --- /dev/null +++ b/src/runtime/compression.c @@ -0,0 +1,26 @@ +#include "compression.h" + +#include + +RT_DLLEXPORT size_t rtGetCompressionBound(size_t uncompressed_size) { + return (size_t)LZ4_compressBound((int)uncompressed_size); +} + +RT_DLLEXPORT size_t rtCompressData(const void *in, + size_t uncompressed_size, + void *out, + size_t out_capacity) { + return (size_t)LZ4_compress_default((const char *)in, + (char *)out, + (int)uncompressed_size, + (int)out_capacity); +} + +RT_DLLEXPORT size_t rtDecompressData(const void *in, + size_t compressed_size, + void *out, + size_t out_capacity) { + int res = + LZ4_decompress_safe((const char *)in, (char *)out, (int)compressed_size, (int)out_capacity); + return (res > 0) ? (size_t)res : 0; +} \ No newline at end of file diff --git a/src/runtime/compression.h b/src/runtime/compression.h new file mode 100644 index 0000000..fe84dea --- /dev/null +++ b/src/runtime/compression.h @@ -0,0 +1,38 @@ +#ifndef RT_COMPRESSION_H +#define RT_COMPRESSION_H + +#include "runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Returns the worst-case size of the compression result of a buffer with the given uncompressed + * size. */ +RT_DLLEXPORT size_t rtGetCompressionBound(size_t uncompressed_size); + +/* Compresses a buffer. + * + * Returns the number of bytes written to the output buffer, or 0 if compression failed + * due to insufficient capacity. + */ +RT_DLLEXPORT size_t rtCompressData(const void *in, + size_t uncompressed_size, + void *out, + size_t out_capacity); + +/* Decompresses a buffer. + * + * Returns the number of bytes written to the output buffer, or 0 if decompression failed + * due to insufficient capacity. + */ +RT_DLLEXPORT size_t rtDecompressData(const void *in, + size_t compressed_size, + void *out, + size_t out_capacity); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/runtime/gfx.h b/src/runtime/gfx.h index 829970b..fe58b49 100644 --- a/src/runtime/gfx.h +++ b/src/runtime/gfx.h @@ -47,6 +47,8 @@ typedef enum { RT_ATTRIBUTE_VALUE_MATERIAL_ALBEDO, RT_ATTRIBUTE_VALUE_MATERIAL_NORMAL, + + RT_ATTRIBUTE_VALUE_count } rt_attribute_value; typedef struct { diff --git a/src/runtime/mem_arena.c b/src/runtime/mem_arena.c index 7fcddf4..7d57a4e 100644 --- a/src/runtime/mem_arena.c +++ b/src/runtime/mem_arena.c @@ -1,7 +1,10 @@ #include "mem_arena.h" +#include "config.h" #include +RT_CVAR_I(rt_TemporaryArenaSize, "Size of temporary arenas in bytes. Default: 32 MB", RT_MB(32)); + #define ALIGNMENT 0xf #define ALIGN(n) (((n) + ALIGNMENT) & ~ALIGNMENT) @@ -59,3 +62,45 @@ RT_DLLEXPORT void rtReleaseArena(rt_arena *arena) { arena->size = 0; arena->needs_free = 0; } + +/* Temporary arena pool */ + +typedef uint32_t rt_thread_id; +extern RT_DLLEXPORT rt_thread_id rtGetCurrentThreadId(void); + +#define NUM_TEMP_ARENAS_PER_THREAD 2 +typedef struct { + rt_arena arenas[NUM_TEMP_ARENAS_PER_THREAD]; +} rt_thread_temp_arenas; + +static RT_THREAD_LOCAL rt_thread_temp_arenas t_arenas; + +RT_DLLEXPORT rt_temp_arena rtGetTemporaryArena(const rt_arena **permanent_arenas, int count) { + if (!t_arenas.arenas[0].base) { + /* Initialize */ + for (int i = 0; i < NUM_TEMP_ARENAS_PER_THREAD; ++i) { + rt_create_arena_result res = rtCreateArena(NULL, (size_t)rt_TemporaryArenaSize.i); + if (!res.ok) { + rtLog("CORE", + "Failed to initialize thread-local temporary arenas for thread: %u", + rtGetCurrentThreadId()); + return (rt_temp_arena){.arena = NULL, .at = 0}; + } + t_arenas.arenas[i] = res.arena; + } + } + for (int i = 0; i < NUM_TEMP_ARENAS_PER_THREAD; ++i) { + int conflict_found = 0; + for (int j = 0; j < count; ++j) { + if (permanent_arenas[j] == &t_arenas.arenas[i]) + conflict_found = 1; + } + if (!conflict_found) { + return rtBeginTempArena(&t_arenas.arenas[i]); + } + } + rtLog("CORE", + "Failed to find a usable thread-local temporary arena for thread: %u", + rtGetCurrentThreadId()); + return (rt_temp_arena){.arena = NULL, .at = 0}; +} diff --git a/src/runtime/mem_arena.h b/src/runtime/mem_arena.h index 52ba107..4b65005 100644 --- a/src/runtime/mem_arena.h +++ b/src/runtime/mem_arena.h @@ -21,7 +21,8 @@ typedef struct { typedef struct { size_t at; -} rt_arena_rewindpoint; + rt_arena *arena; +} rt_temp_arena; RT_DLLEXPORT rt_create_arena_result rtCreateArena(void *memory, size_t size); @@ -37,22 +38,34 @@ RT_INLINE void rtArenaClear(rt_arena *arena) { arena->at = 0; } -RT_INLINE rt_arena_rewindpoint rtGetArenaRewindPoint(rt_arena *arena) { +RT_INLINE rt_temp_arena rtBeginTempArena(rt_arena *arena) { #ifndef __cplusplus - return (rt_arena_rewindpoint){.at = arena->at}; + return (rt_temp_arena){.at = arena->at, .arena = arena}; #else - rt_arena_rewindpoint rp = {arena->at}; + rt_temp_arena rp = {arena->at, arena}; return rp; #endif } -RT_INLINE void rtArenaRewind(rt_arena *arena, rt_arena_rewindpoint rewind) { -#ifndef NDEBUG - if (rewind.at > arena->at) - rtReportError("CORE", - "Tried to rewind an arena to a point that lies beyond the last allocation."); -#endif - arena->at = rewind.at; +RT_INLINE void rtEndTempArena(rt_temp_arena tmp) { + RT_ASSERT(tmp.at <= tmp.arena->at, + "Tried to rewind an arena to a point that lies beyond the last allocation."); + tmp.arena->at = tmp.at; +} + +/* Get a thread-local arena for temporary allocations. + * + * Since it's possible that such an arena was passed into a function via parameters, which might + * lead to conflicts - such as a called function inadvertently freeing its own "permanent" allocations, + * we pass in arenas that are used for allocations lasting beyond the usage of the temporary arena. + * + * DO NOT call rtReleaseArena on a arena returned by this function. Instead return them via + * rtReturnTemporaryArena()! + */ +RT_DLLEXPORT rt_temp_arena rtGetTemporaryArena(const rt_arena **permanent_arenas, int count); + +RT_INLINE void rtReturnTemporaryArena(rt_temp_arena tmp) { + rtEndTempArena(tmp); } /* Helper macros */ diff --git a/src/runtime/pipeline_processor.c b/src/runtime/pipeline_processor.c index f3356b3..ee67fc7 100644 --- a/src/runtime/pipeline_processor.c +++ b/src/runtime/pipeline_processor.c @@ -39,31 +39,6 @@ enum { extern rt_cvar rt_Renderer; -#if 0 -static void -DbgPrintShaderFile(const rt_parse_state *state, unsigned int list_index, unsigned int indent) { - assert(list_index < state->statement_list_count); - const rt_parsed_stmt_list *list = &state->statement_lists[list_index]; - - unsigned int stmt_index = list->first; - for (unsigned int i = 0; i < list->count; ++i) { - const rt_parsed_stmt *stmt = &state->statements[stmt_index]; - for (unsigned int j = 0; j < indent; ++j) - printf(" "); - printf("%.*s: ", stmt->attribute.length, stmt->attribute.start); - if (stmt->form == RT_STMT_FORM_VALUE) { - printf("%.*s\n", stmt->value.length, stmt->value.start); - } else { - printf("{\n"); - DbgPrintShaderFile(state, stmt->list_index, indent + 2); - printf("}\n"); - } - stmt_index = stmt->next; - } - assert(stmt_index == UINT_MAX || stmt_index == 0); -} -#endif - static bool ParseBindingIndex(rt_text_span span, unsigned int *index) { if (span.length == 0) return false; @@ -469,10 +444,8 @@ RT_ASSET_PROCESSOR_FN(PipelineProcessor) { goto out; rt_resource_id shader_resources[3] = {0}; - result = rtCreateResources(pipeline.shader_count, - pipeline.shader_names, - pipeline.shaders, - shader_resources); + result = rtCreateResources(pipeline.shader_count, pipeline.shader_names, pipeline.shaders, + shader_resources); if (result != RT_SUCCESS) goto out; diff --git a/src/runtime/renderer_api.h b/src/runtime/renderer_api.h index 8be231e..b15246d 100644 --- a/src/runtime/renderer_api.h +++ b/src/runtime/renderer_api.h @@ -48,13 +48,15 @@ typedef enum { RT_SHADER_TYPE_INVALID, RT_SHADER_TYPE_VULKAN, - RT_SHADER_TYPE_COUNT, + RT_SHADER_TYPE_count, } rt_shader_type; typedef enum { RT_SHADER_STAGE_VERTEX, RT_SHADER_STAGE_FRAGMENT, RT_SHADER_STAGE_COMPUTE, + + RT_SHADER_STAGE_count, } rt_shader_stage; typedef struct { diff --git a/src/runtime/resource_manager.c b/src/runtime/resource_manager.c index ae751c4..ebb7668 100644 --- a/src/runtime/resource_manager.c +++ b/src/runtime/resource_manager.c @@ -1,10 +1,12 @@ #include "aio.h" #include "buffer_manager.h" +#include "compression.h" #include "config.h" #include "ds.h" #include "file_tab.h" #include "fsutils.h" #include "hashing.h" +#include "mem_arena.h" #include "renderer_api.h" #include "resources.h" #include "threading.h" @@ -29,7 +31,8 @@ RT_CVAR_I(rt_ResourceNamespaceSize, typedef struct { void *buffer; size_t size; - int next_free; + rt_aio_handle load_aio; + unsigned int next_free; int usage_counter; } rt_cached_resource; @@ -52,13 +55,15 @@ typedef struct { size_t current_size; - rt_rwlock lock; + rt_rwlock resource_lock; + rt_mutex *heap_lock; } rt_resource_cache; typedef struct { rt_file_id file; size_t offset; - size_t size; + size_t decompressed_size; + size_t compressed_size; } rt_resource_ref; typedef struct { @@ -102,6 +107,53 @@ static void CopyResourceData(const rt_resource *resource, void *dest) { } } +#if 0 +static rt_resource_ref *GetResourceRefPtr(rt_resource_id id) { + rt_resource_ref *ref = NULL; + rtLockRead(&_namespace.lock); + size_t ns_size = (size_t)rt_ResourceNamespaceSize.i; + for (size_t j = 0; j < ns_size; ++j) { + size_t at = (id + j) % ns_size; + if (_namespace.ids[at] == RT_INVALID_RESOURCE_ID) { + break; + } else if (_namespace.ids[at] == id) { + ref = &_namespace.refs[at]; + break; + } + } + rtUnlockRead(&_namespace.lock); + return ref; +} +#endif + +/* Fills the passed write struct with the necessary information to save the resource to a file */ +static bool PrepareResourceFlushToFile(rt_resource_id id, + const rt_resource *resource, + rt_file_write *write, + rt_arena *arena) { + + RT_ASSERT((uintptr_t)resource->data == (uintptr_t)(resource + 1), + "The resource and data must be laid out in a continous buffer."); + + char file_path[260]; + rtSPrint(file_path, 260, "%s/%llx.bin", rt_ResourceDirectory.s, id); + + size_t total_size = sizeof(rt_resource) + GetResourceDataSize(resource); + size_t compression_bound = rtGetCompressionBound(total_size); + + void *compressed_resource = rtArenaPush(arena, compression_bound); + if (!compressed_resource) + return false; + size_t compressed_bytes = + rtCompressData(resource, total_size, compressed_resource, compression_bound); + + write->file = rtAddFile(file_path); + write->buffer = compressed_resource; + write->offset = 0; + write->num_bytes = compressed_bytes; + return true; +} + /* ~~~ Cache ~~~ */ static rt_resource_cache _cache; @@ -120,11 +172,17 @@ static rt_result InitResourceCache(void) { void *mem = malloc(required_mem); if (!mem) return RT_OUT_OF_MEMORY; - rt_create_rwlock_result lock_create = rtCreateRWLock(); - if (!lock_create.ok) { + rt_create_rwlock_result resource_lock_create = rtCreateRWLock(); + if (!resource_lock_create.ok) { free(mem); return RT_UNKNOWN_ERROR; } + _cache.heap_lock = rtCreateMutex(); + if (!_cache.heap_lock) { + free(mem); + rtDestroyRWLock(&_cache.resource_lock); + return RT_UNKNOWN_ERROR; + } memset(mem, 0, required_mem); _cache.mem = mem; @@ -136,9 +194,9 @@ static rt_result InitResourceCache(void) { (size_t)count, 0); - _cache.current_size = 0; - _cache.resources = (rt_cached_resource *)(reclaim_keys + count); - _cache.lock = lock_create.lock; + _cache.current_size = 0; + _cache.resources = (rt_cached_resource *)(reclaim_keys + count); + _cache.resource_lock = resource_lock_create.lock; for (int i = 0; i < count; ++i) { _cache.resources[i].next_free = (i < count - 1) ? i + 1 : UINT_MAX; @@ -153,19 +211,30 @@ static rt_result InitResourceCache(void) { static void ShutdownResourceCache(void) { free(_cache.mem); - rtDestroyRWLock(&_cache.lock); + rtDestroyRWLock(&_cache.resource_lock); + rtDestroyMutex(_cache.heap_lock); memset(&_cache, 0, sizeof(_cache)); } +/* NOTE(Kevin): Only call this while holding a write-lock on the cache. + * The function locks the reclaim heap lock itself. */ static bool FreeCacheSpace(size_t space) { - size_t total_freed = 0; - while (total_freed < space && !rtMinheapIsEmpty(&_cache.reclaim_heap)) { + size_t free_space = (size_t)rt_ResourceCacheSize.i - _cache.current_size; + rtLockMutex(_cache.heap_lock); + while (free_space < space && !rtMinheapIsEmpty(&_cache.reclaim_heap)) { rt_cached_resource_ref ref; rtMinheapPop(&_cache.reclaim_heap, &ref); rt_cached_resource *res = &_cache.resources[ref.index]; + + if (res->load_aio != RT_AIO_INVALID_HANDLE) { + rtWaitForAIOCompletion(res->load_aio); + rtReleaseAIO(res->load_aio); + } + rtReleaseBuffer(res->buffer, res->size); - total_freed += res->size; + free_space += res->size; + _cache.current_size -= res->size; res->next_free = _cache.first_free; _cache.first_free = ref.index; @@ -185,7 +254,8 @@ static bool FreeCacheSpace(size_t space) { } } } - return total_freed >= space; + rtUnlockMutex(_cache.heap_lock); + return free_space >= space; } static unsigned int FindCachedResource(rt_resource_id id) { @@ -202,13 +272,15 @@ static unsigned int FindCachedResource(rt_resource_id id) { static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { rt_resource *cached = NULL; - rtLockWrite(&_cache.lock); + rtLockWrite(&_cache.resource_lock); unsigned int index = FindCachedResource(id); if (index != UINT_MAX) { rt_cached_resource_ref ref = {.id = id, .index = index}; rt_cached_resource *cache_entry = &_cache.resources[index]; ++cache_entry->usage_counter; + rtLockMutex(_cache.heap_lock); rtMinheapUpdate(&_cache.reclaim_heap, &ref, cache_entry->usage_counter, NULL); + rtUnlockMutex(_cache.heap_lock); cached = cache_entry->buffer; } else { /* Insert into cache */ @@ -218,8 +290,8 @@ static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { rtLog("RESMGR", "Unable to reclaim %zu kB from the resource cache.", total_size / 1024); - rtUnlockWrite(&_cache.lock); - return NULL; + rtUnlockWrite(&_cache.resource_lock); + return cached; } RT_ASSERT(_cache.first_free != UINT_MAX, "There must be a free cache entry after space was freed."); @@ -228,11 +300,11 @@ static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { void *buffer = rtAllocBuffer(total_size); if (!buffer) { rtLog("RESMG", "Unable to allocate %zu kB for the new resource.", total_size / 1024); - rtUnlockWrite(&_cache.lock); - return NULL; + rtUnlockWrite(&_cache.resource_lock); + return cached; } - memcpy(buffer, res, sizeof(rt_resource)); - cached = buffer; + cached = buffer; + memcpy(cached, res, sizeof(rt_resource)); cached->data = (void *)(cached + 1); CopyResourceData(res, cached->data); @@ -242,12 +314,17 @@ static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { _cache.resources[index].usage_counter = 1; _cache.resources[index].size = total_size; _cache.resources[index].next_free = UINT_MAX; + _cache.resources[index].load_aio = RT_AIO_INVALID_HANDLE; + + _cache.current_size += total_size; rt_cached_resource_ref reclaim_ref = { .id = id, .index = index, }; + rtLockMutex(_cache.heap_lock); rtMinheapPush(&_cache.reclaim_heap, 1, &reclaim_ref); + rtUnlockMutex(_cache.heap_lock); /* Insert into lookup table */ bool inserted = false; @@ -256,6 +333,7 @@ static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { size_t slot = (id + off) % ht_size; if (_cache.resource_ids[slot] == RT_INVALID_RESOURCE_ID || _cache.resource_ids[slot] == RT_TOMBSTONE_ID || _cache.resource_ids[slot] == id) { + _cache.resource_ids[slot] = id; _cache.resource_indices[slot] = index; inserted = true; break; @@ -266,10 +344,72 @@ static rt_resource *CacheResource(rt_resource_id id, const rt_resource *res) { "Failed to insert created resource into the resource lookup table."); } } - rtUnlockWrite(&_cache.lock); + rtUnlockWrite(&_cache.resource_lock); return cached; } +static void InsertPrefetchResourceIntoCache(rt_resource_id id, + rt_aio_handle load_aio, + void *load_buffer, + size_t load_buffer_size) { + rtLockWrite(&_cache.resource_lock); + unsigned int index = FindCachedResource(id); + if (index != UINT_MAX) { + rtUnlockWrite(&_cache.resource_lock); + return; + } + + if (_cache.current_size + load_buffer_size >= (size_t)rt_ResourceCacheSize.i) { + if (!FreeCacheSpace(load_buffer_size)) { + rtLog("RESMGR", + "Unable to reclaim %zu kB from the resource cache.", + load_buffer_size / 1024); + rtUnlockWrite(&_cache.resource_lock); + return; + } + RT_ASSERT(_cache.first_free != UINT_MAX, + "There must be a free cache entry after space was freed."); + } + + index = _cache.first_free; + _cache.first_free = _cache.resources[index].next_free; + _cache.resources[index].buffer = load_buffer; + _cache.resources[index].usage_counter = 1; + _cache.resources[index].size = load_buffer_size; + _cache.resources[index].next_free = UINT_MAX; + _cache.resources[index].load_aio = load_aio; + + _cache.current_size += load_buffer_size; + + rt_cached_resource_ref reclaim_ref = { + .id = id, + .index = index, + }; + rtLockMutex(_cache.heap_lock); + rtMinheapPush(&_cache.reclaim_heap, 1, &reclaim_ref); + rtUnlockMutex(_cache.heap_lock); + + /* Insert into lookup table */ + bool inserted = false; + size_t ht_size = (size_t)rt_MaxCachedResources.i * 2; + for (size_t off = 0; off < ht_size; ++off) { + size_t slot = (id + off) % ht_size; + if (_cache.resource_ids[slot] == RT_INVALID_RESOURCE_ID || + _cache.resource_ids[slot] == RT_TOMBSTONE_ID || _cache.resource_ids[slot] == id) { + _cache.resource_ids[slot] = id; + _cache.resource_indices[slot] = index; + inserted = true; + break; + } + } + if (!inserted) { + rtReportError("RESMGR", + "Failed to insert created resource into the resource lookup table."); + } + + rtUnlockWrite(&_cache.resource_lock); +} + /* ~~~ Resource Namespace ~~~ */ static rt_resource_namespace _namespace; @@ -301,40 +441,23 @@ static void ShutdownNamespace(void) { memset(&_namespace, 0, sizeof(_namespace)); } -#if 0 -static rt_resource_ref *GetResourceRefPtr(rt_resource_id id) { - rt_resource_ref *ref = NULL; +static rt_resource_ref GetResourceRef(rt_resource_id id) { + rt_resource_ref ref = {.file = RT_INVALID_FILE_ID}; rtLockRead(&_namespace.lock); size_t ns_size = (size_t)rt_ResourceNamespaceSize.i; - for (size_t j = 0; j < ns_size; ++j) { - size_t at = (id + j) % ns_size; - if (_namespace.ids[at] == RT_INVALID_RESOURCE_ID) { + for (size_t off = 0; off < ns_size; ++off) { + size_t at = (id + off) % ns_size; + if (_namespace.ids[at] == id) { + ref = _namespace.refs[at]; break; - } else if (_namespace.ids[at] == id) { - ref = &_namespace.refs[at]; + } else if (_namespace.ids[at] == RT_INVALID_RESOURCE_ID) { + rtLog("RESMGR", "Tried to load unknown resource %llx", id); break; } } rtUnlockRead(&_namespace.lock); return ref; } -#endif - -/* Fills the passed write struct with the necessary information to save the resource to a file */ -static void -PrepareResourceFlushToFile(rt_resource_id id, const rt_resource *resource, rt_file_write *write) { - /* A file write needs one contiguous buffer */ - RT_ASSERT(((uintptr_t)resource->data == (uintptr_t)resource + sizeof(*resource)), - "The resource must reside in the cache, to ensure the correct memory layout"); - - char file_path[260]; - rtSPrint(file_path, 260, "%s/%llx.bin", rt_ResourceDirectory.s, id); - - write->file = rtAddFile(file_path); - write->buffer = resource; - write->offset = 0; - write->num_bytes = sizeof(rt_resource) + GetResourceDataSize(resource); -} /* ~~~ Public API ~~~ */ @@ -357,6 +480,203 @@ void ShutdownResourceManager(void) { ShutdownNamespace(); } +RT_DLLEXPORT rt_result rtGetResource(rt_resource_id id, void *dest) { + rtLockRead(&_cache.resource_lock); + unsigned int cache_index = FindCachedResource(id); + if (cache_index != UINT_MAX) { + rt_cached_resource *cached = &_cache.resources[cache_index]; + /* TODO(Kevin): It's possible that the load is not finished. */ + if (cached->load_aio != RT_AIO_INVALID_HANDLE) { + rtUnlockRead(&_cache.resource_lock); + rtLockWrite(&_cache.resource_lock); + if (cached->load_aio != RT_AIO_INVALID_HANDLE) { + rt_aio_state state = rtWaitForAIOCompletion(cached->load_aio); + rtReleaseAIO(cached->load_aio); + cached->load_aio = RT_AIO_INVALID_HANDLE; + if (state != RT_AIO_STATE_FINISHED) { + rtLog("RESMGR", "Failed to load resource %llx: %u", id, state); + rtUnlockWrite(&_cache.resource_lock); + return RT_UNKNOWN_ERROR; + } + + /* Need to decompress the resource */ + rt_resource_ref ref = GetResourceRef(id); + if (ref.file == RT_INVALID_FILE_ID) { + rtUnlockWrite(&_cache.resource_lock); + return RT_INVALID_VALUE; + } + + void *decompressed = rtAllocBuffer(ref.decompressed_size); + if (!decompressed) { + rtUnlockWrite(&_cache.resource_lock); + return RT_OUT_OF_MEMORY; + } + + size_t written_bytes = rtDecompressData(cached->buffer, + cached->size, + decompressed, + ref.decompressed_size); + if (written_bytes != ref.decompressed_size) { + rtLog("RESMGR", + "Corrupted resource data %llx: Result of decompression does not match " + "saved " + "metadata.", + id); + rtUnlockWrite(&_cache.resource_lock); + return RT_UNKNOWN_ERROR; + } + rt_resource *resource = decompressed; + /* Patch the data pointer */ + resource->data = (resource + 1); + + /* Note that we allow the cache to grow beyond its configured maximum here. */ + rtReleaseBuffer(cached->buffer, cached->size); + _cache.current_size -= cached->size; + cached->size = ref.decompressed_size; + _cache.current_size += ref.decompressed_size; + cached->buffer = decompressed; + + rtUnlockWrite(&_cache.resource_lock); + rtLockRead(&_cache.resource_lock); + } + } + + RT_ASSERT(cached->size == rtGetResourceSize(id), "Inconsistent resource size"); + memcpy(dest, cached->buffer, cached->size); + + rtLockMutex(_cache.heap_lock); + ++cached->usage_counter; + rt_cached_resource_ref ref = {.id = id, .index = cache_index}; + rtMinheapUpdate(&_cache.reclaim_heap, &ref, cached->usage_counter, NULL); + rtUnlockMutex(_cache.heap_lock); + rtUnlockRead(&_cache.resource_lock); + } else { + /* Load the resource file */ + rtUnlockRead(&_cache.resource_lock); + rt_resource_ref ref = GetResourceRef(id); + if (ref.file == RT_INVALID_FILE_ID) { + return RT_INVALID_VALUE; + } + + rt_temp_arena temp_arena = rtGetTemporaryArena(NULL, 0); + void *compressed_buffer = rtArenaPush(temp_arena.arena, ref.compressed_size); + if (!compressed_buffer) { + rtReturnTemporaryArena(temp_arena); + return RT_OUT_OF_MEMORY; + } + + rt_aio_state state = rtSubmitSingleLoadSync((rt_file_load){ + .file = ref.file, + .dest = compressed_buffer, + .num_bytes = ref.compressed_size, + .offset = ref.offset, + }); + if (state != RT_AIO_STATE_FINISHED) { + rtLog("RESMGR", "Failed to load resource %llx: %u", id, state); + rtReturnTemporaryArena(temp_arena); + return RT_UNKNOWN_ERROR; + } + + /* Decompress */ + size_t written_bytes = + rtDecompressData(compressed_buffer, ref.compressed_size, dest, ref.decompressed_size); + rtReturnTemporaryArena(temp_arena); + if (written_bytes != ref.decompressed_size) { + rtLog("RESMGR", + "Corrupted resource data %llx: Result of decompression does not match saved " + "metadata.", + id); + return RT_UNKNOWN_ERROR; + } + rt_resource *resource = dest; + /* Patch the data pointer */ + resource->data = (resource + 1); + + CacheResource(id, resource); + + rtPrefetchResources(resource->dependencies, resource->dependency_count); + rtPrefetchResources(resource->subresources, resource->subresource_count); + } + return RT_SUCCESS; +} + +RT_DLLEXPORT size_t rtGetResourceSize(rt_resource_id id) { + size_t size = 0; + rtLockRead(&_namespace.lock); + size_t ns_size = (size_t)rt_ResourceNamespaceSize.i; + for (size_t off = 0; off < ns_size; ++off) { + size_t at = (id + off) % ns_size; + if (_namespace.ids[at] == id) { + size = _namespace.refs[at].decompressed_size; + break; + } else if (_namespace.ids[at] == RT_INVALID_RESOURCE_ID) { + rtLog("RESMGR", "Tried to get size of unknown resource %llx", id); + break; + } + } + rtUnlockRead(&_namespace.lock); + return size; +} + +RT_DLLEXPORT void rtPrefetchResources(const rt_resource_id *ids, uint32_t count) { + rt_load_batch loads = {.num_loads = 0}; + rt_aio_handle handles[RT_LOAD_BATCH_MAX_SIZE]; + + for (uint32_t i = 0; i < count; ++i) { + rt_resource_ref ref = GetResourceRef(ids[i]); + if (ref.file == RT_INVALID_FILE_ID) { + rtLog("RESMGR", "Attempted to prefetch unknown resource %llx", ids[i]); + continue; + } + + /* Check if the resource is already cached */ + if (FindCachedResource(ids[i]) != UINT_MAX) + continue; + + void *buffer = rtAllocBuffer(ref.compressed_size); + if (!buffer) { + rtLog("RESMGR", + "Could not prefetch resource %llx because a buffer allocation failed.", + ids[i]); + continue; + } + + loads.loads[loads.num_loads] = (rt_file_load){ + .file = ref.file, + .num_bytes = ref.compressed_size, + .offset = ref.offset, + .dest = buffer, + }; + ++loads.num_loads; + if (loads.num_loads == RT_LOAD_BATCH_MAX_SIZE || i == count - 1) { + if (rtSubmitLoadBatch(&loads, handles) != RT_SUCCESS) { + rtLog("RESMGR", "Prefetch failed because the file loads could not be submitted."); + } + + for (uint32_t j = 0; j < loads.num_loads; ++j) { + InsertPrefetchResourceIntoCache(ids[i - loads.num_loads + j], + handles[j], + loads.loads[j].dest, + loads.loads[j].num_bytes); + } + + loads.num_loads = 0; + } + } + if (loads.num_loads > 0) { + if (rtSubmitLoadBatch(&loads, handles) != RT_SUCCESS) { + rtLog("RESMGR", "Prefetch failed because the file loads could not be submitted."); + } + + for (uint32_t j = 0; j < loads.num_loads; ++j) { + InsertPrefetchResourceIntoCache(ids[count - 1 - loads.num_loads + j], + handles[j], + loads.loads[j].dest, + loads.loads[j].num_bytes); + } + } +} + RT_DLLEXPORT rt_result rtCreateResources(uint32_t count, const char **names, const rt_resource *resources, @@ -368,6 +688,8 @@ RT_DLLEXPORT rt_result rtCreateResources(uint32_t count, rt_aio_handle write_handles[RT_WRITE_BATCH_MAX_SIZE]; uint32_t outstanding_writes = 0; + rt_temp_arena temp_arena = rtGetTemporaryArena(NULL, 0); + rtLockWrite(&_namespace.lock); for (uint32_t i = 0; i < count; ++i) { size_t name_len = strlen(names[i]); @@ -383,14 +705,31 @@ RT_DLLEXPORT rt_result rtCreateResources(uint32_t count, ids[i] = id; - const rt_resource *cached_resource = CacheResource(id, &resources[i]); + rt_resource *cached = CacheResource(id, &resources[i]); - PrepareResourceFlushToFile(id, cached_resource, &writes.writes[writes.num_writes]); - _namespace.ids[at] = id; - _namespace.refs[at].offset = writes.writes[writes.num_writes].offset; - _namespace.refs[at].size = writes.writes[writes.num_writes].num_bytes; - _namespace.refs[at].file = writes.writes[writes.num_writes].file; + if (!PrepareResourceFlushToFile(id, + cached, + &writes.writes[writes.num_writes], + temp_arena.arena)) { + + rtReportError("RESMGR", "Failed to prepare resource %llx for writing.", id); + inserted = false; + break; + } + _namespace.ids[at] = id; + _namespace.refs[at].offset = writes.writes[writes.num_writes].offset; + _namespace.refs[at].compressed_size = writes.writes[writes.num_writes].num_bytes; + _namespace.refs[at].file = writes.writes[writes.num_writes].file; + _namespace.refs[at].decompressed_size = + sizeof(rt_resource) + GetResourceDataSize(&resources[i]); ++writes.num_writes; + + rtLog("RESMGR", + "Created resource %llx: Uncompressed size: %zu bytes, compressed size: %zu " + "bytes.", + id, + _namespace.refs[at].decompressed_size, + _namespace.refs[at].compressed_size); break; } else if (_namespace.ids[at] == id) { rtReportError("RESMGR", @@ -439,6 +778,83 @@ RT_DLLEXPORT rt_result rtCreateResources(uint32_t count, } } out: + rtReturnTemporaryArena(temp_arena); rtUnlockWrite(&_namespace.lock); return result; -} \ No newline at end of file +} + +RT_DLLEXPORT void rDebugLogResource(rt_resource_id id, const rt_resource *resource) { + static const char *type_str[RT_RESOURCE_TYPE_count] = {"Shader", "Pipeline"}; + rtLog("RESMGR", "Resource %llx:", id); + rtLog("RESMGR", + " type: %s", + (resource->type < RT_RESOURCE_TYPE_count) ? type_str[resource->type] : ""); + rtLog("RESMGR", " subresources:"); + for (uint32_t i = 0; i < resource->subresource_count; ++i) { + rtLog("RESMGR", " - %llx", resource->subresources[i]); + } + rtLog("RESMGR", " dependencies:"); + for (uint32_t i = 0; i < resource->dependency_count; ++i) { + rtLog("RESMGR", " - %llx", resource->dependencies[i]); + } + switch (resource->type) { + case RT_RESOURCE_PIPELINE: { + static const char *binding_str[RT_ATTRIBUTE_VALUE_count] = {"", + "MaterialAlbedo", + "MaterialNormal"}; + const rt_pipeline_info *pipeline = resource->data; + rtLog("RESMGR", " pipeline data:"); + rtLog("RESMGR", " vertex shader: %llx", pipeline->vertex_shader); + rtLog("RESMGR", " fragment shader: %llx", pipeline->fragment_shader); + rtLog("RESMGR", " compute shader: %llx", pipeline->compute_shader); + rtLog("RESMGR", " uniform bindings:"); + const rt_attribute_binding *uniform_bindings = + rtResolveConstRelptr(&pipeline->uniform_bindings); + for (uint32_t i = 0; i < pipeline->uniform_binding_count; ++i) { + rtLog("RESMGR", + " - %u : %s", + uniform_bindings[i].index, + (uniform_bindings[i].value < RT_ATTRIBUTE_VALUE_count) + ? binding_str[uniform_bindings[i].value] + : ""); + } + rtLog("RESMGR", " texture bindings:"); + const rt_attribute_binding *texture_bindings = + rtResolveConstRelptr(&pipeline->texture_bindings); + for (uint32_t i = 0; i < pipeline->texture_binding_count; ++i) { + rtLog("RESMGR", + " - %u : %s", + texture_bindings[i].index, + (texture_bindings[i].value < RT_ATTRIBUTE_VALUE_count) + ? binding_str[texture_bindings[i].value] + : ""); + } + rtLog("RESMGR", " storage bindings:"); + const rt_attribute_binding *storage_bindings = + rtResolveConstRelptr(&pipeline->storage_bindings); + for (uint32_t i = 0; i < pipeline->storage_binding_count; ++i) { + rtLog("RESMGR", + " - %u : %s", + storage_bindings[i].index, + (storage_bindings[i].value < RT_ATTRIBUTE_VALUE_count) + ? binding_str[storage_bindings[i].value] + : ""); + } + } break; + case RT_RESOURCE_SHADER: { + static const char *stype_str[RT_SHADER_TYPE_count] = {"", "Vulkan"}; + static const char *stage_str[RT_SHADER_STAGE_count] = {"Vertex", "Fragment", "Compute"}; + const rt_shader_info *shader = resource->data; + rtLog("RESMGR", " shader data:"); + rtLog("RESMGR", + " type: %s", + (shader->type < RT_SHADER_TYPE_count) ? stype_str[shader->type] : ""); + rtLog("RESMGR", + " stage: %s", + (shader->stage < RT_SHADER_STAGE_count) ? stage_str[shader->stage] : ""); + rtLog("RESMGR", " bytecode: %zu bytes", shader->bytecode_length); + } break; + default: + rtLog("RESMGR", " unknown data at: %llx", (uintptr_t)resource->data); + } +} diff --git a/src/runtime/resources.h b/src/runtime/resources.h index 6bee8d6..bfe48d7 100644 --- a/src/runtime/resources.h +++ b/src/runtime/resources.h @@ -34,6 +34,8 @@ typedef enum { /* A pipeline state object */ RT_RESOURCE_PIPELINE, + + RT_RESOURCE_TYPE_count, } rt_resource_type; #define RT_MAX_SUBRESOURCES 32 @@ -55,6 +57,26 @@ typedef struct { rt_resource_id dependencies[RT_MAX_RESOURCE_DEPENDENCIES]; } rt_resource; +/* Retrieves a resource. + * + * This function will also attempt to pre-cache resources that are likely to be accessed + * in the future. Namely subresources and dependencies of the accessed resource. + * + * The passed destination must point to a buffer large enough to hold the resource. + * You can query the resources size with rtGetResourceSize(). + */ +RT_DLLEXPORT rt_result rtGetResource(rt_resource_id id, void *dest); + +/* Notifies the system that a resource might be accessed soon. + * Starts a asynchronous prefetch of that resource. */ +RT_DLLEXPORT void rtPrefetchResources(const rt_resource_id *ids, uint32_t count); + +/* Returns the size of a resource in bytes, or 0 if the resource id is invalid. */ +RT_DLLEXPORT size_t rtGetResourceSize(rt_resource_id id); + +/* Logs information about a resource. Useful for debugging */ +RT_DLLEXPORT void rDebugLogResource(rt_resource_id id, const rt_resource *resource); + /* Registers resources with the resource manager, making them available to the system. * * The runtime will create a standalone file for each resource in the resource directory. diff --git a/src/runtime/runtime.h b/src/runtime/runtime.h index a080307..44af197 100644 --- a/src/runtime/runtime.h +++ b/src/runtime/runtime.h @@ -30,6 +30,21 @@ extern "C" { #define RT_MB(n) ((n)*1024U * 1024U) #define RT_GB(n) ((n)*1024U * 1024U * 1024U) +#if defined(_MSC_VER) +/* For some reason _Thread_local does not work with vs2022, + * despite MS documentation claiming it should. */ +#define RT_THREAD_LOCAL __declspec(thread) +#elif defined(__GNUC__) || defined(__clang__) +/* _Thread_local in C11-C17, thread_local in C23 */ +#if __STDC_VERSION__ > 201710L +#define RT_THREAD_LOCAL thread_local +#elif __STDC_VERSION__ >= 201112L +#define RT_THREAD_LOCAL _Thread_local +#else +#pragma error Pre-C11 not supported. +#endif +#endif + typedef unsigned int rt_result; /* Default result codes */ @@ -80,11 +95,11 @@ RT_DLLEXPORT void rtLog(const char *subsystem, const char *fmt, ...); RT_DLLEXPORT int rtAssertHandler(const char *expr, const char *msg, const char *file, int line); #define RT_ASSERT(x, msg) \ do { \ - if (!(x)) { \ - if (rtAssertHandler(#x, (msg), __FILE__, __LINE__) == 0) { \ + if (!(x)) { \ + if (rtAssertHandler(#x, (msg), __FILE__, __LINE__) == 0) { \ RT_DEBUGBREAK; \ } \ - } \ + } \ } while (0) #else #define RT_ASSERT(x, msg) RT_UNUSED(x) diff --git a/src/runtime/shader_compiler.c b/src/runtime/shader_compiler.c index bb53fa8..ea90644 100644 --- a/src/runtime/shader_compiler.c +++ b/src/runtime/shader_compiler.c @@ -27,7 +27,7 @@ static rt_shader_bytecode CompileNullShader(rt_shader_stage stage, typedef rt_shader_bytecode shader_compiler_fn(rt_shader_stage, rt_shader_optimization_level, rt_text_span, const char *, rt_arena *); -static shader_compiler_fn *_compiler_funcs[RT_SHADER_TYPE_COUNT] = { +static shader_compiler_fn *_compiler_funcs[RT_SHADER_TYPE_count] = { CompileNullShader, #ifdef RT_BUILD_DXC_SHADER_COMPILER @@ -44,7 +44,7 @@ rt_shader_bytecode CompileShader(rt_shader_type type, rt_text_span code, const char *file_path, rt_arena *arena) { - if (type >= RT_SHADER_TYPE_COUNT) { + if (type >= RT_SHADER_TYPE_count) { rtLog("AC", "Invalid shader type %u", type); return (rt_shader_bytecode){.bytes = NULL, .len = 0}; }