~mil/mepo

b472e45bcc467bed3a9949213d3e3793aacf429f — Miles Alan 2 years ago 64ea40a tile-loading-experiment
Experiment to move locking only to LIFO / and use FS as intermediate always
4 files changed, 144 insertions(+), 176 deletions(-)

M src/Mepo.zig
M src/TileCache.zig
M src/datastructure/datastructure.zig
M src/main.zig
M src/Mepo.zig => src/Mepo.zig +83 -68
@@ 33,6 33,7 @@ pin_table: std.hash_map.StringHashMap(types.Pin),
redraw_requested_at_ms_ticks: ?u32 = null,
renderer: *sdl.SDL_Renderer,
signals_table: std.array_hash_map.AutoArrayHashMap(u6, []const u8),
texture_map: std.array_hash_map.AutoArrayHashMap(types.XYZ, *sdl.SDL_Texture),
tile_cache: *TileCache,
win_h: u32 = config.InitWindowH,
win_w: u32 = config.InitWindowW,


@@ 84,7 85,7 @@ pub fn blit_tiles_all(mepo: *@This(), bbox: sdl.SDL_Rect, zoom: u8) !void {
                    vpy,
                    bbox,
                );
                try mepo.blit_tile_text(@intCast(u32, tilex), @intCast(u32, tiley), zoom, vpx, vpy);
                //try mepo.blit_tile_text(@intCast(u32, tilex), @intCast(u32, tiley), zoom, vpx, vpy);
            } else {
                utildbg.log("Blit wont blit for neg x={d} y={d}\n", .{ tilex, tiley });
            }


@@ 96,33 97,44 @@ pub fn blit_tiles_all(mepo: *@This(), bbox: sdl.SDL_Rect, zoom: u8) !void {
    }
}

pub fn blit_tile_text(mepo: *@This(), tile_x: u32, tile_y: u32, zoom: u8, x_off: i32, y_off: i32) !void {
    if (!mepo.debug_tiles) return;
    var dl_now: c_long = 0;
    var dl_total: c_long = 0;

    switch (try mepo.tile_cache.tile_retrieve(.{ .x = tile_x, .y = tile_y, .z = zoom })) {
        .transfer_datum => |transfer_datum| {
            if (transfer_datum.progress_dl_now) |dln| {
                dl_now = dln;
            }
            if (transfer_datum.progress_dl_total) |dlt| {
                dl_total = dlt;
            }
        },
        else => {},
    }

    try mepo.blit_multiline_text(
        0x000000,
        null,
        .{},
        @intCast(i32, tile_x * config.Tsize) - x_off,
        @intCast(i32, tile_y * config.Tsize) - y_off,
        null,
        "tx: {d}\nty: {d}\ndl: {d}/{d}",
        .{ tile_x, tile_y, dl_now, dl_total },
    );
//pub fn blit_tile_text(mepo: *@This(), tile_x: u32, tile_y: u32, zoom: u8, x_off: i32, y_off: i32) !void {
//    if (!mepo.debug_tiles) return;
//    var dl_now: c_long = 0;
//    var dl_total: c_long = 0;
//
//    switch (try mepo.tile_cache.tile_retrieve(.{ .x = tile_x, .y = tile_y, .z = zoom })) {
//        .transfer_datum => |transfer_datum| {
//            if (transfer_datum.progress_dl_now) |dln| {
//                dl_now = dln;
//            }
//            if (transfer_datum.progress_dl_total) |dlt| {
//                dl_total = dlt;
//            }
//        },
//        else => {},
//    }
//
//    try mepo.blit_multiline_text(
//        0x000000,
//        null,
//        .{},
//        @intCast(i32, tile_x * config.Tsize) - x_off,
//        @intCast(i32, tile_y * config.Tsize) - y_off,
//        null,
//        "tx: {d}\nty: {d}\ndl: {d}/{d}",
//        .{ tile_x, tile_y, dl_now, dl_total },
//    );
//}
       
fn load_data_to_texture(mepo: *@This(), coords: types.XYZ, filepath: [:0]const u8) !*sdl.SDL_Texture {
    //defer sdl.SDL_FreeSurface(surface);
    const surface = try utilsdl.errorcheck_ptr(sdl.SDL_Surface, sdl.IMG_Load(filepath));
    const texture = try utilsdl.errorcheck_ptr(sdl.SDL_Texture, sdl.SDL_CreateTextureFromSurface(mepo.renderer, surface));
    // TODO: should there be some limit on size of texture map cache?
    //       currently memory will just accumulate there ad-infinitum
    //defer sdl.SDL_DestroyTexture(texture);
    try mepo.texture_map.put(coords, texture);
    return mepo.texture_map.get(coords).?;
}

pub fn blit_tile_surface(mepo: *@This(), tile_x: u32, tile_y: u32, zoom: u8, x_off: i32, y_off: i32, bbox: sdl.SDL_Rect) !void {


@@ 144,41 156,44 @@ pub fn blit_tile_surface(mepo: *@This(), tile_x: u32, tile_y: u32, zoom: u8, x_o
        .h = rect_intersect.h,
    };

    switch (try mepo.tile_cache.tile_retrieve(.{ .x = tile_x, .y = tile_y, .z = zoom })) {
        .texture => |text| {
            try utilsdl.errorcheck(sdl.SDL_RenderCopy(mepo.renderer, text, &rect_src, &rect_intersect));
        },
        .queued_position => |queued_position| {
            try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xffffff }, rect_intersect, .Fill);
            try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xebebeb }, rect_intersect, .Draw);
            try mepo.blit_multiline_text(
                0x000000,
                null,
                .{ .w = config.Tsize, .h = config.Tsize, .align_x = .Center, .align_y = .Center },
                @intCast(i32, tile_x * config.Tsize) - x_off,
                @intCast(i32, tile_y * config.Tsize) - y_off,
                20,
                "{d}",
                .{queued_position},
            );
        },
        .transfer_datum => {
            try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0x006400 }, rect_intersect, .Fill);
        },
        .error_type => |error_type| {
            try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xffdfd1 }, rect_intersect, .Fill);
            try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xebebeb }, rect_intersect, .Draw);
            try mepo.blit_multiline_text(
                0x000000,
                null,
                .{ .w = config.Tsize, .h = config.Tsize, .align_x = .Center, .align_y = .Center },
                @intCast(i32, tile_x * config.Tsize) - x_off,
                @intCast(i32, tile_y * config.Tsize) - y_off,
                20,
                "Offline",
                .{},
            );
        },
    const coords = .{ .x = tile_x, .y = tile_y, .z = zoom };

    if (mepo.texture_map.get(coords)) |already_loaded_texture| {
        // Data already loaded to texture map, copy
        try utilsdl.errorcheck(sdl.SDL_RenderCopy(mepo.renderer, already_loaded_texture, &rect_src, &rect_intersect));
    } else if (try mepo.tile_cache.tiledata_retrieve_or_queue(mepo.allocator, coords)) |png_filepath| {
        // Data in FS, load to texture map & copy
        //defer mepo.allocator.free(png_datum);
        const newly_loaded_texture = try mepo.load_data_to_texture(coords, png_filepath);
        try utilsdl.errorcheck(sdl.SDL_RenderCopy(mepo.renderer, newly_loaded_texture, &rect_src, &rect_intersect));
    } else if (mepo.tile_cache.download_thread == null) {
        // Offline - no downloading thread
        try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xffdfd1 }, rect_intersect, .Fill);
        try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xebebeb }, rect_intersect, .Draw);
        try mepo.blit_multiline_text(
            0x000000,
            null,
            .{ .w = config.Tsize, .h = config.Tsize, .align_x = .Center, .align_y = .Center },
            @intCast(i32, tile_x * config.Tsize) - x_off,
            @intCast(i32, tile_y * config.Tsize) - y_off,
            20,
            "Offline",
            .{},
        );
    } else {
        // Missing - queued TODO: check if in transfers map.. first
        try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xffffff }, rect_intersect, .Fill);
        try utilsdl.sdl_renderer_rect(mepo.renderer, .{ .value = 0xebebeb }, rect_intersect, .Draw);
        try mepo.blit_multiline_text(
            0x000000,
            null,
            .{ .w = config.Tsize, .h = config.Tsize, .align_x = .Center, .align_y = .Center },
            @intCast(i32, tile_x * config.Tsize) - x_off,
            @intCast(i32, tile_y * config.Tsize) - y_off,
            20,
            "Queued",
            .{},
        );
    }
}



@@ 335,9 350,11 @@ fn blit_download_bar(mepo: *@This()) !void {
        "{s} / Tiles Queued: {d} / Downloading: {d} / Memory: {d}",
        .{
            online_offline,
            mepo.tile_cache.queue_lifo.count(),
            //mepo.tile_cache.queue_lifo.count(),
            222,
            mepo.tile_cache.transfer_map.count(),
            mepo.tile_cache.texture_map.count(),
            9999
            //mepo.tile_cache.texture_map.count(),
        },
    );
}


@@ 774,13 791,11 @@ pub fn init(allocator: *std.mem.Allocator, tile_cache: *TileCache, cfg_opt: ?[]c
        .pin_table = std.hash_map.StringHashMap(types.Pin).init(allocator),
        .renderer = try utilsdl.errorcheck_ptr(sdl.SDL_Renderer, sdl.SDL_CreateRenderer(window, -1, sdl.SDL_RENDERER_SOFTWARE)),
        .signals_table = std.array_hash_map.AutoArrayHashMap(u6, []const u8).init(allocator),
        .texture_map = std.array_hash_map.AutoArrayHashMap(types.XYZ, *sdl.SDL_Texture).init(allocator),
        .tile_cache = tile_cache,
        .window = window,
    });

    // Set renderer for tile cache
    tile_cache.set_renderer(instance.renderer);

    // Run config (base and then user provided STDIN)
    {
        try instance.mepolang_execute(config.DefaultBaseConfig);

M src/TileCache.zig => src/TileCache.zig +42 -106
@@ 20,86 20,55 @@ pub const TransferDatum = struct {
    data_arraylist: std.ArrayList(u8),
    progress_dl_now: ?c_long,
    progress_dl_total: ?c_long,
    load_to_texture: bool,
};
const TileDataTag = enum {
    transfer_datum,
    texture,
    queued_position,
    error_type,
};
pub const TileData = union(TileDataTag) {
    transfer_datum: TransferDatum,
    texture: *sdl.SDL_Texture,
    queued_position: u32,
    error_type: enum { Offline },
    refresh_ui: bool
};

allocator: *std.mem.Allocator,
download_thread: ?*sdl.SDL_Thread,
byte_counter: u64 = 0,
cache_dir: ?std.fs.Dir,
cache_dir: std.fs.Dir,
curl_multi: *curl.CURLM,
max_n_transfers: usize = 20,
mutex: std.Thread.Mutex,
queue_lifo: datastructure.Lifo(types.XYZ, bool),
renderer: ?*sdl.SDL_Renderer,
source_url: ?[:0]const u8,
texture_map: std.array_hash_map.AutoArrayHashMap(types.XYZ, *sdl.SDL_Texture),
transfer_map: std.array_hash_map.AutoArrayHashMap(types.XYZ, *TransferDatum),

/// Retreives a tile from cache
/// If tile is not present & will queue tile to be downloaded if downloading
/// thread is present
pub fn tile_retrieve(tile_cache: *@This(), coords: types.XYZ) !TileData {
    const lock = tile_cache.mutex.acquire();
    defer lock.release();

/// Retreives a tile from the filesystem cache
/// If tile is not present & will queue tile to be downloaded in LIFO
/// Caller is responsible for freeing allocated returned memory
pub fn tiledata_retrieve_or_queue(tile_cache: *@This(), allocator: *std.mem.Allocator, coords: types.XYZ) !?[:0]u8 {
    var file_cached_png_opt: ?[]u8 = null;

    if (tile_cache.texture_map.get(coords)) |tile| {
        return TileData{ .texture = tile };
    } else if (tile_cache.transfer_map.get(coords)) |transfer| {
        return TileData{ .transfer_datum = transfer.* };
    } else if (tile_cache.cache_dir) |cache_dir| load_from_fs: {
        const png = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);
        defer tile_cache.allocator.free(png);
        file_cached_png_opt = cache_dir.readFileAlloc(tile_cache.allocator, png, 500000) catch null;
        if (file_cached_png_opt) |file_cached_png| {
            defer tile_cache.allocator.free(file_cached_png);
            const text = tile_cache.load_data_to_texture(coords, file_cached_png) catch {
                break :load_from_fs;
            };
            return TileData{ .texture = text };
        }
    }
    const png = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);

    if (tile_cache.download_thread == null) {
        return TileData{ .error_type = .Offline };
    } else {
    //defer tile_cache.allocator.free(png);

    // TODO: Add stat call here first rather then relying on readFileAlloc
    // error return
    if (tile_cache.cache_dir.access(png, .{}))  {
        const f = try allocator.alloc(u8, 300);
        const x = try tile_cache.cache_dir.realpathZ(png, f);
        const m = try allocator.dupeZ(u8, x);

        return m;
    } else |err| {
        try tile_cache.queue_lifo.put(coords, true);
        return TileData{
            .queued_position = @intCast(u32, if (tile_cache.queue_lifo.getIndex(coords)) |index| index else 0),
        };
        return null;
    }
}

/// Queues tile without loading to surface/memory
/// Will queue tiles indiscriminate of whether downloading thread is
/// present.
fn tile_queue_no_surface_load(tile_cache: *@This(), coords: types.XYZ) !bool {
    const lock = tile_cache.mutex.acquire();
    defer lock.release();

    if (tile_cache.cache_dir) |cache_dir| check_exists: {
        const png = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);
        defer tile_cache.allocator.free(png);
        cache_dir.access(png, .{}) catch break :check_exists;
fn tile_queue_into_lifo(tile_cache: *@This(), coords: types.XYZ) !bool {
    const png = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);
    defer tile_cache.allocator.free(png);
    if (tile_cache.cache_dir.access(png, .{}))  {
        return false;
    } else |err| {
        try tile_cache.queue_lifo.put(coords, false);
        return true;
    }

    try tile_cache.queue_lifo.put(coords, false);
    return true;
}

const QueuedInfo = struct { n_queued: usize, n_cached: usize };


@@ 130,7 99,7 @@ pub fn tile_queue_no_surface_load_bbox(tile_cache: *@This(), a_lat: f64, a_lon: 
            var y: i32 = y_min;
            while (y <= y_max) : (y += 1) {
                utildbg.log("Tile cache queueing x={d} y={d} z={d}\n", .{ x, y, z });
                if (try tile_cache.tile_queue_no_surface_load(.{ .x = @intCast(u32, x), .y = @intCast(u32, y), .z = @intCast(u8, z) })) {
                if (try tile_cache.tile_queue_into_lifo(.{ .x = @intCast(u32, x), .y = @intCast(u32, y), .z = @intCast(u8, z) })) {
                    n_queued += 1;
                } else {
                    n_cached += 1;


@@ 145,7 114,7 @@ pub fn tile_queue_no_surface_load_bbox(tile_cache: *@This(), a_lat: f64, a_lon: 
    };
}

fn add_to_curl_multi(tile_cache: *@This(), coords: types.XYZ, load_to_texture: bool) !void {
fn add_to_curl_multi(tile_cache: *@This(), coords: types.XYZ, refresh_ui: bool) !void {
    if (tile_cache.transfer_map.get(coords)) |_| return;

    var transfer_datum: *TransferDatum = datum: {


@@ 155,7 124,7 @@ fn add_to_curl_multi(tile_cache: *@This(), coords: types.XYZ, load_to_texture: b
        dat.data_arraylist = std.ArrayList(u8).init(tile_cache.allocator);
        dat.progress_dl_now = null;
        dat.progress_dl_total = null;
        dat.load_to_texture = load_to_texture;
        dat.refresh_ui = refresh_ui;
        break :datum dat;
    };
    try tile_cache.transfer_map.put(coords, transfer_datum);


@@ 212,20 181,6 @@ fn curl_client_to_coords(tile_cache: *@This(), client: ?*curl.CURL) ?types.XYZ {
    return null;
}

fn load_data_to_texture(tile_cache: *@This(), coords: types.XYZ, data: []u8) !*sdl.SDL_Texture {
    if (data.len == 0) return error.LoadToSurfaceFailEmptyData;

    const memory = try utilsdl.errorcheck_ptr(sdl.SDL_RWops, sdl.SDL_RWFromConstMem(@ptrCast(*c_void, &data[0]), @intCast(c_int, data.len)));
    const surface = try utilsdl.errorcheck_ptr(sdl.SDL_Surface, sdl.IMG_Load_RW(memory, 1));
    defer sdl.SDL_FreeSurface(surface);
    const texture = try utilsdl.errorcheck_ptr(sdl.SDL_Texture, sdl.SDL_CreateTextureFromSurface(tile_cache.renderer, surface));
    // TODO: should there be some limit on size of texture map cache?
    //       currently memory will just accumulate there ad-infinitum
    //defer sdl.SDL_DestroyTexture(texture);
    try tile_cache.texture_map.put(coords, texture);
    return tile_cache.texture_map.get(coords).?;
}

fn transfer_complete(tile_cache: *@This(), msg: *curl.CURLMsg) !void {
    switch (msg.data.result) {
        .CURLE_OPERATION_TIMEDOUT => {


@@ 236,16 191,9 @@ fn transfer_complete(tile_cache: *@This(), msg: *curl.CURLMsg) !void {
                const transfer_datum = tile_cache.transfer_map.get(coords).?;
                const datum_array = transfer_datum.data_arraylist.items;
                tile_cache.byte_counter += transfer_datum.data_arraylist.items.len;

                if (tile_cache.cache_dir) |cache_dir| {
                    // Save to FS
                    const path = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);
                    try cache_dir.writeFile(path, datum_array);
                }
                if (tile_cache.transfer_map.get(coords).?.load_to_texture) {
                    // Load to surface
                    _ = try tile_cache.load_data_to_texture(coords, datum_array);
                }
                const path = try png_path(tile_cache.allocator, tile_cache.source_url.?, coords);
                try tile_cache.cache_dir.writeFile(path, datum_array);
                if (transfer_datum.refresh_ui) utilsdl.sdl_push_resize_event();
            } else {
                utildbg.log("Failed to find coordinates associated with downloaded data?\n", .{});
            }


@@ 268,10 216,8 @@ fn transfer_cleanup(tile_cache: *@This(), client: ?*curl.CURL) !void {
        tile_cache.transfer_map.get(coords).?.data_arraylist.deinit();
        _ = tile_cache.transfer_map.swapRemove(coords);
        tile_cache.allocator.destroy(transfer_datum);

        try curl_errorcheck(curl.curl_multi_remove_handle(tile_cache.curl_multi, client));
        curl.curl_easy_cleanup(client);
        utilsdl.sdl_push_resize_event();
    }
}



@@ 302,10 248,10 @@ fn tile_write_curl_callback(
    return nmemb * size;
}

fn png_path(allocator: *std.mem.Allocator, source: []const u8, coords: types.XYZ) ![]u8 {
fn png_path(allocator: *std.mem.Allocator, source: []const u8, coords: types.XYZ) ![:0]u8 {
    var source_hash: [32:0]u8 = undefined;
    std.crypto.hash.sha2.Sha256.hash(source, &source_hash, .{});
    return try std.fmt.allocPrint(
    return try std.fmt.allocPrintZ(
        allocator,
        "{s}_{d}_{d}_{d}.png",
        .{ std.fmt.fmtSliceHexLower(source_hash[0..8]), coords.z, coords.x, coords.y },


@@ 331,28 277,23 @@ pub fn download_loop(tile_cache: *@This(), graphical_mode: bool) !void {
        //    transfers out to textures & cleans up
        curl_processing_loop: while (tile_cache.should_continue_download_loop(graphical_mode)) {
            try curl_errorcheck(curl.curl_multi_perform(tile_cache.curl_multi, &running));
            if (graphical_mode and sdl.SDL_HasEvents(0, 99999999) == sdl.SDL_bool.SDL_TRUE) continue;
            if (curl.curl_multi_info_read(tile_cache.curl_multi, &n_msgs)) |msg| {
                if (msg.?.*.msg == ._DONE) {
                    //if (graphical_mode and sdl.SDL_HasEvents(0, 99999999) == sdl.SDL_bool.SDL_TRUE) continue;
                    const lock = tile_cache.mutex.acquire();
                    tile_cache.transfer_complete(msg) catch |err| {
                        utildbg.log("Failed to successfully complete transfer: {}!\n", .{err});
                    };
                    tile_cache.transfer_cleanup(msg.*.easy_handle) catch |err| {
                        utildbg.log("Failed to successfully cleanup transfer: {}!\n", .{err});
                    };
                    lock.release();
                }
            } else break :curl_processing_loop;
        }

        // 2. Transfer from LIFO into transfers
        while (tile_cache.queue_lifo.count() > 0 and tile_cache.transfer_map.count() < tile_cache.max_n_transfers) {
            //if (graphical_mode and sdl.SDL_HasEvents(0, 99999999) == sdl.SDL_bool.SDL_TRUE) continue;
            const lock = tile_cache.mutex.acquire();
            var coords = tile_cache.queue_lifo.pop();
            try tile_cache.add_to_curl_multi(coords.key, coords.value);
            lock.release();
        }

        // 3. Print status message if non graphical


@@ 433,8 374,8 @@ pub fn set_cache_url(tile_cache: *@This(), url: [:0]const u8) !void {
    defer utilsdl.sdl_push_resize_event();

    // Empty Texture map & queue
    for (tile_cache.texture_map.values()) |t| sdl.SDL_DestroyTexture(t);
    tile_cache.texture_map.clearAndFree();
    // for (tile_cache.texture_map.values()) |t| sdl.SDL_DestroyTexture(t);
    //tile_cache.texture_map.clearAndFree();
    tile_cache.queue_lifo.clearAndFree();

    // Set new URL


@@ 444,28 385,23 @@ pub fn set_cache_url(tile_cache: *@This(), url: [:0]const u8) !void {
    tile_cache.source_url = try tile_cache.allocator.dupeZ(u8, url);
}

pub fn set_renderer(tile_cache: *@This(), renderer: *sdl.SDL_Renderer) void {
    tile_cache.renderer = renderer;
}

pub fn set_max_n_transfers(tile_cache: *@This(), max_n_transfers: usize) void {
    tile_cache.max_n_transfers = max_n_transfers;
}

pub fn init(allocator: *std.mem.Allocator) anyerror!@This() {
pub fn init(allocator: *std.mem.Allocator, cache_dir_path: [:0]const u8) anyerror!@This() {
    if (curl.curl_multi_init()) |curl_multi| {
        return @as(@This(), .{
        var tc = @as(@This(), .{
            .allocator = allocator,
            .cache_dir = null,
            .cache_dir = undefined,
            .curl_multi = curl.curl_multi_init().?,
            .download_thread = null,
            .mutex = std.Thread.Mutex{},
            .queue_lifo = datastructure.Lifo(types.XYZ, bool).init(allocator),
            .renderer = null,
            .source_url = null,
            .texture_map = std.array_hash_map.AutoArrayHashMap(types.XYZ, *sdl.SDL_Texture).init(allocator),
            .transfer_map = std.array_hash_map.AutoArrayHashMap(types.XYZ, *TransferDatum).init(allocator),
        });
        try tc.set_cache_dir(cache_dir_path);
        return tc;
    } else {
        return error.MultiInitFail;
    }

M src/datastructure/datastructure.zig => src/datastructure/datastructure.zig +17 -0
@@ 2,22 2,35 @@ const std = @import("std");

pub fn Lifo(comptime key_type: type, comptime metadata_type: type) type {
    return struct {
        mutex: std.Thread.Mutex,
        array_hash_map: std.array_hash_map.AutoArrayHashMap(comptime key_type, comptime metadata_type),

        pub fn count(self: *@This()) usize {
            const lock = self.mutex.acquire();
            defer lock.release();

            return self.array_hash_map.count();
        }

        pub fn pop(self: *@This()) struct { key: key_type, value: metadata_type } {
            const lock = self.mutex.acquire();
            defer lock.release();

            const popped = self.array_hash_map.pop();
            return .{ .key = popped.key, .value = popped.value };
        }

        pub fn getIndex(self: *@This(), key: key_type) ?usize {
            const lock = self.mutex.acquire();
            defer lock.release();

            return self.array_hash_map.getIndex(key);
        }

        pub fn put(self: *@This(), k: key_type, v: metadata_type) !void {
            const lock = self.mutex.acquire();
            defer lock.release();

            // E.g. ensures when putting items, we always reorder / bump
            // the most recently added item to the tail, thus makes lifo
            if (self.array_hash_map.contains(k)) {


@@ 27,11 40,15 @@ pub fn Lifo(comptime key_type: type, comptime metadata_type: type) type {
        }

        pub fn clearAndFree(self: *@This()) void {
            const lock = self.mutex.acquire();
            defer lock.release();

            self.array_hash_map.clearAndFree();
        }

        pub fn init(allocator: *std.mem.Allocator) @This() {
            return @as(@This(), .{
                .mutex = std.Thread.Mutex{},
                .array_hash_map = std.array_hash_map.AutoArrayHashMap(key_type, metadata_type).init(allocator),
            });
        }

M src/main.zig => src/main.zig +2 -2
@@ 34,12 34,12 @@ pub fn main() !void {
    utildbg.log("Boot Mepo in mode: {}\n", .{mode});
    switch (mode) {
        .Graphical => {
            var tile_cache = try TileCache.init(allocator);
            var tile_cache = try TileCache.init(allocator, "~/.cache/mepo/tiles");
            var mepo = try Mepo.init(allocator, &tile_cache, cfg);
            mepo.sdl_event_loop() catch Mepo.graceful_terminate();
        },
        .Download => {
            var tile_cache = try TileCache.init(allocator);
            var tile_cache = try TileCache.init(allocator, "~/.cache/mepo/tiles");
            try Downloader.download_bbox(&tile_cache, download_request);
        },
    }