Skip to content

Commit 07c6467

Browse files
authored
Chunk saving (feather-rs#446)
* 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
1 parent 32ab106 commit 07c6467

23 files changed

Lines changed: 569 additions & 313 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

feather/base/src/anvil/region.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,14 @@ impl RegionHandle {
243243
Ok((chunk, level.entities.clone(), level.block_entities.clone()))
244244
}
245245

246+
/// Checks if the specified chunk position is generated in this region.
247+
/// # Panics
248+
/// Panics if the specified chunk position is not within this
249+
/// region file.
250+
pub fn check_chunk_existence(&self, pos: ChunkPosition) -> bool {
251+
self.header.location_for_chunk(pos).exists()
252+
}
253+
246254
/// Saves the given chunk to this region file. The header will be updated
247255
/// accordingly and saved as well.
248256
///
@@ -387,7 +395,7 @@ fn read_section_into_chunk(section: &mut LevelSection, chunk: &mut Chunk) -> Res
387395

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

390-
chunk.set_section_at(section.y as isize, Some(chunk_section));
398+
chunk.set_section_at_raw(section.y as isize, Some(chunk_section));
391399

392400
Ok(())
393401
}

feather/base/src/chunk.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,20 +254,25 @@ impl Chunk {
254254
}
255255

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

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

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

271+
/// Directly sets the section at index `y` without offseting it. Useful when loading from region files
272+
pub fn set_section_at_raw(&mut self, y: isize, section: Option<ChunkSection>) {
273+
self.sections[y as usize] = section;
274+
}
275+
271276
/// Gets the sections of this chunk.
272277
pub fn sections(&self) -> &[Option<ChunkSection>] {
273278
&self.sections
@@ -419,7 +424,7 @@ mod tests {
419424

420425
chunk.set_block_at(0, 0, 0, BlockId::andesite());
421426
assert_eq!(chunk.block_at(0, 0, 0).unwrap(), BlockId::andesite());
422-
assert!(chunk.section(1).is_some());
427+
assert!(chunk.section(0).is_some());
423428
}
424429

425430
#[test]
@@ -472,7 +477,7 @@ mod tests {
472477
assert_eq!(chunk.block_at(x, (section * 16) + y, z), Some(block));
473478
if counter != 0 {
474479
assert!(
475-
chunk.section(section + 1).is_some(),
480+
chunk.section(section as isize).is_some(),
476481
"Section {} failed",
477482
section
478483
);
@@ -485,14 +490,17 @@ mod tests {
485490

486491
// Go through again to be sure
487492
for section in 0..16 {
488-
assert!(chunk.section(section + 1).is_some());
493+
assert!(chunk.section(section).is_some());
489494
let mut counter = 0;
490495
for x in 0..16 {
491496
for y in 0..16 {
492497
for z in 0..16 {
493498
let block = BlockId::from_vanilla_id(counter);
494-
assert_eq!(chunk.block_at(x, (section * 16) + y, z), Some(block));
495-
assert!(chunk.section(section + 1).is_some());
499+
assert_eq!(
500+
chunk.block_at(x, (section as usize * 16) + y, z),
501+
Some(block)
502+
);
503+
assert!(chunk.section(section).is_some());
496504
counter += 1;
497505
}
498506
}

feather/base/src/chunk_lock.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use std::sync::{
2+
atomic::{AtomicBool, Ordering},
3+
Arc,
4+
};
5+
6+
use crate::Chunk;
7+
use anyhow::bail;
8+
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
9+
10+
pub type ChunkHandle = Arc<ChunkLock>;
11+
/// A wrapper around a RwLock. Cannot be locked for writing when unloaded.
12+
/// This structure exists so that a chunk can be read from even after being unloaded without accidentaly writing to it.
13+
#[derive(Debug)]
14+
pub struct ChunkLock {
15+
loaded: AtomicBool,
16+
lock: RwLock<Chunk>,
17+
}
18+
impl ChunkLock {
19+
pub fn new(chunk: Chunk, loaded: bool) -> Self {
20+
Self {
21+
loaded: AtomicBool::new(loaded),
22+
lock: RwLock::new(chunk),
23+
}
24+
}
25+
/// Returns whether the chunk is loaded.
26+
pub fn is_loaded(&self) -> bool {
27+
self.loaded.load(Ordering::SeqCst)
28+
}
29+
/// Attempts to set the chunk as unloaded. Returns an error if the chunk is locked as writable.
30+
pub fn set_unloaded(&self) -> anyhow::Result<()> {
31+
if self.loaded.swap(false, Ordering::SeqCst) {
32+
// FIXME: Decide what to do when unloading an unloaded chunk
33+
}
34+
if self.lock.try_read().is_none() {
35+
// Locking fails when someone else already owns a write lock
36+
bail!("Cannot unload chunk because it is locked as writable!")
37+
}
38+
Ok(())
39+
}
40+
/// Sets the chunk as loaded and returns the previous state.
41+
pub fn set_loaded(&self) -> bool {
42+
self.loaded.swap(true, Ordering::SeqCst)
43+
}
44+
45+
/// Locks this chunk with read acccess. Doesn't block.
46+
/// Returns None if the chunk is unloaded or locked for writing, Some otherwise.
47+
pub fn try_read(&self) -> Option<RwLockReadGuard<Chunk>> {
48+
self.lock.try_read()
49+
}
50+
51+
/// Locks this chunk with read acccess, blocking the current thread until it can be acquired.
52+
/// Returns None if the chunk is unloaded, Some otherwise.
53+
pub fn read(&self) -> RwLockReadGuard<Chunk> {
54+
self.lock.read()
55+
}
56+
/// Locks this chunk with exclusive write acccess. Doesn't block.
57+
/// Returns None if the chunk is unloaded or locked already, Some otherwise.
58+
pub fn try_write(&self) -> Option<RwLockWriteGuard<Chunk>> {
59+
if self.is_loaded() {
60+
self.lock.try_write()
61+
} else {
62+
None
63+
}
64+
}
65+
/// Locks this chunk with exclusive write acccess, blocking the current thread until it can be acquired.
66+
/// Returns None if the chunk is unloaded, Some otherwise.
67+
pub fn write(&self) -> Option<RwLockWriteGuard<Chunk>> {
68+
if self.is_loaded() {
69+
Some(self.lock.write())
70+
} else {
71+
None
72+
}
73+
}
74+
75+
pub fn is_locked(&self) -> bool {
76+
self.lock.is_locked()
77+
}
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use std::{
83+
thread::{sleep, spawn, JoinHandle},
84+
time::Duration,
85+
};
86+
87+
use libcraft_core::ChunkPosition;
88+
89+
use super::*;
90+
fn empty_lock(x: i32, z: i32, loaded: bool) -> ChunkLock {
91+
ChunkLock::new(Chunk::new(ChunkPosition::new(x, z)), loaded)
92+
}
93+
#[test]
94+
fn normal_function() {
95+
let lock = empty_lock(0, 0, true);
96+
for _ in 0..100 {
97+
// It should be possible to lock in any way
98+
if rand::random::<bool>() {
99+
let _guard = lock.try_read().unwrap();
100+
} else {
101+
let _guard = lock.try_write().unwrap();
102+
}
103+
}
104+
}
105+
#[test]
106+
fn cannot_write_unloaded() {
107+
let lock = empty_lock(0, 0, false);
108+
assert!(lock.try_write().is_none())
109+
}
110+
#[test]
111+
fn can_read_unloaded() {
112+
let lock = empty_lock(0, 0, false);
113+
assert!(lock.try_read().is_some())
114+
}
115+
#[test]
116+
fn multithreaded() {
117+
let lock = Arc::new(empty_lock(0, 0, true));
118+
let mut handles: Vec<JoinHandle<()>> = vec![];
119+
for _ in 0..20 {
120+
let l = lock.clone();
121+
handles.push(spawn(move || {
122+
while let Some(guard) = l.write() {
123+
sleep(Duration::from_millis(10));
124+
drop(guard)
125+
}
126+
}))
127+
}
128+
sleep(Duration::from_millis(1000));
129+
lock.set_unloaded().unwrap_or(()); // Discard error
130+
for h in handles {
131+
h.join().unwrap() // Wait for all threads to stop
132+
}
133+
}
134+
}

feather/base/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ use serde::{Deserialize, Serialize};
1111

1212
pub mod anvil;
1313
pub mod chunk;
14+
pub mod chunk_lock;
1415
pub mod inventory;
1516
pub mod metadata;
1617

1718
pub use blocks::*;
1819
pub use chunk::{Chunk, ChunkSection, CHUNK_HEIGHT, CHUNK_WIDTH};
20+
pub use chunk_lock::*;
1921
pub use generated::{Area, Biome, EntityKind, Inventory, Item, ItemStack};
2022
pub use libcraft_blocks::{BlockKind, BlockState};
2123
pub use libcraft_core::{position, vec3, BlockPosition, ChunkPosition, Gamemode, Position, Vec3d};

feather/common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ uuid = { version = "0.8", features = [ "v4" ] }
2222
libcraft-core = { path = "../../libcraft/core" }
2323
rayon = "1.5"
2424
worldgen = { path = "../worldgen", package = "feather-worldgen" }
25+
rand = "0.8"

0 commit comments

Comments
 (0)