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 / terrain.rs

//! Section meshes on the GPU: a buffer per section, a table of them that `terrain.slang` culls
//! and draws from, a shared quad index buffer and background meshing.

use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};

use bytemuck::{Pod, Zeroable};
use common::world::ChunkPos;
use common::world::chunk::HEIGHT;
use glam::{DVec3, IVec3, Mat4, Vec3, Vec4};
use rustc_hash::{FxHashMap, FxHashSet};

use super::mesher::{self, GROUPS, MAX_QUADS, SectionMesh};
use super::staging::Staging;
use crate::gpu::vk::*;
use crate::gpu::{Buffer, Device, MemoryLocation};
use crate::world::ClientWorld;

pub const SECTIONS_PER_COLUMN: i32 = (HEIGHT / 16) as i32;
const _: () = assert!(SECTIONS_PER_COLUMN <= u8::BITS as i32);
const INITIAL_SLOTS: u32 = 1 << 12;
/// Draws a section adds to a layer at most: runs of visible facings among the seven groups.
const DRAWS_PER_SECTION: u64 = 4;
const COMMAND_SIZE: u64 = size_of::<VkDrawIndexedIndirectCommand>() as u64;
const RECORD_SIZE: u64 = size_of::<SectionRecord>() as u64;
/// The work buffer's counters: solid and cutout draws, sections drawn this frame, then the
/// section box test's `VkDrawIndirectCommand`.
pub const HEADER: [u32; 8] = [0, 0, 0, 0, 0, 1, 0, 0];
pub const HEADER_SIZE: u64 = size_of::<[u32; 8]>() as u64;
pub const DRAWN_OFFSET: u64 = 8;
pub const BOX_DRAW_OFFSET: u64 = 16;
/// Frames between reorderings while the camera stays in its section and sections change.
const REORDER_FRAMES: u64 = 30;

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SectionPos {
    pub x: i32,
    pub y: i32,
    pub z: i32,
}

impl SectionPos {
    fn new(column: ChunkPos, y: i32) -> Self {
        let (x, z) = (column.x, column.z);
        Self { x, y, z }
    }

    fn column(self) -> ChunkPos {
        ChunkPos::new(self.x, self.z)
    }

    fn min_corner(self) -> DVec3 {
        DVec3::new(
            f64::from(self.x * 16),
            f64::from(self.y * 16),
            f64::from(self.z * 16),
        )
    }
}

/// A section's row in the table, `Section` in `terrain.slang`.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct SectionRecord {
    position: [i32; 3],
    visible: u32,
    was_visible: u32,
    /// Bounds, then facing planes, see [`SectionMesh`].
    extents: [i8; 12],
    shapes: VkDeviceAddress,
    lights: VkDeviceAddress,
    tints: VkDeviceAddress,
    faces: VkDeviceAddress,
    counts: [u32; GROUPS],
    /// Faces in all groups; 0 marks a free slot.
    total: u32,
}

/// Push data of `cullMain`.
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct CullPush {
    planes: [[f32; 4]; 5],
    eye_fraction: [f32; 4],
    /// The camera relative to the eye.
    camera: [f32; 4],
    eye_block: [i32; 4],
    sections: VkDeviceAddress,
    order: VkDeviceAddress,
    counters: VkDeviceAddress,
    tested: VkDeviceAddress,
    commands: VkDeviceAddress,
    translucent: VkDeviceAddress,
    translucent_commands: VkDeviceAddress,
    count: u32,
    capacity: u32,
    mode: u32,
    _padding: u32,
}

impl CullPush {
    /// The push data for one pass over `count` items.
    pub fn pass(self, mode: CullMode, count: u32) -> Self {
        Self {
            mode: mode as u32,
            count,
            ..self
        }
    }
}

/// The culling passes, see `cullMain`.
#[derive(Clone, Copy)]
pub enum CullMode {
    /// Draws what was visible last frame and lists the boxes to test.
    Previous = 0,
    /// Draws what the box test newly found visible.
    Revealed = 1,
    /// Fills the translucent draws, back to front.
    Translucent = 2,
}

/// The eye split into whole blocks and a fraction, as the shaders take it.
pub fn split_eye(eye: DVec3) -> ([i32; 4], [f32; 4]) {
    let block = eye.floor();
    (
        block.as_ivec3().extend(0).to_array(),
        (eye - block).as_vec3().extend(0.0).to_array(),
    )
}

struct GpuMesh {
    buffer: Buffer,
    slot: u32,
}

struct MeshResult {
    pos: SectionPos,
    version: u64,
    mesh: SectionMesh,
}

/// The device buffers sized by the slot capacity.
struct Tables {
    capacity: u32,
    sections: Buffer,
    order: Buffer,
    /// [`HEADER`], tested slots, solid then cutout draws, then translucent draws.
    work: Buffer,
}

impl Tables {
    fn new(device: &Arc<Device>, capacity: u32) -> crate::gpu::Result<Self> {
        let usage = VkBufferUsageFlags2::TRANSFER_DST;
        let slots = u64::from(capacity);
        let work_usage =
            usage | VkBufferUsageFlags2::TRANSFER_SRC | VkBufferUsageFlags2::INDIRECT_BUFFER;
        let work_size =
            HEADER_SIZE + slots * 4 + (2 * DRAWS_PER_SECTION + 1) * slots * COMMAND_SIZE;
        Ok(Self {
            capacity,
            sections: Buffer::new(device, slots * RECORD_SIZE, usage, MemoryLocation::GpuOnly)?,
            order: Buffer::new(device, slots * 4, usage, MemoryLocation::GpuOnly)?,
            work: Buffer::new(device, work_size, work_usage, MemoryLocation::GpuOnly)?,
        })
    }

    fn draws_per_layer(&self) -> u64 {
        DRAWS_PER_SECTION * u64::from(self.capacity)
    }

    fn tested_offset(&self) -> u64 {
        HEADER_SIZE
    }

    /// Solid draws for layer 0, cutout draws for layer 1.
    fn commands_offset(&self, layer: u64) -> u64 {
        HEADER_SIZE + u64::from(self.capacity) * 4 + layer * self.draws_per_layer() * COMMAND_SIZE
    }

    fn translucent_offset(&self) -> u64 {
        self.commands_offset(2)
    }
}

/// Copies [`Terrain::upload`] records: into each new mesh buffer, and into the section table and
/// culling order.
#[derive(Default)]
pub struct TerrainCopies {
    pub meshes: Vec<(VkBuffer, VkBufferCopy)>,
    pub sections: Vec<VkBufferCopy>,
    pub order: Vec<VkBufferCopy>,
}

pub struct Terrain {
    device: Arc<Device>,
    indices: Buffer,
    tables: Tables,
    /// Buffers no longer referenced, with the last frame that may read them.
    retired: VecDeque<(i64, Buffer)>,
    /// Meshed sections; `None` for those without geometry.
    meshes: FxHashMap<SectionPos, Option<GpuMesh>>,
    /// The section table as uploaded, by slot.
    records: Vec<SectionRecord>,
    free_slots: Vec<u32>,
    dirty_slots: Vec<u32>,
    /// Every slot is uploaded again, after the table grew.
    dirty_all: bool,
    /// Slots in the order `cullMain` goes through them, near to far when sorted.
    order: Vec<u32>,
    in_order: Vec<bool>,
    /// Entries from this index on are not uploaded yet.
    order_uploaded: usize,
    /// The camera section and frame of the last sort, and whether slots changed since.
    sorted: (IVec3, u64),
    order_changed: bool,
    translucent: FxHashSet<SectionPos>,
    versions: FxHashMap<SectionPos, u64>,
    next_version: u64,
    building: FxHashSet<SectionPos>,
    /// Sections to mesh, as bits of their y per column.
    dirty: FxHashMap<ChunkPos, u8>,
    in_flight: usize,
    /// `WorldRenderer.chunksUpdated`: meshes uploaded since the last [`Self::take_updates`].
    updates: u32,
    waiting: VecDeque<MeshResult>,
    results_sender: Sender<MeshResult>,
    results: Receiver<MeshResult>,
}

impl Terrain {
    pub fn new(device: &Arc<Device>) -> crate::gpu::Result<Self> {
        let quad_indices: Vec<u32> = (0..MAX_QUADS as u32)
            .flat_map(|q| [0, 1, 2, 0, 2, 3].map(|i| q * 4 + i))
            .collect();
        let size = size_of_val(quad_indices.as_slice()) as u64;
        let usage = VkBufferUsageFlags2::INDEX_BUFFER | VkBufferUsageFlags2::TRANSFER_DST;
        let indices = Buffer::new(device, size, usage, MemoryLocation::GpuOnly)?;
        let usage = VkBufferUsageFlags2::TRANSFER_SRC;
        let mut staging = Buffer::new(device, size, usage, MemoryLocation::CpuToGpu)?;
        staging.write(0, bytemuck::cast_slice(&quad_indices))?;
        device.submit_now(|command_buffer| {
            let region = VkBufferCopy {
                srcOffset: 0,
                dstOffset: 0,
                size,
            };
            let (source, target) = (staging.handle(), indices.handle());
            let commands = &device.commands;
            unsafe { commands.vkCmdCopyBuffer(command_buffer, source, target, 1, &region) };
        })?;

        let (results_sender, results) = mpsc::channel();
        Ok(Self {
            device: device.clone(),
            indices,
            tables: Tables::new(device, INITIAL_SLOTS)?,
            retired: VecDeque::new(),
            meshes: FxHashMap::default(),
            records: Vec::new(),
            free_slots: Vec::new(),
            dirty_slots: Vec::new(),
            dirty_all: false,
            order: Vec::new(),
            in_order: Vec::new(),
            order_uploaded: 0,
            sorted: (IVec3::MAX, 0),
            order_changed: false,
            translucent: FxHashSet::default(),
            versions: FxHashMap::default(),
            next_version: 0,
            building: FxHashSet::default(),
            dirty: FxHashMap::default(),
            in_flight: 0,
            updates: 0,
            waiting: VecDeque::new(),
            results_sender,
            results,
        })
    }

    pub fn column_ready(&self, pos: ChunkPos) -> bool {
        (0..SECTIONS_PER_COLUMN).all(|y| self.meshes.contains_key(&SectionPos::new(pos, y)))
    }

    pub fn clear(&mut self, frame: u64) {
        let sections: Vec<_> = self.meshes.keys().copied().collect();
        for section in sections {
            self.remove(section, frame);
        }
        self.versions.clear();
        self.dirty.clear();
        self.waiting.clear();
    }

    pub fn rebuild(&mut self) {
        let sections: Vec<_> = self.versions.keys().copied().collect();
        for section in sections {
            self.mark_section_dirty(section.column(), section.y);
        }
    }

    pub fn index_buffer(&self) -> VkBuffer {
        self.indices.handle()
    }

    /// Queues every section of a column for (re)meshing.
    pub fn mark_column_dirty(&mut self, pos: ChunkPos) {
        for y in 0..SECTIONS_PER_COLUMN {
            self.mark_section_dirty(pos, y);
        }
    }

    /// Queues one section for (re)meshing.
    pub fn mark_section_dirty(&mut self, pos: ChunkPos, y: i32) {
        if !(0..SECTIONS_PER_COLUMN).contains(&y) {
            return;
        }
        self.next_version += 1;
        self.versions
            .insert(SectionPos::new(pos, y), self.next_version);
        *self.dirty.entry(pos).or_default() |= 1 << y;
    }

    pub fn remove_column(&mut self, pos: ChunkPos, frame: u64) {
        self.dirty.remove(&pos);
        for y in 0..SECTIONS_PER_COLUMN {
            let section = SectionPos::new(pos, y);
            self.versions.remove(&section);
            self.remove(section, frame);
        }
    }

    /// Starts meshing dirty sections nearest to `eye` whose neighborhoods are loaded. Columns
    /// with an incomplete neighborhood are dropped: the neighbor completing it marks them again.
    pub fn schedule(
        &mut self,
        world: &ClientWorld,
        eye: DVec3,
        fancy: bool,
        smooth_lighting: bool,
    ) {
        self.collect();
        // Enough jobs to keep every worker busy until the next frame collects them
        let budget =
            (rayon::current_num_threads() * 4).saturating_sub(self.in_flight + self.waiting.len());
        if budget == 0 || self.dirty.is_empty() {
            return;
        }
        let mut ready = Vec::new();
        let building = &self.building;
        self.dirty.retain(|&column, sections| {
            let complete =
                (-1..=1).all(|dx| (-1..=1).all(|dz| world.contains(column.offset(dx, dz))));
            if complete {
                ready.extend(
                    (0..SECTIONS_PER_COLUMN)
                        .filter(|y| *sections & 1 << y != 0)
                        .map(|y| SectionPos::new(column, y))
                        .filter(|section| !building.contains(section)),
                );
            }
            complete
        });
        let nearer = |a: &SectionPos, b: &SectionPos| {
            let distance = |s: &SectionPos| (s.min_corner() + 8.0).distance_squared(eye);
            distance(a).total_cmp(&distance(b))
        };
        if ready.len() > budget {
            ready.select_nth_unstable_by(budget, nearer);
            ready.truncate(budget);
        }
        ready.sort_unstable_by(nearer);

        for section in ready {
            let Some(input) = world.mesh_input(section.column(), section.y, fancy, smooth_lighting)
            else {
                continue;
            };
            if let Some(sections) = self.dirty.get_mut(&section.column()) {
                *sections &= !(1 << section.y);
                if *sections == 0 {
                    self.dirty.remove(&section.column());
                }
            }
            self.in_flight += 1;
            self.building.insert(section);
            let version = self.versions.get(&section).copied().unwrap_or_default();
            let sender = self.results_sender.clone();
            rayon::spawn(move || {
                let mesh = mesher::build(&input);
                let _ = sender.send(MeshResult {
                    pos: section,
                    version,
                    mesh,
                });
            });
        }
    }

    /// Queues the meshes the workers finished for upload.
    fn collect(&mut self) {
        for result in self.results.try_iter() {
            self.in_flight -= 1;
            self.building.remove(&result.pos);
            self.waiting.push_back(result);
        }
    }

    /// Drops a section's mesh and frees its slot.
    fn remove(&mut self, pos: SectionPos, frame: u64) {
        if let Some(Some(mesh)) = self.meshes.remove(&pos) {
            self.release(mesh, frame);
        }
        self.translucent.remove(&pos);
    }

    fn release(&mut self, mesh: GpuMesh, frame: u64) {
        self.retired.push_back((frame as i64 - 1, mesh.buffer));
        self.records[mesh.slot as usize] = SectionRecord::zeroed();
        self.dirty_slots.push(mesh.slot);
        self.free_slots.push(mesh.slot);
    }

    /// Staging space kept for the table and order uploads of this frame, plus one more slot.
    fn reserved(&self) -> usize {
        let slots = if self.dirty_all {
            self.records.len()
        } else {
            self.dirty_slots.len()
        };
        (slots + 1) * (RECORD_SIZE as usize + 16) + (self.order.len() + 1) * 4 + 16
    }

    /// Moves finished meshes into their own buffers through `staging` and updates the section
    /// table and culling order, recording the copies. `frame` is the frame being recorded;
    /// `completed` the last one the GPU has finished.
    pub fn upload(
        &mut self,
        staging: &mut Staging,
        copies: &mut TerrainCopies,
        frame: u64,
        completed: i64,
        camera: DVec3,
    ) -> crate::gpu::Result<()> {
        while self
            .retired
            .pop_front_if(|(last_use, _)| *last_use <= completed)
            .is_some()
        {}
        if self.records.len() as u64 * 4 >= u64::from(self.tables.capacity) * 3 {
            self.grow(frame)?;
        }

        self.collect();
        for _ in 0..self.waiting.len() {
            let Some(result) = self.waiting.pop_front() else {
                break;
            };
            if self.versions.get(&result.pos) != Some(&result.version) {
                continue;
            }
            let mesh = &result.mesh;
            if mesh.is_empty() {
                self.remove(result.pos, frame);
                self.meshes.insert(result.pos, None);
                self.updates += 1;
                continue;
            }
            let bytes = size_of_val(mesh.data.as_slice());
            let replaces = matches!(self.meshes.get(&result.pos), Some(Some(_)));
            let room = replaces
                || !self.free_slots.is_empty()
                || self.records.len() < self.tables.capacity as usize;
            if !room || bytes + 16 + self.reserved() > staging.remaining() {
                self.waiting.push_front(result);
                break;
            }
            let usage = VkBufferUsageFlags2::TRANSFER_DST;
            let buffer =
                match Buffer::new(&self.device, bytes as u64, usage, MemoryLocation::GpuOnly) {
                    Ok(buffer) => buffer,
                    Err(error) => {
                        tracing::warn!("no memory for a section mesh: {error}");
                        *self.dirty.entry(result.pos.column()).or_default() |= 1 << result.pos.y;
                        break;
                    }
                };
            let source = staging
                .push(bytemuck::cast_slice(&mesh.data))
                .expect("reserved staging space");
            copies.meshes.push((
                buffer.handle(),
                VkBufferCopy {
                    srcOffset: source,
                    dstOffset: 0,
                    size: bytes as u64,
                },
            ));
            let slot = match self.meshes.remove(&result.pos).flatten() {
                Some(old) => {
                    let slot = old.slot;
                    self.retired.push_back((frame as i64 - 1, old.buffer));
                    slot
                }
                None => self.allocate_slot(),
            };
            self.records[slot as usize] = record(result.pos, mesh, buffer.address());
            self.dirty_slots.push(slot);
            if mesh.counts[GROUPS - 1] > 0 {
                self.translucent.insert(result.pos);
            } else {
                self.translucent.remove(&result.pos);
            }
            self.meshes
                .insert(result.pos, Some(GpuMesh { buffer, slot }));
            self.updates += 1;
        }

        self.upload_order(staging, copies, frame, camera);
        self.upload_records(staging, copies);
        Ok(())
    }

    fn allocate_slot(&mut self) -> u32 {
        let slot = self.free_slots.pop().unwrap_or_else(|| {
            self.records.push(SectionRecord::zeroed());
            self.in_order.push(false);
            self.records.len() as u32 - 1
        });
        if !self.in_order[slot as usize] {
            self.in_order[slot as usize] = true;
            self.order.push(slot);
        }
        self.order_changed = true;
        slot
    }

    /// Replaces the tables with ones of twice the capacity, uploaded in full.
    fn grow(&mut self, frame: u64) -> crate::gpu::Result<()> {
        let tables = Tables::new(&self.device, self.tables.capacity * 2)?;
        let old = std::mem::replace(&mut self.tables, tables);
        let last_use = frame as i64 - 1;
        self.retired
            .extend([old.sections, old.order, old.work].map(|buffer| (last_use, buffer)));
        self.dirty_all = true;
        self.order_uploaded = 0;
        Ok(())
    }

    /// Sorts the order near to far when the camera entered another section, or a while after
    /// slots changed, and uploads what changed.
    fn upload_order(
        &mut self,
        staging: &mut Staging,
        copies: &mut TerrainCopies,
        frame: u64,
        camera: DVec3,
    ) {
        let section = (camera / 16.0).floor().as_ivec3();
        let (sorted_section, sorted_frame) = self.sorted;
        if section != sorted_section || self.order_changed && frame >= sorted_frame + REORDER_FRAMES
        {
            self.sort_order(section);
            self.sorted = (section, frame);
            self.order_changed = false;
            self.order_uploaded = 0;
        }
        if self.order_uploaded < self.order.len() {
            let source = staging
                .push(bytemuck::cast_slice(&self.order[self.order_uploaded..]))
                .expect("reserved staging space");
            copies.order.push(VkBufferCopy {
                srcOffset: source,
                dstOffset: self.order_uploaded as u64 * 4,
                size: (self.order.len() - self.order_uploaded) as u64 * 4,
            });
            self.order_uploaded = self.order.len();
        }
    }

    /// Orders the live slots by their distance in sections from `camera`, with a counting sort.
    fn sort_order(&mut self, camera: IVec3) {
        let distance = |slot: u32| {
            let position = IVec3::from_array(self.records[slot as usize].position);
            (position - camera).abs().max_element() as usize
        };
        self.in_order.fill(false);
        self.order
            .retain(|&slot| self.records[slot as usize].total != 0);
        let mut starts = vec![0; self.order.iter().map(|&s| distance(s)).max().unwrap_or(0) + 2];
        for &slot in &self.order {
            starts[distance(slot) + 1] += 1;
        }
        for i in 1..starts.len() {
            starts[i] += starts[i - 1];
        }
        let mut sorted = vec![0; self.order.len()];
        for &slot in &self.order {
            let start = &mut starts[distance(slot)];
            sorted[*start] = slot;
            *start += 1;
            self.in_order[slot as usize] = true;
        }
        self.order = sorted;
    }

    /// Uploads changed records, one copy per run of consecutive slots.
    fn upload_records(&mut self, staging: &mut Staging, copies: &mut TerrainCopies) {
        if std::mem::take(&mut self.dirty_all) {
            self.dirty_slots = (0..self.records.len() as u32).collect();
        }
        self.dirty_slots.sort_unstable();
        self.dirty_slots.dedup();
        for run in self.dirty_slots.chunk_by(|a, b| a + 1 == *b) {
            let records = &self.records[run[0] as usize..=run[run.len() - 1] as usize];
            let source = staging
                .push(bytemuck::cast_slice(records))
                .expect("reserved staging space");
            copies.sections.push(VkBufferCopy {
                srcOffset: source,
                dstOffset: u64::from(run[0]) * RECORD_SIZE,
                size: size_of_val(records) as u64,
            });
        }
        self.dirty_slots.clear();
    }

    pub fn take_updates(&mut self) -> u32 {
        std::mem::take(&mut self.updates)
    }

    /// Sections with geometry.
    pub fn section_count(&self) -> usize {
        self.records.len() - self.free_slots.len()
    }

    /// Slots `cullMain` goes through.
    pub fn order_len(&self) -> u32 {
        self.order.len() as u32
    }

    pub fn tables(&self) -> (VkBuffer, VkBuffer, VkBuffer) {
        let tables = &self.tables;
        (
            tables.sections.handle(),
            tables.order.handle(),
            tables.work.handle(),
        )
    }

    pub fn sections_address(&self) -> VkDeviceAddress {
        self.tables.sections.address()
    }

    pub fn tested_address(&self) -> VkDeviceAddress {
        self.tables.work.address() + self.tables.tested_offset()
    }

    /// Where the solid (0) or cutout (1) draws start in the work buffer, and how many fit.
    pub fn commands(&self, layer: u64) -> (u64, u32) {
        let tables = &self.tables;
        (
            tables.commands_offset(layer),
            tables.draws_per_layer() as u32,
        )
    }

    pub fn translucent_offset(&self) -> u64 {
        self.tables.translucent_offset()
    }

    /// Push data for `cullMain`, reading the back to front translucent slots at `translucent`.
    pub fn cull_push(
        &self,
        eye: DVec3,
        camera: Vec3,
        view_projection: Mat4,
        translucent: VkDeviceAddress,
    ) -> CullPush {
        let (eye_block, eye_fraction) = split_eye(eye);
        let tables = &self.tables;
        let work = tables.work.address();
        CullPush {
            planes: Frustum::from_matrix(view_projection)
                .planes
                .map(|plane| plane.to_array()),
            eye_fraction,
            camera: camera.extend(0.0).to_array(),
            eye_block,
            sections: tables.sections.address(),
            order: tables.order.address(),
            counters: work,
            tested: work + tables.tested_offset(),
            commands: work + tables.commands_offset(0),
            translucent,
            translucent_commands: work + tables.translucent_offset(),
            count: 0,
            capacity: tables.draws_per_layer() as u32,
            mode: 0,
            _padding: 0,
        }
    }

    /// Slots of the sections with translucent geometry inside the frustum, back to front.
    pub fn translucent(&self, eye: DVec3, view_projection: Mat4, slots: &mut Vec<u32>) {
        let frustum = Frustum::from_matrix(view_projection);
        let mut sections: Vec<(f64, u32)> = self
            .translucent
            .iter()
            .filter_map(|pos| {
                let mesh = self.meshes.get(pos)?.as_ref()?;
                let record = &self.records[mesh.slot as usize];
                let origin = (pos.min_corner() - eye).as_vec3();
                let e = record.extents.map(f32::from);
                let (min, max) = (Vec3::new(e[0], e[1], e[2]), Vec3::new(e[3], e[4], e[5]));
                frustum
                    .intersects_box(origin + min, origin + max)
                    .then(|| ((pos.min_corner() + 8.0).distance_squared(eye), mesh.slot))
            })
            .collect();
        sections.sort_by(|a, b| b.0.total_cmp(&a.0));
        slots.extend(sections.iter().map(|&(_, slot)| slot));
    }
}

fn record(pos: SectionPos, mesh: &SectionMesh, address: VkDeviceAddress) -> SectionRecord {
    let mut extents = [0; 12];
    extents[..6].copy_from_slice(&mesh.bounds);
    extents[6..].copy_from_slice(&mesh.planes);
    SectionRecord {
        position: [pos.x, pos.y, pos.z],
        visible: 0,
        was_visible: 0,
        extents,
        shapes: address,
        lights: address + u64::from(mesh.lights),
        tints: address + u64::from(mesh.tints),
        faces: address + u64::from(mesh.faces),
        counts: mesh.counts,
        total: mesh.counts.iter().sum(),
    }
}

/// View frustum planes in camera-relative space.
struct Frustum {
    planes: [Vec4; 5],
}

impl Frustum {
    /// Extracts the side and near planes of a reverse-Z projection with an infinite far plane.
    fn from_matrix(view_projection: Mat4) -> Self {
        let row = |index| view_projection.row(index);
        let planes = [
            row(3) + row(0),
            row(3) - row(0),
            row(3) + row(1),
            row(3) - row(1),
            row(3) - row(2),
        ];
        Self { planes }
    }

    fn intersects_box(&self, min: Vec3, max: Vec3) -> bool {
        self.planes.iter().all(|plane| {
            let corner = Vec3::new(
                if plane.x >= 0.0 { max.x } else { min.x },
                if plane.y >= 0.0 { max.y } else { min.y },
                if plane.z >= 0.0 { max.z } else { min.z },
            );
            plane.truncate().dot(corner) + plane.w >= 0.0
        })
    }
}