From 21a0d0c1c424f7db90c3282aad4bf6ad4ef809b7 Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Sat, 8 Jul 2023 14:37:29 -0700
Subject: Load tile maps and tile sets from files.

---
 gfx-iso/CMakeLists.txt          |  11 +-
 gfx-iso/app/app.h               |  11 +
 gfx-iso/app/checkerboard.c      | 120 +++++++++
 gfx-iso/app/checkerboard.h      |   9 +
 gfx-iso/app/isogfx-demo.c       |  67 +++++
 gfx-iso/app/isogfx-demo.h       |   9 +
 gfx-iso/app/main.c              | 185 +++++++++++++
 gfx-iso/asset/mkasset.py        | 155 +++++++++++
 gfx-iso/demo/isogfx-demo.c      | 213 ---------------
 gfx-iso/include/isogfx/isogfx.h |  63 +++--
 gfx-iso/src/isogfx.c            | 561 ++++++++++++++++++++++++++++------------
 11 files changed, 1012 insertions(+), 392 deletions(-)
 create mode 100644 gfx-iso/app/app.h
 create mode 100644 gfx-iso/app/checkerboard.c
 create mode 100644 gfx-iso/app/checkerboard.h
 create mode 100644 gfx-iso/app/isogfx-demo.c
 create mode 100644 gfx-iso/app/isogfx-demo.h
 create mode 100644 gfx-iso/app/main.c
 create mode 100644 gfx-iso/asset/mkasset.py
 delete mode 100644 gfx-iso/demo/isogfx-demo.c

(limited to 'gfx-iso')

diff --git a/gfx-iso/CMakeLists.txt b/gfx-iso/CMakeLists.txt
index 8f95f7f..b57a83f 100644
--- a/gfx-iso/CMakeLists.txt
+++ b/gfx-iso/CMakeLists.txt
@@ -13,18 +13,21 @@ target_include_directories(isogfx PUBLIC
   include)
 
 target_link_libraries(isogfx PRIVATE
+  filesystem
   mempool)
 
 target_compile_options(isogfx PRIVATE -Wall -Wextra -Wpedantic)
 
 # Demo
 
-project(isogfx-demo)
+project(isogfx-app)
 
-add_executable(isogfx-demo
-  demo/isogfx-demo.c)
+add_executable(isogfx-app
+  app/checkerboard.c
+  app/isogfx-demo.c
+  app/main.c)
 
-target_link_libraries(isogfx-demo PRIVATE
+target_link_libraries(isogfx-app PRIVATE
   gfx
   gfx-app
   isogfx)
diff --git a/gfx-iso/app/app.h b/gfx-iso/app/app.h
new file mode 100644
index 0000000..160da47
--- /dev/null
+++ b/gfx-iso/app/app.h
@@ -0,0 +1,11 @@
+#pragma once
+
+typedef struct IsoGfx    IsoGfx;
+typedef struct IsoGfxApp IsoGfxApp;
+
+typedef struct IsoGfxApp {
+  void* state;
+  void (*shutdown)(IsoGfx*, void* state);
+  void (*update)(IsoGfx*, void* state, double t, double dt);
+  void (*render)(IsoGfx*, void* state);
+} IsoGfxApp;
diff --git a/gfx-iso/app/checkerboard.c b/gfx-iso/app/checkerboard.c
new file mode 100644
index 0000000..8b394c4
--- /dev/null
+++ b/gfx-iso/app/checkerboard.c
@@ -0,0 +1,120 @@
+#include "isogfx-demo.h"
+
+#include <gfx/gfx_app.h>
+#include <isogfx/isogfx.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+static const int TILE_WIDTH   = 64;
+static const int TILE_HEIGHT  = TILE_WIDTH / 2;
+static const int WORLD_WIDTH  = 20;
+static const int WORLD_HEIGHT = 20;
+
+static const TileDesc tile_set[] = {
+    {.type   = TileFromColour,
+     .width  = TILE_WIDTH,
+     .height = TILE_HEIGHT,
+     .colour = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46, .a = 0xff}},
+    {.type   = TileFromColour,
+     .width  = TILE_WIDTH,
+     .height = TILE_HEIGHT,
+     .colour = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0, .a = 0xff}},
+    {.type   = TileFromColour,
+     .width  = TILE_WIDTH,
+     .height = TILE_HEIGHT,
+     .colour = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84, .a = 0xff}},
+};
+
+typedef enum Colour {
+  Black,
+  White,
+  Red,
+} Colour;
+
+typedef struct State {
+  Tile red;
+  int  xpick;
+  int  ypick;
+} State;
+
+static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) {
+  assert(iso);
+  for (int y = 0; y < isogfx_world_height(iso); ++y) {
+    for (int x = 0; x < isogfx_world_width(iso); ++x) {
+      const int  odd_col = x & 1;
+      const int  odd_row = y & 1;
+      const Tile value   = (odd_row ^ odd_col) == 0 ? black : white;
+      isogfx_set_tile(iso, x, y, value);
+    }
+  }
+}
+
+static void shutdown(IsoGfx* iso, void* app_state) {
+  assert(iso);
+  if (app_state) {
+    free(app_state);
+  }
+}
+
+static void update(IsoGfx* iso, void* app_state, double t, double dt) {
+  assert(iso);
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  double mouse_x, mouse_y;
+  gfx_app_get_mouse_position(&mouse_x, &mouse_y);
+
+  isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick);
+
+  printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick);
+}
+
+static void render(IsoGfx* iso, void* app_state) {
+  assert(iso);
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  isogfx_render(iso);
+  if ((state->xpick != -1) && (state->ypick != -1)) {
+    isogfx_draw_tile(iso, state->xpick, state->ypick, state->red);
+  }
+}
+
+bool make_checkerboard_app(IsoGfx* iso, IsoGfxApp* app) {
+  assert(iso);
+  assert(app);
+
+  State* state = calloc(1, sizeof(State));
+  if (!state) {
+    return false;
+  }
+
+  if (!isogfx_make_world(
+          iso, &(WorldDesc){
+                   .tile_width   = TILE_WIDTH,
+                   .tile_height  = TILE_HEIGHT,
+                   .world_width  = WORLD_WIDTH,
+                   .world_height = WORLD_HEIGHT})) {
+    goto cleanup;
+  }
+
+  const Tile black = isogfx_make_tile(iso, &tile_set[Black]);
+  const Tile white = isogfx_make_tile(iso, &tile_set[White]);
+  state->red       = isogfx_make_tile(iso, &tile_set[Red]);
+  make_checkerboard(iso, black, white);
+  isogfx_render(iso);
+
+  app->state    = state;
+  app->shutdown = shutdown;
+  app->update   = update;
+  app->render   = render;
+
+  return true;
+
+cleanup:
+  free(state);
+  return false;
+}
diff --git a/gfx-iso/app/checkerboard.h b/gfx-iso/app/checkerboard.h
new file mode 100644
index 0000000..61725a5
--- /dev/null
+++ b/gfx-iso/app/checkerboard.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "app.h"
+
+#include <stdbool.h>
+
+typedef struct IsoGfxApp IsoGfxApp;
+
+bool make_checkerboard_app(IsoGfx*, IsoGfxApp*);
diff --git a/gfx-iso/app/isogfx-demo.c b/gfx-iso/app/isogfx-demo.c
new file mode 100644
index 0000000..15ab6be
--- /dev/null
+++ b/gfx-iso/app/isogfx-demo.c
@@ -0,0 +1,67 @@
+#include "isogfx-demo.h"
+
+#include <gfx/gfx_app.h>
+#include <isogfx/isogfx.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+typedef struct State {
+  int xpick;
+  int ypick;
+} State;
+
+static void shutdown(IsoGfx* iso, void* app_state) {
+  assert(iso);
+  if (app_state) {
+    free(app_state);
+  }
+}
+
+static void update(IsoGfx* iso, void* app_state, double t, double dt) {
+  assert(iso);
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  double mouse_x, mouse_y;
+  gfx_app_get_mouse_position(&mouse_x, &mouse_y);
+
+  isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick);
+
+  // printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick);
+}
+
+static void render(IsoGfx* iso, void* app_state) {
+  assert(iso);
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  isogfx_render(iso);
+}
+
+bool make_demo_app(IsoGfx* iso, IsoGfxApp* app) {
+  assert(iso);
+  assert(app);
+
+  State* state = calloc(1, sizeof(State));
+  if (!state) {
+    return false;
+  }
+
+  if (!isogfx_load_world(iso, "/home/jeanne/assets/tilemaps/demo1.tm")) {
+    goto cleanup;
+  }
+
+  app->state    = state;
+  app->shutdown = shutdown;
+  app->update   = update;
+  app->render   = render;
+
+  return true;
+
+cleanup:
+  free(state);
+  return false;
+}
diff --git a/gfx-iso/app/isogfx-demo.h b/gfx-iso/app/isogfx-demo.h
new file mode 100644
index 0000000..d099824
--- /dev/null
+++ b/gfx-iso/app/isogfx-demo.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include "app.h"
+
+#include <stdbool.h>
+
+typedef struct IsoGfxApp IsoGfxApp;
+
+bool make_demo_app(IsoGfx*, IsoGfxApp*);
diff --git a/gfx-iso/app/main.c b/gfx-iso/app/main.c
new file mode 100644
index 0000000..fa5a76b
--- /dev/null
+++ b/gfx-iso/app/main.c
@@ -0,0 +1,185 @@
+#include "app.h"
+#include "checkerboard.h"
+#include "isogfx-demo.h"
+
+#include <isogfx/isogfx.h>
+
+#include <gfx/gfx.h>
+#include <gfx/gfx_app.h>
+#include <gfx/render_backend.h>
+#include <gfx/renderer.h>
+#include <gfx/scene.h>
+#include <gfx/util/geometry.h>
+#include <gfx/util/shader.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+static const int SCREEN_WIDTH  = 1408;
+static const int SCREEN_HEIGHT = 960;
+
+typedef struct State {
+  Gfx*      gfx;
+  IsoGfx*   iso;
+  IsoGfxApp app;
+  Texture*  screen_texture;
+  Scene*    scene;
+} State;
+
+static bool init(const GfxAppDesc* desc, void** app_state) {
+  State* state = calloc(1, sizeof(State));
+  if (!state) {
+    return false;
+  }
+
+  if (!(state->iso = isogfx_new(&(IsoGfxDesc){
+            .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) {
+    goto cleanup;
+  }
+  //  if (!make_checkerboard_app(state->iso, &state->app)) {
+  //    goto cleanup;
+  //  }
+  if (!make_demo_app(state->iso, &state->app)) {
+    goto cleanup;
+  }
+  if (!(state->gfx = gfx_init())) {
+    goto cleanup;
+  }
+  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
+
+  if (!(state->screen_texture = gfx_make_texture(
+            render_backend, &(TextureDesc){
+                                .width     = SCREEN_WIDTH,
+                                .height    = SCREEN_HEIGHT,
+                                .dimension = Texture2D,
+                                .format    = TextureSRGBA8,
+                                .filtering = NearestFiltering,
+                                .wrap      = ClampToEdge,
+                                .mipmaps   = false}))) {
+    goto cleanup;
+  }
+
+  ShaderProgram* shader = gfx_make_view_texture_shader(render_backend);
+  if (!shader) {
+    goto cleanup;
+  }
+
+  Geometry* geometry = gfx_make_quad_11(render_backend);
+  if (!geometry) {
+    goto cleanup;
+  }
+
+  MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1};
+  material_desc.uniforms[0]  = (ShaderUniform){
+       .type          = UniformTexture,
+       .value.texture = state->screen_texture,
+       .name          = sstring_make("Texture")};
+  Material* material = gfx_make_material(&material_desc);
+  if (!material) {
+    return false;
+  }
+
+  const MeshDesc mesh_desc =
+      (MeshDesc){.geometry = geometry, .material = material, .shader = shader};
+  Mesh* mesh = gfx_make_mesh(&mesh_desc);
+  if (!mesh) {
+    goto cleanup;
+  }
+
+  SceneObject* object = gfx_make_object();
+  if (!object) {
+    goto cleanup;
+  }
+  gfx_add_object_mesh(object, mesh);
+
+  state->scene    = gfx_make_scene();
+  SceneNode* node = gfx_make_object_node(object);
+  SceneNode* root = gfx_get_scene_root(state->scene);
+  gfx_set_node_parent(node, root);
+
+  *app_state = state;
+  return true;
+
+cleanup:
+  if (state->gfx) {
+    gfx_destroy(&state->gfx);
+  }
+  free(state);
+  return false;
+}
+
+static void shutdown(void* app_state) {
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  if (state->app.state) {
+    assert(state->iso);
+    (*state->app.shutdown)(state->iso, state->app.state);
+  }
+  isogfx_del(&state->iso);
+  gfx_destroy(&state->gfx);
+  free(app_state);
+}
+
+static void update(void* app_state, double t, double dt) {
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  assert(state->app.update);
+  (*state->app.update)(state->iso, state->app.state, t, dt);
+}
+
+static void render(void* app_state) {
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  assert(state->app.render);
+  (*state->app.render)(state->iso, state->app.state);
+
+  const Pixel* screen = isogfx_get_screen_buffer(state->iso);
+  assert(screen);
+  gfx_update_texture(
+      state->screen_texture, &(TextureDataDesc){.pixels = screen});
+
+  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
+  Renderer*      renderer       = gfx_get_renderer(state->gfx);
+
+  gfx_start_frame(render_backend);
+  gfx_render_scene(
+      renderer, &(RenderSceneParams){
+                    .mode = RenderDefault, .scene = state->scene, .camera = 0});
+  gfx_end_frame(render_backend);
+}
+
+static void resize(void* app_state, int width, int height) {
+  assert(app_state);
+  State* state = (State*)(app_state);
+
+  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
+  gfx_set_viewport(render_backend, width, height);
+}
+
+int main(int argc, const char** argv) {
+  const int initial_width  = SCREEN_WIDTH;
+  const int initial_height = SCREEN_HEIGHT;
+  const int max_fps        = 60;
+
+  gfx_app_run(
+      &(GfxAppDesc){
+          .argc              = argc,
+          .argv              = argv,
+          .width             = initial_width,
+          .height            = initial_height,
+          .max_fps           = max_fps,
+          .update_delta_time = max_fps > 0 ? 1.0 / (double)max_fps : 0.0,
+          .title             = "Isometric Renderer"},
+      &(GfxAppCallbacks){
+          .init     = init,
+          .update   = update,
+          .render   = render,
+          .resize   = resize,
+          .shutdown = shutdown});
+
+  return 0;
+}
diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py
new file mode 100644
index 0000000..15f7912
--- /dev/null
+++ b/gfx-iso/asset/mkasset.py
@@ -0,0 +1,155 @@
+# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine.
+#
+# Currently handles Tiled's .tsx and .tmx file formats.
+#
+# The output is a binary tile set file (.TS) or a binary tile map file (.TM).
+import argparse
+import ctypes
+from PIL import Image
+import sys
+from xml.etree import ElementTree
+
+# Maximum length of path strings in .TS and .TM files.
+MAX_PATH_LENGTH = 128
+
+
+def drop_extension(filepath):
+    return filepath[:filepath.rfind('.')]
+
+
+def to_char_array(string, length):
+    """Convert a string to a fixed-length ASCII char array.
+
+    The length of str must be at most length-1 so that the resulting string can
+    be null-terminated.
+    """
+    assert (len(string) < length)
+    chars = string.encode("ascii")
+    nulls = ("\0" * (length - len(string))).encode("ascii")
+    return chars + nulls
+
+
+def convert_tsx(input_filepath, output_filepath):
+    """Converts a Tiled .tsx tileset file to a .TS tile set file."""
+    xml = ElementTree.parse(input_filepath)
+    root = xml.getroot()
+
+    tile_count = int(root.attrib["tilecount"])
+    max_tile_width = int(root.attrib["tilewidth"])
+    max_tile_height = int(root.attrib["tileheight"])
+
+    print(f"Tile count: {tile_count}")
+    print(f"Max width:  {max_tile_width}")
+    print(f"Max height: {max_tile_height}")
+
+    with open(output_filepath, 'bw') as output:
+        output.write(ctypes.c_uint16(tile_count))
+        output.write(ctypes.c_uint16(max_tile_width))
+        output.write(ctypes.c_uint16(max_tile_height))
+
+        num_tile = 0
+        for tile in root:
+            # Skip the "grid" and other non-tile elements.
+            if not tile.tag == "tile":
+                continue
+
+            # Assuming tiles are numbered 0..N.
+            tile_id = int(tile.attrib["id"])
+            assert (tile_id == num_tile)
+            num_tile += 1
+
+            image = tile[0]
+            tile_width = int(image.attrib["width"])
+            tile_height = int(image.attrib["height"])
+            tile_path = image.attrib["source"]
+
+            output.write(ctypes.c_uint16(tile_width))
+            output.write(ctypes.c_uint16(tile_height))
+
+            with Image.open(tile_path) as im:
+                bytes = im.convert('RGBA').tobytes()
+                output.write(bytes)
+
+
+def convert_tmx(input_filepath, output_filepath):
+    """Converts a Tiled .tmx file to a .TM tile map file."""
+    xml = ElementTree.parse(input_filepath)
+    root = xml.getroot()
+
+    map_width = int(root.attrib["width"])
+    map_height = int(root.attrib["height"])
+    base_tile_width = int(root.attrib["tilewidth"])
+    base_tile_height = int(root.attrib["tileheight"])
+    num_layers = 1
+
+    print(f"Map width:   {map_width}")
+    print(f"Map height:  {map_height}")
+    print(f"Tile width:  {base_tile_width}")
+    print(f"Tile height: {base_tile_height}")
+
+    with open(output_filepath, 'bw') as output:
+        output.write(ctypes.c_uint16(map_width))
+        output.write(ctypes.c_uint16(map_height))
+        output.write(ctypes.c_uint16(base_tile_width))
+        output.write(ctypes.c_uint16(base_tile_height))
+        output.write(ctypes.c_uint16(num_layers))
+
+        tileset_path = None
+
+        for child in root:
+            if child.tag == "tileset":
+                tileset = child
+                tileset_path = tileset.attrib["source"]
+
+                print(f"Tile set: {tileset_path}")
+
+                tileset_path = tileset_path.replace("tsx", "ts")
+            elif child.tag == "layer":
+                layer = child
+                layer_id = int(layer.attrib["id"])
+                layer_width = int(layer.attrib["width"])
+                layer_height = int(layer.attrib["height"])
+
+                print(f"Layer:  {layer_id}")
+                print(f"Width:  {layer_width}")
+                print(f"Height: {layer_height}")
+
+                assert (tileset_path)
+                output.write(to_char_array(tileset_path, MAX_PATH_LENGTH))
+
+                # Assume the layer's dimensions matches the map's.
+                assert (layer_width == map_width)
+                assert (layer_height == map_height)
+
+                data = layer[0]
+                # Handle other encodings later.
+                assert (data.attrib["encoding"] == "csv")
+
+                csv = data.text.strip()
+                rows = csv.split('\n')
+                for row in rows:
+                    tile_ids = [x.strip() for x in row.split(',') if x]
+                    for tile_id in tile_ids:
+                        output.write(ctypes.c_uint16(int(tile_id)))
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("input", help="Input file (.tsx, .tmx)")
+    args = parser.parse_args()
+
+    output_filepath_no_ext = drop_extension(args.input)
+    if ".tsx" in args.input:
+        output_filepath = output_filepath_no_ext + ".ts"
+        convert_tsx(args.input, output_filepath)
+    elif ".tmx" in args.input:
+        output_filepath = output_filepath_no_ext + ".tm"
+        convert_tmx(args.input, output_filepath)
+    else:
+        print(f"Unhandled file format: {args.input}")
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/gfx-iso/demo/isogfx-demo.c b/gfx-iso/demo/isogfx-demo.c
deleted file mode 100644
index d6c1ab0..0000000
--- a/gfx-iso/demo/isogfx-demo.c
+++ /dev/null
@@ -1,213 +0,0 @@
-#include <isogfx/isogfx.h>
-
-#include <gfx/gfx.h>
-#include <gfx/gfx_app.h>
-#include <gfx/render_backend.h>
-#include <gfx/renderer.h>
-#include <gfx/scene.h>
-#include <gfx/util/geometry.h>
-#include <gfx/util/shader.h>
-
-#include <assert.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-static const int SCREEN_WIDTH  = 1408;
-static const int SCREEN_HEIGHT = 960;
-static const int TILE_WIDTH    = 64;
-static const int TILE_HEIGHT   = TILE_WIDTH / 2;
-static const int WORLD_WIDTH   = 20;
-static const int WORLD_HEIGHT  = 20;
-
-static const Pixel BLACK = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46};
-static const Pixel WHITE = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0};
-static const Pixel RED   = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84};
-
-typedef struct State {
-  Gfx*     gfx;
-  IsoGfx*  iso;
-  Tile     red;
-  int      xpick;
-  int      ypick;
-  Texture* screen_texture;
-  Scene*   scene;
-} State;
-
-static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) {
-  assert(iso);
-  for (int y = 0; y < isogfx_world_height(iso); ++y) {
-    for (int x = 0; x < isogfx_world_width(iso); ++x) {
-      const int  odd_col = x & 1;
-      const int  odd_row = y & 1;
-      const Tile value   = (odd_row ^ odd_col) == 0 ? black : white;
-      isogfx_set_tile(iso, x, y, value);
-    }
-  }
-}
-
-static bool init(const GfxAppDesc* desc, void** app_state) {
-  State* state = calloc(1, sizeof(State));
-  if (!state) {
-    return false;
-  }
-
-  if (!(state->iso = isogfx_new(&(IsoGfxDesc){
-            .screen_width  = SCREEN_WIDTH,
-            .screen_height = SCREEN_HEIGHT,
-            .tile_width    = TILE_WIDTH,
-            .tile_height   = TILE_HEIGHT,
-            .world_width   = WORLD_WIDTH,
-            .world_height  = WORLD_HEIGHT}))) {
-    goto cleanup;
-  }
-  if (!(state->gfx = gfx_init())) {
-    goto cleanup;
-  }
-  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
-
-  if (!(state->screen_texture = gfx_make_texture(
-            render_backend, &(TextureDesc){
-                                .width     = SCREEN_WIDTH,
-                                .height    = SCREEN_HEIGHT,
-                                .dimension = Texture2D,
-                                .format    = TextureRGB8,
-                                .filtering = NearestFiltering,
-                                .wrap      = ClampToEdge,
-                                .mipmaps   = false}))) {
-    goto cleanup;
-  }
-
-  ShaderProgram* shader = gfx_make_view_texture_shader(render_backend);
-  if (!shader) {
-    goto cleanup;
-  }
-
-  Geometry* geometry = gfx_make_quad_11(render_backend);
-  if (!geometry) {
-    goto cleanup;
-  }
-
-  MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1};
-  material_desc.uniforms[0]  = (ShaderUniform){
-       .type          = UniformTexture,
-       .value.texture = state->screen_texture,
-       .name          = sstring_make("Texture")};
-  Material* material = gfx_make_material(&material_desc);
-  if (!material) {
-    return false;
-  }
-
-  const MeshDesc mesh_desc =
-      (MeshDesc){.geometry = geometry, .material = material, .shader = shader};
-  Mesh* mesh = gfx_make_mesh(&mesh_desc);
-  if (!mesh) {
-    goto cleanup;
-  }
-
-  SceneObject* object = gfx_make_object();
-  if (!object) {
-    goto cleanup;
-  }
-  gfx_add_object_mesh(object, mesh);
-
-  state->scene    = gfx_make_scene();
-  SceneNode* node = gfx_make_object_node(object);
-  SceneNode* root = gfx_get_scene_root(state->scene);
-  gfx_set_node_parent(node, root);
-
-  const Tile black = isogfx_make_tile(
-      state->iso, &(TileDesc){.type = TileFromColour, .colour = BLACK});
-  const Tile white = isogfx_make_tile(
-      state->iso, &(TileDesc){.type = TileFromColour, .colour = WHITE});
-  state->red = isogfx_make_tile(
-      state->iso, &(TileDesc){.type = TileFromColour, .colour = RED});
-  make_checkerboard(state->iso, black, white);
-  isogfx_render(state->iso);
-
-  *app_state = state;
-  return true;
-
-cleanup:
-  if (state->gfx) {
-    gfx_destroy(&state->gfx);
-  }
-  free(state);
-  return false;
-}
-
-static void shutdown(void* app_state) {
-  assert(app_state);
-  State* state = (State*)(app_state);
-  isogfx_del(&state->iso);
-  gfx_destroy(&state->gfx);
-  free(app_state);
-}
-
-static void update(void* app_state, double t, double dt) {
-  assert(app_state);
-  State* state = (State*)(app_state);
-
-  double mouse_x, mouse_y;
-  gfx_app_get_mouse_position(&mouse_x, &mouse_y);
-
-  isogfx_pick_tile(state->iso, mouse_x, mouse_y, &state->xpick, &state->ypick);
-
-  printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick);
-}
-
-static void render(void* app_state) {
-  assert(app_state);
-  State* state = (State*)(app_state);
-
-  isogfx_render(state->iso);
-  if ((state->xpick != -1) && (state->ypick != -1)) {
-    isogfx_draw_tile(state->iso, state->xpick, state->ypick, state->red);
-  }
-
-  const Pixel* screen = isogfx_get_screen_buffer(state->iso);
-  assert(screen);
-  gfx_update_texture(
-      state->screen_texture, &(TextureDataDesc){.pixels = screen});
-
-  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
-  Renderer*      renderer       = gfx_get_renderer(state->gfx);
-
-  gfx_start_frame(render_backend);
-  gfx_render_scene(
-      renderer, &(RenderSceneParams){
-                    .mode = RenderDefault, .scene = state->scene, .camera = 0});
-  gfx_end_frame(render_backend);
-}
-
-static void resize(void* app_state, int width, int height) {
-  assert(app_state);
-  State* state = (State*)(app_state);
-
-  RenderBackend* render_backend = gfx_get_render_backend(state->gfx);
-  gfx_set_viewport(render_backend, width, height);
-}
-
-int main(int argc, const char** argv) {
-  const int initial_width  = SCREEN_WIDTH;
-  const int initial_height = SCREEN_HEIGHT;
-  const int max_fps        = 60;
-
-  gfx_app_run(
-      &(GfxAppDesc){
-          .argc              = argc,
-          .argv              = argv,
-          .width             = initial_width,
-          .height            = initial_height,
-          .max_fps           = max_fps,
-          .update_delta_time = max_fps > 0 ? 1.0 / (double)max_fps : 0.0,
-          .title             = "Isometric Renderer"},
-      &(GfxAppCallbacks){
-          .init     = init,
-          .update   = update,
-          .render   = render,
-          .resize   = resize,
-          .shutdown = shutdown});
-
-  return 0;
-}
diff --git a/gfx-iso/include/isogfx/isogfx.h b/gfx-iso/include/isogfx/isogfx.h
index a5f7770..22c8fd5 100644
--- a/gfx-iso/include/isogfx/isogfx.h
+++ b/gfx-iso/include/isogfx/isogfx.h
@@ -3,64 +3,97 @@
  */
 #pragma once
 
+#include <stdbool.h>
 #include <stdint.h>
 
 typedef struct IsoGfx IsoGfx;
 
-typedef uint8_t Tile;
+/// Tile handle.
+typedef uint16_t Tile;
+
+/// Colour channel.
 typedef uint8_t Channel;
 
 typedef struct Pixel {
-  Channel r, g, b;
+  Channel r, g, b, a;
 } Pixel;
 
 typedef enum TileDescType {
   TileFromColour,
   TileFromFile,
-  TileFromMemory
+  TileFromMemory,
 } TileDescType;
 
 typedef struct TileDesc {
   TileDescType type;
+  int          width;  /// Tile width in pixels.
+  int          height; /// Tile height in pixels.
   union {
-    Pixel colour;
+    Pixel colour; /// Constant colour tile.
     struct {
       const char* path;
     } file;
     struct {
-      const void* data;
+      const uint8_t* data; /// sizeof(Pixel) * width * height
     } mem;
   };
 } TileDesc;
 
+typedef struct WorldDesc {
+  int tile_width;    /// Base tile width in pixels.
+  int tile_height;   /// Base tile height in pixels.
+  int world_width;   /// World width in tiles.
+  int world_height;  /// World height in tiles.
+  int max_num_tiles; /// 0 for an implementation-defined default.
+} WorldDesc;
+
 typedef struct IsoGfxDesc {
-  int screen_width;
-  int screen_height;
-  int tile_width;
-  int tile_height;
-  int world_width;
-  int world_height;
-  int max_num_tiles; // 0 for an implementation-defined default.
+  int screen_width;  /// Screen width in pixels.
+  int screen_height; /// Screen height in pixels.
 } IsoGfxDesc;
 
+/// Create a new isometric graphics engine.
 IsoGfx* isogfx_new(const IsoGfxDesc*);
 
+/// Destroy the isometric graphics engine.
 void isogfx_del(IsoGfx**);
 
+/// Create an empty world.
+bool isogfx_make_world(IsoGfx*, const WorldDesc*);
+
+/// Load a world from a tile map (.TM) file.
+bool isogfx_load_world(IsoGfx*, const char* filepath);
+
+/// Return the world's width.
+int isogfx_world_width(const IsoGfx*);
+
+/// Return the world's height.
+int isogfx_world_height(const IsoGfx*);
+
+/// Create a new tile.
 Tile isogfx_make_tile(IsoGfx*, const TileDesc*);
 
+/// Set the tile at position (x,y).
 void isogfx_set_tile(IsoGfx*, int x, int y, Tile);
 
+/// Set the tiles in positions in the range (x0,y0) - (x1,y1).
 void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile);
 
+/// Translate Cartesian to isometric coordinates.
 void isogfx_pick_tile(
     const IsoGfx*, double xcart, double ycart, int* xiso, int* yiso);
 
+/// Render the world.
 void isogfx_render(IsoGfx*);
 
+/// Draw/overlay a tile at position (x,y).
+///
+/// This function just renders a tile at position (x,y) and should be called
+/// after isogfx_render() to obtain the correct result. To set the tile at
+/// position (x,y) instead, use isogfx_set_tile().
 void isogfx_draw_tile(IsoGfx*, int x, int y, Tile);
 
+/// Return a pointer to the internal colour buffer.
+///
+/// Call after each call to isogfx_render() to retrieve the render output.
 const Pixel* isogfx_get_screen_buffer(const IsoGfx*);
-
-int isogfx_world_width(const IsoGfx*);
-int isogfx_world_height(const IsoGfx*);
diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c
index b38efe7..17b88b2 100644
--- a/gfx-iso/src/isogfx.c
+++ b/gfx-iso/src/isogfx.c
@@ -1,36 +1,106 @@
 #include <isogfx/isogfx.h>
 
+#include <filesystem.h>
 #include <mempool.h>
 
+#include <linux/limits.h>
+
 #include <assert.h>
 #include <stdbool.h>
 #include <stdint.h>
+#include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 /// Maximum number of tiles unless the user chooses a non-zero value.
 #define DEFAULT_MAX_NUM_TILES 1024
 
+// -----------------------------------------------------------------------------
+// Tile set (TS) and tile map (TM) file formats.
+// -----------------------------------------------------------------------------
+
+/// Maximum length of path strings in .TS and .TM files.
+#define MAX_PATH_LENGTH 128
+
+typedef struct Ts_Tile {
+  uint16_t width;     /// Tile width in pixels.
+  uint16_t height;    /// Tile height in pixels.
+  Pixel    pixels[1]; /// Count: width * height.
+} Ts_Tile;
+
+typedef struct Ts_TileSet {
+  uint16_t num_tiles;
+  uint16_t max_tile_width;  /// Maximum tile width in pixels.
+  uint16_t max_tile_height; /// Maximum tile height in pixels.
+  Ts_Tile  tiles[1];        /// Count: num_tiles.
+} Ts_TileSet;
+
+typedef struct Tm_Layer {
+  union {
+    char tileset_path[MAX_PATH_LENGTH]; // Relative to the Tm_Map file.
+  };
+  Tile tiles[1]; /// Count: world_width * world_height.
+} Tm_Layer;
+
+typedef struct Tm_Map {
+  uint16_t world_width;  /// World width in number of tiles.
+  uint16_t world_height; /// World height in number of tiles.
+  uint16_t base_tile_width;
+  uint16_t base_tile_height;
+  uint16_t num_layers;
+  Tm_Layer layers[1]; // Count: num_layers.
+} Tm_Map;
+
+static inline const Tm_Layer* tm_map_get_next_layer(
+    const Tm_Map* map, const Tm_Layer* layer) {
+  assert(map);
+  assert(layer);
+  return (const Tm_Layer*)((const uint8_t*)layer + sizeof(Tm_Layer) +
+                           ((map->world_width * map->world_height - 1) *
+                            sizeof(Tile)));
+}
+
+static inline const Ts_Tile* ts_tileset_get_next_tile(
+    const Ts_TileSet* tileset, const Ts_Tile* tile) {
+  assert(tileset);
+  assert(tile);
+  return (const Ts_Tile*)((const uint8_t*)tile + sizeof(Ts_Tile) +
+                          ((tile->width * tile->height - 1) * sizeof(Pixel)));
+}
+
+// -----------------------------------------------------------------------------
+// Renderer state.
+// -----------------------------------------------------------------------------
+
+// typedef Ts_Tile TileData;
+
 typedef struct TileData {
-  Pixel pixels[1]; // Dynamically allocated.
+  uint16_t width;
+  uint16_t height;
+  uint16_t num_blocks;   // Number of pixel blocks in the pixels mempool.
+  uint16_t pixels_index; // Offset into the pixels mempool.
 } TileData;
 
 DEF_MEMPOOL_DYN(TilePool, TileData)
+DEF_MEMPOOL_DYN(PixelPool, Pixel)
 
 typedef struct IsoGfx {
-  Tile*    world;
-  Pixel*   screen;
-  uint8_t* tile_mask;
-  TilePool tiles;
-  int      screen_width;
-  int      screen_height;
-  int      tile_width;
-  int      tile_height;
-  int      world_width;
-  int      world_height;
-  int      max_num_tiles;
+  int       screen_width;
+  int       screen_height;
+  int       tile_width;
+  int       tile_height;
+  int       world_width;
+  int       world_height;
+  Tile*     world;
+  Pixel*    screen;
+  TilePool  tiles;
+  PixelPool pixels;
 } IsoGfx;
 
+// -----------------------------------------------------------------------------
+// Math and world / tile / screen access.
+// -----------------------------------------------------------------------------
+
 typedef struct ivec2 {
   int x, y;
 } ivec2;
@@ -70,38 +140,27 @@ static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
       .y = (-one_over_s * x + one_over_t * cart.y)};
 }
 
-Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) {
+static const Pixel* tile_xy_const_ref(
+    const IsoGfx* iso, const TileData* tile, int x, int y) {
   assert(iso);
   assert(tile);
-  assert(tile->pixels);
   assert(x >= 0);
   assert(y >= 0);
-  assert(x < iso->tile_width);
-  assert(y < iso->tile_height);
-  return &tile->pixels[y * iso->tile_width + x];
+  assert(x < tile->width);
+  assert(y < tile->height);
+  return &mempool_get_block(
+      &iso->pixels, tile->pixels_index)[y * tile->width + x];
 }
 
-Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) {
-  assert(iso);
-  assert(tile);
-  assert(tile->pixels);
-  assert(x >= 0);
-  assert(y >= 0);
-  assert(x < iso->tile_width);
-  assert(y < iso->tile_height);
-  return tile->pixels[y * iso->tile_width + x];
+static Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) {
+  return *tile_xy_const_ref(iso, tile, x, y);
 }
 
-static inline Tile world_xy(IsoGfx* iso, int x, int y) {
-  assert(iso);
-  assert(x >= 0);
-  assert(y >= 0);
-  assert(x < iso->world_width);
-  assert(y < iso->world_height);
-  return iso->world[y * iso->world_width + x];
+static Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) {
+  return (Pixel*)tile_xy_const_ref(iso, tile, x, y);
 }
 
-static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) {
+static inline const Tile* world_xy_const_ref(const IsoGfx* iso, int x, int y) {
   assert(iso);
   assert(x >= 0);
   assert(y >= 0);
@@ -110,16 +169,16 @@ static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) {
   return &iso->world[y * iso->world_width + x];
 }
 
-static inline Pixel screen_xy(IsoGfx* iso, int x, int y) {
-  assert(iso);
-  assert(x >= 0);
-  assert(y >= 0);
-  assert(x < iso->screen_width);
-  assert(y < iso->screen_height);
-  return iso->screen[y * iso->screen_width + x];
+static inline Tile world_xy(const IsoGfx* iso, int x, int y) {
+  return *world_xy_const_ref(iso, x, y);
 }
 
-static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) {
+static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) {
+  return (Tile*)world_xy_const_ref(iso, x, y);
+}
+
+static inline const Pixel* screen_xy_const_ref(
+    const IsoGfx* iso, int x, int y) {
   assert(iso);
   assert(x >= 0);
   assert(y >= 0);
@@ -128,169 +187,279 @@ static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) {
   return &iso->screen[y * iso->screen_width + x];
 }
 
-static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) {
-  assert(iso);
-
-  const TileData* data = mempool_get_block(&iso->tiles, tile);
-  assert(data);
-
-  for (int py = 0; py < iso->tile_height; ++py) {
-    for (int px = 0; px < iso->tile_width; ++px) {
-      const Pixel colour = tile_xy(iso, data, px, py);
-      const int   sx     = origin.x + px;
-      const int   sy     = origin.y + py;
-      if ((sx >= 0) && (sy >= 0) && (sx < iso->screen_width) &&
-          (sy < iso->screen_height)) {
-        const uint8_t mask = iso->tile_mask[py * iso->tile_width + px];
-        if (mask == 1) {
-          *screen_xy_mut(iso, sx, sy) = colour;
-        }
-      }
-    }
-  }
+static inline Pixel screen_xy(IsoGfx* iso, int x, int y) {
+  return *screen_xy_const_ref(iso, x, y);
 }
 
-static void draw(IsoGfx* iso) {
-  assert(iso);
-
-  const int W = iso->screen_width;
-  const int H = iso->screen_height;
+static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) {
+  return (Pixel*)screen_xy_const_ref(iso, x, y);
+}
 
-  memset(iso->screen, 0, W * H * sizeof(Pixel));
+// -----------------------------------------------------------------------------
+// Renderer, world and tile management.
+// -----------------------------------------------------------------------------
 
-  const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
-  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
-  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
+  assert(desc->screen_width > 0);
+  assert(desc->screen_height > 0);
+  // Part of our implementation assumes even widths and heights for precision.
+  assert((desc->screen_width & 1) == 0);
+  assert((desc->screen_height & 1) == 0);
 
-  // TODO: Culling.
-  // Ex: map the screen corners to tile space to cull.
-  // Ex: walk in screen space and fetch the tile.
-  // The tile-centric approach might be more cache-friendly, however, since the
-  // screen-centric approach would juggle multiple tiles throughout the scan.
-  for (int ty = 0; ty < iso->world_height; ++ty) {
-    for (int tx = 0; tx < iso->world_width; ++tx) {
-      const Tile  tile = world_xy(iso, tx, ty);
-      const ivec2 so =
-          ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty)));
-      draw_tile(iso, so, tile);
-    }
+  IsoGfx* iso = calloc(1, sizeof(IsoGfx));
+  if (!iso) {
+    return 0;
   }
-}
-
-/// Creates a tile mask procedurally.
-static void make_tile_mask(IsoGfx* iso) {
-  assert(iso);
-  assert(iso->tile_mask);
 
-  for (int y = 0; y < iso->tile_height / 2; ++y) {
-    const int mask_start = iso->tile_width / 2 - 2 * y - 1;
-    const int mask_end   = iso->tile_width / 2 + 2 * y + 1;
-    for (int x = 0; x < iso->tile_width; ++x) {
-      const bool    masked = (mask_start <= x) && (x <= mask_end);
-      const uint8_t val    = masked ? 1 : 0;
+  iso->screen_width  = desc->screen_width;
+  iso->screen_height = desc->screen_height;
 
-      // Top half.
-      iso->tile_mask[y * iso->tile_width + x] = val;
+  const int screen_size = desc->screen_width * desc->screen_height;
 
-      // Bottom half reflects the top half.
-      const int y_reflected = iso->tile_height - y - 1;
-      iso->tile_mask[y_reflected * iso->tile_width + x] = val;
-    }
+  if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
+    goto cleanup;
   }
+
+  return iso;
+
+cleanup:
+  isogfx_del(&iso);
+  return 0;
 }
 
-/// Creates a tile with a constant colour.
-static void make_tile_from_colour(
-    const IsoGfx* iso, Pixel colour, TileData* tile) {
+/// Destroy the world and its tile set.
+static void destroy_world(IsoGfx* iso) {
   assert(iso);
-  assert(tile);
+  if (iso->world) {
+    free(iso->world);
+    iso->world = 0;
+  }
+  mempool_del(&iso->tiles);
+  mempool_del(&iso->pixels);
+}
 
-  for (int y = 0; y < iso->tile_height; ++y) {
-    for (int x = 0; x < iso->tile_width; ++x) {
-      *tile_xy_mut(iso, tile, x, y) = colour;
+void isogfx_del(IsoGfx** pIso) {
+  assert(pIso);
+  IsoGfx* iso = *pIso;
+  if (iso) {
+    destroy_world(iso);
+    if (iso->screen) {
+      free(iso->screen);
+      iso->screen = 0;
     }
+    free(iso);
+    *pIso = 0;
   }
 }
 
-IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
-  assert(desc->screen_width > 0);
-  assert(desc->screen_height > 0);
+bool isogfx_make_world(IsoGfx* iso, const WorldDesc* desc) {
+  assert(iso);
+  assert(desc);
   assert(desc->tile_width > 0);
   assert(desc->tile_height > 0);
   // Part of our implementation assumes even widths and heights for greater
   // precision.
-  assert((desc->screen_width & 1) == 0);
-  assert((desc->screen_height & 1) == 0);
   assert((desc->tile_width & 1) == 0);
   assert((desc->tile_height & 1) == 0);
 
-  IsoGfx* iso = calloc(1, sizeof(IsoGfx));
-  if (!iso) {
-    return 0;
-  }
-
-  iso->screen_width  = desc->screen_width;
-  iso->screen_height = desc->screen_height;
-  iso->tile_width    = desc->tile_width;
-  iso->tile_height   = desc->tile_height;
-  iso->world_width   = desc->world_width;
-  iso->world_height  = desc->world_height;
-  iso->max_num_tiles =
-      desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES;
+  // Handle recreation by destroying the previous world.
+  destroy_world(iso);
 
-  const int world_size  = desc->world_width * desc->world_height;
-  const int screen_size = desc->screen_width * desc->screen_height;
-  const int tile_size   = desc->tile_width * desc->tile_height;
+  iso->tile_width   = desc->tile_width;
+  iso->tile_height  = desc->tile_height;
+  iso->world_width  = desc->world_width;
+  iso->world_height = desc->world_height;
 
+  const int world_size      = desc->world_width * desc->world_height;
+  const int tile_size       = desc->tile_width * desc->tile_height;
   const int tile_size_bytes = tile_size * (int)sizeof(Pixel);
+  const int tile_pool_size =
+      desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES;
 
   if (!(iso->world = calloc(world_size, sizeof(Tile)))) {
     goto cleanup;
   }
-  if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
+  if (!mempool_make_dyn(&iso->tiles, tile_pool_size, tile_size_bytes)) {
+    goto cleanup;
+  }
+
+  return true;
+
+cleanup:
+  destroy_world(iso);
+  mempool_del(&iso->tiles);
+  return false;
+}
+
+bool isogfx_load_world(IsoGfx* iso, const char* filepath) {
+  assert(iso);
+  assert(filepath);
+
+  bool success = false;
+
+  // Handle recreation by destroying the previous world.
+  destroy_world(iso);
+
+  // Load the map.
+  printf("Load tile map: %s\n", filepath);
+  Tm_Map* map = read_file(filepath);
+  if (!map) {
     goto cleanup;
   }
-  if (!(iso->tile_mask = calloc(tile_size, sizeof(uint8_t)))) {
+
+  // Allocate memory for the map and tile sets.
+  const int world_size           = map->world_width * map->world_height;
+  const int base_tile_size       = map->base_tile_width * map->base_tile_height;
+  const int base_tile_size_bytes = base_tile_size * (int)sizeof(Pixel);
+  // TODO: Need to get the total number of tiles from the map.
+  const int tile_pool_size = DEFAULT_MAX_NUM_TILES;
+
+  if (!(iso->world = calloc(world_size, sizeof(Tile)))) {
     goto cleanup;
   }
-  if (!mempool_make_dyn(&iso->tiles, iso->max_num_tiles, tile_size_bytes)) {
+  if (!mempool_make_dyn(&iso->tiles, tile_pool_size, sizeof(TileData))) {
+    goto cleanup;
+  }
+  if (!mempool_make_dyn(&iso->pixels, tile_pool_size, base_tile_size_bytes)) {
     goto cleanup;
   }
 
-  make_tile_mask(iso);
+  // Load the tile sets.
+  const Tm_Layer* layer = &map->layers[0];
+  // TODO: Handle num_layers layers.
+  for (int i = 0; i < 1; ++i) {
+    const char* ts_path = layer->tileset_path;
+
+    // Tile set path is relative to the tile map file. Make it relative to the
+    // current working directory before loading.
+    char ts_path_cwd[PATH_MAX] = {0};
+    if (!make_relative_path(MAX_PATH_LENGTH, filepath, ts_path, ts_path_cwd)) {
+      goto cleanup;
+    }
 
-  return iso;
+    Ts_TileSet* tileset = read_file(ts_path_cwd);
+    if (!tileset) {
+      goto cleanup;
+    };
+
+    // Load tile data.
+    const Ts_Tile* tile = &tileset->tiles[0];
+    for (uint16_t j = 0; j < tileset->num_tiles; ++j) {
+      // Tile dimensions should be a multiple of the base tile size.
+      assert((tile->width % map->base_tile_width) == 0);
+      assert((tile->height % map->base_tile_height) == 0);
+
+      const uint16_t tile_size = tile->width * tile->height;
+
+      // TODO: Add function in mempool to alloc N consecutive blocks.
+      const int num_blocks = tile_size / base_tile_size;
+      Pixel*    pixels     = mempool_alloc(&iso->pixels);
+      assert(pixels);
+      // This is ugly and assumes that blocks are allocated consecutively.
+      for (int b = 1; b < num_blocks; ++b) {
+        Pixel* block = mempool_alloc(&iso->pixels);
+        assert(block);
+      }
+      memcpy(pixels, tile->pixels, tile_size * sizeof(Pixel));
 
-cleanup:
-  isogfx_del(&iso);
-  return 0;
-}
+      TileData* tile_data = mempool_alloc(&iso->tiles);
+      assert(tile_data);
+      tile_data->width      = tile->width;
+      tile_data->height     = tile->height;
+      tile_data->num_blocks = (uint16_t)num_blocks;
+      tile_data->pixels_index =
+          (uint16_t)mempool_get_block_index(&iso->pixels, pixels);
 
-void isogfx_del(IsoGfx** pIso) {
-  assert(pIso);
-  IsoGfx* iso = *pIso;
-  if (iso) {
-    if (iso->world) {
-      free(iso->world);
+      tile = ts_tileset_get_next_tile(tileset, tile);
     }
-    if (iso->screen) {
-      free(iso->screen);
+
+    printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd);
+
+    free(tileset);
+    layer = tm_map_get_next_layer(map, layer);
+  }
+
+  // Load the map into the world.
+  layer = &map->layers[0];
+  // TODO: Handle num_layers layers.
+  for (int i = 0; i < 1; ++i) {
+    memcpy(iso->world, layer->tiles, world_size * sizeof(Tile));
+
+    // TODO: We need to handle 'firsgid' in TMX files.
+    for (int j = 0; j < world_size; ++j) {
+      iso->world[j] -= 1;
     }
-    if (iso->tile_mask) {
-      free(iso->tile_mask);
+
+    layer = tm_map_get_next_layer(map, layer);
+  }
+
+  iso->world_width  = map->world_width;
+  iso->world_height = map->world_height;
+  iso->tile_width   = map->base_tile_width;
+  iso->tile_height  = map->base_tile_height;
+
+  success = true;
+
+cleanup:
+  if (map) {
+    free(map);
+  }
+  if (!success) {
+    destroy_world(iso);
+  }
+  return success;
+}
+
+int isogfx_world_width(const IsoGfx* iso) {
+  assert(iso);
+  return iso->world_width;
+}
+
+int isogfx_world_height(const IsoGfx* iso) {
+  assert(iso);
+  return iso->world_height;
+}
+
+/// Create a tile mask procedurally.
+static void make_tile_from_colour(
+    const IsoGfx* iso, Pixel colour, TileData* tile) {
+  assert(iso);
+  assert(tile);
+
+  const int width  = tile->width;
+  const int height = tile->height;
+  const int r      = width / height;
+
+  for (int y = 0; y < height / 2; ++y) {
+    const int mask_start = width / 2 - r * y - 1;
+    const int mask_end   = width / 2 + r * y + 1;
+    for (int x = 0; x < width; ++x) {
+      const bool  mask = (mask_start <= x) && (x <= mask_end);
+      const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0};
+
+      // Top half.
+      *tile_xy_mut(iso, tile, x, y) = val;
+
+      // Bottom half reflects the top half.
+      const int y_reflected                   = height - y - 1;
+      *tile_xy_mut(iso, tile, x, y_reflected) = val;
     }
-    mempool_del(&iso->tiles);
-    free(iso);
   }
 }
 
 Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) {
   assert(iso);
   assert(desc);
+  // Client must create world before creating tiles.
+  assert(iso->tile_width > 0);
+  assert(iso->tile_height > 0);
 
   TileData* tile = mempool_alloc(&iso->tiles);
   assert(tile); // TODO: Make this a hard assert.
 
+  tile->width  = desc->width;
+  tile->height = desc->height;
+
   switch (desc->type) {
   case TileFromColour:
     make_tile_from_colour(iso, desc->colour, tile);
@@ -311,6 +480,88 @@ void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) {
   *world_xy_mut(iso, x, y) = tile;
 }
 
+void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) {
+  assert(iso);
+  for (int y = y0; y < y1; ++y) {
+    for (int x = x0; x < x1; ++x) {
+      isogfx_set_tile(iso, x, y, tile);
+    }
+  }
+}
+
+// -----------------------------------------------------------------------------
+// Rendering and picking.
+// -----------------------------------------------------------------------------
+
+static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) {
+  assert(iso);
+
+  const TileData* tile_data = mempool_get_block(&iso->tiles, tile);
+  assert(tile_data);
+
+  // Tile can exceed screen bounds, so we must clip it.
+#define max(a, b) (a > b ? a : b)
+  const int py_offset = max(0, (int)tile_data->height - origin.y);
+  origin.y            = max(0, origin.y - (int)tile_data->height);
+
+  // Clip along Y and X as we draw.
+  for (int py = py_offset;
+       (py < tile_data->height) && (origin.y + py < iso->screen_height); ++py) {
+    const int sy = origin.y + py - py_offset;
+    for (int px = 0;
+         (px < tile_data->width) && (origin.x + px < iso->screen_width); ++px) {
+      const Pixel colour = tile_xy(iso, tile_data, px, py);
+      if (colour.a > 0) {
+        const int sx                = origin.x + px;
+        *screen_xy_mut(iso, sx, sy) = colour;
+      }
+    }
+  }
+
+  //  for (int py = 0; py < tile_data->height; ++py) {
+  //    for (int px = 0; px < tile_data->width; ++px) {
+  //      const Pixel colour = tile_xy(iso, tile_data, px, py);
+  //      if (colour.a > 0) {
+  //        const int sx = origin.x + px;
+  //        const int sy = origin.y + py;
+  //        if ((sx >= 0) && (sy >= 0) && (sx < iso->screen_width) &&
+  //            (sy < iso->screen_height)) {
+  //          *screen_xy_mut(iso, sx, sy) = colour;
+  //        }
+  //      }
+  //    }
+  //  }
+}
+
+static void draw(IsoGfx* iso) {
+  assert(iso);
+
+  const int W = iso->screen_width;
+  const int H = iso->screen_height;
+
+  memset(iso->screen, 0, W * H * sizeof(Pixel));
+
+  // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
+  const ivec2 o = {
+      (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height};
+  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
+  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+
+  // TODO: Culling.
+  // Ex: map the screen corners to tile space to cull.
+  // Ex: walk in screen space and fetch the tile.
+  // The tile-centric approach might be more cache-friendly since the
+  // screen-centric approach would juggle multiple tiles throughout the scan.
+  for (int ty = 0; ty < iso->world_height; ++ty) {
+    for (int tx = 0; tx < iso->world_width; ++tx) {
+      const Tile  tile = world_xy(iso, tx, ty);
+      const ivec2 so =
+          ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty)));
+      draw_tile(iso, so, tile);
+    }
+  }
+}
+
 void isogfx_pick_tile(
     const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) {
   assert(iso);
@@ -356,13 +607,3 @@ const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) {
   assert(iso);
   return iso->screen;
 }
-
-int isogfx_world_width(const IsoGfx* iso) {
-  assert(iso);
-  return iso->world_width;
-}
-
-int isogfx_world_height(const IsoGfx* iso) {
-  assert(iso);
-  return iso->world_height;
-}
-- 
cgit v1.2.3