From d572203c41cfff817465fc3cddcdea56d5d7d9f8 Mon Sep 17 00:00:00 2001 From: skal Date: Tue, 3 Mar 2026 09:00:50 +0100 Subject: feat(assets): replace is_procedural/is_gpu_procedural bools with AssetType enum, add MP3 type - Add AssetType enum {STATIC, PROC, PROC_GPU, MP3} to AssetRecord - Add GetAssetType() API to asset_manager.h/cc - asset_packer: parse 'MP3' compression keyword in assets.txt - tracker: remove magic-byte is_mp3_asset(); use GetAssetType() instead - assets.txt: NEVER_MP3 now uses 'MP3' compression type - doc/ASSET_SYSTEM.md: rewritten to document new types and API handoff(Gemini): AssetType enum landed; MP3 detection is now explicit via asset metadata. --- doc/ASSET_SYSTEM.md | 136 ++++++++++++++++----------------------------- src/audio/tracker.cc | 13 +---- src/util/asset_manager.cc | 26 +++++---- src/util/asset_manager.h | 9 ++- tools/asset_packer.cc | 32 ++++++----- workspaces/main/assets.txt | 8 ++- workspaces/test/assets.txt | 6 ++ 7 files changed, 102 insertions(+), 128 deletions(-) diff --git a/doc/ASSET_SYSTEM.md b/doc/ASSET_SYSTEM.md index f52e382..6104353 100644 --- a/doc/ASSET_SYSTEM.md +++ b/doc/ASSET_SYSTEM.md @@ -1,42 +1,50 @@ # Compact Asset System for the 64k Demo -This file describes the asset system architecture and runtime API. - -## Core Concept - All assets are const byte arrays embedded in the binary. Runtime retrieval via `GetAsset(AssetId)` returns direct pointers with O(1) lookup. -## Runtime Asset Retrieval - -```cpp -#include "assets.h" -const uint8_t* mysample = GetAsset(ASSET_SAMPLE_142, &size); -// ... use asset -DropAsset(ASSET_SAMPLE_142, mysample); // For lazy decompression cleanup -``` - ## Asset Manifest Format Files: `workspaces/main/assets.txt`, `workspaces/test/assets.txt` Format (CSV): ``` -ASSET_NAME, COMPRESSION, filename.ext, "Description" +ASSET_NAME, COMPRESSION, path/to/file.ext, "Description" ``` -Example: +### Compression Types + +| Type | Behavior | +|------|----------| +| `NONE` | Raw binary embed (shaders, meshes, `.spec` files) | +| `MP3` | MP3 audio; decoded to spectrogram at init | +| `PROC(func, ...)` | CPU procedural generation at init | +| `PROC_GPU(func, ...)` | GPU compute procedural generation at init | + +## AssetType Enum + +Each `AssetRecord` carries an `AssetType` field (replaces the former `is_procedural`/`is_gpu_procedural` bools): + +```cpp +enum class AssetType : uint8_t { STATIC, PROC, PROC_GPU, MP3 }; ``` -SAMPLE_142, sample_142.spec, NONE, "This is a drum kick sample" -SHADER_COMMON, shaders/common.wgsl, NONE, "Common utilities" + +Query at runtime: +```cpp +if (GetAssetType(AssetId::NEVER_MP3) == AssetType::MP3) { ... } ``` -This generates: +## Runtime API + ```cpp -enum class AssetId : uint16_t { - ASSET_SAMPLE_142 = 6323, - SHADER_COMMON = 6324, - // ... -}; +#include "util/asset_manager.h" + +// Retrieve asset data +size_t size; +const uint8_t* data = GetAsset(AssetId::MY_ASSET, &size); +DropAsset(AssetId::MY_ASSET, data); // Release procedural/cached data + +// Query type +AssetType t = GetAssetType(AssetId::MY_ASSET); ``` ## Build Pipeline @@ -44,80 +52,32 @@ enum class AssetId : uint16_t { Tool: `tools/asset_packer.cc` 1. Parse workspace `assets.txt` -2. Read files from workspace `assets/` or `assets/common/` -3. Generate `assets.h` (enum definitions) -4. Generate `assets_data.cc` (embedded byte arrays) +2. Read/process files (images → RGBA8 header, meshes → vertex/index binary, others → raw) +3. Generate `src/generated/assets.h` (enum + declarations) +4. Generate `src/generated/assets_data.cc` (byte arrays + `AssetRecord` table) 5. Auto-triggered by CMake on manifest changes ## Technical Guarantees -1. **Alignment**: All arrays declared `alignas(16)` for safe `reinterpret_cast` -2. **String Safety**: Null-terminator appended (safe as C-string) -3. **Size Reporting**: `size` reflects original file size (buffer is `size + 1`) +- **Alignment**: All arrays declared `alignas(16)` for safe `reinterpret_cast` +- **String Safety**: Null-terminator appended (safe as C-string) +- **Size**: `size` reflects original file size (buffer is `size + 1`) -## Shader Asset Usage +## Developer Workflow -```cpp -size_t size; -const uint8_t* shader_src = GetAsset(AssetId::SHADER_COMMON, &size); -std::string_view code(reinterpret_cast(shader_src), size); -ShaderComposer::register_snippet("common", code); +**Add a static asset:** +``` +MY_ASSET, NONE, path/to/file.ext, "Description" ``` -## Procedural Assets - -Format: `PROC(function_name, params...)` - -Example: +**Add an MP3 sample:** ``` -NOISE_TEX, PROC(gen_noise, 1234, 16), NONE, "Noise texture seed=1234 freq=16" +MY_SAMPLE, MP3, music/sample.mp3, "Description" ``` -Runtime: First `GetAsset()` call invokes generator, caches result. - -## Design Strengths - -- Enum-based type safety (no magic numbers) -- Zero runtime cost for static assets -- Procedural generation support (size savings) -- Thread-safe for immutable assets - -## Design Weaknesses - -- **No compression**: Only `NONE` supported (critical blocker for 64k) -- **STL dependencies**: `std::map` in runtime code (conflicts with CRT removal) -- **Hardcoded procedural dimensions**: Assumes 256×256 RGBA8 -- **No integrity checks**: No CRC/hash validation - -## Developer Workflow +**Add a procedural texture:** +``` +MY_TEX, PROC(gen_noise, 1234, 16), _, "Description" +``` -**Add new asset:** -1. Place file in workspace `assets/` (or `assets/common/` for shared) -2. Edit workspace `assets.txt`: - ``` - MY_ASSET, NONE, assets/myfile.ext, "Description" - ``` -3. Rebuild: `cmake --build build -j4` (auto-triggered) -4. Use in code: `GetAsset(AssetId::MY_ASSET, &size)` - -**Modify existing asset:** -- Edit source file in `assets/final/` -- CMake auto-detects change and rebuilds - -## Planned Improvements - -See **TODO.md** for detailed task breakdown: -- **Task #27**: Asset compression (zlib, RLE) -- **Task #28**: Spectrogram quantization (float32 → uint16_t) -- **Task #29**: WGSL shader minification -- **Task #30**: Procedural asset generalization -- **Task #31**: Asset integrity validation (CRC32) -- **Task #32**: Hot-reloading (dev builds) -- **Task #33**: Dead asset elimination - -## Current Manifest - -33 assets total: -- 15 audio samples (`.spec`) -- 17 WGSL shaders -- 1 procedural texture +Rebuild: `cmake --build build -j4` — CMake detects manifest changes automatically. diff --git a/src/audio/tracker.cc b/src/audio/tracker.cc index 333a337..7ce96fc 100644 --- a/src/audio/tracker.cc +++ b/src/audio/tracker.cc @@ -44,15 +44,6 @@ static bool g_cache_initialized = false; // Forward declarations static int get_free_pool_slot(); -// Returns true if the asset blob looks like an MP3 file. -static bool is_mp3_asset(const uint8_t* data, size_t size) { - if (!data || size < 3) return false; - // ID3v2 tag prefix - if (data[0] == 'I' && data[1] == 'D' && data[2] == '3') return true; - // Raw MP3 sync word: 0xFF followed by 0xE0-0xFF - if (size >= 2 && data[0] == 0xFF && (data[1] & 0xE0) == 0xE0) return true; - return false; -} #if !defined(STRIP_ALL) // Decode an in-memory MP3 blob to a heap-allocated spectrogram (caller owns). @@ -147,7 +138,7 @@ void tracker_init() { size_t size; const uint8_t* data = GetAsset(aid, &size); #if !defined(STRIP_ALL) - if (data && size > 0 && is_mp3_asset(data, size)) { + if (data && size > 0 && GetAssetType(aid) == AssetType::MP3) { int num_frames = 0; float* spec_data = convert_mp3_to_spectrogram(data, size, &num_frames); @@ -165,7 +156,7 @@ void tracker_init() { } } else #else - FATAL_CHECK(data == nullptr || !is_mp3_asset(data, size), + FATAL_CHECK(data == nullptr || GetAssetType(aid) != AssetType::MP3, "MP3 assets not supported in STRIP_ALL builds\n"); #endif if (data && size >= sizeof(SpecHeader)) { diff --git a/src/util/asset_manager.cc b/src/util/asset_manager.cc index a606b46..2285f3a 100644 --- a/src/util/asset_manager.cc +++ b/src/util/asset_manager.cc @@ -37,7 +37,7 @@ static const size_t kNumProcGenFuncs = sizeof(kAssetManagerProcGenFuncs) / sizeof(kAssetManagerProcGenFuncs[0]); // Array-based cache for assets. -// Initialized to all zeros (nullptr data, 0 size, false is_procedural) +// Initialized to all zeros (nullptr data, 0 size, AssetType::STATIC) // The size is derived from the generated ASSET_LAST_ID enum value. static AssetRecord g_asset_cache[(size_t)AssetId::ASSET_LAST_ID] = {}; @@ -78,7 +78,8 @@ const uint8_t* GetAsset(AssetId asset_id, size_t* out_size) { AssetRecord cached_record = source_record; - if (source_record.is_procedural) { + if (source_record.type == AssetType::PROC || + source_record.type == AssetType::PROC_GPU) { // Dynamically generate the asset ProcGenFunc proc_gen_func_ptr = nullptr; for (size_t i = 0; i < kNumProcGenFuncs; ++i) { @@ -131,13 +132,8 @@ const uint8_t* GetAsset(AssetId asset_id, size_t* out_size) { cached_record.data = generated_data; cached_record.size = data_size; - cached_record.is_procedural = true; - // proc_gen_func, proc_params, num_proc_params already copied from + // type, proc_gen_func, proc_params, num_proc_params already copied from // source_record - - } else { - // Static asset (copy from g_assets) - cached_record.is_procedural = false; } // Store in cache for future use @@ -187,7 +183,8 @@ void DropAsset(AssetId asset_id, const uint8_t* asset) { // Check if the asset is in cache and is procedural, and if the pointer // matches. This prevents accidentally freeing static data or freeing twice. if (g_asset_cache[index].data == asset && - g_asset_cache[index].is_procedural) { + (g_asset_cache[index].type == AssetType::PROC || + g_asset_cache[index].type == AssetType::PROC_GPU)) { delete[] g_asset_cache[index].data; g_asset_cache[index] = {}; // Zero out the struct to force re-generation } @@ -203,7 +200,9 @@ bool ReloadAssetsFromFile(const char* config_path) { // Clear cache to force reload for (size_t i = 0; i < (size_t)AssetId::ASSET_LAST_ID; ++i) { - if (g_asset_cache[i].is_procedural && g_asset_cache[i].data) { + if ((g_asset_cache[i].type == AssetType::PROC || + g_asset_cache[i].type == AssetType::PROC_GPU) && + g_asset_cache[i].data) { delete[] g_asset_cache[i].data; } g_asset_cache[i] = {}; @@ -213,3 +212,10 @@ bool ReloadAssetsFromFile(const char* config_path) { return true; } #endif // !defined(STRIP_ALL) + +AssetType GetAssetType(AssetId asset_id) { + uint16_t index = (uint16_t)asset_id; + if (index >= (uint16_t)AssetId::ASSET_LAST_ID) + return AssetType::STATIC; + return GetAssetRecordTable()[index].type; +} diff --git a/src/util/asset_manager.h b/src/util/asset_manager.h index 1714c21..09483e5 100644 --- a/src/util/asset_manager.h +++ b/src/util/asset_manager.h @@ -5,12 +5,12 @@ #pragma once #include "asset_manager_dcl.h" +enum class AssetType : uint8_t { STATIC, PROC, PROC_GPU, MP3 }; + struct AssetRecord { const uint8_t* data; // Pointer to asset data (static or dynamic) size_t size; // Size of the asset data - bool is_procedural; // True if data was dynamically allocated by a procedural - // generator - bool is_gpu_procedural; // True if GPU compute shader generates texture + AssetType type; const char* proc_func_name_str; // Name of procedural generation function // (string literal) const float* proc_params; // Parameters for procedural generation (static, @@ -30,6 +30,9 @@ void DropAsset(AssetId asset_id, const uint8_t* asset); // found. AssetId GetAssetIdByName(const char* name); +// Returns the AssetType for a given asset id. +AssetType GetAssetType(AssetId asset_id); + #if !defined(STRIP_ALL) // Hot-reload: Parse and reload assets from config file (debug only) bool ReloadAssetsFromFile(const char* config_path); diff --git a/tools/asset_packer.cc b/tools/asset_packer.cc index 7e186b6..0d48906 100644 --- a/tools/asset_packer.cc +++ b/tools/asset_packer.cc @@ -80,9 +80,8 @@ static bool ParseProceduralParams(const std::string& params_str, // Helper struct to hold all information about an asset during parsing struct AssetBuildInfo { std::string name; - std::string filename; // Original filename for static assets - bool is_procedural; - bool is_gpu_procedural; + std::string filename; // Original filename for static assets + std::string asset_type; // "STATIC", "PROC", "PROC_GPU", "MP3" std::string proc_func_name; // Function name string std::vector proc_params; // Parameters for procedural function @@ -439,21 +438,23 @@ int main(int argc, char* argv[]) { info.data_array_name = "ASSET_DATA_" + info.name; info.params_array_name = "ASSET_PROC_PARAMS_" + info.name; info.func_name_str_name = "ASSET_PROC_FUNC_STR_" + info.name; - info.is_procedural = false; - info.is_gpu_procedural = false; + info.asset_type = "STATIC"; if (compression_type_str.rfind("PROC_GPU(", 0) == 0) { - info.is_procedural = true; - info.is_gpu_procedural = true; + info.asset_type = "PROC_GPU"; if (!ParseProceduralFunction(compression_type_str, &info, true)) { return 1; } } else if (compression_type_str.rfind("PROC(", 0) == 0) { - info.is_procedural = true; - info.is_gpu_procedural = false; + info.asset_type = "PROC"; if (!ParseProceduralFunction(compression_type_str, &info, false)) { return 1; } + } else if (compression_type_str == "MP3") { + info.asset_type = "MP3"; + } else if (compression_type_str != "NONE") { + fprintf(stderr, "Warning: Unknown compression type '%s' for asset: %s\n", + compression_type_str.c_str(), info.name.c_str()); } asset_build_infos.push_back(info); @@ -479,7 +480,7 @@ int main(int argc, char* argv[]) { std::fclose(assets_h_file); for (const auto& info : asset_build_infos) { - if (!info.is_procedural) { + if (info.asset_type != "PROC" && info.asset_type != "PROC_GPU") { std::string base_dir = assets_txt_path.substr(0, assets_txt_path.find_last_of("/\\") + 1); std::filesystem::path base_path = std::filesystem::absolute(base_dir); @@ -542,15 +543,16 @@ int main(int argc, char* argv[]) { fprintf(assets_data_cc_file, " static const AssetRecord assets[] = {\n"); for (const auto& info : asset_build_infos) { fprintf(assets_data_cc_file, " { "); - if (info.is_procedural) { - fprintf(assets_data_cc_file, "nullptr, 0, true, %s, %s, %s, %zu", - info.is_gpu_procedural ? "true" : "false", + if (info.asset_type == "PROC" || info.asset_type == "PROC_GPU") { + fprintf(assets_data_cc_file, "nullptr, 0, AssetType::%s, %s, %s, %zu", + info.asset_type.c_str(), info.func_name_str_name.c_str(), info.params_array_name.c_str(), info.proc_params.size()); } else { fprintf(assets_data_cc_file, - "%s, ASSET_SIZE_%s, false, false, nullptr, nullptr, 0", - info.data_array_name.c_str(), info.name.c_str()); + "%s, ASSET_SIZE_%s, AssetType::%s, nullptr, nullptr, 0", + info.data_array_name.c_str(), info.name.c_str(), + info.asset_type.c_str()); } fprintf(assets_data_cc_file, " },\n"); } diff --git a/workspaces/main/assets.txt b/workspaces/main/assets.txt index 2cc3896..a324cfd 100644 --- a/workspaces/main/assets.txt +++ b/workspaces/main/assets.txt @@ -1,5 +1,11 @@ # WORKSPACE: main # Asset Name, Compression Type, Filename/Placeholder, Description +# +# Compression types: +# NONE - Raw binary embed (shaders, meshes, .spec files) +# MP3 - MP3 audio; decoded to spectrogram at runtime +# PROC(func, ...) - CPU procedural generation at init +# PROC_GPU(func,..) - GPU compute procedural generation at init # --- Drum & Percussion Samples --- KICK_1, NONE, music/KICK_606.spec, "606 Kick" @@ -18,7 +24,7 @@ SPLASH_1, NONE, music/SPLASH_GROUNDED.spec, "Splash Cymbal" BASS_1, NONE, music/BASS_GUITAR_FEEL.spec, "Bass Guitar" BASS_2, NONE, music/BASS_SYNTH_1.spec, "Synth Bass 1" BASS_3, NONE, music/SYNTH_BASS_DISTORT.spec, "Distorted Synth Bass" -NEVER_MP3, NONE, music/never.mp3, "MP3 Sample" +NEVER_MP3, MP3, music/never.mp3, "MP3 Sample" # --- Procedural Textures --- NOISE_TEX, PROC(gen_noise, 1234, 16), _, "Procedural noise texture for bump mapping" diff --git a/workspaces/test/assets.txt b/workspaces/test/assets.txt index 1e95084..1cdaf32 100644 --- a/workspaces/test/assets.txt +++ b/workspaces/test/assets.txt @@ -1,5 +1,11 @@ # WORKSPACE: test # Asset Name, Compression Type, Filename/Placeholder, Description +# +# Compression types: +# NONE - Raw binary embed (shaders, meshes, .spec files) +# MP3 - MP3 audio; decoded to spectrogram at runtime +# PROC(func, ...) - CPU procedural generation at init +# PROC_GPU(func,..) - GPU compute procedural generation at init # --- Drum & Percussion Samples --- KICK_1, NONE, music/KICK_606.spec, "606 Kick" -- cgit v1.2.3