#include <Controller/Controller.hpp>
#include <Controller/GadgetController/GadgetController.hpp>
#include <Entity/PhysicsEntity/CameraEntity/Player/Player.hpp>
#include <GraphicsContext/GraphicsContext.hpp>
#include <Item/IEquipment.hpp>
#include <Item/Item.hpp>
#include <PipelineStage/LightingStage/LightingStage.hpp>
#include <Utilities/FixedPoint.hpp>
#include <Vk/Buffer/Buffer.hpp>
#include <Vk/DescriptorSet/DescriptorSet.hpp>
#include <Vk/DescriptorSetLayout/DescriptorSetLayout.hpp>
#include <Vk/Pipeline/Pipeline.hpp>
#include <Vk/ShaderModule/ShaderModule.hpp>
#include <Widget/Builder/Builder.hpp>
#include <Widget/Layout/CompoundWidget/EscapeMenu/EscapeMenu.hpp>
#include <Widget/TabList/TabList.hpp>
#include <algorithm>
#include <cmath>
#include <memory>
#include <sstream>
#include <iomanip>
#include <Display/WidgetDisplay/WorldDisplay/WorldDisplay.hpp>
#include <BlockRegistry/BlockRegistry.hpp>
#include <Utilities/Utilities.hpp>
#include <Log/Log.hpp>
#include <Display/WidgetDisplay/MessageDisplay/MessageDisplay.hpp>
#include <Display/WidgetDisplay/MainMenuDisplay/MainMenuDisplay.hpp>
#include <Widget/Button/Button.hpp>
#include <Widget/Layout/Center/Center.hpp>
#include <Widget/Layout/Column/Column.hpp>
#include <Widget/Label/Label.hpp>
#include <Widget/Panel/Panel.hpp>
#include <Widget/Slider/Slider.hpp>
#include <Settings/Settings.hpp>
#include <Widget/Layout/Row/GuiControllerView/GuiControllerView.hpp>
#include <Widget/ItemView/ItemView.hpp>
#include <Display/WidgetDisplay/ShutdownDisplay/ShutdownDisplay.hpp>
#include <Widget/TextField/TextField.hpp>
#include <Widget/TextField/ChatTextField/ChatTextField.hpp>
#include <Cubemap/Cubemap.hpp>
#include <IndexedUnitCube/IndexedUnitCube.hpp>
#include <AudioManager/AudioManager.hpp>
#include <Widget/Layout/CompoundWidget/SettingsPanel/SettingsPanel.hpp>
#include <Vk/ShaderConst.hpp>
#include <Model/Model.hpp>
#include <Utilities/Utilities.hpp>
#include <Widget/Layout/Column/HorizontalSeparator/HorizontalSeparator.hpp>
#include <VoxelSpriteRenderer/VoxelSpriteRenderer.hpp>
#include <Utilities/Perf.hpp>
#include <ChunkWindow/ChunkWindow.hpp>
#include <Widget/Layout/CompoundWidget/AchievementsTab/AchievementsTab.hpp>
#include <World/RemoteWorld/RemoteWorld.hpp>
#include <InputMethod/InputMethod.hpp>
#include <Widget/Layout/CompoundWidget/WorldStatsTab/WorldStatsTab.hpp>
#include <vulkan/vulkan_core.h>

std::shared_ptr<Vk::DescriptorSetLayout> WorldDisplay::worldModelDescriptorSetLayout;
std::shared_ptr<Vk::DescriptorSetLayout> WorldDisplay::modelDescriptorSetLayout;

std::unordered_map<Material*, std::shared_ptr<Model>> WorldDisplay::materialModels;
const char *WorldDisplay::gpuDebugModeNames[] = {
#define DECL_GLSL_DEBUGMODE(name, enumval, label) label,
#include <Vk/ShaderDebugModeList.hpp>
};

void WorldDisplay::recreateGraphicsObjects(GraphicsContext &context)
{
    framebufferExtent = lastKnownExtent;
    framebufferExtent.width = framebufferExtent.width * settings.framebufferResolution / 100;
    framebufferExtent.height = framebufferExtent.height * settings.framebufferResolution / 100;

    context.getDevice()->waitIdle();
    for (int i=0; i<GraphicsContext::maxFramesInFlight; i++)
    {
        frameDrawers[i] = nullptr;
    }

    VkFormat depthFormat = context.getDevice()->findDepthFormat();

    std::shared_ptr<Vk::Image> colorImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R8G8B8A8_SRGB,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    normalImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R32G32B32A32_SFLOAT,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    posSpecularImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R32G32B32A32_SFLOAT,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    lightImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R32G32B32A32_SFLOAT,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_STORAGE_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    std::shared_ptr<Vk::Image> depthImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        depthFormat,
        VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    std::shared_ptr<Vk::Image> particlesImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R8G8B8A8_SRGB,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    std::shared_ptr<Vk::Image> godRaysImage = std::make_shared<Vk::Image>(
        context.getDevice(),
        VK_IMAGE_TYPE_2D,
        framebufferExtent.width, framebufferExtent.height,
        VK_FORMAT_R8G8B8A8_SRGB,
        VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    colorImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        colorImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    normalImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        normalImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    posSpecularImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        posSpecularImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    lightImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        lightImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    std::shared_ptr<Vk::ImageView> depthImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        depthImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_DEPTH_BIT
    );

    std::shared_ptr<Vk::ImageView> particlesImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        particlesImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    std::shared_ptr<Vk::ImageView> godRaysImageView = std::make_shared<Vk::ImageView>(
        context.getDevice(),
        godRaysImage,
        VK_IMAGE_VIEW_TYPE_2D,
        VK_IMAGE_ASPECT_COLOR_BIT
    );

    glm::uvec2 screenTiles = (glm::uvec2(framebufferExtent.width, framebufferExtent.height) +
        glm::uvec2(GLSL_LIGHT_TILE_SIZE - 1)) /
        glm::uvec2(GLSL_LIGHT_TILE_SIZE);
    
    lightClusterBuffer = std::make_shared<Vk::Buffer>(
        context.getDevice(),
        screenTiles.y * screenTiles.x * GLSL_LIGHT_DEPTH_SLICES * sizeof(LightingStage::Cluster) + 16 * sizeof(uint32_t),
        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    std::shared_ptr<Vk::DescriptorPool> lightClusterDescriptorPool = std::make_shared<Vk::DescriptorPool>(
        context.getDevice(),
        VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
        2,
        2
    );

    lightClusterDescSet = std::make_shared<Vk::DescriptorSet>(
        context.getDevice(),
        lightClusterDescriptorPool,
        lightClusterDescSetLayout
    );

    lightClusterDescSet->setStorageBuffer(0, *lightClusterBuffer, 0, VK_WHOLE_SIZE);

    tileDepthBuffer = std::make_shared<Vk::Buffer>(
        context.getDevice(),
        screenTiles.x * screenTiles.y * sizeof(float),
        VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
        VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
    );

    tileDepthDescSet = std::make_shared<Vk::DescriptorSet>(
        context.getDevice(),
        lightClusterDescriptorPool,
        GraphicsContext::arrayDescriptorSetLayout
    );

    tileDepthDescSet->setStorageBuffer(0, *tileDepthBuffer, 0, VK_WHOLE_SIZE);

    tileDepthPipeline = Vk::Pipeline::ComputeBuilder{}
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(lightApplyGbufferDescSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .build(GraphicsContext::instance->getDevice(), *Vk::ShaderModule::comp_tileDepth);

    lightClusterResetPipeline = Vk::Pipeline::ComputeBuilder{}
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(lightClusterDescSetLayout)
        .build(GraphicsContext::instance->getDevice(), *Vk::ShaderModule::comp_lightClusterReset);
    
    lightScopesPipeline = Vk::Pipeline::ComputeBuilder{}
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(lightClusterDescSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addPushConstantRange(0, sizeof(uint32_t), VK_SHADER_STAGE_COMPUTE_BIT)
        .build(GraphicsContext::instance->getDevice(), *Vk::ShaderModule::comp_lightScopes);
    
    lightCountPipeline = Vk::Pipeline::ComputeBuilder{}
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(lightClusterDescSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addPushConstantRange(0, sizeof(uint32_t), VK_SHADER_STAGE_COMPUTE_BIT)
        .build(GraphicsContext::instance->getDevice(), *Vk::ShaderModule::comp_lightCount);
    
    lightApplyPipeline = Vk::Pipeline::ComputeBuilder{}
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(lightClusterDescSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(lightApplyGbufferDescSetLayout)
        .addDescriptorSetLayout(lightApplyIrradianceDescSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::voxelPaletteDescriptorSetLayout)
        .build(GraphicsContext::instance->getDevice(), *Vk::ShaderModule::comp_lightApply);

    std::shared_ptr<Vk::DescriptorPool> lightApplyDescriptorPool = std::make_shared<Vk::DescriptorPool>(
        context.getDevice(),
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
        1,
        1
    );

    lightApplyIrradianceDescSet = std::make_shared<Vk::DescriptorSet>(
        context.getDevice(),
        lightApplyDescriptorPool,
        lightApplyIrradianceDescSetLayout
    );

    lightApplyIrradianceDescSet->setStorageImage(0, *lightImageView);

    lightApplyGbufferDescPool = std::make_shared<Vk::DescriptorPool>(
        context.getDevice(),
        VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
        2,
        1
    );

    lightApplyGbufferDescSet = std::make_shared<Vk::DescriptorSet>(
        context.getDevice(),
        lightApplyGbufferDescPool,
        lightApplyGbufferDescSetLayout
    );
    lightApplyGbufferDescSet->setStorageImage(GLSL_GBUFFER_DESCSET_NORMAL_INDEX, *normalImageView);
    lightApplyGbufferDescSet->setStorageImage(GLSL_GBUFFER_DESCSET_POS_SPEC_INDEX, *posSpecularImageView);

    tintDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            GLSL_TINT_TABLE_BINDING_INDEX,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_TINT_HASH_TABLE_BINDING_INDEX,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .build(context.getDevice());

    particlesRenderPass = Vk::RenderPass::Builder()
        .addColorAttachment(
            // Particle sum image.
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_ATTACHMENT_LOAD_OP_CLEAR,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
        )
        .addDepthAttachment(
            // Depth buffer.
            depthFormat,
            VK_ATTACHMENT_LOAD_OP_LOAD,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
        )
        .build(context.getDevice());

    particlesFramebuffer = std::make_shared<Vk::Framebuffer>(
        context.getDevice(),
        particlesRenderPass,
        std::vector<std::shared_ptr<Vk::ImageView>>{
            particlesImageView,
            depthImageView,
        },
        framebufferExtent.width, framebufferExtent.height
    );

    particlesPipeline = Vk::Pipeline::GraphicsBuilder()
        .addShaderStage(VK_SHADER_STAGE_VERTEX_BIT, *Vk::ShaderModule::vert_particles)
        .addShaderStage(VK_SHADER_STAGE_FRAGMENT_BIT, *Vk::ShaderModule::frag_particles)
        .addDynamicState(VK_DYNAMIC_STATE_VIEWPORT)
        .addDynamicState(VK_DYNAMIC_STATE_SCISSOR)
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addAdditiveBlendedAttachment()
        .enableDepthTest()
        .setFrontFace(VK_FRONT_FACE_CLOCKWISE)
        .build(context.getDevice(), particlesRenderPass);

    terrainRenderPass = Vk::RenderPass::Builder()
        .addColorAttachment(
            // Pixel color image.
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .addColorAttachment(
            // Pixel normal image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .addColorAttachment(
            // Pixel position-specular image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .addColorAttachment(
            // Pixel light image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE, 
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .addColorAttachment(
            // Pixel "god rays" image.
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE, 
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
        )
        .addDepthAttachment(
            // Depth buffer.
            depthFormat,
            VK_ATTACHMENT_LOAD_OP_DONT_CARE,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
        )
        .build(context.getDevice());
    
    terrainFramebuffer = std::make_shared<Vk::Framebuffer>(
        context.getDevice(),
        terrainRenderPass,
        std::vector<std::shared_ptr<Vk::ImageView>>{
            colorImageView,
            normalImageView,
            posSpecularImageView,
            lightImageView,
            godRaysImageView,
            depthImageView,
        },
        framebufferExtent.width, framebufferExtent.height
    );

    terrainPipeline = Vk::Pipeline::GraphicsBuilder()
        .addShaderStage(VK_SHADER_STAGE_VERTEX_BIT, *Vk::ShaderModule::vert_terrain)
        .addShaderStage(VK_SHADER_STAGE_FRAGMENT_BIT, *Vk::ShaderModule::frag_terrain)
        .addDynamicState(VK_DYNAMIC_STATE_VIEWPORT)
        .addDynamicState(VK_DYNAMIC_STATE_SCISSOR)
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::arrayDescriptorSetLayout)
        .addDescriptorSetLayout(skyboxDescriptorSetLayout)
        .addDescriptorSetLayout(GraphicsContext::voxelPaletteDescriptorSetLayout)
        .addDescriptorSetLayout(tintDescriptorSetLayout)
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .enableDepthWrite()
        .setCullMode(VK_CULL_MODE_NONE)
        .build(context.getDevice(), terrainRenderPass);

    modelRenderPass = Vk::RenderPass::Builder()
        .addColorAttachment(
            // Pixel color image.
            VK_FORMAT_R8G8B8A8_SRGB,
            VK_ATTACHMENT_LOAD_OP_LOAD,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
        )
        .addColorAttachment(
            // Pixel normal image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_LOAD,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
        )
        .addColorAttachment(
            // Pixel position-specular image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_LOAD,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
        )
        .addColorAttachment(
            // Pixel light image.
            VK_FORMAT_R32G32B32A32_SFLOAT,
            VK_ATTACHMENT_LOAD_OP_LOAD, 
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .addDepthAttachment(
            // Depth buffer.
            depthFormat,
            VK_ATTACHMENT_LOAD_OP_LOAD,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,
            VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL
        )
        .build(context.getDevice());

    modelFramebuffer = std::make_shared<Vk::Framebuffer>(
        context.getDevice(),
        modelRenderPass,
        std::vector<std::shared_ptr<Vk::ImageView>>{
            colorImageView,
            normalImageView,
            posSpecularImageView,
            lightImageView,
            depthImageView,
        },
        framebufferExtent.width, framebufferExtent.height
    );

    modelPipeline = Vk::Pipeline::GraphicsBuilder()
        .addShaderStage(VK_SHADER_STAGE_VERTEX_BIT, *Vk::ShaderModule::vert_model)
        .addShaderStage(VK_SHADER_STAGE_FRAGMENT_BIT, *Vk::ShaderModule::frag_model)
        .addDynamicState(VK_DYNAMIC_STATE_VIEWPORT)
        .addDynamicState(VK_DYNAMIC_STATE_SCISSOR)
        .addDescriptorSetLayout(worldFrameDescriptorSetLayout)
        .addDescriptorSetLayout(modelDescriptorSetLayout)
        .addPushConstantRange(0, sizeof(ModelStage::ModelData), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT)
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .addNonBlendedAttachment()
        .enableDepthTest()
        .enableDepthWrite()
        .setFrontFace(VK_FRONT_FACE_COUNTER_CLOCKWISE)
        .build(context.getDevice(), modelRenderPass);

    combineRenderPass = Vk::RenderPass::Builder()
        .addColorAttachment(
            context.swapchain().getImageFormat(),
            VK_ATTACHMENT_LOAD_OP_DONT_CARE,
            VK_ATTACHMENT_STORE_OP_STORE,
            VK_IMAGE_LAYOUT_UNDEFINED,
            VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
        )
        .build(context.getDevice());
    
    combinePipeline = Vk::Pipeline::GraphicsBuilder()
        .addShaderStage(VK_SHADER_STAGE_VERTEX_BIT, *Vk::ShaderModule::vert_combine)
        .addShaderStage(VK_SHADER_STAGE_FRAGMENT_BIT, *Vk::ShaderModule::frag_combine)
        .addDynamicState(VK_DYNAMIC_STATE_VIEWPORT)
        .addDynamicState(VK_DYNAMIC_STATE_SCISSOR)
        .addDescriptorSetLayout(skyboxDescriptorSetLayout)
        .addDescriptorSetLayout(combineDescriptorSetLayout)
        .addNonBlendedAttachment()
        .setCullMode(VK_CULL_MODE_NONE)
        .build(context.getDevice(), combineRenderPass);
    
    combineDescriptorPool = std::make_shared<Vk::DescriptorPool>(
        context.getDevice(),
        VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
        8,
        1
    );

    combineDescriptorSet = std::make_shared<Vk::DescriptorSet>(
        context.getDevice(),
        combineDescriptorPool,
        combineDescriptorSetLayout
    );
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_COLOR_INDEX, *colorImageView, Vk::Sampler::pixelated());
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_LIGHT_INDEX, *lightImageView, Vk::Sampler::pixelated());
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_PARTICLES_INDEX, *particlesImageView, Vk::Sampler::pixelated());
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_NORMALS_INDEX, *normalImageView, Vk::Sampler::pixelated());
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_POS_SPEC_INDEX, *posSpecularImageView, Vk::Sampler::pixelated());
    combineDescriptorSet->setImageSampler(GLSL_COMBINE_GOD_RAYS_INDEX, *godRaysImageView, Vk::Sampler::bilinear());

    std::shared_ptr<Vk::DescriptorPool> frameDataDescriptorPool = Vk::DescriptorPool::Builder()
        .addDescriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, GraphicsContext::maxFramesInFlight)
        .addDescriptorType(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, GraphicsContext::maxFramesInFlight)
        .build(GraphicsContext::instance->getDevice(), 2 * GraphicsContext::maxFramesInFlight);

    std::shared_ptr<Vk::DescriptorPool> combineDescriptorPool = Vk::DescriptorPool::Builder()
        .addDescriptorType(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, GraphicsContext::maxFramesInFlight)
        .addDescriptorType(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2 * GraphicsContext::maxFramesInFlight)
        .build(GraphicsContext::instance->getDevice(), GraphicsContext::maxFramesInFlight);

    for (int i=0; i<GraphicsContext::maxFramesInFlight; i++)
    {
        frameDrawers[i] = std::make_unique<FrameDrawer>(
            *this,
            frameDataDescriptorPool,
            combineDescriptorPool
        );
    }
}

void WorldDisplay::prepareFrame(GraphicsContext &context)
{
    VkExtent2D swapchainExtent = context.swapchain().getExtent();
    if (swapchainExtent.width != lastKnownExtent.width || swapchainExtent.height != lastKnownExtent.height)
    {
        lastKnownExtent = swapchainExtent;
        if (lastKnownExtent.width * lastKnownExtent.height == 0)
        {
            return;
        }

        lastKnownFramebufferResolution = settings.framebufferResolution;
    
        recreateGraphicsObjects(context);
    }

    if (lastKnownFramebufferResolution != settings.framebufferResolution)
    {
        lastKnownFramebufferResolution = settings.framebufferResolution;
        recreateGraphicsObjects(context);
    }

    if (gadgets.empty())
    {
        gadgets.push_back(
            new Row(
                new Center(
                    new Column(
                        new ItemView(player.get(), 0),
                        new ItemView(player.get(), 1),
                        new ItemView(player.get(), 2),
                        new ItemView(player.get(), 3),
                        new ItemView(player.get(), 4),
                        new ItemView(player.get(), 5),
                        new ItemView(player.get(), 6),
                        new ItemView(player.get(), 7),
                        new ItemView(player.get(), 8),
                        new ItemView(player.get(), 9)
                    )
                )
            )
        );

        for (size_t i = 0; i < Player::numEquipmentSlots; i++)
        {
            gadgets.push_back(
                new Builder<Controller*>(
                    [this, i](){ return player->gadgetChannels[i].currentController.get(); },
                    [i](Controller *ctrl) -> Widget*
                    {
                        if (ctrl == nullptr)
                        {
                            return new Panel();
                        }

                        static_cast<GadgetController*>(ctrl)->bindEquipmentSlot(i);
                        return ctrl->getWidget();
                    }
                )
            );
        }
    }
}

void WorldDisplay::drawOverlays(GraphicsContext &context)
{
    // Switch to GUI mode.
    context.beginGui();

    context.color = glm::vec4(1.0f);

    // Draw the crosshair.
    if (!thirdPerson && player->gadgetsEnabled)
    {
        context.drawMaterial(
            Material::crosshair,
            context.getVirtualWidth() / 2 - 2,
            context.getVirtualHeight() / 2 - 2
        );
    }

    // Draw the player list.
    RemoteWorld *remoteWorld = dynamic_cast<RemoteWorld*>(world.get());
    if (remoteWorld != nullptr && showPlayerList)
    {
        std::stringstream ss;
        ss << "Online players:";

        for (const auto &name : remoteWorld->playerNames)
        {
            ss << std::endl << "●" << name;
        }

        int listWidth = 200;

        int height = context.measureText(Font::generic, ss.str()).y;
        int baseX = (context.getVirtualWidth() - listWidth) / 2;

        context.color = glm::vec4(0.0f, 0.0f, 0.0f, 0.3f);
        context.drawRect(baseX, 2, listWidth, height);

        context.color = glm::vec4(1.0f);
        context.drawText(Font::generic, baseX, 2, ss.str());
    }

    // Draw the debug overlay.
    if (showDebugInfo)
    {
        context.color = glm::vec4(1.0f, 1.0f, 1.0f, 0.3f);

        double updateTime = getAverageUpdateTime();
        double fastUpdateTime = getAverageFastUpdateTime();
        double frameTime = getAverageFrameTime();
        double drawTime = context.drawTracker.getAverage();
        
        int timeOfDay = world->getTimeOfDay() * ((double) (60 * 60 * 24) / (double) World::cycleDuration);
        int currentSecond = timeOfDay % 60;
        timeOfDay /= 60;
        int currentMinute = timeOfDay % 60;
        timeOfDay /= 60;
        int currentHour = (timeOfDay + 6) % 24;

        World::BlockHit blockHit = world->castRay(player->getEyePos(), player->getForwardVector(), player->handRange);

        std::stringstream ss;
        if (envVars.devMode)
        {
            ss << std::fixed << std::setprecision(3);
            ss << "Player position: " << player->pos << std::endl;
            ss << "Player velocity: " << player->vel << std::endl;
            ss << "Selected block: ";
            if (!blockHit.hasHit)
            {
                ss << "NONE";
            }
            else
            {
                ss << glm::ivec3(blockHit.coords) << " " << blockHit.side;
            }
            ss << std::endl;
        }
        ss << "Average update time: " << updateTime << " (" << (int) std::floor(1000.0 / updateTime) << " UPS)" << std::endl;
        ss << "Average fast-update time: " << fastUpdateTime << " (" << (int) std::floor(1000.0 / fastUpdateTime) << " fuPS)" << std::endl;
        ss << "Average frame time: " << frameTime << " (" << (int) std::floor(1000.0 / frameTime) << " CEPS)" << std::endl;
        ss << "Average draw time: " << drawTime << " (" << (int) std::floor(1000.0 / drawTime) << " FPS)" << std::endl;
        frameDrawers[0]->writeStageDebugLines(ss);
        if (envVars.devMode)
        {
            ss << "Camera angle: (" << player->theta << ", " << player->phi << ")" << std::endl;
            ss << "  Temperature: " << world->weather.getTemperature() << std::endl;
            ss << "  Humidity: " << world->weather.getHumidity() << std::endl;
            ss << "  Cloud cover: " << (world->weather.getCloudCover() * 100.0f) << "%" << std::endl;
            ss << "  Rain: " << (world->weather.getRainIntensity() * 100.0f) << "%" << std::endl;
            ss << "Loaded chunks: " << world->getNumLoadedChunks() << std::endl;
            ss << "Purge queue size: " << world->getPurgeQueueSize() << std::endl;
            ss << "Pending tasks: " << getPendingTasks() << std::endl;
            ss << "SVO nodes in use: " << world->window->svo.getNodesInUse() << std::endl;
            ss << "Active lights: " << world->window->getActiveLights() << std::endl;
        }
        ss << "Total VRAM usage: " << formatDataSize(context.getTotalVramUsage()) << std::endl;
        world->writeDebugStats(ss);
    
        if (envVars.devMode)
        {
            ss << "Time of day: " <<
                std::setfill('0') << std::setw(2) << currentHour << ":" <<
                std::setfill('0') << std::setw(2) << currentMinute << ":" <<
                std::setfill('0') << std::setw(2) << currentSecond << std::endl;
        }

        context.drawText(Font::generic, 50, 3, ss.str());
        
        ss.str("");
        ss.clear();
        
        if (envVars.devMode && blockHit.hasHit)
        {
            Block *lookingAt = world->getBlockAt(blockHit.coords);
            uint8_t meta = *world->getBlockMeta(blockHit.coords);

            ss << lookingAt->displayName << ":" << std::endl;

            for (const auto &blockDetail : lookingAt->getDetails())
            {
                ss << blockDetail.name << ": " << blockDetail.value << std::endl;
            }

            ss << "Meta: 0x" << std::hex << std::setw(2) << (int) meta << std::endl << std::endl;

            world->writeBlockDebugInfo(ss, blockHit.coords);
            
            int maxWidth = context.measureText(Font::generic, ss.str()).x;
            context.drawText(Font::generic, context.getVirtualWidth() - maxWidth - 1, 15, ss.str());
        }
    }

    // Draw the GPU debug mode name.
    if (gpuDebugMode != GLSL_DEBUGMODE_OFF)
    {
        context.color = glm::vec4(0.0f, 0.0f, 0.0f, gpuDebugModeOpacity);
        context.drawRect(
            context.getVirtualWidth() - gpuDebugModeLabelOffsetRight - gpuDebugModeLabelWidth,
            gpuDebugModeLabelOffsetTop,
            gpuDebugModeLabelWidth,
            Font::generic.glyphHeight + 2
        );
        context.color = glm::vec4(1.0f, 1.0f, 1.0f, 1.0f);
        context.drawText(
            Font::generic,
            context.getVirtualWidth() - gpuDebugModeLabelOffsetRight - gpuDebugModeLabelWidth + 1,
            gpuDebugModeLabelOffsetTop + 1,
            gpuDebugModeNames[gpuDebugMode]
        );
    }

    // Draw the chat log.
    int chatEntryHeight = Font::generic.glyphHeight + 1;
    int chatPlotY = context.getVirtualHeight() - chatEntryHeight * Player::maxChatLogLines - 52;
    int paddingLines = Player::maxChatLogLines - player->chatLog.size();
    chatPlotY += chatEntryHeight * paddingLines;

    if (!isWidgetSet())
    {
        // Draw fading chats when no GUI is open.
        for (Player::ChatEntry &entry : player->chatLog)
        {
            float alpha = 1.0f;

            Uint32 ticksPassed = SDL_GetTicks() - entry.receiveTime;
            if (ticksPassed >= chatFadeAfter)
            {
                float wayThrough = (float) (ticksPassed - chatFadeAfter) / (float) chatFadeDuration;
                alpha = std::max(0.0f, std::min(1.0f, 1.0f - wayThrough));
            }

            context.color = glm::vec4(0.0f, 0.0f, 0.0f, alpha * 0.2f);
            context.drawRect(20, chatPlotY, 5 * 35, chatEntryHeight);

            context.color = glm::vec4(entry.color, alpha);
            context.drawText(Font::generic, 20, chatPlotY, entry.text);
            chatPlotY += chatEntryHeight;
        }
    }

    context.color = Color::faded;
    context.drawText(Font::generic, 19, context.getVirtualHeight() - 36, world->getTypingIndicatorString());

    // Draw the gadgets.
    if (player->gadgetsEnabled)
    {
        for (Widget *gadget : gadgets)
        {
            gadget->onResize(glm::ivec2(context.getVirtualWidth(), context.getVirtualHeight()));
            gadget->draw(context);
        }
    }

    // If a controller is open on the player, display its widget.
    if (player->guiChannel.currentController != nullptr && !isWidgetSet())
    {
        setOverlayWidget(new GuiControllerView(
            player,
            player->guiChannel.currentController->getWidget(),
            player->guiChannel.getStamp()
        ));
    }

    // Draw the widget if any
    if (isWidgetSet())
    {
        context.color = glm::vec4(0.0f, 0.0f, 0.0f, 0.5f);
        context.drawRect(0, 0, context.getVirtualWidth(), context.getVirtualHeight());

        // Draw the whole chat log when a GUI is open.
        for (Player::ChatEntry &entry : player->chatLog)
        {
            context.color = glm::vec4(entry.color, 1.0f);
            context.drawText(Font::generic, 20, chatPlotY, entry.text);
            chatPlotY += chatEntryHeight;
        }

        showMouse = player->movingStack.isEmpty();
        drawPartial(context);

        glm::ivec2 mousePhys = context.getPhysicalMousePos();

        int virtualMouseX = mousePhys.x/GraphicsContext::pixelSize;
        int virtualMouseY = mousePhys.y/GraphicsContext::pixelSize;

        player->movingStack.draw(context, virtualMouseX, virtualMouseY);
    }

    // Draw the frame history.
    if (showFrameHistory)
    {
        context.color = glm::vec4(0.0f, 0.0f, 0.0f, 0.5f);
        context.drawRect(0, 0, context.getVirtualWidth(), context.getVirtualHeight());
        drawPerfHistoryGraph(context, PerfHistory::frame, 0, 0, context.getVirtualWidth(), context.getVirtualHeight(), 0.5f);
    }

    // End the GUI stage.
    context.endGui();
}

void WorldDisplay::setOverlayWidget(Widget *widget)
{
    if (!isWidgetSet() && widget != nullptr)
    {
        GraphicsContext::instance->centerMouse();
    }

    setWidget(widget);
}

void WorldDisplay::openChatOverlay()
{
    auto onSendChat = [this]()
    {
        if (chatToSend.empty())
        {
            return;
        }

        player->sendChat(chatToSend);
        shouldCloseMenu = true;

        if (player->chatHistory.size() != 0 && player->chatHistory.back() == chatToSend)
        {
            return;
        }

        if (player->chatHistory.size() == chatMax)
        {
            player->chatHistory.erase(player->chatHistory.begin());
        }
        
        player->chatHistory.push_back(chatToSend);
    };

    setOverlayWidget(
        new Column(
            Layout::WithProportion<1>(),
            new Panel(),

            new Row(
                new Panel(glm::ivec2(18, 0)),

                Layout::WithProportion<1>(),
                (Widget::focused = new ChatTextField(
                    chatToSend,
                    *player.get(),
                    world->lastTypingTicks,
                    onSendChat
                )),

                new Panel(glm::ivec2(60, 0))
            ),

            new Row(
                new Panel(glm::ivec2(18, 0)),
                new Builder<uint64_t>(
                    world->typingPlayerUpdateCount,
                    [this](uint64_t)
                    {
                        return new Label(world->getTypingIndicatorString(), Color::faded);
                    }
                )
            ),

            new Panel(glm::ivec2(0, 29))
        )
    );
}

WorldDisplay::WorldDisplay(std::shared_ptr<World> world, std::shared_ptr<Player> player) :
    world(world),
    player(player),
    inputManager(*this),
    gpuDebugMode(GLSL_DEBUGMODE_OFF)
{
    worldFrameDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            0,
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            1,
            VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT
        )
        .build(GraphicsContext::instance->getDevice());
    
    lightApplyGbufferDescSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            GLSL_GBUFFER_DESCSET_NORMAL_INDEX,
            VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT
        )
        .addBinding(
            GLSL_GBUFFER_DESCSET_POS_SPEC_INDEX,
            VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT | VK_SHADER_STAGE_COMPUTE_BIT
        )
        .build(GraphicsContext::instance->getDevice());
    
    skyboxDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            GLSL_SKYBOX_DATA_INDEX,
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
            1,
            VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_SKYBOX_STAR_NOISE_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_SKYBOX_SUNMAP_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .build(GraphicsContext::instance->getDevice());

    combineDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            GLSL_COMBINE_COLOR_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_COMBINE_LIGHT_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_COMBINE_PARTICLES_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_COMBINE_NORMALS_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_COMBINE_POS_SPEC_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .addBinding(
            GLSL_COMBINE_GOD_RAYS_INDEX,
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            1,
            VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .build(GraphicsContext::instance->getDevice());

    lightClusterDescSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            0,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            1,
            VK_SHADER_STAGE_COMPUTE_BIT
        )
        .build(GraphicsContext::instance->getDevice());

    lightApplyIrradianceDescSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            0,
            VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            1,
            VK_SHADER_STAGE_COMPUTE_BIT
        )
        .build(GraphicsContext::instance->getDevice());

    lastKnownFramebufferResolution = settings.framebufferResolution;
    lastKnownExtent = GraphicsContext::instance->swapchain().getExtent();
    recreateGraphicsObjects(*GraphicsContext::instance);

    world->window = std::make_unique<ChunkWindow>(*world, player, *this);
    world->onWindowInitDone();

    rainSource.setGain(0.0f);
    rainSource.setSound(Sound::rain);
}

WorldDisplay::~WorldDisplay()
{
    GraphicsContext::instance->setRelativeMouseMode(false);
    GraphicsContext::instance->getDevice()->waitIdle();
    
    for (Widget *gadget : gadgets)
    {
        delete gadget;
    }
}

std::shared_ptr<Player> WorldDisplay::getPlayer()
{
    return player;
}

bool WorldDisplay::inThirdPersonView()
{
    return thirdPerson;
}

std::shared_ptr<World> WorldDisplay::getWorld()
{
    return world;
}

void WorldDisplay::drawMeshedIcon(GraphicsContext &context, Material &mat, glm::vec3 pos)
{
    if (materialModels.count(&mat) != 1)
    {
        VoxelSprite sprite(glm::ivec3(mat.width(), 1, mat.height()));
        unsigned char *pixels = mat.getPixels();

        int x, z;
        for (x=0; x<mat.width(); x++)
        {
            for (z=0; z<mat.height(); z++)
            {
                VoxelSprite::Color &color = sprite.at(mat.width() - 1 - x, 0, mat.height() - 1 - z);
                std::memcpy(color.channels, &pixels[4 * (z * mat.width() + x)], 4);
            }
        }

        VoxelSpriteRenderer renderer(sprite);
        materialModels[&mat] = renderer.makeModel();
    }

    glm::mat4 modelMatrix = glm::translate(glm::mat4(1.0f), pos) *
        glm::rotate(glm::mat4(1.0f), (float) SDL_GetTicks() / 300.0f, glm::vec3(0.0f, 0.0f, 1.0f)) *
        glm::scale(glm::mat4(1.0f), glm::vec3(0.5f, 0.5f, 0.5f)) *
        glm::translate(glm::mat4(1.0f), glm::vec3(-0.5f, -0.5f/8.0f, -0.5f));

    getFrameDrawer().setModelMatrix(modelMatrix);
    materialModels[&mat]->draw(context, *this);
}

void WorldDisplay::drawMeshedIconFlat(GraphicsContext &context, Material &mat, glm::mat4 matrix)
{
    if (materialModels.count(&mat) != 1)
    {
        VoxelSprite sprite(glm::ivec3(mat.width(), 1, mat.height()));
        unsigned char *pixels = mat.getPixels();

        int x, z;
        for (x=0; x<mat.width(); x++)
        {
            for (z=0; z<mat.height(); z++)
            {
                VoxelSprite::Color &color = sprite.at(mat.width() - 1 - x, 0, mat.height() - 1 - z);
                std::memcpy(color.channels, &pixels[4 * (z * mat.width() + x)], 4);
            }
        }

        VoxelSpriteRenderer renderer(sprite);
        materialModels[&mat] = renderer.makeModel();
    }

    glm::mat4 modelMatrix = matrix *
        glm::translate(glm::mat4(1.0f), glm::vec3(-0.25f, 0.25f, 0.0f)) *
        glm::scale(glm::mat4(1.0f), glm::vec3(0.5f, 0.5f, 0.5f)) *
        glm::mat4(
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            0.0f, -1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f
        );

    getFrameDrawer().setModelMatrix(modelMatrix);
    materialModels[&mat]->draw(context, *this);
}

void WorldDisplay::drawModelInBox(Model &model, const BoundingBox<wfixed> &bounds)
{
    glm::vec3 relPos = wfvec3(bounds.x0, bounds.y0, bounds.z0) - player->getEyePos() - wfvec3(wfixed(0.0001));
    glm::vec3 size(bounds.x1-bounds.x0+0.0002, bounds.y1-bounds.y0+0.0002, bounds.z1-bounds.z0+0.0002);

    glm::mat4 matrix(
        size.x, 0.0f, 0.0f, 0.0f,
        0.0f, size.y, 0.0f, 0.0f,
        0.0f, 0.0f, size.z, 0.0f,
        relPos.x, relPos.y, relPos.z, 1.0f
    );

    getFrameDrawer().setModelMatrix(matrix);
    model.draw(*GraphicsContext::instance, *this);
}

FrameDrawer& WorldDisplay::getFrameDrawer()
{
    return *frameDrawers[GraphicsContext::instance->getThisFrameIndex()];
}

void WorldDisplay::requestGuiClose()
{
    shouldCloseMenu = true;
}

void WorldDisplay::draw(GraphicsContext &context)
{
    if (player == nullptr)
    {
        return;
    }

    PERF_EVENT(frame, drawFrame,
    {
        prepareFrame(context);
        if (framebufferExtent.width * framebufferExtent.height == 0)
        {
            return;
        }

        frameDrawers[context.getThisFrameIndex()]->draw(context);
        drawOverlays(context);
    })
}

void WorldDisplay::update()
{
    world->update();
    rainSource.setGain(world->weather.getRainIntensity());
    WidgetDisplay::update();

    if (SDL_GetTicks() - windLastUpdateTicks > 10000)
    {
        glm::vec2 randomWindSpeed = glm::vec2(
            randomSignedUnit() * glm::radians(0.2f),
            randomSignedUnit() * glm::radians(0.2f)
        );

        targetWindSpeed = glm::lerp(targetWindSpeed, randomWindSpeed, 0.05f);
    }

    cloudTransform *= glm::yawPitchRoll(windSpeed.x, windSpeed.y, 0.0f);
    windSpeed = glm::lerp(windSpeed, targetWindSpeed, 0.005f);
}

void WorldDisplay::fastUpdate()
{
    GraphicsContext::instance->setRelativeMouseMode(!isWidgetSet());

    std::string fatalError = world->getFatalError();
    if (fatalError.size() != 0)
    {
        world->window->purge();
        world->close();

        GraphicsContext::instance->setRelativeMouseMode(false);

        Display::setCurrent<MessageDisplay>(Material::alert, "Error", fatalError);
        Display::navPush<ShutdownDisplay>(world);

        return;
    }

    world->fastUpdate(world->window.get());
    world->window->update(*this);

    if (shouldCloseMenu)
    {
        shouldCloseMenu = false;
        setOverlayWidget(nullptr);
    }

    if (isWidgetSet())
    {
        player->controlFlags = Player::ControlFlags();
    }
    else
    {
        world->lastTypingTicks = 0;
    }

    AudioManager::updateListener(
        player->getEyePos(),
        player->vel,
        player->getForwardVector(),
        player->getUpVector()
    );
}

void WorldDisplay::onEvent(SDL_Event &event)
{
    inputManager.onEvent(event);

    if (event.type == SDL_EVENT_MOUSE_MOTION && !isWidgetSet())
    {
        int dx = event.motion.xrel;
        int dy = event.motion.yrel;

        if (dx != 0 || dy != 0)
        {
            player->theta += (float) dx * 0.0001 * (float) settings.cameraSensitivity;
            player->phi += (float) dy * 0.0001 * (float) settings.cameraSensitivity;

            player->phi = std::max(std::min(player->phi, 3.1415f * 0.5f), -3.1415f * 0.5f);
        }
    }
    else if (event.type == SDL_EVENT_MOUSE_WHEEL && !isWidgetSet())
    {
        int newSlot = player->getSelectedSlot() - std::max(-1, std::min(1, (int) event.wheel.y));
        player->onSetHUD(1 + std::max(0, std::min(9, newSlot)));
    }
    else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && !isWidgetSet())
    {
        if (event.button.button == SDL_BUTTON_LEFT)
        {
            player->controlFlags += Player::ControlFlag::Dig;
        }
        else if (event.button.button == SDL_BUTTON_RIGHT)
        {
            player->onRightClick();
        }
    }
    else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && !isWidgetSet())
    {
        if (event.button.button == SDL_BUTTON_LEFT)
        {
            player->controlFlags -= Player::ControlFlag::Dig;
        }
    }
    else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_ESCAPE)
    {
        if (isWidgetSet())
        {
            if (!player->isGuiBusy())
            {
                setOverlayWidget(nullptr);
            }
        }
        else
        {
            setOverlayWidget(new EscapeMenu(*this));
        }
    }
    else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key >= SDLK_0 && event.key.key <= SDLK_9 && !isWidgetSet())
    {
        player->onSetHUD(event.key.key - SDLK_0);
    }
#ifndef NO_PERF_TIMERS
    else if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F7)
    {
        showFrameHistory = !showFrameHistory;
    }
#endif
    else if (event.type == SDL_EVENT_KEY_UP && event.key.key == SDLK_SLASH && !isWidgetSet())
    {
        chatToSend = "/";
        openChatOverlay();
    }
    else
    {
        WidgetDisplay::onEvent(event);
    }
}

void WorldDisplay::onQuit()
{
    Log::info("Closing world...");
    world->shuttingDown = true;
    world->window->purge();
    world->close();
    Display::setCurrent<ShutdownDisplay>(world);
}

void WorldDisplay::initStaticObjects()
{
    worldModelDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            0,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            1,
            VK_SHADER_STAGE_VERTEX_BIT
        )
        .build(GraphicsContext::instance->getDevice());

    modelDescriptorSetLayout = Vk::DescriptorSetLayout::Builder{}
        .addBinding(
            0,
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            1,
            VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT
        )
        .build(GraphicsContext::instance->getDevice());
}