trueno/src/assets/asset_manager.jai
2026-04-23 11:41:44 +03:00

571 lines
18 KiB
Plaintext

#scope_file
#import "String";
hash :: #import "Hash";
Pool :: #import "Pool";
#load "loaders.jai";
// MAX_PACK_SIZE, MAX_WORLD_SIZE, MAX_CHUNKS_SIZE, MAX_SHGRID_SIZE,
// MAX_RDM_ATLAS_SIZE, MAX_RDM_MANIFEST_SIZE are injected as build strings
// by first.jai after scanning the actual asset files at compile time.
MAX_FETCH_SLOTS :: 16;
#scope_export
// Replaces the old should_block / should_block_engine bool pair.
Load_Priority :: enum u8 {
BLOCK_ENGINE; // must finish before the first game frame
LOADING_SCREEN; // show loading screen while pending
BACKGROUND; // stream silently; game runs normally
}
Fetch_Type :: enum {
PACK;
WORLD;
WORLD_CHUNKS;
RDM_ATLAS;
RDM_MANIFEST;
SHGRID;
}
Fetch_Request :: struct {
type : Fetch_Type;
path : string;
priority : Load_Priority;
// PACK
pack_name : string;
// WORLD / RDM / SH
world_name : string;
chunk_key : Chunk_Key;
// WORLD_CHUNKS: JSON carried forward from the WORLD fetch
world_json_data : []u8;
}
// One in-flight sfetch request. `buf` is allocated when dispatched and
// freed (or handed to Loaded_Pack) when the callback fires.
Fetch_Slot :: struct {
occupied : bool;
req : Fetch_Request;
buf : []u8;
}
Asset_Manager :: struct {
slots : [MAX_FETCH_SLOTS]Fetch_Slot;
queue : [..]Fetch_Request;
loadedPacks : [..]Loaded_Pack;
pending_hot_reload : bool;
hot_reload_unpause_pending : bool;
}
g_asset_manager : Asset_Manager;
#scope_file
#load "rdm_loader.jai";
#load "sh_loader.jai";
fetch_type_buffer_size :: (type: Fetch_Type) -> s64 {
if #complete type == {
case .PACK; return MAX_PACK_SIZE;
case .WORLD; return MAX_WORLD_SIZE;
case .WORLD_CHUNKS; return MAX_CHUNKS_SIZE;
case .RDM_ATLAS; return MAX_RDM_ATLAS_SIZE;
case .RDM_MANIFEST; return MAX_RDM_MANIFEST_SIZE;
case .SHGRID; return MAX_SHGRID_SIZE;
}
}
fetch_callback :: (res: *sfetch_response_t) #c_call {
push_context,defer_pop default_context;
// sokol copied the slot index into res.user_data when we sent the request.
slot_idx := (cast(*u32) res.user_data).*;
slot := *g_asset_manager.slots[slot_idx];
if !res.finished then return;
slot.occupied = false;
req := slot.req;
if res.failed {
handle_fetch_failed(*req);
free(slot.buf.data);
slot.buf = .{};
return;
}
data: []u8;
data.data = cast(*u8) res.data.ptr;
data.count = cast(s64) res.data.size;
process_completed_fetch(*req, data);
if req.type == .PACK {
// process_completed_fetch transferred ownership to Loaded_Pack.data_buffer.
slot.buf = .{};
} else {
free(slot.buf.data);
slot.buf = .{};
}
}
handle_fetch_failed :: (req: *Fetch_Request) {
if req.type == {
case .PACK;
log_error("Failed to load pack '%'", req.pack_name);
case .WORLD;
if ends_with(req.path, "world.json") {
fallback: Fetch_Request;
fallback.type = .WORLD;
fallback.priority = req.priority;
fallback.world_name = req.world_name;
fallback.path = sprint("%/worlds/%/index.world", GAME_RESOURCES_DIR, req.world_name);
array_add(*g_asset_manager.queue, fallback);
return;
}
log_error("Failed to load world '%'", req.world_name);
case .WORLD_CHUNKS;
free(req.world_json_data.data);
log_error("Failed to load chunks.bin for world '%'", req.world_name);
case .RDM_ATLAS;
log_info("RDM: no global atlas for world '%' (ok if none baked yet)", req.world_name);
case .RDM_MANIFEST;
log_info("RDM: no manifest for world '%' (ok if none baked yet)", req.world_name);
case .SHGRID;
sh_loader_handle_failed(req);
}
}
process_completed_fetch :: (req: *Fetch_Request, data: []u8) {
if req.type == {
case .PACK;
pack: Loaded_Pack;
Pool.set_allocators(*pack.pool);
pack.nameHash = hash.get_hash(req.pack_name);
pack.name = sprint("%", req.pack_name);
pack.data_buffer = data;
success := init_from_memory(*pack.content, pack.data_buffer, sprint("%", req.pack_name));
if !success {
log_error("Failed to load pack!!");
free(pack.data_buffer.data);
pack.data_buffer = .{};
return;
}
add_resources_from_pack(*pack);
array_add(*g_asset_manager.loadedPacks, pack);
case .WORLD;
is_json := data.count > 0 && data[0] == #char "{";
if is_json {
json_copy := NewArray(data.count, u8, false);
memcpy(json_copy.data, data.data, data.count);
chunks_req: Fetch_Request;
chunks_req.type = .WORLD_CHUNKS;
chunks_req.priority = req.priority;
chunks_req.world_name = req.world_name;
chunks_req.path = sprint("%/worlds/%/chunks.bin", GAME_RESOURCES_DIR, req.world_name);
chunks_req.world_json_data = json_copy;
array_add(*g_asset_manager.queue, chunks_req);
} else {
world, ok := load_world_from_data(data);
if ok {
set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world (legacy): %", req.world_name);
} else {
log_error("Failed to parse world '%'", req.world_name);
}
}
case .WORLD_CHUNKS;
json_data := req.world_json_data;
defer free(json_data.data);
json_str: string;
json_str.data = json_data.data;
json_str.count = json_data.count;
world, ok := load_world_from_json(json_str, data);
if ok {
set_loaded_world(world);
rdm_loader_enqueue_world(*get_current_world().world);
shgrid_loader_enqueue_world(*get_current_world().world);
log_info("Loaded world: %", req.world_name);
} else {
log_error("Failed to parse world '%'", req.world_name);
}
case .RDM_ATLAS;
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name then return;
header_size := cast(s64) size_of(RDM_File_Header);
if data.count < header_size {
log_error("RDM: global atlas too small (world %)", req.world_name);
return;
}
header := cast(*RDM_File_Header) data.data;
if header.magic != RDM_FILE_MAGIC {
log_error("RDM: bad atlas magic (world %)", req.world_name);
return;
}
atlas_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(u16);
atlas_imgdata: sg_image_data;
atlas_imgdata.subimage[0][0] = .{ data.data + header_size, atlas_pixel_bytes };
atlas_desc: sg_image_desc = .{
render_target = false,
width = header.width,
height = header.height,
pixel_format = sg_pixel_format.RGBA16F,
sample_count = 1,
data = atlas_imgdata,
};
if g_rdm_atlas.id != 0 then sg_destroy_image(g_rdm_atlas);
g_rdm_atlas = sg_make_image(*atlas_desc);
log_debug("RDM: loaded global atlas (%x%)", header.width, header.height);
case .RDM_MANIFEST;
curworld := get_current_world();
if !curworld.valid || curworld.world.name != req.world_name then return;
json_str : string;
json_str.data = data.data;
json_str.count = data.count;
ok, entries := Jaison.json_parse_string(json_str, [..]Rdm_Atlas_Entry,, temp);
if !ok {
log_error("RDM: failed to parse manifest for world '%'", req.world_name);
return;
}
array_reset_keeping_memory(*curworld.world.rdm_lookup);
for e: entries array_add(*curworld.world.rdm_lookup, e);
log_debug("RDM: loaded manifest (% entries)", entries.count);
case .SHGRID;
sh_loader_handle_completed(req, data);
}
}
Loaded_Pack :: struct {
name : string;
nameHash : u32 = 0;
data_buffer : []u8;
content : Load_Package;
textures : Table(string, sg_image);
animations : Table(string, Animation);
audio : Table(string, Audio_Data);
pool : Pool.Pool;
}
add_resources_from_pack :: (pack: *Loaded_Pack) {
push_allocator(.{Pool.pool_allocator_proc, *pack.pool});
Queued_Sheet_File :: struct {
name : string;
image : sg_image;
sheet_w : s32;
sheet_h : s32;
sheet : Aseprite_Sheet;
}
sheets_to_init : Table(string, Queued_Sheet_File);
sheets_to_init.allocator = temp;
for v : pack.content.lookup {
_, name, extension := split_from_left(v.name, ".");
if extension == {
case "png";
img, w, h := create_texture_from_memory(v.data);
table_set(*pack.textures, sprint("%", v.name), img);
case "sheet.png";
img, w, h := create_texture_from_memory(v.data);
queuedSheet := table_find_pointer(*sheets_to_init, name);
if !queuedSheet {
table_set(*sheets_to_init, name, .{ name = name, image = img, sheet_w = w, sheet_h = h });
} else {
queuedSheet.image = img;
queuedSheet.sheet_w = w;
queuedSheet.sheet_h = h;
}
case "sheet.json";
s := create_string_from_memory(v.data);
success, sheet := Jaison.json_parse_string(s, Aseprite_Sheet,, temp);
if !success {
log_error("Failed to parse animation sheet JSON for sheet(%)", name);
continue;
}
queuedSheet := table_find_pointer(*sheets_to_init, name);
if !queuedSheet {
table_set(*sheets_to_init, name, .{ name = name, sheet = sheet });
} else {
queuedSheet.sheet = sheet;
}
case "colorgrade.png";
img, x, y := create_texture_from_memory(v.data);
add_image_to_lut_list(img, v.name);
case "wav";
audio := load_wav_from_memory(v.data);
table_set(*pack.audio, name, audio);
case "json";
if name == "particles" {
s := create_string_from_memory(v.data);
success, defs := Jaison.json_parse_string(s, [..]Particle_Emitter_Config,, temp);
if success {
array_reset(*g_emitter_defs);
for defs {
def := it;
def.name = sprint("%", it.name);
def.animation_name = sprint("%", it.animation_name);
array_add(*g_emitter_defs, def);
}
log_info("Loaded % particle definitions from pack", g_emitter_defs.count);
}
}
case "hex";
pal : Loaded_Palette;
pal.name = sprint("%", name);
pal.entries = load_hex_palette_from_memory(v.data);
array_add(*g_palettes, pal);
log_info("Loaded % colors from hex palette(%)", pal.entries.count, v.name);
case "ttf";
case;
log_warn("File(%) in pack(%) has unknown format", v.name, pack.name);
}
log_debug("% -> %", v.name, extension);
}
for qsheet : sheets_to_init {
for qsheet.sheet.meta.frameTags {
anim : Animation;
anim.name = sprint("%", it.name);
anim.sheet = qsheet.image;
anim.sheet_w = qsheet.sheet_w;
anim.sheet_h = qsheet.sheet_h;
for idx : it.from..it.to {
frameData := qsheet.sheet.frames[idx];
array_add(*anim.frames, Frame.{
frameData.frame.x,
frameData.frame.y,
frameData.frame.w,
frameData.frame.h,
frameData.duration
});
}
table_add(*pack.animations, anim.name, anim);
log_debug("Added anim(%)", anim.name);
}
}
}
find_pack_by_name :: (name: string) -> (bool, Loaded_Pack) {
nameHash := get_hash(name);
for g_asset_manager.loadedPacks {
if it.nameHash == nameHash {
return true, it;
}
}
log_warn("Unable to find pack: %", name);
return false, .{};
}
// Removes the highest-priority item from the queue (lowest enum value = most urgent).
// Within equal priority, the earliest-queued item wins (stable).
dequeue_highest_priority :: (queue: *[..]Fetch_Request) -> (Fetch_Request, bool) {
if queue.count == 0 return .{}, false;
best := 0;
for i: 1..queue.count - 1 {
if queue.*[i].priority < queue.*[best].priority then best = i;
}
req := queue.*[best];
array_ordered_remove_by_index(queue, best);
return req, true;
}
dispatch_slots :: () {
for i: 0..MAX_FETCH_SLOTS - 1 {
slot := *g_asset_manager.slots[i];
if slot.occupied continue;
if g_asset_manager.queue.count == 0 break;
req, found := dequeue_highest_priority(*g_asset_manager.queue);
if !found break;
slot.occupied = true;
slot.req = req;
slot.buf = NewArray(fetch_type_buffer_size(req.type), u8, false);
// sokol copies user_data bytes immediately on sfetch_send, so a
// stack-local index is safe here.
slot_idx : u32 = cast(u32) i;
// Route BACKGROUND requests to channel 1 so they don't starve
// LOADING_SCREEN / BLOCK_ENGINE work on native (two IO threads).
channel : u32 = ifx req.priority == .BACKGROUND then cast(u32)1 else cast(u32)0;
path_c := to_c_string(req.path);
defer free(path_c);
sfetch_send(*(sfetch_request_t.{
channel = channel,
path = path_c,
callback = fetch_callback,
buffer = .{ ptr = slot.buf.data, size = cast(u64) slot.buf.count },
user_data = .{ ptr = *slot_idx, size = cast(u64) size_of(u32) },
}));
}
}
#scope_export
free_resources_from_pack :: (pack: *Loaded_Pack) {
for pack.textures sg_destroy_image(it);
table_reset(*pack.textures);
table_reset(*pack.audio);
table_reset(*pack.animations);
Pool.reset(*pack.pool);
if pack.data_buffer.data {
free(pack.data_buffer.data);
pack.data_buffer = .{};
}
}
asset_manager_init :: () {
// No upfront allocations; buffers are allocated per-request in dispatch_slots.
}
mandatory_loads_done :: () -> bool {
for g_asset_manager.slots {
if it.occupied && it.req.priority == .BLOCK_ENGINE then return false;
}
for g_asset_manager.queue {
if it.priority == .BLOCK_ENGINE then return false;
}
return true;
}
show_loading_screen :: () -> bool {
for g_asset_manager.slots {
if it.occupied && it.req.priority != .BACKGROUND then return true;
}
for g_asset_manager.queue {
if it.priority != .BACKGROUND then return true;
}
return false;
}
asset_manager_tick :: () {
#if !FLAG_RELEASE_BUILD && OS != .WASM {
all_idle := true;
for g_asset_manager.slots if it.occupied { all_idle = false; break; }
if g_asset_manager.pending_hot_reload && all_idle && g_asset_manager.queue.count == 0 {
g_asset_manager.pending_hot_reload = false;
hot_reload_all_packs();
}
if g_asset_manager.hot_reload_unpause_pending && !show_loading_screen() {
g_mixer.paused = false;
g_asset_manager.hot_reload_unpause_pending = false;
}
}
sfetch_dowork();
dispatch_slots();
}
load_world :: (name: string) {
unload_current_world();
req: Fetch_Request;
req.type = .WORLD;
req.priority = .LOADING_SCREEN;
req.world_name = sprint("%", name);
req.path = sprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name);
array_add(*g_asset_manager.queue, req);
}
load_pack :: (name: string, priority: Load_Priority = .LOADING_SCREEN) {
req: Fetch_Request;
req.type = .PACK;
req.priority = priority;
req.pack_name = sprint("%", name);
req.path = sprint("%/%.pack", PACK_DIR, name);
array_add(*g_asset_manager.queue, req);
}
// Asset accessors:
#scope_file
#scope_export
get_texture_from_pack :: (pack: string, path: string) -> (sg_image) {
found, pack := find_pack_by_name(pack);
invalid_img : sg_image;
if !found {
return invalid_img;
}
ok, img := table_find(*pack.textures, path);
if !ok {
log_warn("Failed to find texture(%) from pack(%)", path, pack.name);
return invalid_img;
}
return img;
}
get_animation_from_pack :: (pack: string, path: string) -> *Animation {
found, pack := find_pack_by_name(pack);
if !found then return null;
return table_find_pointer(*pack.animations, path);
}
get_audio_from_pack :: (pack: string, path: string) -> *Audio_Data {
found, pack := find_pack_by_name(pack);
if !found then return null;
audio := table_find_pointer(*pack.audio, path);
if !audio then log_error("Failed to find audio(%)", path);
return audio;
}
add_font_from_pack :: (pack: string, path: string) {
pack_ok, pack := find_pack_by_name(pack);
if !pack_ok then return;
ok, entry := table_find(*pack.content.lookup, path);
if !ok {
log_error("Failed to find font % from pack", path);
return;
}
state.font_default.fons_font = fonsAddFontMem(state.fons, "sans", entry.data.data, xx entry.data.count, 0);
}
load_string_from_pack :: (pack: string, path: string) -> string {
log_warn("You are circumventing the asset management system");
pack_ok, pack := find_pack_by_name(pack);
if !pack_ok return "";
ok, entry := table_find(*pack.content.lookup, path);
if !ok {
log_error("Failed to load string from pack: %", path);
return "";
}
s: string;
s.data = entry.data.data;
s.count = entry.data.count;
return s;
}