#scope_file #import "String"; hash :: #import "Hash"; Pool :: #import "Pool"; #load "loaders.jai"; MAIN_CHUNK_BUFFER_SIZE : s64 : 256 * 1024; RDM_CHUNK_BUFFER_SIZE : s64 : 64 * 1024 * 1024; Active_Fetch :: struct { occupied : bool; req : Fetch_Request; accumulated : [..]u8; chunk_buf : []u8; } #scope_export CHANNEL_MAIN : u32 : 0; CHANNEL_RDM : u32 : 1; Fetch_Type :: enum { PACK; WORLD; WORLD_CHUNKS; RDM_ATLAS; RDM_LOOKUP; } Fetch_Request :: struct { type : Fetch_Type; path : string; // Pack pack_name : string; should_block : bool; should_block_engine : bool; // World / RDM world_name : string; chunk_key : Chunk_Key; // Atlas GPU image held between RDM_ATLAS and its paired RDM_LOOKUP fetch. rdm_pending_atlas : sg_image; // Copy of world.json carried between WORLD and WORLD_CHUNKS fetches. world_json_data : []u8; } Asset_Manager :: struct { active : [2]Active_Fetch; main_queue : [..]Fetch_Request; rdm_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"; fetch_callback :: (res: *sfetch_response_t) #c_call { push_context,defer_pop default_context; af := *g_asset_manager.active[res.channel]; if res.fetched { chunk_count := res.data.size.(s64); needed := af.accumulated.count + chunk_count; if af.accumulated.allocated < needed { new_cap := max(needed, max(af.accumulated.allocated * 2, 1024 * 1024)); array_reserve(*af.accumulated, new_cap); } memcpy(af.accumulated.data + af.accumulated.count, res.data.ptr, chunk_count); af.accumulated.count = needed; } if !res.finished then return; // Mark channel free before processing so that enqueued follow-up requests // (WORLD -> WORLD_CHUNKS, RDM_ATLAS -> RDM_LOOKUP) are visible on the next tick. af.occupied = false; req := af.req; if res.failed { handle_fetch_failed(*req); array_reset(*af.accumulated); return; } data : []u8; data.data = af.accumulated.data; data.count = af.accumulated.count; process_completed_fetch(*req, data); if req.type == .PACK { // process_completed_fetch either stored data in Loaded_Pack.data_buffer or // freed it on parse failure. Either way, zero af.accumulated without freeing. af.accumulated.data = null; af.accumulated.count = 0; af.accumulated.allocated = 0; } else { array_reset(*af.accumulated); } } 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_req : Fetch_Request; fallback_req.type = .WORLD; fallback_req.world_name = req.world_name; fallback_req.path = sprint("%/worlds/%/index.world", GAME_RESOURCES_DIR, req.world_name); fallback_req.should_block = true; array_add(*g_asset_manager.main_queue, fallback_req); 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_error("RDM: failed to load atlas for chunk %", req.chunk_key); case .RDM_LOOKUP; if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas); log_error("RDM: failed to load lookup for chunk %", req.chunk_key); } } 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.world_name = req.world_name; chunks_req.path = sprint("%/worlds/%/chunks.bin", GAME_RESOURCES_DIR, req.world_name); chunks_req.should_block = true; chunks_req.world_json_data = json_copy; array_add(*g_asset_manager.main_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); 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); 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: atlas too small for chunk %", req.chunk_key); return; } header := cast(*RDM_File_Header) data.data; if header.magic != RDM_FILE_MAGIC { log_error("RDM: bad atlas magic for chunk %", req.chunk_key); return; } atlas_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(float); 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.RGBA32F, sample_count = 1, data = atlas_imgdata, }; chunk_ptr := table_find_pointer(*curworld.world.chunks, req.chunk_key); lookup_path: string; if chunk_ptr != null && chunk_ptr.rdm_lookup_path.count > 0 { lookup_path = sprint("%/worlds/%/%", GAME_RESOURCES_DIR, req.world_name, chunk_ptr.rdm_lookup_path); } else { lookup_path = rdm_chunk_filename(req.world_name, req.chunk_key, "rdm_lookup"); } lookup_req : Fetch_Request; lookup_req.type = .RDM_LOOKUP; lookup_req.world_name = req.world_name; lookup_req.chunk_key = req.chunk_key; lookup_req.path = lookup_path; lookup_req.rdm_pending_atlas = sg_make_image(*atlas_desc); array_add(*g_asset_manager.rdm_queue, lookup_req); case .RDM_LOOKUP; curworld := get_current_world(); world_ok := curworld.valid && curworld.world.name == req.world_name; if !world_ok { if req.rdm_pending_atlas.id != 0 then sg_destroy_image(req.rdm_pending_atlas); return; } header_size := cast(s64) size_of(RDM_File_Header); if data.count < header_size { log_error("RDM: lookup too small for chunk %", req.chunk_key); sg_destroy_image(req.rdm_pending_atlas); return; } header := cast(*RDM_File_Header) data.data; if header.magic != RDM_FILE_MAGIC { log_error("RDM: bad lookup magic for chunk %", req.chunk_key); sg_destroy_image(req.rdm_pending_atlas); return; } lookup_pixel_bytes := cast(u64) header.width * cast(u64) header.height * 4 * size_of(float); lookup_imgdata : sg_image_data; lookup_imgdata.subimage[0][0] = .{ data.data + header_size, lookup_pixel_bytes }; lookup_desc : sg_image_desc = .{ render_target = false, width = header.width, height = header.height, pixel_format = sg_pixel_format.RGBA32F, sample_count = 1, data = lookup_imgdata, }; chunk := table_find_pointer(*curworld.world.chunks, req.chunk_key); if chunk != null { chunk.rdm_atlas = req.rdm_pending_atlas; chunk.rdm_lookup = sg_make_image(*lookup_desc); #if !FLAG_RELEASE_BUILD { chunk.rdm_lookup_w = header.width; chunk.rdm_lookup_h = header.height; num_floats := cast(s64)(header.width * header.height * 4); cpu_copy := NewArray(num_floats, float); memcpy(cpu_copy.data, data.data + header_size, cast(s64)lookup_pixel_bytes); chunk.rdm_lookup_cpu = cpu_copy; } chunk.rdm_valid = true; log_debug("RDM: loaded chunk %", req.chunk_key); } else { sg_destroy_image(req.rdm_pending_atlas); } } } 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"; // Load into a font. Add to free list. case; log_warn("File(%) in pack(%) has unknown format", v.name, pack.name); } log_debug("% -> %", v.name, extension); } // Properly initialize animations from the pack now that we have the images // and the JSON files both combined. 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, .{}; } dispatch_channel :: (queue: *[..]Fetch_Request, channel: u32) { af := *g_asset_manager.active[channel]; if af.occupied || queue.count == 0 then return; req := queue.data[0]; for i: 0..queue.count - 2 { queue.data[i] = queue.data[i + 1]; } queue.count -= 1; af.occupied = true; af.req = req; af.accumulated.count = 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, chunk_size = cast(u32) af.chunk_buf.count, buffer = .{ ptr = af.chunk_buf.data, size = cast(u64) af.chunk_buf.count }, })); } #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 :: () { g_asset_manager.active[CHANNEL_MAIN].chunk_buf = NewArray(MAIN_CHUNK_BUFFER_SIZE, u8, false); g_asset_manager.active[CHANNEL_RDM].chunk_buf = NewArray(RDM_CHUNK_BUFFER_SIZE, u8, false); } mandatory_loads_done :: () -> bool { for channel: 0..1 { af := *g_asset_manager.active[channel]; if af.occupied && af.req.should_block_engine then return false; } for g_asset_manager.main_queue { if it.should_block_engine then return false; } for g_asset_manager.rdm_queue { if it.should_block_engine then return false; } return true; } show_loading_screen :: () -> bool { for channel: 0..1 { af := *g_asset_manager.active[channel]; if af.occupied && af.req.should_block then return true; } for g_asset_manager.main_queue { if it.should_block then return true; } for g_asset_manager.rdm_queue { if it.should_block then return true; } return false; } asset_manager_tick :: () { #if !FLAG_RELEASE_BUILD && OS != .WASM { if g_asset_manager.pending_hot_reload && !g_asset_manager.active[CHANNEL_MAIN].occupied && g_asset_manager.main_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; } } dispatch_channel(*g_asset_manager.main_queue, CHANNEL_MAIN); dispatch_channel(*g_asset_manager.rdm_queue, CHANNEL_RDM); sfetch_dowork(); } load_world :: (name: string) { unload_current_world(); req : Fetch_Request; req.type = .WORLD; req.world_name = sprint("%", name); req.path = sprint("%/worlds/%/world.json", GAME_RESOURCES_DIR, name); req.should_block = true; array_add(*g_asset_manager.main_queue, req); } load_pack :: (name: string, shouldBlock: bool = true, shouldBlockEngine: bool = false) { req : Fetch_Request; req.type = .PACK; req.pack_name = sprint("%", name); req.path = sprint("%/%.pack", PACK_DIR, name); req.should_block = shouldBlock; req.should_block_engine = shouldBlockEngine; array_add(*g_asset_manager.main_queue, req); } // Asset management: #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 { // find_pack_by_name already logs this 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; }