Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / client / src / render / mod.rs

//! Vulkan renderer: sky, terrain and animated textures.

pub mod animated;
mod capture;
pub mod item;
pub mod mesher;
pub mod scene;
mod sky;
mod staging;
mod terrain;

use std::ops::Range;
use std::ptr::null;
use std::sync::Arc;
use std::time::{Duration, Instant};

use crate::gpu::vk::*;
use crate::gpu::vk::{
    VkAccessFlags2 as Access, VkImageLayout as Layout, VkPipelineStageFlags2 as Stage,
};
use crate::gpu::{
    Binding, Blend, Buffer, ComputePipeline, Descriptors, Device, GraphicsPipeline, Image,
    Instance, MemoryLocation, PipelineDesc, PresentMode, Presentation, Result, Surface, Swapchain,
    Validation, check, subresource_range, zeroed,
};
use bytemuck::{Pod, Zeroable};
use common::world::ChunkPos;
use glam::{DVec3, Mat4, Vec3, Vec4};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle};

use self::animated::TileUpdate;
use self::sky::{SkyGeometry, SkyParams, SkyPass, SkyVertex};
use self::staging::Staging;
use self::terrain::{CullMode, Terrain, TerrainCopies};
use crate::assets::{Assets, RgbaImage};
use crate::world::ClientWorld;

const FRAMES_IN_FLIGHT: usize = 2;
const STAGING_SIZE: u64 = 64 << 20;
/// Initial capacity of the per-frame translucent section lists, which grow to the frame's.
const TRANSLUCENT_SECTIONS: usize = 1 << 12;
const SKY_VERTICES: usize = 1 << 14;
const GUI_VERTICES: usize = 1 << 16;
const SCENE_VERTICES: usize = 1 << 18;
const DEPTH_FORMAT: VkFormat = VkFormat::D32Sfloat;

const TERRAIN_SPIRV: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/terrain.spv"));
const SKY_SPIRV: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/sky.spv"));
const GUI_SPIRV: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/gui.spv"));
const SCENE_SPIRV: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/scene.spv"));

const COLOR: VkImageAspectFlags = VkImageAspectFlags::COLOR;

type Scope = (Stage, Access);
const COPY_WRITE: Scope = (Stage::COPY, Access::TRANSFER_WRITE);
const COPY_READ: Scope = (Stage::COPY, Access::TRANSFER_READ);
const COLOR_WRITE: Scope = (
    Stage::COLOR_ATTACHMENT_OUTPUT,
    Access::COLOR_ATTACHMENT_WRITE,
);
const NO_ACCESS: Scope = (Stage::NONE, Access::NONE);
const TRANSFER_WRITE: Scope = (Stage::ALL_TRANSFER, Access::TRANSFER_WRITE);
/// Color and depth attachment access, ordering one rendering's stores before the next's loads.
const ATTACHMENTS: Scope = (
    Stage::COLOR_ATTACHMENT_OUTPUT
        .union(Stage::EARLY_FRAGMENT_TESTS)
        .union(Stage::LATE_FRAGMENT_TESTS),
    Access::COLOR_ATTACHMENT_READ
        .union(Access::COLOR_ATTACHMENT_WRITE)
        .union(Access::DEPTH_STENCIL_ATTACHMENT_READ)
        .union(Access::DEPTH_STENCIL_ATTACHMENT_WRITE),
);
const COMPUTE_WRITE: Scope = (Stage::COMPUTE_SHADER, Access::SHADER_WRITE);
/// Everything that reads or writes the terrain tables and section meshes, and the frame's
/// sampled textures.
const CULLING: Scope = (
    Stage::DRAW_INDIRECT
        .union(Stage::VERTEX_SHADER)
        .union(Stage::FRAGMENT_SHADER)
        .union(Stage::COMPUTE_SHADER)
        .union(Stage::ALL_TRANSFER),
    Access::INDIRECT_COMMAND_READ
        .union(Access::SHADER_READ)
        .union(Access::SHADER_WRITE)
        .union(Access::TRANSFER_READ),
);

const ATLAS_SLOT: u32 = 0;
const SUN_SLOT: u32 = 1;
const MOON_SLOT: u32 = 2;

/// Everything about the viewer and environment needed to draw one frame.
pub struct FrameParams<'a> {
    pub eye: DVec3,
    /// Camera rotation (and the original's small back offset); world positions are camera-relative.
    pub view: Mat4,
    pub fov_y_degrees: f32,
    /// `farPlaneDistance`: fog ends here.
    pub view_distance: f32,
    pub fog_color: Vec3,
    /// Exponential fog of this density under water or lava, instead of distance fog.
    pub fog_density: Option<f32>,
    pub sky_color: Vec3,
    /// `getRainStrength`, which fades out the sun, moon and stars.
    pub rain: f32,
    pub celestial_angle: f32,
    pub sunrise: Option<[f32; 4]>,
    pub sky_darkness: u32,
    /// Without a sky (the Nether), only fog fills the background and starts at the eye.
    pub has_sky: bool,
    pub brightness: [f32; 16],
    pub tile_updates: &'a [TileUpdate],
    pub gui: &'a crate::gui::Geometry,
    pub scene: &'a scene::Geometry,
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct TerrainPush {
    view_projection: [f32; 16],
    fog_color: [f32; 4],
    /// The camera relative to the eye, which fog distances are measured from.
    camera: [f32; 4],
    eye_fraction: [f32; 4],
    eye_block: [i32; 4],
    sections: u64,
    fog_start: f32,
    fog_end: f32,
    sky_darkness: u32,
    texture: u32,
    /// Scales the output alpha. A depth-only pass draws with 0 through the pipeline of its color
    /// pass: blending then keeps the color, and both passes compute bit-identical depths.
    opacity: f32,
    _padding: u32,
    brightness: [f32; 16],
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct BoxPush {
    view_projection: [f32; 16],
    eye_fraction: [f32; 4],
    eye_block: [i32; 4],
    sections: u64,
    tested: u64,
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SkyPush {
    view_projection: [f32; 16],
    vertices: u64,
    _padding: u64,
    fog_color: [f32; 4],
    /// The camera relative to the eye, which fog distances are measured from.
    camera: [f32; 4],
    fog_start: f32,
    fog_end: f32,
    fog: u32,
    texture: i32,
}

#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct ScenePush {
    view_projection: [f32; 16],
    vertices: u64,
    fog: u32,
    /// Fragments with less alpha are discarded (`glAlphaFunc(GL_GREATER, ...)`).
    alpha_cutoff: f32,
    fog_color: [f32; 4],
    /// The camera relative to the eye, which fog distances are measured from.
    camera: [f32; 4],
    fog_start: f32,
    fog_end: f32,
    /// See [`TerrainPush::opacity`].
    opacity: f32,
    _padding: u32,
}

struct FrameResources {
    pool: VkCommandPool,
    command_buffer: VkCommandBuffer,
    fence: VkFence,
    image_available: VkSemaphore,
    staging: Staging,
    /// Slots of the translucent sections to draw, back to front.
    translucent: Buffer,
    /// The sections the frame drew, read back once it finished.
    drawn: Buffer,
    sky: Buffer,
    gui: Buffer,
    scene: Buffer,
    capture: Option<capture::Capture>,
    map_image: Image,
    map_colors: Option<Box<[u8; 16384]>>,
}

/// Defines `Pipelines` from rows of shader, blend and changes to the default state: depth
/// tested, no depth writes, no culling and triangle lists.
macro_rules! pipelines {
    ($($name:ident: $spirv:ident, $blend:ident { $($field:ident: $value:expr),* };)*) => {
        struct Pipelines {
            $($name: GraphicsPipeline,)*
            cull: ComputePipeline,
        }

        impl Pipelines {
            fn new(device: &Arc<Device>, color_format: VkFormat, binding: Binding) -> Result<Self> {
                Ok(Self {
                    cull: ComputePipeline::new(device, TERRAIN_SPIRV, c"cullMain", binding)?,
                    $($name: GraphicsPipeline::new(device, &PipelineDesc {
                        $($field: $value,)*
                        ..PipelineDesc {
                            spirv: $spirv,
                            vertex_entry: c"vertexMain",
                            fragment_entry: c"fragmentMain",
                            color_format,
                            depth_format: Some(DEPTH_FORMAT),
                            depth_test: true,
                            topology: VkPrimitiveTopology::TriangleList,
                            depth_write: false,
                            cull: false,
                            blend: Blend::$blend,
                            color_write: true,
                            binding,
                        }
                    })?,)*
                })
            }
        }
    };
}

pipelines! {
    terrain_solid: TERRAIN_SPIRV, Opaque {
        depth_write: true, cull: true, fragment_entry: c"solidFragment"
    };
    terrain_cutout: TERRAIN_SPIRV, Opaque { depth_write: true, cull: true };
    terrain_translucent: TERRAIN_SPIRV, Alpha { depth_write: true };
    section_boxes: TERRAIN_SPIRV, Opaque {
        vertex_entry: c"boxVertex", fragment_entry: c"boxFragment", color_write: false
    };
    sky_plane: SKY_SPIRV, Opaque {};
    sky_alpha: SKY_SPIRV, Alpha {};
    sky_additive: SKY_SPIRV, Additive {};
    gui: GUI_SPIRV, Alpha {};
    gui_invert: GUI_SPIRV, Invert {};
    scene: SCENE_SPIRV, Alpha { depth_write: true };
    scene_additive: SCENE_SPIRV, Additive {};
    outline: SCENE_SPIRV, Alpha {};
    lines: SCENE_SPIRV, Opaque { depth_write: true, topology: VkPrimitiveTopology::LineList };
    nameplate: SCENE_SPIRV, Alpha { depth_test: false };
    cracks: SCENE_SPIRV, Multiply {};
}

pub struct Renderer {
    pipelines: Pipelines,
    terrain: Terrain,
    sky: SkyGeometry,
    translucent: Vec<u32>,
    /// Sections the GPU drew in the last finished frame.
    drawn: u32,
    fancy: bool,
    /// `GameSettings.ambientOcclusion`, on by default.
    smooth_lighting: bool,
    frames: Vec<FrameResources>,
    frame: u64,
    resize: Option<VkExtent2D>,
    capture_requested: bool,
    captures: Vec<RgbaImage>,
    descriptors: Descriptors,
    /// The atlas, sun, moon and GUI textures, at their descriptor slots.
    textures: Vec<Image>,
    /// Whether `gui/items.png` has the original layout that compass and clock tiles target.
    animated_items: bool,
    depth: Image,
    swapchain: Swapchain,
    device: Arc<Device>,
}

impl Renderer {
    /// # Safety
    /// The window and display handles must outlive the renderer.
    pub unsafe fn new(
        display: RawDisplayHandle,
        window: RawWindowHandle,
        extent: VkExtent2D,
        assets: &Assets,
        validation: Option<Validation>,
        descriptor_heaps: bool,
        presentation: Presentation,
    ) -> Result<Self> {
        let instance = Instance::new(display, validation)?;
        let surface = unsafe { Surface::new(&instance, display, window)? };
        let device = Device::new(&instance, &surface)?;
        let swapchain = Swapchain::new(&device, surface, extent, presentation)?;
        let depth = create_depth(&device, swapchain.extent())?;

        let textures = load_textures(&device, assets)?;
        mesher::set_atlas(&assets.terrain, &animated::TRANSPARENT_TILES);
        let upload =
            |size: usize, usage| Buffer::new(&device, size as u64, usage, MemoryLocation::CpuToGpu);
        let vertices = |size: usize| upload(size, VkBufferUsageFlags2::SHADER_DEVICE_ADDRESS);
        let frames = (0..FRAMES_IN_FLIGHT)
            .map(|_| {
                let pool = device.create_command_pool(true)?;
                Ok(FrameResources {
                    capture: None,
                    map_image: upload_texture(&device, &blank_map())?,
                    map_colors: None,
                    pool,
                    command_buffer: device.allocate_command_buffer(pool)?,
                    fence: device.create_fence(true)?,
                    image_available: device.create_semaphore()?,
                    staging: Staging::new(&device, STAGING_SIZE)?,
                    translucent: vertices(TRANSLUCENT_SECTIONS * size_of::<u32>())?,
                    drawn: {
                        let size = size_of::<u32>() as u64;
                        let usage = VkBufferUsageFlags2::TRANSFER_DST;
                        let mut drawn =
                            Buffer::new(&device, size, usage, MemoryLocation::GpuToCpu)?;
                        drawn.write(0, &[0; 4])?;
                        drawn
                    },
                    sky: vertices(SKY_VERTICES * size_of::<SkyVertex>())?,
                    gui: vertices(GUI_VERTICES * size_of::<crate::gui::Vertex>())?,
                    scene: vertices(SCENE_VERTICES * size_of::<scene::Vertex>())?,
                })
            })
            .collect::<Result<Vec<_>>>()?;
        let heaps = descriptor_heaps && device.descriptor_heap;
        let descriptors = describe(&device, heaps, &textures, &frames)?;
        let pipelines = Pipelines::new(&device, swapchain.format(), descriptors.binding())?;

        Ok(Self {
            pipelines,
            terrain: Terrain::new(&device)?,
            sky: SkyGeometry::default(),
            translucent: Vec::new(),
            drawn: 0,
            fancy: true,
            smooth_lighting: true,
            frames,
            frame: 0,
            resize: None,
            capture_requested: false,
            captures: Vec::new(),
            descriptors,
            textures,
            animated_items: assets.gui[2].width == 256,
            depth,
            swapchain,
            device,
        })
    }

    pub fn device_name(&self) -> &str {
        &self.device.name
    }

    pub fn supports_descriptor_heaps(&self) -> bool {
        self.device.descriptor_heap
    }

    /// Whether shaders reach their textures through descriptor heaps rather than a descriptor set.
    pub fn descriptor_heaps(&self) -> bool {
        matches!(self.descriptors, Descriptors::Heaps(_))
    }

    pub fn present_modes(&self) -> &[PresentMode] {
        self.swapchain.present_modes()
    }

    pub fn supports_frame_pacing(&self) -> bool {
        self.swapchain.supports_pacing()
    }

    pub fn presentation(&self) -> Presentation {
        self.swapchain.presentation()
    }

    /// When pacing, plans the frame about to be rendered and returns when it will be displayed.
    pub fn begin_frame(&mut self, min_interval: Duration) -> Option<Instant> {
        self.swapchain.begin_frame(min_interval)
    }

    /// When pacing, when to start rendering the next frame.
    pub fn next_frame(&self) -> Option<Instant> {
        self.swapchain.next_frame()
    }

    pub fn request_screenshot(&mut self) {
        self.capture_requested = true;
    }

    pub fn finish_screenshots(&mut self) -> Result<()> {
        self.device.wait_idle()?;
        for frame in &mut self.frames {
            if let Some(capture) = frame.capture.take() {
                self.captures.push(capture.finish()?);
            }
        }
        Ok(())
    }

    pub fn take_screenshots(&mut self) -> Vec<RgbaImage> {
        std::mem::take(&mut self.captures)
    }

    pub fn resize(&mut self, extent: VkExtent2D) {
        self.resize = Some(extent);
    }

    /// `getDebugInfoRenders`' counts: sections drawn last frame and with geometry, and meshes
    /// uploaded since the last call.
    pub fn debug_counts(&mut self) -> (usize, usize, u32) {
        (
            self.drawn as usize,
            self.terrain.section_count(),
            self.terrain.take_updates(),
        )
    }

    pub fn column_ready(&self, pos: ChunkPos) -> bool {
        self.terrain.column_ready(pos)
    }

    pub fn clear_world(&mut self) {
        self.terrain.clear(self.frame);
    }

    pub fn set_options(
        &mut self,
        fancy: bool,
        smooth_lighting: bool,
        descriptor_heaps: bool,
        presentation: Presentation,
    ) -> Result<()> {
        if self.swapchain.configure(presentation) {
            self.resize.get_or_insert(self.swapchain.extent());
        }
        if self.fancy != fancy || self.smooth_lighting != smooth_lighting {
            self.fancy = fancy;
            self.smooth_lighting = smooth_lighting;
            self.terrain.rebuild();
        }
        let heaps = descriptor_heaps && self.device.descriptor_heap;
        if heaps != self.descriptor_heaps() {
            let descriptors = describe(&self.device, heaps, &self.textures, &self.frames)?;
            self.replace_descriptors(descriptors)?;
        }
        Ok(())
    }

    pub fn reload_assets(&mut self, assets: &Assets) -> Result<()> {
        let textures = load_textures(&self.device, assets)?;
        mesher::set_atlas(&assets.terrain, &animated::TRANSPARENT_TILES);
        let heaps = self.descriptor_heaps();
        let descriptors = describe(&self.device, heaps, &textures, &self.frames)?;
        self.replace_descriptors(descriptors)?;
        self.textures = textures;
        self.animated_items = assets.gui[2].width == 256;
        self.terrain.rebuild();
        Ok(())
    }

    /// Waits for the GPU, then switches to `descriptors` and to pipelines for their binding.
    fn replace_descriptors(&mut self, descriptors: Descriptors) -> Result<()> {
        let binding = descriptors.binding();
        let pipelines = (binding != self.descriptors.binding())
            .then(|| Pipelines::new(&self.device, self.swapchain.format(), binding))
            .transpose()?;
        self.device.wait_idle()?;
        if let Some(pipelines) = pipelines {
            self.pipelines = pipelines;
        }
        self.descriptors = descriptors;
        Ok(())
    }

    /// Remeshes a filled column and its filled neighbors; columns that arrive later do the same.
    pub fn column_loaded(&mut self, pos: ChunkPos, world: &ClientWorld) {
        for dx in -1..=1 {
            for dz in -1..=1 {
                let column = pos.offset(dx, dz);
                if world.contains(column) {
                    self.terrain.mark_column_dirty(column);
                }
            }
        }
    }

    pub fn section_changed(&mut self, pos: ChunkPos, y: i32) {
        self.terrain.mark_section_dirty(pos, y);
    }

    pub fn column_unloaded(&mut self, pos: ChunkPos) {
        self.terrain.remove_column(pos, self.frame);
    }

    pub fn render(&mut self, world: Option<&ClientWorld>, params: &FrameParams) -> Result<()> {
        if let Some(extent) = self.resize.take() {
            if extent.width == 0 || extent.height == 0 {
                self.resize = Some(extent);
                return Ok(());
            }
            self.swapchain.recreate(extent)?;
            self.depth = create_depth(&self.device, self.swapchain.extent())?;
        }

        let slot = (self.frame % FRAMES_IN_FLIGHT as u64) as usize;
        let commands = &self.device.commands;
        let device = self.device.handle;
        let waiting = Instant::now();
        {
            let frame = &mut self.frames[slot];
            check(unsafe { commands.vkWaitForFences(device, 1, &frame.fence, VK_TRUE, u64::MAX) })?;
            check(unsafe {
                commands.vkResetCommandPool(device, frame.pool, VkCommandPoolResetFlags::empty())
            })?;
            frame.staging.reset();
            if let Some(capture) = frame.capture.take() {
                self.captures.push(capture.finish()?);
            }
            frame.drawn.invalidate()?;
            self.drawn = bytemuck::pod_read_unaligned(&frame.drawn.mapped()[..4]);
        }

        let Some(image) = self.swapchain.acquire(self.frames[slot].image_available)? else {
            self.resize = Some(self.swapchain.extent());
            return Ok(());
        };
        self.swapchain.waited(waiting.elapsed());

        if let Some(world) = world {
            self.terrain
                .schedule(world, params.eye, self.fancy, self.smooth_lighting);
        }
        let frame = &mut self.frames[slot];
        let mut terrain_copies = TerrainCopies::default();
        let completed = self.frame as i64 - FRAMES_IN_FLIGHT as i64;
        let camera = params.view.inverse().transform_point3(Vec3::ZERO);
        self.terrain.upload(
            &mut frame.staging,
            &mut terrain_copies,
            self.frame,
            completed,
            params.eye + camera.as_dvec3(),
        )?;
        // Copies into the atlas and into `gui/items.png`
        let mut tile_copies = [Vec::new(), Vec::new()];
        for update in params.tile_updates {
            if update.texture != 0 && !self.animated_items {
                continue;
            }
            if let Some(offset) = frame.staging.push(&update.pixels) {
                let tile = i32::from(update.tile);
                let copy = image_copy(offset, [tile % 16 * 16, tile / 16 * 16], 16, 16);
                tile_copies[usize::from(update.texture != 0)].push(copy);
            }
        }

        let extent = self.swapchain.extent();
        if self.capture_requested {
            let format = self.swapchain.format();
            frame.capture = Some(capture::Capture::new(&self.device, extent, format)?);
            self.capture_requested = false;
        }
        let aspect = extent.width as f32 / extent.height as f32;
        let projection = glam::camera::rh::proj::directx::perspective_infinite_reverse(
            params.fov_y_degrees.to_radians(),
            aspect,
            0.05,
        );
        let view_projection = projection * params.view;
        let terrain_active = world.is_some() && self.terrain.order_len() > 0;
        self.translucent.clear();
        if terrain_active {
            let slots = &mut self.translucent;
            self.terrain.translucent(params.eye, view_projection, slots);
        }
        // Grown to the frame's sections; this slot's previous frame has finished with it
        let translucent_bytes = size_of_val(self.translucent.as_slice()) as u64;
        if translucent_bytes > frame.translucent.size() {
            frame.translucent = Buffer::new(
                &self.device,
                translucent_bytes.next_power_of_two(),
                VkBufferUsageFlags2::SHADER_DEVICE_ADDRESS,
                MemoryLocation::CpuToGpu,
            )?;
        }
        frame
            .translucent
            .write(0, bytemuck::cast_slice(&self.translucent))?;
        let translucent_count = self.translucent.len() as u32;
        let translucent = frame.translucent.address();
        let cull_push = self
            .terrain
            .cull_push(params.eye, camera, view_projection, translucent);
        let (eye_block, eye_fraction) = terrain::split_eye(params.eye);
        let camera = camera.extend(0.0).to_array();

        if world.is_some() && params.has_sky {
            self.sky.build(&SkyParams {
                sky_color: params.sky_color,
                celestial_angle: params.celestial_angle,
                sunrise: params.sunrise,
                sun_texture: SUN_SLOT,
                moon_texture: MOON_SLOT,
                clear: 1.0 - params.rain,
            });
        } else {
            self.sky.clear();
        }
        let sky_count = write_prefix(&mut frame.sky, &self.sky.vertices, SKY_VERTICES)?;

        let cmd = frame.command_buffer;
        let gui_count = write_prefix(&mut frame.gui, &params.gui.vertices, GUI_VERTICES)? as u32;
        // Grown to the frame's geometry; this slot's previous frame has finished with it
        let scene_bytes = size_of_val(params.scene.vertices.as_slice()) as u64;
        if scene_bytes > frame.scene.size() {
            frame.scene = Buffer::new(
                &self.device,
                scene_bytes.next_power_of_two(),
                VkBufferUsageFlags2::SHADER_DEVICE_ADDRESS,
                MemoryLocation::CpuToGpu,
            )?;
        }
        let scene_count = write_prefix(&mut frame.scene, &params.scene.vertices, usize::MAX)?;
        if params.scene.map.is_some() {
            let bytes = scene_count * size_of::<scene::Vertex>();
            let vertices: &mut [scene::Vertex] =
                bytemuck::cast_slice_mut(&mut frame.scene.mapped()[..bytes]);
            for vertex in vertices {
                if vertex.texture == -2 {
                    vertex.texture = (self.textures.len() + slot) as i32;
                }
            }
            frame.scene.flush(0, bytes)?;
        }
        let held_map = world.and_then(|world| params.scene.map.and_then(|id| world.maps.get(&id)));
        let map_pixels = if let Some(map) = held_map {
            if frame.map_colors.as_deref() == Some(&*map.colors) {
                None
            } else {
                Some(map.rgba())
            }
        } else if params.scene.map.is_some() && frame.map_colors.is_some() {
            Some(blank_map().pixels)
        } else {
            None
        };
        let map_copy = map_pixels
            .and_then(|pixels| frame.staging.push(&pixels))
            .map(|offset| image_copy(offset, [0, 0], 128, 128));
        if map_copy.is_some() {
            frame.map_colors = held_map.map(|map| map.colors.clone());
        }
        frame.staging.flush()?;
        let begin = VkCommandBufferBeginInfo {
            sType: VkStructureType::CommandBufferBeginInfo,
            flags: VkCommandBufferUsageFlags::ONE_TIME_SUBMIT,
            ..zeroed()
        };
        check(unsafe { commands.vkBeginCommandBuffer(cmd, &begin) })?;
        let staging = frame.staging.handle();
        unsafe {
            let device = &*self.device;
            if let Some(copy) = map_copy {
                let map = frame.map_image.handle();
                let shader_read = (Stage::FRAGMENT_SHADER, Access::SHADER_READ);
                let general = (Layout::General, Layout::General);
                image_barrier(device, cmd, map, COLOR, general, shader_read, COPY_WRITE);
                commands.vkCmdCopyBufferToImage(cmd, staging, map, Layout::General, 1, &copy);
            }
            for (buffer, copy) in &terrain_copies.meshes {
                commands.vkCmdCopyBuffer(cmd, staging, *buffer, 1, copy);
            }
            // The previous frame's culling and box test are done with the tables
            memory_barrier(device, cmd, CULLING, TRANSFER_WRITE);
            let (sections, order, work) = self.terrain.tables();
            for (target, copies) in [
                (sections, &terrain_copies.sections),
                (order, &terrain_copies.order),
            ] {
                if !copies.is_empty() {
                    let count = copies.len() as u32;
                    commands.vkCmdCopyBuffer(cmd, staging, target, count, copies.as_ptr());
                }
            }
            let header = terrain::HEADER;
            let header_size = terrain::HEADER_SIZE;
            commands.vkCmdUpdateBuffer(cmd, work, 0, header_size, header.as_ptr().cast());
            if tile_copies.iter().any(|copies| !copies.is_empty()) {
                let fragment = (Stage::FRAGMENT_SHADER, Access::NONE);
                memory_barrier(device, cmd, fragment, COPY_WRITE);
            }
            let items = &self.textures[crate::gui::ITEMS as usize];
            for (image, copies) in [&self.textures[ATLAS_SLOT as usize], items]
                .iter()
                .zip(&tile_copies)
            {
                if !copies.is_empty() {
                    commands.vkCmdCopyBufferToImage(
                        cmd,
                        staging,
                        image.handle(),
                        Layout::General,
                        copies.len() as u32,
                        copies.as_ptr(),
                    );
                }
            }
            memory_barrier(device, cmd, TRANSFER_WRITE, CULLING);
            let descriptors = &self.descriptors;
            descriptors.bind(cmd);
            let pipelines = &self.pipelines;
            let cull = |mode: CullMode, count: u32| {
                if count == 0 {
                    return;
                }
                let push = cull_push.pass(mode, count);
                let pipeline = pipelines.cull.handle();
                commands.vkCmdBindPipeline(cmd, VkPipelineBindPoint::Compute, pipeline);
                descriptors.push(cmd, bytemuck::bytes_of(&push));
                commands.vkCmdDispatch(cmd, count.div_ceil(64), 1, 1);
            };
            let order_len = self.terrain.order_len();
            if terrain_active {
                cull(CullMode::Previous, order_len);
                memory_barrier(device, cmd, COMPUTE_WRITE, CULLING);
            }
            let (target, depth) = (image.image, self.depth.handle());
            let layouts = (Layout::Undefined, Layout::ColorAttachmentOptimal);
            let output = (Stage::COLOR_ATTACHMENT_OUTPUT, Access::NONE);
            image_barrier(device, cmd, target, COLOR, layouts, output, COLOR_WRITE);
            let tests = Stage::EARLY_FRAGMENT_TESTS | Stage::LATE_FRAGMENT_TESTS;
            let write = Access::DEPTH_STENCIL_ATTACHMENT_WRITE;
            let read_write = (tests, write | Access::DEPTH_STENCIL_ATTACHMENT_READ);
            let layouts = (Layout::Undefined, Layout::DepthAttachmentOptimal);
            let aspect = VkImageAspectFlags::DEPTH;
            image_barrier(
                device,
                cmd,
                depth,
                aspect,
                layouts,
                (tests, write),
                read_write,
            );

            let clear_depth = VkClearValue {
                depthStencil: VkClearDepthStencilValue {
                    depth: 0.0,
                    stencil: 0,
                },
            };
            let color_attachment = VkRenderingAttachmentInfo {
                sType: VkStructureType::RenderingAttachmentInfo,
                imageView: image.view,
                imageLayout: Layout::ColorAttachmentOptimal,
                loadOp: VkAttachmentLoadOp::Clear,
                storeOp: VkAttachmentStoreOp::Store,
                clearValue: VkClearValue {
                    color: VkClearColorValue {
                        float32: params.fog_color.extend(1.0).to_array(),
                    },
                },
                ..zeroed()
            };
            let depth_attachment = VkRenderingAttachmentInfo {
                sType: VkStructureType::RenderingAttachmentInfo,
                imageView: self.depth.view(),
                imageLayout: Layout::DepthAttachmentOptimal,
                loadOp: VkAttachmentLoadOp::Clear,
                // Kept between the culling passes
                storeOp: if terrain_active {
                    VkAttachmentStoreOp::Store
                } else {
                    VkAttachmentStoreOp::DontCare
                },
                clearValue: clear_depth,
                ..zeroed()
            };
            let scissor = VkRect2D {
                offset: VkOffset2D { x: 0, y: 0 },
                extent,
            };
            let rendering = VkRenderingInfo {
                sType: VkStructureType::RenderingInfo,
                renderArea: scissor,
                layerCount: 1,
                colorAttachmentCount: 1,
                pColorAttachments: &color_attachment,
                pDepthAttachment: &depth_attachment,
                ..zeroed()
            };
            commands.vkCmdBeginRendering(cmd, &rendering);
            let viewport = VkViewport {
                x: 0.0,
                y: extent.height as f32,
                width: extent.width as f32,
                height: -(extent.height as f32),
                minDepth: 0.0,
                maxDepth: 1.0,
            };
            commands.vkCmdSetViewport(cmd, 0, 1, &viewport);
            commands.vkCmdSetScissor(cmd, 0, 1, &scissor);

            let fog_color = params.fog_color.extend(1.0).to_array();
            let fog_start = if params.has_sky {
                params.view_distance * 0.25
            } else {
                0.0
            };
            for batch in &self.sky.batches {
                let (pipeline, fog, texture) = match batch.pass {
                    SkyPass::Plane => (&pipelines.sky_plane, 1, -1),
                    SkyPass::Sunrise => (&pipelines.sky_alpha, 0, -1),
                    SkyPass::Body(slot) => (&pipelines.sky_additive, 0, slot as i32),
                    SkyPass::Stars => (&pipelines.sky_additive, 0, -1),
                };
                if batch.first + batch.count > sky_count as u32 {
                    continue;
                }
                bind(
                    device,
                    descriptors,
                    cmd,
                    pipeline,
                    &SkyPush {
                        view_projection: view_projection.to_cols_array(),
                        vertices: frame.sky.address(),
                        _padding: 0,
                        fog_color,
                        camera,
                        fog_start: params.fog_density.map_or(0.0, |density| -density),
                        fog_end: params.view_distance * 0.8,
                        fog,
                        texture,
                    },
                );
                commands.vkCmdDraw(cmd, batch.count, 1, batch.first, 0);
            }

            let fog_start = params.fog_density.map_or(fog_start, |density| -density);
            let terrain_push = TerrainPush {
                view_projection: view_projection.to_cols_array(),
                fog_color,
                camera,
                eye_fraction,
                eye_block,
                sections: self.terrain.sections_address(),
                fog_start,
                fog_end: params.view_distance,
                sky_darkness: params.sky_darkness,
                texture: ATLAS_SLOT,
                opacity: 1.0,
                _padding: 0,
                brightness: params.brightness,
            };
            let stride = size_of::<VkDrawIndexedIndirectCommand>() as u32;
            let draw_opaque = || {
                if !terrain_active {
                    return;
                }
                let index_type = VkIndexType::UINT32;
                commands.vkCmdBindIndexBuffer(cmd, self.terrain.index_buffer(), 0, index_type);
                for (layer, pipeline) in [&pipelines.terrain_solid, &pipelines.terrain_cutout]
                    .into_iter()
                    .enumerate()
                {
                    bind(device, descriptors, cmd, pipeline, &terrain_push);
                    let (offset, max) = self.terrain.commands(layer as u64);
                    let count = layer as u64 * 4;
                    commands
                        .vkCmdDrawIndexedIndirectCount(cmd, work, offset, work, count, max, stride);
                }
            };
            draw_opaque();
            if terrain_active {
                let boxes = BoxPush {
                    view_projection: view_projection.to_cols_array(),
                    eye_fraction,
                    eye_block,
                    sections: self.terrain.sections_address(),
                    tested: self.terrain.tested_address(),
                };
                bind(device, descriptors, cmd, &pipelines.section_boxes, &boxes);
                commands.vkCmdDrawIndirect(cmd, work, terrain::BOX_DRAW_OFFSET, 1, 16);
                commands.vkCmdEndRendering(cmd);

                // Draw what the boxes revealed, then fill the translucent draws
                memory_barrier(device, cmd, ATTACHMENTS, ATTACHMENTS);
                memory_barrier(device, cmd, CULLING, TRANSFER_WRITE);
                commands.vkCmdFillBuffer(cmd, work, 0, 8, 0);
                memory_barrier(device, cmd, TRANSFER_WRITE, CULLING);
                cull(CullMode::Revealed, order_len);
                memory_barrier(device, cmd, COMPUTE_WRITE, CULLING);
                cull(CullMode::Translucent, translucent_count);
                memory_barrier(device, cmd, COMPUTE_WRITE, CULLING);
                let drawn = VkBufferCopy {
                    srcOffset: terrain::DRAWN_OFFSET,
                    dstOffset: 0,
                    size: size_of::<u32>() as u64,
                };
                commands.vkCmdCopyBuffer(cmd, work, frame.drawn.handle(), 1, &drawn);
                memory_barrier(
                    device,
                    cmd,
                    TRANSFER_WRITE,
                    (Stage::HOST, Access::HOST_READ),
                );

                let load = |attachment: &VkRenderingAttachmentInfo| VkRenderingAttachmentInfo {
                    loadOp: VkAttachmentLoadOp::Load,
                    storeOp: VkAttachmentStoreOp::DontCare,
                    ..*attachment
                };
                let color_attachment = VkRenderingAttachmentInfo {
                    storeOp: VkAttachmentStoreOp::Store,
                    ..load(&color_attachment)
                };
                let depth_attachment = load(&depth_attachment);
                let rendering = VkRenderingInfo {
                    pColorAttachments: &color_attachment,
                    pDepthAttachment: &depth_attachment,
                    ..rendering
                };
                commands.vkCmdBeginRendering(cmd, &rendering);
                draw_opaque();
            }
            let draw_translucent = |opacity| {
                if translucent_count == 0 {
                    return;
                }
                let push = TerrainPush {
                    opacity,
                    ..terrain_push
                };
                bind(
                    device,
                    descriptors,
                    cmd,
                    &pipelines.terrain_translucent,
                    &push,
                );
                let offset = self.terrain.translucent_offset();
                commands.vkCmdDrawIndexedIndirect(cmd, work, offset, translucent_count, stride);
            };
            let scene = params.scene;
            let fogged = |alpha_cutoff| ScenePush {
                view_projection: view_projection.to_cols_array(),
                vertices: frame.scene.address(),
                fog: 1,
                alpha_cutoff,
                fog_color,
                camera,
                fog_start,
                fog_end: params.view_distance,
                opacity: 1.0,
                _padding: 0,
            };
            let unfogged = |view_projection: Mat4, camera, alpha_cutoff| ScenePush {
                view_projection: view_projection.to_cols_array(),
                vertices: frame.scene.address(),
                fog: 0,
                alpha_cutoff,
                fog_color,
                camera,
                fog_start: 0.0,
                fog_end: 0.0,
                opacity: 1.0,
                _padding: 0,
            };
            let draw =
                |range: &Range<u32>| commands.vkCmdDraw(cmd, range.len() as u32, 1, range.start, 0);
            let draw_scene = |pipeline: &GraphicsPipeline, range: &Range<u32>, push: ScenePush| {
                let valid = !range.is_empty() && range.end <= scene_count as u32;
                if valid {
                    bind(device, descriptors, cmd, pipeline, &push);
                    draw(range);
                }
                valid
            };
            draw_scene(&pipelines.scene, &scene.objects, fogged(0.1));
            draw_scene(&pipelines.lines, &scene.lines, fogged(0.0));
            draw_scene(&pipelines.scene, &scene.moving, fogged(0.1));
            draw_scene(&pipelines.scene, &scene.signs, fogged(0.1));
            draw_scene(&pipelines.scene_additive, &scene.flashes, fogged(0.0));
            draw_scene(&pipelines.outline, &scene.shadows, fogged(0.1));
            draw_scene(&pipelines.scene_additive, &scene.bolts, fogged(0.0));
            if self.fancy {
                draw_translucent(0.0);
            }
            draw_translucent(1.0);
            let names = unfogged(view_projection, camera, 0.0);
            draw_scene(&pipelines.nameplate, &scene.names_hidden, names);
            draw_scene(&pipelines.outline, &scene.names, names);
            draw_scene(&pipelines.outline, &scene.weather, fogged(0.01));
            if scene.fancy_clouds {
                let depth_only = ScenePush {
                    opacity: 0.0,
                    ..fogged(0.1)
                };
                draw_scene(&pipelines.scene, &scene.clouds, depth_only);
                draw_scene(&pipelines.scene, &scene.clouds, fogged(0.1));
            } else {
                draw_scene(&pipelines.outline, &scene.clouds, fogged(0.1));
            }
            draw_scene(&pipelines.outline, &scene.outline, fogged(0.1));
            draw_scene(&pipelines.cracks, &scene.cracks, fogged(0.1));
            let depth_range = Mat4::from_cols(
                Vec4::X,
                Vec4::Y,
                Vec4::new(0.0, 0.0, 0.1, 0.0),
                Vec4::new(0.0, 0.0, 0.9, 1.0),
            );
            let cutoff = if scene.map.is_some() { 0.0 } else { 0.1 };
            let hand = unfogged(depth_range * projection, [0.0; 4], cutoff);
            if draw_scene(&pipelines.scene, &scene.hand, hand)
                && !scene.map_text.is_empty()
                && scene.map_text.end <= scene_count as u32
            {
                draw(&scene.map_text);
            }

            #[repr(C)]
            #[derive(Clone, Copy, Pod, Zeroable)]
            struct GuiPush {
                vertices: u64,
                size: [f32; 2],
            }
            let gui_push = GuiPush {
                vertices: frame.gui.address(),
                size: params.gui.projection,
            };
            let draw_gui_with = |pipeline: &GraphicsPipeline, range: Range<u32>| {
                if !range.is_empty() {
                    bind(device, descriptors, cmd, pipeline, &gui_push);
                    draw(&range);
                }
            };
            // The crosshair keeps its place in the drawing order with its own blend
            let crosshair =
                params.gui.crosshair.start.min(gui_count)..params.gui.crosshair.end.min(gui_count);
            let draw_gui = |range: Range<u32>| {
                if crosshair.is_empty()
                    || crosshair.start < range.start
                    || crosshair.end > range.end
                {
                    return draw_gui_with(&pipelines.gui, range);
                }
                draw_gui_with(&pipelines.gui, range.start..crosshair.start);
                draw_gui_with(&pipelines.gui_invert, crosshair.clone());
                draw_gui_with(&pipelines.gui, crosshair.end..range.end);
            };
            let portrait = params
                .gui
                .portrait
                .filter(|_| !scene.portrait.is_empty() && scene.portrait.end <= scene_count as u32);
            let split = portrait.map_or(gui_count, |portrait| portrait.split.min(gui_count));
            draw_gui(0..split);
            if portrait.is_some() {
                let clear = VkClearAttachment {
                    aspectMask: VkImageAspectFlags::DEPTH,
                    colorAttachment: 0,
                    clearValue: clear_depth,
                };
                let rect = VkClearRect {
                    rect: scissor,
                    baseArrayLayer: 0,
                    layerCount: 1,
                };
                commands.vkCmdClearAttachments(cmd, 1, &clear, 1, &rect);
                let projection = scene::portrait_projection(params.gui.projection);
                let portrait = unfogged(projection, [0.0; 4], 0.1);
                draw_scene(&pipelines.scene, &scene.portrait, portrait);
            }
            draw_gui(split..gui_count);
            commands.vkCmdEndRendering(cmd);

            if let Some(capture) = &frame.capture {
                capture.record(device, cmd, target);
            } else {
                let layouts = (Layout::ColorAttachmentOptimal, Layout::PresentSrcKhr);
                image_barrier(device, cmd, target, COLOR, layouts, COLOR_WRITE, NO_ACCESS);
            }
        }
        check(unsafe { commands.vkEndCommandBuffer(cmd) })?;

        let wait = VkSemaphoreSubmitInfo {
            sType: VkStructureType::SemaphoreSubmitInfo,
            semaphore: frame.image_available,
            stageMask: Stage::COLOR_ATTACHMENT_OUTPUT,
            ..zeroed()
        };
        let signal = VkSemaphoreSubmitInfo {
            sType: VkStructureType::SemaphoreSubmitInfo,
            semaphore: image.render_finished,
            stageMask: Stage::ALL_COMMANDS,
            ..zeroed()
        };
        let command_info = VkCommandBufferSubmitInfo {
            sType: VkStructureType::CommandBufferSubmitInfo,
            commandBuffer: cmd,
            ..zeroed()
        };
        let submit = VkSubmitInfo2 {
            sType: VkStructureType::SubmitInfo2,
            waitSemaphoreInfoCount: 1,
            pWaitSemaphoreInfos: &wait,
            commandBufferInfoCount: 1,
            pCommandBufferInfos: &command_info,
            signalSemaphoreInfoCount: 1,
            pSignalSemaphoreInfos: &signal,
            ..zeroed()
        };
        check(unsafe { commands.vkResetFences(device, 1, &frame.fence) })?;
        check(unsafe { commands.vkQueueSubmit2(self.device.queue, 1, &submit, frame.fence) })?;
        if !self.swapchain.present(&image)? {
            self.resize = Some(self.swapchain.extent());
        }
        self.frame += 1;
        Ok(())
    }
}

impl Drop for Renderer {
    fn drop(&mut self) {
        let _ = self.device.wait_idle();
        let commands = &self.device.commands;
        let device = self.device.handle;
        for frame in &self.frames {
            unsafe {
                commands.vkDestroyFence(device, frame.fence, null());
                commands.vkDestroySemaphore(device, frame.image_available, null());
                commands.vkDestroyCommandPool(device, frame.pool, null());
            }
        }
    }
}

/// Binds a graphics pipeline and pushes its data.
fn bind<T: Pod>(
    device: &Device,
    descriptors: &Descriptors,
    cmd: VkCommandBuffer,
    pipeline: &GraphicsPipeline,
    data: &T,
) {
    unsafe {
        let commands = &device.commands;
        commands.vkCmdBindPipeline(cmd, VkPipelineBindPoint::Graphics, pipeline.handle());
        descriptors.push(cmd, bytemuck::bytes_of(data));
    }
}

/// Writes up to `max` items to the start of `buffer` and returns how many.
fn write_prefix<T: Pod>(buffer: &mut Buffer, items: &[T], max: usize) -> Result<usize> {
    let count = items.len().min(max);
    buffer.write(0, bytemuck::cast_slice(&items[..count]))?;
    Ok(count)
}

/// A copy between tightly packed pixels and a region of a color image.
fn image_copy(offset: u64, [x, y]: [i32; 2], width: u32, height: u32) -> VkBufferImageCopy {
    VkBufferImageCopy {
        bufferOffset: offset,
        imageSubresource: VkImageSubresourceLayers {
            aspectMask: COLOR,
            layerCount: 1,
            ..zeroed()
        },
        imageOffset: VkOffset3D { x, y, z: 0 },
        imageExtent: VkExtent3D {
            width,
            height,
            depth: 1,
        },
        ..zeroed()
    }
}

fn memory_barrier(
    device: &Device,
    cmd: VkCommandBuffer,
    (src_stage, src_access): Scope,
    (dst_stage, dst_access): Scope,
) {
    let barrier = VkMemoryBarrier2 {
        sType: VkStructureType::MemoryBarrier2,
        srcStageMask: src_stage,
        srcAccessMask: src_access,
        dstStageMask: dst_stage,
        dstAccessMask: dst_access,
        ..zeroed()
    };
    let dependency = VkDependencyInfo {
        sType: VkStructureType::DependencyInfo,
        memoryBarrierCount: 1,
        pMemoryBarriers: &barrier,
        ..zeroed()
    };
    unsafe { device.commands.vkCmdPipelineBarrier2(cmd, &dependency) };
}

fn image_barrier(
    device: &Device,
    cmd: VkCommandBuffer,
    image: VkImage,
    aspect: VkImageAspectFlags,
    (old_layout, new_layout): (Layout, Layout),
    (src_stage, src_access): Scope,
    (dst_stage, dst_access): Scope,
) {
    let barrier = VkImageMemoryBarrier2 {
        sType: VkStructureType::ImageMemoryBarrier2,
        srcStageMask: src_stage,
        srcAccessMask: src_access,
        dstStageMask: dst_stage,
        dstAccessMask: dst_access,
        oldLayout: old_layout,
        newLayout: new_layout,
        srcQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED,
        dstQueueFamilyIndex: VK_QUEUE_FAMILY_IGNORED,
        image,
        subresourceRange: subresource_range(aspect),
        ..zeroed()
    };
    let dependency = VkDependencyInfo {
        sType: VkStructureType::DependencyInfo,
        imageMemoryBarrierCount: 1,
        pImageMemoryBarriers: &barrier,
        ..zeroed()
    };
    unsafe { device.commands.vkCmdPipelineBarrier2(cmd, &dependency) };
}

/// Uploads the atlas, sun, moon and GUI textures.
fn load_textures(device: &Arc<Device>, assets: &Assets) -> Result<Vec<Image>> {
    [&assets.terrain, &assets.sun, &assets.moon]
        .into_iter()
        .chain(&assets.gui)
        .chain(&assets.entities)
        .chain(&assets.armor)
        .chain(&assets.menu)
        .map(|image| upload_texture(device, image))
        .collect()
}

/// Describes the textures, then each frame's map, at slots matching their indices.
fn describe(
    device: &Arc<Device>,
    heaps: bool,
    textures: &[Image],
    frames: &[FrameResources],
) -> Result<Descriptors> {
    let images = textures
        .iter()
        .chain(frames.iter().map(|frame| &frame.map_image));
    let mut descriptors =
        Descriptors::new(device, heaps, (textures.len() + frames.len()) as u32, 2)?;
    for (slot, image) in images.enumerate() {
        descriptors.write_image(slot as u32, image, Layout::General)?;
    }
    write_samplers(&mut descriptors)?;
    Ok(descriptors)
}

fn blank_map() -> RgbaImage {
    RgbaImage {
        width: 128,
        height: 128,
        pixels: common::map::Map::new([0, 0], 0).rgba(),
    }
}

fn create_depth(device: &Arc<Device>, extent: VkExtent2D) -> Result<Image> {
    Image::new(
        device,
        extent,
        DEPTH_FORMAT,
        VkImageUsageFlags::DEPTH_STENCIL_ATTACHMENT,
        VkImageAspectFlags::DEPTH,
    )
}

/// Creates a sampled RGBA texture in the `GENERAL` layout, so tiles can be updated in place.
fn upload_texture(device: &Arc<Device>, image: &RgbaImage) -> Result<Image> {
    let texture = Image::new(
        device,
        VkExtent2D {
            width: image.width,
            height: image.height,
        },
        VkFormat::R8G8B8A8Unorm,
        VkImageUsageFlags::SAMPLED | VkImageUsageFlags::TRANSFER_DST,
        VkImageAspectFlags::COLOR,
    )?;
    let mut staging = Buffer::new(
        device,
        image.pixels.len() as u64,
        VkBufferUsageFlags2::TRANSFER_SRC,
        MemoryLocation::CpuToGpu,
    )?;
    staging.write(0, &image.pixels)?;
    device.submit_now(|cmd| {
        let layouts = (Layout::Undefined, Layout::General);
        image_barrier(
            device,
            cmd,
            texture.handle(),
            COLOR,
            layouts,
            NO_ACCESS,
            COPY_WRITE,
        );
        let region = image_copy(0, [0, 0], image.width, image.height);
        unsafe {
            device.commands.vkCmdCopyBufferToImage(
                cmd,
                staging.handle(),
                texture.handle(),
                Layout::General,
                1,
                &region,
            );
        }
    })?;
    Ok(texture)
}

/// Sampler 0 keeps texels sharp and repeats; sampler 1 smooths and clamps, like the original's
/// `%blur%` textures.
fn write_samplers(descriptors: &mut Descriptors) -> Result<()> {
    let sampler = |filter, address| VkSamplerCreateInfo {
        sType: VkStructureType::SamplerCreateInfo,
        magFilter: filter,
        minFilter: filter,
        mipmapMode: VkSamplerMipmapMode::Nearest,
        addressModeU: address,
        addressModeV: address,
        addressModeW: address,
        ..zeroed()
    };
    descriptors.write_sampler(0, &sampler(VkFilter::Nearest, VkSamplerAddressMode::Repeat))?;
    descriptors.write_sampler(
        1,
        &sampler(VkFilter::Linear, VkSamplerAddressMode::ClampToEdge),
    )
}