Skip to content

Commit

Permalink
Chunk saving (#446)
Browse files Browse the repository at this point in the history
* First draft

* Fix chunk loading

* Make clippy happy

* Revert "Make clippy happy"

This reverts commit 6ea6468.

* Implemented caching unloaded chunks

* cache gets purged now

* Added tests, fixed unload queue

* cargo fmt

* found it
  • Loading branch information
koskja authored Aug 19, 2021
1 parent 32ab106 commit 07c6467
Show file tree
Hide file tree
Showing 23 changed files with 569 additions and 313 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion feather/base/src/anvil/region.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ impl RegionHandle {
Ok((chunk, level.entities.clone(), level.block_entities.clone()))
}

/// Checks if the specified chunk position is generated in this region.
/// # Panics
/// Panics if the specified chunk position is not within this
/// region file.
pub fn check_chunk_existence(&self, pos: ChunkPosition) -> bool {
self.header.location_for_chunk(pos).exists()
}

/// Saves the given chunk to this region file. The header will be updated
/// accordingly and saved as well.
///
Expand Down Expand Up @@ -387,7 +395,7 @@ fn read_section_into_chunk(section: &mut LevelSection, chunk: &mut Chunk) -> Res

let chunk_section = ChunkSection::new(blocks, light);

chunk.set_section_at(section.y as isize, Some(chunk_section));
chunk.set_section_at_raw(section.y as isize, Some(chunk_section));

Ok(())
}
Expand Down
26 changes: 17 additions & 9 deletions feather/base/src/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,20 +254,25 @@ impl Chunk {
}

/// Gets the chunk section at index `y`.
pub fn section(&self, y: usize) -> Option<&ChunkSection> {
self.sections.get(y)?.as_ref()
pub fn section(&self, y: isize) -> Option<&ChunkSection> {
self.sections.get((y + 1) as usize)?.as_ref()
}

/// Mutably gets the chunk section at index `y`.
pub fn section_mut(&mut self, y: usize) -> Option<&mut ChunkSection> {
self.sections.get_mut(y)?.as_mut()
pub fn section_mut(&mut self, y: isize) -> Option<&mut ChunkSection> {
self.sections.get_mut((y + 1) as usize)?.as_mut()
}

/// Sets the section at index `y`.
pub fn set_section_at(&mut self, y: isize, section: Option<ChunkSection>) {
self.sections[(y + 1) as usize] = section;
}

/// Directly sets the section at index `y` without offseting it. Useful when loading from region files
pub fn set_section_at_raw(&mut self, y: isize, section: Option<ChunkSection>) {
self.sections[y as usize] = section;
}

/// Gets the sections of this chunk.
pub fn sections(&self) -> &[Option<ChunkSection>] {
&self.sections
Expand Down Expand Up @@ -419,7 +424,7 @@ mod tests {

chunk.set_block_at(0, 0, 0, BlockId::andesite());
assert_eq!(chunk.block_at(0, 0, 0).unwrap(), BlockId::andesite());
assert!(chunk.section(1).is_some());
assert!(chunk.section(0).is_some());
}

#[test]
Expand Down Expand Up @@ -472,7 +477,7 @@ mod tests {
assert_eq!(chunk.block_at(x, (section * 16) + y, z), Some(block));
if counter != 0 {
assert!(
chunk.section(section + 1).is_some(),
chunk.section(section as isize).is_some(),
"Section {} failed",
section
);
Expand All @@ -485,14 +490,17 @@ mod tests {

// Go through again to be sure
for section in 0..16 {
assert!(chunk.section(section + 1).is_some());
assert!(chunk.section(section).is_some());
let mut counter = 0;
for x in 0..16 {
for y in 0..16 {
for z in 0..16 {
let block = BlockId::from_vanilla_id(counter);
assert_eq!(chunk.block_at(x, (section * 16) + y, z), Some(block));
assert!(chunk.section(section + 1).is_some());
assert_eq!(
chunk.block_at(x, (section as usize * 16) + y, z),
Some(block)
);
assert!(chunk.section(section).is_some());
counter += 1;
}
}
Expand Down
134 changes: 134 additions & 0 deletions feather/base/src/chunk_lock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};

use crate::Chunk;
use anyhow::bail;
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};

pub type ChunkHandle = Arc<ChunkLock>;
/// A wrapper around a RwLock. Cannot be locked for writing when unloaded.
/// This structure exists so that a chunk can be read from even after being unloaded without accidentaly writing to it.
#[derive(Debug)]
pub struct ChunkLock {
loaded: AtomicBool,
lock: RwLock<Chunk>,
}
impl ChunkLock {
pub fn new(chunk: Chunk, loaded: bool) -> Self {
Self {
loaded: AtomicBool::new(loaded),
lock: RwLock::new(chunk),
}
}
/// Returns whether the chunk is loaded.
pub fn is_loaded(&self) -> bool {
self.loaded.load(Ordering::SeqCst)
}
/// Attempts to set the chunk as unloaded. Returns an error if the chunk is locked as writable.
pub fn set_unloaded(&self) -> anyhow::Result<()> {
if self.loaded.swap(false, Ordering::SeqCst) {
// FIXME: Decide what to do when unloading an unloaded chunk
}
if self.lock.try_read().is_none() {
// Locking fails when someone else already owns a write lock
bail!("Cannot unload chunk because it is locked as writable!")
}
Ok(())
}
/// Sets the chunk as loaded and returns the previous state.
pub fn set_loaded(&self) -> bool {
self.loaded.swap(true, Ordering::SeqCst)
}

/// Locks this chunk with read acccess. Doesn't block.
/// Returns None if the chunk is unloaded or locked for writing, Some otherwise.
pub fn try_read(&self) -> Option<RwLockReadGuard<Chunk>> {
self.lock.try_read()
}

/// Locks this chunk with read acccess, blocking the current thread until it can be acquired.
/// Returns None if the chunk is unloaded, Some otherwise.
pub fn read(&self) -> RwLockReadGuard<Chunk> {
self.lock.read()
}
/// Locks this chunk with exclusive write acccess. Doesn't block.
/// Returns None if the chunk is unloaded or locked already, Some otherwise.
pub fn try_write(&self) -> Option<RwLockWriteGuard<Chunk>> {
if self.is_loaded() {
self.lock.try_write()
} else {
None
}
}
/// Locks this chunk with exclusive write acccess, blocking the current thread until it can be acquired.
/// Returns None if the chunk is unloaded, Some otherwise.
pub fn write(&self) -> Option<RwLockWriteGuard<Chunk>> {
if self.is_loaded() {
Some(self.lock.write())
} else {
None
}
}

pub fn is_locked(&self) -> bool {
self.lock.is_locked()
}
}

#[cfg(test)]
mod tests {
use std::{
thread::{sleep, spawn, JoinHandle},
time::Duration,
};

use libcraft_core::ChunkPosition;

use super::*;
fn empty_lock(x: i32, z: i32, loaded: bool) -> ChunkLock {
ChunkLock::new(Chunk::new(ChunkPosition::new(x, z)), loaded)
}
#[test]
fn normal_function() {
let lock = empty_lock(0, 0, true);
for _ in 0..100 {
// It should be possible to lock in any way
if rand::random::<bool>() {
let _guard = lock.try_read().unwrap();
} else {
let _guard = lock.try_write().unwrap();
}
}
}
#[test]
fn cannot_write_unloaded() {
let lock = empty_lock(0, 0, false);
assert!(lock.try_write().is_none())
}
#[test]
fn can_read_unloaded() {
let lock = empty_lock(0, 0, false);
assert!(lock.try_read().is_some())
}
#[test]
fn multithreaded() {
let lock = Arc::new(empty_lock(0, 0, true));
let mut handles: Vec<JoinHandle<()>> = vec![];
for _ in 0..20 {
let l = lock.clone();
handles.push(spawn(move || {
while let Some(guard) = l.write() {
sleep(Duration::from_millis(10));
drop(guard)
}
}))
}
sleep(Duration::from_millis(1000));
lock.set_unloaded().unwrap_or(()); // Discard error
for h in handles {
h.join().unwrap() // Wait for all threads to stop
}
}
}
2 changes: 2 additions & 0 deletions feather/base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ use serde::{Deserialize, Serialize};

pub mod anvil;
pub mod chunk;
pub mod chunk_lock;
pub mod inventory;
pub mod metadata;

pub use blocks::*;
pub use chunk::{Chunk, ChunkSection, CHUNK_HEIGHT, CHUNK_WIDTH};
pub use chunk_lock::*;
pub use generated::{Area, Biome, EntityKind, Inventory, Item, ItemStack};
pub use libcraft_blocks::{BlockKind, BlockState};
pub use libcraft_core::{position, vec3, BlockPosition, ChunkPosition, Gamemode, Position, Vec3d};
Expand Down
1 change: 1 addition & 0 deletions feather/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ uuid = { version = "0.8", features = [ "v4" ] }
libcraft-core = { path = "../../libcraft/core" }
rayon = "1.5"
worldgen = { path = "../worldgen", package = "feather-worldgen" }
rand = "0.8"
Loading

0 comments on commit 07c6467

Please sign in to comment.