use dejson; use encoding::json; use fmt; use gl::*; use io; use math; use strings; use trace; let ATLAS_BLOCKS = ATLAS_EMPTY; fn load_atlases(assets: *Pack) void = { ATLAS_BLOCKS = load_atlas("minecraft:blocks", assets); }; type Atlas = struct { sprites: ObjectRegistry, width: u32, gl_texture: uint, }; type Sprite = struct { Object, texture: *Texture, width: u32, height: u32, x: u32, y: u32, }; def ATLAS_EMPTY = Atlas { sprites = OBJREG_EMPTY, ... }; fn atlas_findsprite(atlas: *Atlas, ident: str) nullable *Sprite = objreg_find(&atlas.sprites, ident): nullable *Sprite; fn load_atlas(ident: str, assets: *Pack) Atlas = { trace::info(&trace::root, "loading atlas {}...", ident); const tr = trace::ctx(&trace::root, "load atlas {}", ident); const json = match (resource_load_json( assets, "atlases", ident, ".json", &tr)) { case let json: json::value => yield json; case trace::failed => abort("TODO fallback"); }; defer json::finish(json); const deser = dejson::newdeser(&json); const decl = match (deser_atlas_decl(&deser)) { case let x: AtlasDecl => yield x; case let err: dejson::error => trace::error(&tr, "deser: {}", err): void; abort("TODO fallback"); }; defer atlas_decl_finish(decl); let sprites = newobjreg(); load_atlas_register_sprite(MISSINGNO, textures_find(MISSINGNO) as *Texture, &sprites, &tr); for (let i = len(decl.sources); i > 0) { i -= 1; load_atlas_search_source(&decl.sources[i], &sprites, &tr); }; const max_cells = TEXTURES_MAX_WIDTH: u32 >> 4; const max_cells = max_cells * max_cells; let ncells = 0u32; let sizes: [8][]*Sprite = [[]...]; defer for (let i = 0z; i < len(sizes); i += 1) { free(sizes[i]); }; let fits = true; let it = objreg_iter(&sprites); for (true) match (objreg_next(&it)) { case let spr: *Object => const spr = spr: *Sprite; const imgsize = if (spr.width > spr.height) spr.width else spr.height; const slotsize = math::bit_size_u32(imgsize: u32 - 1) - 4; const slotcells = 1 << (slotsize: u32 << 1); if (ncells + slotcells > max_cells) { fits = false; continue; }; ncells += slotcells; append(sizes[slotsize], spr); case null => break; }; if (!fits) { trace::error(&tr, "Can't fit all sprites in image (max width {})", TEXTURES_MAX_WIDTH): void; }; // Reference: https://lisyarus.github.io/blog/graphics/2022/08/06/texture-packing.html // TODO: make extra sure this impl is correct this time... const gridsize: u16 = math::bit_size_u32(ncells - 1); const gridsize = (gridsize >> 1) + (gridsize & 1); // ceil(gridsize / 2) const gridsize = 1 << gridsize; const width = gridsize << 4; let gl_texture: uint = 0; glGenTextures(1, &gl_texture); glBindTexture(GL_TEXTURE_2D, gl_texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR: i32); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST: i32); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8: i32, width: i32, width: i32, 0, GL_RGBA, GL_UNSIGNED_BYTE, null); let pbo = 0u; glGenBuffers(1, &pbo); defer glDeleteBuffers(1, &pbo); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); let steps: [8][2]u32 = [[0, gridsize]...]; let steps = steps[..1]; let pos: [2]u32 = [0...]; for (let slotsize = len(sizes): u32; slotsize > 0) { slotsize -= 1; const slots = &sizes[slotsize]; const slotwidth = 1 << slotsize; if (pos[0] > steps[len(steps) - 1][0]) { static append(steps, [pos[0], pos[1] + 2 * slotwidth]); }; for (let i = 0z; i < len(slots); i += 1) { const spr = slots[i]; spr.x = pos[0] << 4; spr.y = pos[1] << 4; load_atlas_upload_sprite(spr, &tr); pos[0] += slotwidth; if (pos[0] == gridsize) { pos[0] = steps[len(steps) - 1][0]; pos[1] += slotwidth; if (len(steps) != 1 && pos[1] + slotwidth == steps[len(steps) - 1][1]) { static delete(steps[len(steps) - 1]); }; }; }; }; glGenerateMipmap(GL_TEXTURE_2D); return Atlas { sprites = sprites, width = width, gl_texture = gl_texture, }; }; fn load_atlas_search_source( source: *AtlasDeclSource, sprites: *ObjectRegistry, tr: *trace::tracer, ) void = { match (*source) { case let source: AtlasDeclSourceDir => const srcpath = strings::trimsuffix(source.source, "/"); const srcpath = strings::concat(srcpath, "/"); defer free(srcpath); let it = textures_iter(); for (true) match (textures_next(&it)) { case let tex: *Texture => const (ns, name) = ident_split(tex.name); if (strings::hasprefix(name, srcpath)) { const name = strings::trimprefix(name, srcpath); const ident = strings::concat( ns, ":", source.prefix, name); defer free(ident); load_atlas_register_sprite( ident, tex, sprites, tr); }; case null => break; }; case let source: AtlasDeclSourceSingle => match (textures_find(source.resource)) { case let tex: *Texture => load_atlas_register_sprite( source.sprite, tex, sprites, tr); case null => trace::error(tr, "Unknown texture {}", source.resource): void; }; }; }; fn load_atlas_register_sprite( ident: str, tex: *Texture, sprites: *ObjectRegistry, tr: *trace::tracer, ) void = { const tr = trace::ctx(tr, "sprite {}", ident); if (!(objreg_find(sprites, ident) is null)) { return; }; const spr = alloc(Sprite { name = strings::dup(ident), texture = tex, width = tex.width, height = tex.height, ... }); objreg_register(sprites, spr); // TODO: once mipmapping works properly, there's probably no reason to // forbid non-16-pixel-aligned textures. if (spr.width == 0 || spr.height == 0 || spr.width & 7 != 0 || spr.height & 7 != 0 || spr.width > 2048 || spr.height > 2048) { trace::error(&tr, "Invalid dimensions: {}, {}", spr.width, spr.height): void; spr.texture = textures_find(MISSINGNO) as *Texture; spr.width = spr.texture.width; spr.height = spr.texture.height; }; }; fn load_atlas_upload_sprite(spr: *Sprite, tr: *trace::tracer) void = { const tr = trace::ctx(tr, "sprite {}", spr.name); const tex = spr.texture; const bufsize = tex.width * tex.height * 4; for (true) { glBufferData(GL_PIXEL_UNPACK_BUFFER, bufsize: uintptr, null, GL_STREAM_DRAW); const pixels = glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, bufsize: uintptr, GL_MAP_WRITE_BIT) as *opaque; texture_load(tex, pixels: *[*]u8, tex.width * 4, &tr); if (glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER) == GL_TRUE) { break; }; trace::warn(&tr, "glUnmapBuffer returned false; retrying upload"); }; glTexSubImage2D(GL_TEXTURE_2D, 0, spr.x: i32, spr.y: i32, spr.width: i32, spr.height: i32, GL_RGBA, GL_UNSIGNED_BYTE, null); }; type AtlasDecl = struct { sources: []AtlasDeclSource, }; type AtlasDeclSource = (AtlasDeclSourceDir | AtlasDeclSourceSingle); type AtlasDeclSourceDir = struct { source: str, prefix: str, }; type AtlasDeclSourceSingle = struct { resource: str, sprite: str, }; fn atlas_decl_finish(decl: AtlasDecl) void = { for (let i = 0z; i < len(decl.sources); i += 1) { atlas_decl_source_finish(decl.sources[i]); }; free(decl.sources); }; fn atlas_decl_source_finish(source: AtlasDeclSource) void = { match (source) { case let source: AtlasDeclSourceDir => free(source.source); free(source.prefix); case let source: AtlasDeclSourceSingle => free(source.resource); free(source.sprite); }; }; fn deser_atlas_decl(de: *dejson::deser) (AtlasDecl | dejson::error) = { let success = false; wassert_fields(de, "sources")?; const de_sources = dejson::field(de, "sources")?; let nsources = dejson::length(&de_sources)?; let res = AtlasDecl { sources = alloc([], nsources), }; defer if (!success) atlas_decl_finish(res); for (let i = 0z; i < nsources; i += 1) { const de_source = dejson::index(&de_sources, i)?; const srctype = dejson::field(&de_source, "type")?; const srctype = dejson::string(&srctype)?; switch (srctype) { case "directory" => const source = dejson::field(&de_source, "source")?; const source = dejson::string(&source)?; const prefix = dejson::field(&de_source, "prefix")?; const prefix = dejson::string(&prefix)?; append(res.sources, AtlasDeclSourceDir { source = strings::dup(source), prefix = strings::dup(prefix), }); case "single" => const resource = dejson::field(&de_source, "resource")?; const resource = dejson::string(&resource)?; const sprite = match ( dejson::optfield(&de_source, "sprite")?) { case let de_sprite: dejson::deser => yield dejson::string(&de_sprite)?; case void => yield resource; }; append(res.sources, AtlasDeclSourceSingle { resource = ident_qual(resource), sprite = ident_qual(sprite), }); case => return dejson::fail(&de_source, "Unknown source type {}", srctype); }; }; success = true; return res; };