From ae44478b30d890fe0fb04022f44d474dcdcc3f9d Mon Sep 17 00:00:00 2001 From: Lassi Pulkkinen Date: Thu, 31 Oct 2024 03:11:21 +0200 Subject: Initial commit (import old repo) --- atlas.ha | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 atlas.ha (limited to 'atlas.ha') diff --git a/atlas.ha b/atlas.ha new file mode 100644 index 0000000..79aadbf --- /dev/null +++ b/atlas.ha @@ -0,0 +1,337 @@ +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; +}; -- cgit v1.2.3