summaryrefslogtreecommitdiff
path: root/atlas.ha
diff options
context:
space:
mode:
authorLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:11:21 +0200
committerLassi Pulkkinen <lassi@pulk.fi>2024-10-31 03:51:35 +0200
commitae44478b30d890fe0fb04022f44d474dcdcc3f9d (patch)
tree5f462459ae4b47d22114eed717d1382d08cf4dfe /atlas.ha
Initial commit (import old repo)HEADmain
Diffstat (limited to 'atlas.ha')
-rw-r--r--atlas.ha337
1 files changed, 337 insertions, 0 deletions
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;
+};