package assets import "common:encoding/sexp" import "common:name" import "core:log" import "core:math" import lg "core:math/linalg" import "game:physics/bvh" import "game:physics/collision" import "libs:physfs" import rl "libs:raylib" import "libs:tracy" _ :: math DEV_BUILD :: #config(DEV, false) Loaded_BVH :: struct { // AABB of all bvhs aabb: bvh.AABB, // BVH for each mesh in a model bvh: bvh.BVH, vertices: []rl.Vector3, indices: []u16, } Loaded_Convex :: struct { mesh: collision.Convex, center_of_mass: rl.Vector3, inertia_tensor: lg.Matrix3f32, total_volume: f32, } destroy_loaded_bvh :: proc(loaded_bvh: ^Loaded_BVH) { tracy.Zone() bvh.destroy_bvh(&loaded_bvh.bvh) delete(loaded_bvh.vertices) delete(loaded_bvh.indices) } Curve_2D :: [][2]f32 Scene_Instance :: struct { model: name.Name, // TODO: common format definitions pos: rl.Vector3, rot: rl.Quaternion, scale: rl.Vector3, } Scene_Desc :: struct { instances: []Scene_Instance, } Asset_Type :: enum { Texture, Model, Shader, Sound, Music, Curve_1D, Curve_2D, Scene, BVH, Convex, } Curve_1D :: []f32 Asset :: union { rl.Texture2D, rl.Model, Loaded_Shader, rl.Sound, rl.Music, Curve_1D, Curve_2D, Scene_Desc, Loaded_BVH, Loaded_Convex, } Asset_Key :: struct { path: name.Name, type: Asset_Type, } Asset_Entry :: struct { asset: Asset, modtime: physfs.sint64, // Current modtime on file system, used to detect when asset changes fs_modtime: physfs.sint64, } Asset_Manager :: struct { assets: map[Asset_Key]Asset_Entry, watcher: Asset_Modtime_Watcher, } Asset_Cache_Entry :: struct($E: typeid) { value: E, modtime: i64, fs_modtime: i64, } Asset_Cache_Loader_Payload :: union { cstring, } Asset_Cache_Loader :: struct { load: proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ), unload: proc(value: Asset), should_reload: proc(assetman: ^Asset_Manager, key: Asset_Key, entry: Asset_Entry) -> bool, } Asset_Cache :: struct($E: typeid) { cache: map[cstring]Asset_Cache_Entry(E), loader: Asset_Cache_Loader(E), } Shader_Location :: enum { Ambient, LightDir, LightColor, Heightmap, } Shader_Location_Set :: bit_set[Shader_Location] Shader_Location_Array :: [Shader_Location]i32 SHADER_LOCATION_NAMES := [Shader_Location]cstring { .Ambient = "ambient", .LightDir = "lightDir", .LightColor = "lightColor", .Heightmap = "heightmap", } Loaded_Shader :: struct { shader: rl.Shader, location_set: Shader_Location_Set, locations: Shader_Location_Array, } SHADER_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { shader := rl.LoadShader(path, payload.(cstring)) return Loaded_Shader{shader = shader}, rl.IsShaderValid(shader) }, unload = proc(asset: Asset) { rl.UnloadShader(asset.(Loaded_Shader).shader) }, } SOUND_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { sound := rl.LoadSound(path) return sound, rl.IsSoundValid(sound) }, unload = proc(asset: Asset) { rl.UnloadSound(asset.(rl.Sound)) }, } MUSIC_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { music := rl.LoadMusicStream(path) return music, rl.IsMusicValid(music) }, unload = proc(asset: Asset) { rl.UnloadMusicStream(asset.(rl.Music)) }, } MODEL_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { model := rl.LoadModel(path) return model, rl.IsModelValid(model) }, unload = proc(asset: Asset) { rl.UnloadModel(asset.(rl.Model)) }, } TEXTURE_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { texture := rl.LoadTexture(path) return texture, rl.IsTextureValid(texture) }, unload = proc(asset: Asset) { rl.UnloadTexture(asset.(rl.Texture2D)) }, } CURVE_1D_CSV_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { data, err := physfs.read_entire_file(string(path), context.temp_allocator) if err != nil { log.errorf("Failed to read curve: %s, %v", path, err) return nil, false } values, err2 := parse_csv_1d(data) if err2 != nil { log.errorf("Failed to parse curve: %s, %v", path, err2) } return values, true }, unload = proc(asset: Asset) { delete(asset.(Curve_1D)) }, } CURVE_2D_CSV_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { data, err := physfs.read_entire_file(string(path), context.temp_allocator) if err != nil { log.errorf("Failed to read curve: %s, %v", path, err) return nil, false } curve, err2 := parse_csv_2d(data) if err2 != nil { log.errorf("Failed to parse curve: %s, %v", path, err2) } return curve, true }, unload = proc(asset: Asset) { delete(asset.(Curve_2D)) }, } SCENE_DESC_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { data, err := physfs.read_entire_file(string(path), context.temp_allocator) if err != nil { log.errorf("Failed to read curve: %s, %v", path, err) return {}, false } parser: sexp.SEXP_Parser parser.data = string(data) parsed, err2 := sexp.parse(&parser, context.temp_allocator) if err2 != nil { log.errorf( "Failed to parse curve: %s\n%s", path, sexp.temp_pretty_error(parser.data, err2), ) } inst_identifier := sexp.Ident(name.from_string("inst")) num_instances: int top_it := sexp.iterator_list(parsed) for top_level_expr in sexp.iterator_next(&top_it) { list_expr := top_level_expr.expr.(sexp.Sexp_List) or_continue it := sexp.iterator_list(list_expr) ident := sexp.iterator_expect_atom(&it, sexp.Ident) or_continue if ident != inst_identifier { continue } num_instances += 1 } instances := make([]Scene_Instance, num_instances) num_instances = 0 top_it = sexp.iterator_list(parsed) for top_level_expr in sexp.iterator_next(&top_it) { list_expr := top_level_expr.expr.(sexp.Sexp_List) or_continue it := sexp.iterator_list(list_expr) ident := sexp.iterator_expect_atom(&it, sexp.Ident) or_continue if ident != inst_identifier { continue } inst: Scene_Instance for sexp.iterator_has_more(it) { key := sexp.iterator_expect_atom(&it, sexp.Tag) or_continue switch name.Name(key) { case name.from_string("model"): model_path := sexp.iterator_expect_atom(&it, string) or_continue inst.model = name.from_string(model_path) case name.from_string("pos"): pos_list := sexp.iterator_expect_list(&it) or_continue pos_it := sexp.iterator_list(pos_list) x := sexp.iterator_expect_atom(&pos_it, f64) or_continue y := sexp.iterator_expect_atom(&pos_it, f64) or_continue z := sexp.iterator_expect_atom(&pos_it, f64) or_continue inst.pos = {f32(x), f32(y), f32(z)} case name.from_string("rot"): rot_list := sexp.iterator_expect_list(&it) or_continue rot_it := sexp.iterator_list(rot_list) inst.rot.x = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue) inst.rot.y = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue) inst.rot.z = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue) inst.rot.w = f32(sexp.iterator_expect_atom(&rot_it, f64) or_continue) case name.from_string("scale"): scale_list := sexp.iterator_expect_list(&it) or_continue scale_it := sexp.iterator_list(scale_list) x := sexp.iterator_expect_atom(&scale_it, f64) or_continue y := sexp.iterator_expect_atom(&scale_it, f64) or_continue z := sexp.iterator_expect_atom(&scale_it, f64) or_continue inst.scale = {f32(x), f32(y), f32(z)} } } instances[num_instances] = inst num_instances += 1 } return Scene_Desc{instances = instances[:num_instances]}, true }, unload = proc(asset: Asset) { delete(asset.(Scene_Desc).instances) }, } BVH_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { model_asset, _, model_res := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Model}, ) if model_res == .Error { return nil, false } model := model_asset.(rl.Model) vert_count := 0 indices_count := 0 for i in 0 ..< model.meshCount { mesh := model.meshes[i] vert_count += int(mesh.vertexCount) indices_count += int(mesh.triangleCount * 3) } assert(vert_count < int(max(u16))) vertices := make([]bvh.Vec3, vert_count) indices := make([]u16, indices_count) vert_count = 0 indices_count = 0 for i in 0 ..< model.meshCount { mesh := model.meshes[i] mesh_vertices := (cast([^]rl.Vector3)mesh.vertices)[:mesh.vertexCount] mesh_indices := mesh.indices[:mesh.triangleCount * 3] copy(vertices[vert_count:], mesh_vertices) for j in 0 ..< len(mesh_indices) { index := vert_count + int(mesh_indices[j]) indices[indices_count + j] = u16(index) } vert_count += len(mesh_vertices) indices_count += len(mesh_indices) } mesh_bvh := bvh.build_bvh_from_mesh( {vertices = vertices, indices = indices}, context.allocator, ) root_aabb := mesh_bvh.bvh.nodes[0].aabb return Loaded_BVH { aabb = root_aabb, bvh = mesh_bvh.bvh, vertices = vertices, indices = indices, }, true }, unload = proc(asset: Asset) { loaded_bvh := asset.(Loaded_BVH) bvh.destroy_bvh(&loaded_bvh.bvh) delete(loaded_bvh.vertices) delete(loaded_bvh.indices) }, should_reload = proc(assetman: ^Asset_Manager, key: Asset_Key, entry: Asset_Entry) -> bool { _, modtime, _ := assetman_fetch_or_load( assetman, Asset_Key{path = key.path, type = .Model}, ) return entry.modtime != modtime }, } CONVEX_LOADER :: Asset_Cache_Loader { load = proc( assetman: ^Asset_Manager, path: cstring, payload: Asset_Cache_Loader_Payload, ) -> ( Asset, bool, ) { tracy.Zone() bytes, err := physfs.read_entire_file(string(path), context.temp_allocator) if err != nil { log.errorf("error reading file %v %s", err) return {}, false } return parse_convex(bytes) }, unload = proc(asset: Asset) { convex := asset.(Loaded_Convex) delete(convex.mesh.vertices) delete(convex.mesh.edges) delete(convex.mesh.faces) }, } ASSET_LOADERS := [Asset_Type]Asset_Cache_Loader { .Texture = TEXTURE_LOADER, .Model = MODEL_LOADER, .Shader = SHADER_LOADER, .Sound = SOUND_LOADER, .Music = MUSIC_LOADER, .Curve_1D = CURVE_1D_CSV_LOADER, .Curve_2D = CURVE_2D_CSV_LOADER, .Scene = SCENE_DESC_LOADER, .BVH = BVH_LOADER, .Convex = CONVEX_LOADER, } assetman_init :: proc(assetman: ^Asset_Manager) { modtime_watcher_init(&assetman.watcher) } assetman_tick :: proc(assetman: ^Asset_Manager) { tracy.Zone() for asset in modtime_watcher_next(&assetman.watcher) { key := Asset_Key { path = asset.path, type = asset.type, } entry, ok := &assetman.assets[key] if ok { log.debugf( "asset changed {} {} {} {}", name.to_string(asset.path), asset, entry.modtime, asset.modtime, ) entry.fs_modtime = asset.modtime } } } Asset_Cache_Result :: enum { Cached, Loaded, Reloaded, Error, } assetman_fetch_or_load_internal :: proc( assetman: ^Asset_Manager, key: Asset_Key, payload: Asset_Cache_Loader_Payload = nil, force_no_reload := false, ) -> ( value: Asset, modtime: physfs.sint64, result: Asset_Cache_Result, ) { existing, has_existing := assetman.assets[key] if has_existing { wants_reload := ASSET_LOADERS[key.type].should_reload(assetman, key, existing) if ASSET_LOADERS[key.type].should_reload != nil else false if force_no_reload || (existing.modtime == existing.fs_modtime && !wants_reload) { result = .Cached return existing.asset, existing.modtime, result } else { path := name.to_cstring(key.path) new_modtime := physfs.getLastModTime(path) // Try to load the new version new_value, ok := ASSET_LOADERS[key.type].load(assetman, path, payload) if ok { result = .Reloaded ASSET_LOADERS[key.type].unload(existing.asset) assetman.assets[key] = { asset = new_value, modtime = new_modtime, fs_modtime = new_modtime, } log.debugf("reloaded asset: %s", path) return new_value, new_modtime, result } else { log.warnf("failed to reload asset after modification %s", path) result = .Cached return existing.asset, existing.modtime, result } } } else { path := name.to_cstring(key.path) modtime = physfs.getLastModTime(path) ok: bool value, ok = ASSET_LOADERS[key.type].load(assetman, path, payload) if ok { assetman.assets[key] = { asset = value, modtime = modtime, fs_modtime = modtime, } result = .Loaded log.debugf("loaded asset: %s", path) return value, modtime, result } else { log.errorf("failed to load asset %s", path) result = .Error return {}, 0, .Error } } } assetman_fetch_or_load :: proc( assetman: ^Asset_Manager, key: Asset_Key, payload: Asset_Cache_Loader_Payload = nil, force_no_reload := false, ) -> ( value: Asset, modtime: physfs.sint64, result: Asset_Cache_Result, ) { value, modtime, result = assetman_fetch_or_load_internal( assetman, key, payload, force_no_reload, ) if result != .Cached { log.debugf("asset {}: [{}] {}", key.type, name.to_string(key.path), result) } if result == .Loaded { modtime_watcher_add_asset(&assetman.watcher, key.type, key.path, modtime) } return } assetcache_destroy :: proc(ac: ^$T/Asset_Cache($E)) { for _, v in ac.cache { ac.loader.unload(v.value) } delete(ac.cache) } get_texture :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Texture2D { tracy.Zone() texture, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Texture}, ) return texture.(rl.Texture2D) } get_model_ex :: proc( assetman: ^Asset_Manager, path: cstring, ref_modtime: physfs.sint64 = 0, // will check reload status using reference load time. When 0 reloaded will be true only if this call triggered reload ) -> ( model: rl.Model, modtime: physfs.sint64, reloaded: bool, ) { tracy.Zone() asset: Asset result: Asset_Cache_Result asset, modtime, result = assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Model}, ) model = asset.(rl.Model) reloaded = result == .Reloaded || ref_modtime != modtime return } get_model :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Model { model, _, _ := get_model_ex(assetman, path) return model } get_shader :: proc( assetman: ^Asset_Manager, vs_path: cstring, ps_path: cstring, location_set: Shader_Location_Set, ) -> Loaded_Shader { asset, _, result := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(vs_path), type = .Shader}, ps_path, ) loaded_shader := asset.(Loaded_Shader) if location_set > loaded_shader.location_set || result == .Loaded || result == .Reloaded { loaded_shader.location_set = location_set loaded_shader.locations = {} for location in location_set { loaded_shader.locations[location] = rl.GetShaderLocation( loaded_shader.shader, SHADER_LOCATION_NAMES[location], ) } } return loaded_shader } get_sound :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Sound { sound, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Sound}, ) return sound.(rl.Sound) } get_music :: proc(assetman: ^Asset_Manager, path: cstring) -> rl.Music { music, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Music}, ) return music.(rl.Music) } get_bvh :: proc(assetman: ^Asset_Manager, path: name.Name) -> (Loaded_BVH, bool) { tracy.Zone() asset, _, res := assetman_fetch_or_load(assetman, Asset_Key{path = path, type = .BVH}) return asset.(Loaded_BVH), res != Asset_Cache_Result.Error } get_curve_1d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_1D) { asset, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Curve_1D}, ) return asset.(Curve_1D) } // Reads a two column comma separated csv file as a curve get_curve_2d :: proc(assetman: ^Asset_Manager, path: cstring) -> (curve: Curve_2D) { asset, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Curve_2D}, ) return asset.(Curve_2D) } // Reads a two column comma separated csv file as a curve get_scene_desc :: proc( assetman: ^Asset_Manager, path: name.Name, force_no_reload := false, ) -> ( scene: Scene_Desc, modtime: i64, ) { asset, mtime, _ := assetman_fetch_or_load( assetman, Asset_Key{path = path, type = .Scene}, nil, force_no_reload, ) return asset.(Scene_Desc), mtime } get_convex :: proc(assetman: ^Asset_Manager, path: cstring) -> (result: Loaded_Convex) { asset, _, _ := assetman_fetch_or_load( assetman, Asset_Key{path = name.from_cstring(path), type = .Convex}, ) return asset.(Loaded_Convex) } shutdown :: proc(assetman: ^Asset_Manager) { tracy.Zone() modtime_watcher_deinit(&assetman.watcher) for k, v in assetman.assets { ASSET_LOADERS[k.type].unload(v.asset) } delete_map(assetman.assets) }