#include "Chaser.hpp"
 #include "RandomWalk.hpp"
+#include "../model/shapes.hpp"
 #include "../world/BlockLookup.hpp"
 #include "../world/BlockType.hpp"
-#include "../world/BlockTypeRegistry.hpp"
 #include "../world/Entity.hpp"
 #include "../world/World.hpp"
 
 , max_entities(16)
 , chunk_range(4) {
        EntityModel::Buffer buf;
-       for (size_t i = 0; i < 14; ++i) {
-               world.BlockTypes()[i + 1].FillEntityModel(buf);
-               models[i].Update(buf);
+       {
+               CuboidShape shape({{ -0.25f, -0.5f, -0.25f }, { 0.25f, 0.5f, 0.25f }});
+               shape.Vertices(buf, 1.0f);
+               buf.colors.resize(shape.VertexCount(), { 1.0f, 1.0f, 0.0f });
+               models[0].Update(buf);
+       }
+       {
+               CuboidShape shape({{ -0.5f, -0.25f, -0.5f }, { 0.5f, 0.25f, 0.5f }});
+               buf.Clear();
+               shape.Vertices(buf, 2.0f);
+               buf.colors.resize(shape.VertexCount(), { 0.0f, 1.0f, 1.0f });
+               models[1].Update(buf);
+       }
+       {
+               StairShape shape({{ -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f }}, { 0.4f, 0.4f });
                buf.Clear();
+               shape.Vertices(buf, 3.0f);
+               buf.colors.resize(shape.VertexCount(), { 1.0f, 0.0f, 1.0f });
+               models[2].Update(buf);
        }
 
        timer.Start();
        e.Position(chunk, pos);
        e.Bounds({ { -0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f } });
        e.WorldCollidable(true);
-       e.GetModel().SetNodeModel(&models[rand() % 14]);
+       e.GetModel().SetNodeModel(&models[rand() % 3]);
        e.AngularVelocity(rot);
        Controller *ctrl;
        if (rand() % 2) {
 
        World &world;
        std::vector<Controller *> controllers;
 
-       EntityModel models[14];
+       EntityModel models[3];
 
        IntervalTimer timer;
        float despawn_range;
 
 
 #include "Environment.hpp"
 #include "../world/ChunkLoader.hpp"
+#include "../world/ChunkRenderer.hpp"
 
 
 namespace blank {
 
-PreloadState::PreloadState(Environment &env, ChunkLoader &loader)
+PreloadState::PreloadState(Environment &env, ChunkLoader &loader, ChunkRenderer &render)
 : env(env)
 , loader(loader)
+, render(render)
 , progress(env.assets.large_ui_font)
 , total(loader.ToLoad())
 , per_update(64) {
 void PreloadState::Update(int dt) {
        loader.LoadN(per_update);
        if (loader.ToLoad() == 0) {
-               for (auto &chunk : loader.Loaded()) {
-                       chunk.CheckUpdate();
-               }
                env.state.Pop();
+               render.Update(render.MissingChunks());
        } else {
                progress.Update(total - loader.ToLoad(), total);
        }
 
 namespace blank {
 
 class ChunkLoader;
+class ChunkRenderer;
 class Environment;
 
 class PreloadState
 : public State {
 
 public:
-       PreloadState(Environment &, ChunkLoader &);
+       PreloadState(Environment &, ChunkLoader &, ChunkRenderer &);
 
        void Handle(const SDL_Event &) override;
        void Update(int dt) override;
 private:
        Environment &env;
        ChunkLoader &loader;
+       ChunkRenderer &render;
        Progress progress;
        std::size_t total;
        std::size_t per_update;
 
 #include "WorldState.hpp"
 
 #include "Environment.hpp"
+#include "TextureIndex.hpp"
 
 #include <SDL.h>
 
        const WorldSave &save
 )
 : env(env)
-, world(env.assets, wc, save)
+, block_types()
+, world(block_types, wc, save)
+, chunk_renderer(world, wc.load.load_dist)
 , spawner(world)
 , interface(ic, env, world)
-, preload(env, world.Loader())
+, preload(env, world.Loader(), chunk_renderer)
 , unload(env, world.Loader()) {
-
+       TextureIndex tex_index;
+       env.assets.LoadBlockTypes("default", block_types, tex_index);
+       chunk_renderer.LoadTextures(env.assets, tex_index);
+       chunk_renderer.FogDensity(wc.fog_density);
+       // TODO: better solution for initializing HUD
+       interface.SelectNext();
 }
 
 
        interface.Update(dt);
        spawner.Update(dt);
        world.Update(dt);
+       chunk_renderer.Rebase(world.Player().ChunkCoords());
+       chunk_renderer.Update(dt);
 
-       glm::mat4 trans = world.Player().Transform(Chunk::Pos(0, 0, 0));
+       glm::mat4 trans = world.Player().Transform(world.Player().ChunkCoords());
        glm::vec3 dir(trans * glm::vec4(0.0f, 0.0f, -1.0f, 0.0f));
        glm::vec3 up(trans * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f));
        env.audio.Position(world.Player().Position());
 }
 
 void WorldState::Render(Viewport &viewport) {
+       viewport.WorldPosition(world.Player().Transform(world.Player().ChunkCoords()));
+       chunk_renderer.Render(viewport);
        world.Render(viewport);
        interface.Render(viewport);
 }
 
 #include "UnloadState.hpp"
 #include "../ai/Spawner.hpp"
 #include "../ui/Interface.hpp"
+#include "../world/BlockTypeRegistry.hpp"
+#include "../world/ChunkRenderer.hpp"
 #include "../world/World.hpp"
 
 
 
 private:
        Environment &env;
+       BlockTypeRegistry block_types;
        World world;
+       ChunkRenderer chunk_renderer;
        Spawner spawner;
        Interface interface;
 
 
 #include "filesystem.hpp"
 
 #include <cctype>
+#include <cstring>
 #include <fstream>
 #include <iostream>
 #include <limits>
 
        HUD(const HUD &) = delete;
        HUD &operator =(const HUD &) = delete;
 
+       void DisplayNone();
        void Display(const Block &);
 
        void Render(Viewport &) noexcept;
 
 }
 
 
+void HUD::DisplayNone() {
+       block_visible = false;
+}
+
 void HUD::Display(const Block &b) {
        const BlockType &type = types.Get(b.type);
 
 , place_timer(256)
 , remove_timer(256)
 , remove(0)
-, selection(1)
+, selection(0)
 , place_sound(env.assets.LoadSound("thump"))
 , remove_sound(env.assets.LoadSound("plop"))
 , fwd(0)
        messages.Position(glm::vec3(25.0f, -25.0f, 0.0f), Gravity::SOUTH_WEST);
        messages.Foreground(glm::vec4(1.0f));
        messages.Background(glm::vec4(0.5f));
-       hud.Display(selection);
+       hud.DisplayNone();
 }
 
 
 
                }
        }
 
+       /// returns 1 for pro-axis, -1 for retro-axis, 0 for invalid faces
+       static int Direction(Face f) noexcept {
+               switch (f) {
+                       case FACE_RIGHT:
+                       case FACE_UP:
+                       case FACE_FRONT:
+                               return 1;
+                       case FACE_LEFT:
+                       case FACE_DOWN:
+                       case FACE_BACK:
+                               return -1;
+                       default:
+                               return 0;
+               }
+       }
+
        static glm::ivec3 FaceNormal(Face face) noexcept {
                return face2normal[face];
        }
 
        size_t Size() const noexcept { return types.size(); }
 
        BlockType &operator [](Block::Type id) { return types[id]; }
+       const BlockType &operator [](Block::Type id) const { return types[id]; }
+
+       BlockType &Get(Block::Type id) { return types[id]; }
        const BlockType &Get(Block::Type id) const { return types[id]; }
 
 private:
 
 
 #include "Block.hpp"
 #include "BlockTypeRegistry.hpp"
-#include "../model/BlockModel.hpp"
 #include "../model/geometry.hpp"
 
 #include <vector>
        bool ShouldUpdateModel() const noexcept { return dirty_model; }
        bool ShouldUpdateSave() const noexcept { return dirty_save; }
 
-       void CheckUpdate() noexcept;
-       void Draw() noexcept;
-
-private:
-       void Update() noexcept;
+       void Update(BlockModel &) noexcept;
 
 private:
        const BlockTypeRegistry *types;
        Chunk *neighbor[Block::FACE_COUNT];
        Block blocks[size];
        unsigned char light[size];
-       BlockModel model;
        Pos position;
        bool dirty_model;
        bool dirty_save;
 
--- /dev/null
+#ifndef BLANK_WORLD_CHUNKRENDERER_HPP_
+#define BLANK_WORLD_CHUNKRENDERER_HPP_
+
+#include "Block.hpp"
+#include "Chunk.hpp"
+#include "../graphics/ArrayTexture.hpp"
+#include "../model/BlockModel.hpp"
+
+#include <vector>
+
+
+namespace blank {
+
+class Assets;
+class TextureIndex;
+class Viewport;
+class World;
+
+class ChunkRenderer {
+
+public:
+       /// render_distance in chunks, excluding the base chunk which is always rendered
+       ChunkRenderer(World &, int render_distance);
+
+       void LoadTextures(const Assets &, const TextureIndex &);
+       void FogDensity(float d) noexcept { fog_density = d; }
+
+       bool InRange(const Chunk::Pos &) const noexcept;
+       int IndexOf(const Chunk::Pos &) const noexcept;
+
+       int TotalChunks() const noexcept { return total_length; }
+       int IndexedChunks() const noexcept { return total_indexed; }
+       int MissingChunks() const noexcept { return total_length - total_indexed; }
+
+       void Rebase(const Chunk::Pos &);
+       void Rescan();
+       void Scan();
+       void Update(int dt);
+
+       void Render(Viewport &);
+
+private:
+       int GetCol(int) const noexcept;
+
+       void Shift(Block::Face);
+
+private:
+       World &world;
+       ArrayTexture block_tex;
+
+       int render_dist;
+       int side_length;
+       int total_length;
+       int total_indexed;
+       glm::ivec3 stride;
+       std::vector<BlockModel> models;
+       std::vector<Chunk *> chunks;
+
+       Chunk::Pos base;
+
+       float fog_density;
+
+};
+
+}
+
+#endif
 
 #include "EntityCollision.hpp"
 #include "WorldCollision.hpp"
 #include "../app/Assets.hpp"
-#include "../app/TextureIndex.hpp"
 #include "../graphics/Format.hpp"
 #include "../graphics/Viewport.hpp"
 
 
 namespace blank {
 
-World::World(const Assets &assets, const Config &config, const WorldSave &save)
-: block_type()
-, block_tex()
+World::World(const BlockTypeRegistry &types, const Config &config, const WorldSave &save)
+: block_type(types)
 , generate(config.gen)
-, chunks(config.load, block_type, generate, save)
+, chunks(config.load, types, generate, save)
 , player()
 , entities()
 , light_direction(config.light_direction)
 , fog_density(config.fog_density) {
-       TextureIndex tex_index;
-       assets.LoadBlockTypes("default", block_type, tex_index);
-
-       block_tex.Bind();
-       assets.LoadTextures(tex_index, block_tex);
-       block_tex.FilterNearest();
-
        generate.Space(0);
        generate.Light(13);
        generate.Solids({ 1, 4, 7, 10 });
 
 
 void World::Render(Viewport &viewport) {
-       viewport.WorldPosition(player->Transform(player->ChunkCoords()));
-
-       BlockLighting &chunk_prog = viewport.ChunkProgram();
-       chunk_prog.SetTexture(block_tex);
-       chunk_prog.SetFogDensity(fog_density);
-
-       for (Chunk &chunk : chunks.Loaded()) {
-               glm::mat4 m(chunk.Transform(player->ChunkCoords()));
-               chunk_prog.SetM(m);
-               glm::mat4 mvp(chunk_prog.GetVP() * m);
-               if (!CullTest(Chunk::Bounds(), mvp)) {
-                       chunk.Draw();
-               }
-       }
-
        DirectionalLighting &entity_prog = viewport.EntityProgram();
        entity_prog.SetLightDirection(light_direction);
        entity_prog.SetFogDensity(fog_density);
 
 #ifndef BLANK_WORLD_WORLD_HPP_
 #define BLANK_WORLD_WORLD_HPP_
 
-#include "BlockTypeRegistry.hpp"
 #include "ChunkLoader.hpp"
 #include "Entity.hpp"
 #include "Generator.hpp"
-#include "../graphics/ArrayTexture.hpp"
 
 #include <list>
 #include <vector>
 
 namespace blank {
 
-class Assets;
+class BlockTypeRegistry;
 class EntityCollision;
 class Viewport;
 class WorldCollision;
                ChunkLoader::Config load = ChunkLoader::Config();
        };
 
-       World(const Assets &, const Config &, const WorldSave &);
+       World(const BlockTypeRegistry &, const Config &, const WorldSave &);
 
        /// check if this ray hits a block
        /// depth in the collision is the distance between the ray's
        bool Intersection(const Entity &e, std::vector<WorldCollision> &);
        void Resolve(Entity &e, std::vector<WorldCollision> &);
 
-       BlockTypeRegistry &BlockTypes() noexcept { return block_type; }
+       const BlockTypeRegistry &BlockTypes() noexcept { return block_type; }
        ChunkLoader &Loader() noexcept { return chunks; }
 
        Entity &Player() { return *player; }
        void Render(Viewport &);
 
 private:
-       BlockTypeRegistry block_type;
-
-       ArrayTexture block_tex;
+       const BlockTypeRegistry &block_type;
 
        Generator generate;
        ChunkLoader chunks;
 
 , neighbor{0}
 , blocks{}
 , light{0}
-, model()
 , position(0, 0, 0)
 , dirty_model(false)
 , dirty_save(false) {
 
 Chunk::Chunk(Chunk &&other) noexcept
 : types(other.types)
-, model(std::move(other.model))
 , position(other.position)
 , dirty_model(other.dirty_model)
 , dirty_save(other.dirty_save) {
        std::copy(other.neighbor, other.neighbor + sizeof(neighbor), neighbor);
        std::copy(other.blocks, other.blocks + sizeof(blocks), blocks);
        std::copy(other.light, other.light + sizeof(light), light);
-       model = std::move(other.model);
        position = other.position;
        dirty_model = other.dirty_save;
        dirty_save = other.dirty_save;
 }
 
 
-void Chunk::Draw() noexcept {
-       if (ShouldUpdateModel()) {
-               Update();
-       }
-       model.Draw();
-}
-
-
 bool Chunk::Intersection(
        const Ray &ray,
        const glm::mat4 &M,
 
 }
 
-void Chunk::CheckUpdate() noexcept {
-       if (ShouldUpdateModel()) {
-               Update();
-       }
-}
-
-void Chunk::Update() noexcept {
+void Chunk::Update(BlockModel &model) noexcept {
        int vtx_count = 0, idx_count = 0;
        for (const auto &block : blocks) {
                const Shape *shape = Type(block).shape;
 
--- /dev/null
+#include "ChunkRenderer.hpp"
+
+#include "World.hpp"
+#include "../app/Assets.hpp"
+#include "../graphics/BlockLighting.hpp"
+#include "../graphics/Viewport.hpp"
+
+
+namespace blank {
+
+ChunkRenderer::ChunkRenderer(World &world, int rd)
+: world(world)
+, block_tex()
+, render_dist(rd)
+, side_length(2 * rd + 1)
+, total_length(side_length * side_length * side_length)
+, total_indexed(0)
+, stride(1, side_length, side_length * side_length)
+, models(total_length)
+, chunks(total_length)
+, base(0, 0, 0)
+, fog_density(0.0f) {
+
+}
+
+
+void ChunkRenderer::LoadTextures(const Assets &assets, const TextureIndex &tex_index) {
+       block_tex.Bind();
+       assets.LoadTextures(tex_index, block_tex);
+       block_tex.FilterNearest();
+}
+
+
+bool ChunkRenderer::InRange(const Chunk::Pos &pos) const noexcept {
+       return manhattan_radius(pos - base) <= render_dist;
+}
+
+int ChunkRenderer::IndexOf(const Chunk::Pos &pos) const noexcept {
+       Chunk::Pos mod_pos(
+               GetCol(pos.x),
+               GetCol(pos.y),
+               GetCol(pos.z)
+       );
+       return mod_pos.x * stride.x
+               +  mod_pos.y * stride.y
+               +  mod_pos.z * stride.z;
+}
+
+
+void ChunkRenderer::Rebase(const Chunk::Pos &new_base) {
+       if (new_base == base) return;
+
+       Chunk::Pos diff(new_base - base);
+
+       if (manhattan_radius(diff) > render_dist) {
+               // that's more than half, so probably not worth shifting
+               base = new_base;
+               Rescan();
+               return;
+       }
+
+       while (diff.x > 0) {
+               Shift(Block::FACE_RIGHT);
+               --diff.x;
+       }
+       while (diff.x < 0) {
+               Shift(Block::FACE_LEFT);
+               ++diff.x;
+       }
+       while (diff.y > 0) {
+               Shift(Block::FACE_UP);
+               --diff.y;
+       }
+       while (diff.y < 0) {
+               Shift(Block::FACE_DOWN);
+               ++diff.y;
+       }
+       while (diff.z > 0) {
+               Shift(Block::FACE_FRONT);
+               --diff.z;
+       }
+       while (diff.z < 0) {
+               Shift(Block::FACE_BACK);
+               ++diff.z;
+       }
+}
+
+int ChunkRenderer::GetCol(int c) const noexcept {
+       c %= side_length;
+       if (c < 0) c += side_length;
+       return c;
+}
+
+void ChunkRenderer::Shift(Block::Face f) {
+       int a_axis = Block::Axis(f);
+       int b_axis = (a_axis + 1) % 3;
+       int c_axis = (a_axis + 2) % 3;
+       int dir = Block::Direction(f);
+       base[a_axis] += dir;
+       int a = GetCol(base[a_axis] + (render_dist * dir));
+       int a_stride = a * stride[a_axis];
+       for (int b = 0; b < side_length; ++b) {
+               int b_stride = b * stride[b_axis];
+               for (int c = 0; c < side_length; ++c) {
+                       int bc_stride = b_stride + c * stride[c_axis];
+                       int index = a_stride + bc_stride;
+                       if (chunks[index]) {
+                               chunks[index] = nullptr;
+                               --total_indexed;
+                       }
+                       int neighbor = ((a - dir + side_length) % side_length) * stride[a_axis] + bc_stride;
+                       if (chunks[neighbor] && chunks[neighbor]->HasNeighbor(f)) {
+                               chunks[index] = &chunks[neighbor]->GetNeighbor(f);
+                               chunks[index]->InvalidateModel();
+                               ++total_indexed;
+                       }
+               }
+       }
+}
+
+
+void ChunkRenderer::Rescan() {
+       chunks.assign(total_length, nullptr);
+       total_indexed = 0;
+       Scan();
+}
+
+void ChunkRenderer::Scan() {
+       for (Chunk &chunk : world.Loader().Loaded()) {
+               if (!InRange(chunk.Position())) continue;
+               int index = IndexOf(chunk.Position());
+               if (!chunks[index]) {
+                       chunks[index] = &chunk;
+                       chunk.InvalidateModel();
+                       ++total_indexed;
+               }
+       }
+}
+
+void ChunkRenderer::Update(int dt) {
+       if (MissingChunks()) {
+               Scan();
+       }
+
+       // maximum of 1000 per second too high?
+       for (int i = 0, updates = 0; i < total_length && updates < dt; ++i) {
+               if (chunks[i] && chunks[i]->ShouldUpdateModel()) {
+                       chunks[i]->Update(models[i]);
+                       ++updates;
+               }
+       }
+}
+
+
+void ChunkRenderer::Render(Viewport &viewport) {
+       BlockLighting &chunk_prog = viewport.ChunkProgram();
+       chunk_prog.SetTexture(block_tex);
+       chunk_prog.SetFogDensity(fog_density);
+
+       for (int i = 0; i < total_length; ++i) {
+               if (!chunks[i]) continue;
+               glm::mat4 m(chunks[i]->Transform(base));
+               glm::mat4 mvp(chunk_prog.GetVP() * m);
+               if (!CullTest(Chunk::Bounds(), mvp)) {
+                       if (chunks[i]->ShouldUpdateModel()) {
+                               chunks[i]->Update(models[i]);
+                       }
+                       chunk_prog.SetM(m);
+                       models[i].Draw();
+               }
+       }
+}
+
+}
 
-#include "app/init.hpp"
-
 #include <cppunit/extensions/TestFactoryRegistry.h>
 #include <cppunit/ui/text/TestRunner.h>
 
 
 
 int main(int, char **) {
-       blank::Init init;
-
        TestRunner runner;
        TestFactoryRegistry ®istry = TestFactoryRegistry::getRegistry();
        runner.addTest(registry.makeTest());