mirror of
https://github.com/griffi-gh/kubi.git
synced 2024-11-26 16:58:48 -06:00
initial impl
This commit is contained in:
parent
be1e24ba0c
commit
2466c02937
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
.direnv
|
||||||
|
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
debug/
|
debug/
|
||||||
|
@ -15,6 +17,7 @@ _src
|
||||||
_visualizer.json
|
_visualizer.json
|
||||||
|
|
||||||
*.kubi
|
*.kubi
|
||||||
|
*.kbi
|
||||||
|
|
||||||
/*_log*.txt
|
/*_log*.txt
|
||||||
/*.log
|
/*.log
|
||||||
|
@ -37,4 +40,4 @@ _visualizer.json
|
||||||
|
|
||||||
/mods
|
/mods
|
||||||
|
|
||||||
.direnv
|
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1307,8 +1307,10 @@ dependencies = [
|
||||||
"bincode",
|
"bincode",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"fastnoise-lite",
|
"fastnoise-lite",
|
||||||
|
"flume",
|
||||||
"glam",
|
"glam",
|
||||||
"hashbrown 0.14.5",
|
"hashbrown 0.14.5",
|
||||||
|
"log",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"nz",
|
"nz",
|
||||||
|
|
|
@ -14,6 +14,7 @@ serde = { version = "1.0", default-features = false, features = ["alloc", "deriv
|
||||||
serde_with = "3.4"
|
serde_with = "3.4"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
flume = "0.11"
|
||||||
fastnoise-lite = { version = "1.1", features = ["std", "f64"] }
|
fastnoise-lite = { version = "1.1", features = ["std", "f64"] }
|
||||||
rand = { version = "0.8", default_features = false, features = ["std", "min_const_gen"] }
|
rand = { version = "0.8", default_features = false, features = ["std", "min_const_gen"] }
|
||||||
rand_xoshiro = "0.6"
|
rand_xoshiro = "0.6"
|
||||||
|
@ -23,6 +24,7 @@ bytemuck = { version = "1.14", features = ["derive"] }
|
||||||
static_assertions = "1.1"
|
static_assertions = "1.1"
|
||||||
nz = "0.4"
|
nz = "0.4"
|
||||||
atomic = "0.6"
|
atomic = "0.6"
|
||||||
|
log = "0.4"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
|
@ -17,6 +17,8 @@ use crate::{
|
||||||
chunk::{CHUNK_SIZE, BlockDataRef, BlockData}
|
chunk::{CHUNK_SIZE, BlockDataRef, BlockData}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod io_thread;
|
||||||
|
|
||||||
const SECTOR_SIZE: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE * size_of::<Block>();
|
const SECTOR_SIZE: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE * size_of::<Block>();
|
||||||
const RESERVED_SIZE: usize = 1048576; //~1mb (16 sectors assuming 32x32x32 world of 1byte blocks)
|
const RESERVED_SIZE: usize = 1048576; //~1mb (16 sectors assuming 32x32x32 world of 1byte blocks)
|
||||||
const RESERVED_SECTOR_COUNT: usize = RESERVED_SIZE / SECTOR_SIZE;
|
const RESERVED_SECTOR_COUNT: usize = RESERVED_SIZE / SECTOR_SIZE;
|
||||||
|
|
166
kubi-shared/src/data/io_thread.rs
Normal file
166
kubi-shared/src/data/io_thread.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use glam::IVec3;
|
||||||
|
use flume::{Receiver, Sender, TryIter};
|
||||||
|
use shipyard::Unique;
|
||||||
|
use crate::chunk::BlockData;
|
||||||
|
|
||||||
|
use super::WorldSaveFile;
|
||||||
|
|
||||||
|
pub enum IOCommand {
|
||||||
|
SaveChunk {
|
||||||
|
position: IVec3,
|
||||||
|
data: BlockData,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Load a chunk from the disk and send it to the main thread
|
||||||
|
LoadChunk {
|
||||||
|
position: IVec3,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Process all pending write commands and make the thread end itself
|
||||||
|
/// LoadChunk commands will be ignored after this command is received
|
||||||
|
Kys,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub enum IOResponse {
|
||||||
|
/// A chunk has been loaded from the disk
|
||||||
|
/// Or not, in which case the data will be None
|
||||||
|
/// and chunk should be generated
|
||||||
|
ChunkLoaded {
|
||||||
|
position: IVec3,
|
||||||
|
data: Option<BlockData>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The IO thread has been terminated
|
||||||
|
Terminated,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IOThreadContext {
|
||||||
|
tx: Sender<IOResponse>,
|
||||||
|
rx: Receiver<IOCommand>,
|
||||||
|
save: WorldSaveFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Implement proper error handling (I/O errors are rlly common)
|
||||||
|
|
||||||
|
impl IOThreadContext {
|
||||||
|
/// Should be called ON the IO thread
|
||||||
|
///
|
||||||
|
/// Initializes the IO thread context, opening the file at the given path
|
||||||
|
/// If there's an error opening the file, the thread will panic (TODO: handle this more gracefully)
|
||||||
|
pub fn initialize(
|
||||||
|
tx: Sender<IOResponse>,
|
||||||
|
rx: Receiver<IOCommand>,
|
||||||
|
save: WorldSaveFile,
|
||||||
|
) -> Self {
|
||||||
|
// save.load_data().unwrap();
|
||||||
|
Self { tx, rx, save }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) {
|
||||||
|
loop {
|
||||||
|
match self.rx.recv().unwrap() {
|
||||||
|
IOCommand::SaveChunk { position, data } => {
|
||||||
|
self.save.save_chunk(position, &data).unwrap();
|
||||||
|
}
|
||||||
|
IOCommand::LoadChunk { position } => {
|
||||||
|
let data = self.save.load_chunk(position).unwrap();
|
||||||
|
self.tx.send(IOResponse::ChunkLoaded { position, data }).unwrap();
|
||||||
|
}
|
||||||
|
IOCommand::Kys => {
|
||||||
|
// Process all pending write commands
|
||||||
|
while let IOCommand::SaveChunk { position, data } = self.rx.recv().unwrap() {
|
||||||
|
self.save.save_chunk(position, &data).unwrap();
|
||||||
|
}
|
||||||
|
self.tx.send(IOResponse::Terminated).unwrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IOSingleThread {
|
||||||
|
tx: Sender<IOCommand>,
|
||||||
|
rx: Receiver<IOResponse>,
|
||||||
|
handle: std::thread::JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IOSingleThread {
|
||||||
|
pub fn spawn(save: WorldSaveFile) -> Self {
|
||||||
|
// Create channels
|
||||||
|
let (command_tx, command_rx) = flume::unbounded();
|
||||||
|
let (response_tx, response_rx) = flume::unbounded();
|
||||||
|
|
||||||
|
// Spawn the thread
|
||||||
|
let builder = std::thread::Builder::new()
|
||||||
|
.name("io-thread".into());
|
||||||
|
let handle = builder.spawn(move || {
|
||||||
|
let context = IOThreadContext::initialize(response_tx, command_rx, save);
|
||||||
|
context.run();
|
||||||
|
}).unwrap();
|
||||||
|
|
||||||
|
IOSingleThread {
|
||||||
|
tx: command_tx,
|
||||||
|
rx: response_rx,
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a command to the IO thread
|
||||||
|
pub fn send(&self, cmd: IOCommand) {
|
||||||
|
self.tx.send(cmd).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll the IO thread for responses (non-blocking)
|
||||||
|
pub fn poll(&self) -> TryIter<IOResponse> {
|
||||||
|
self.rx.try_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signal the IO thread to process the remaining requests and wait for it to terminate
|
||||||
|
pub fn stop_sync(&self) {
|
||||||
|
// Tell the thread to terminate and wait for it to finish
|
||||||
|
self.tx.send(IOCommand::Kys).unwrap();
|
||||||
|
while !matches!(self.rx.recv().unwrap(), IOResponse::Terminated) {}
|
||||||
|
|
||||||
|
// HACK "we have .join at home"
|
||||||
|
while !self.handle.is_finished() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as stop_sync but doesn't wait for the IO thread to terminate
|
||||||
|
pub fn stop_async(&self) {
|
||||||
|
self.tx.send(IOCommand::Kys).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for IOSingleThread {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop_sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// This is a stub for future implemntation that may use multiple IO threads
|
||||||
|
#[derive(Unique)]
|
||||||
|
pub struct IOThreadManager {
|
||||||
|
thread: IOSingleThread,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IOThreadManager {
|
||||||
|
pub fn new(save: WorldSaveFile) -> Self {
|
||||||
|
Self {
|
||||||
|
thread: IOSingleThread::spawn(save)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, cmd: IOCommand) {
|
||||||
|
self.thread.send(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn poll(&self) -> TryIter<IOResponse> {
|
||||||
|
self.thread.poll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// i think im a girl :3 (noone will ever read this right? :p)
|
||||||
|
|
|
@ -5,14 +5,15 @@ use crate::{
|
||||||
networking::{GameType, ServerAddress},
|
networking::{GameType, ServerAddress},
|
||||||
state::{GameState, NextState}
|
state::{GameState, NextState}
|
||||||
};
|
};
|
||||||
use kubi_shared::data::WorldSaveFile;
|
use kubi_shared::data::{io_thread::IOThreadManager, WorldSaveFile};
|
||||||
|
|
||||||
fn open_local_save_file(path: &Path) -> Result<WorldSaveFile> {
|
fn open_local_save_file(path: &Path) -> Result<WorldSaveFile> {
|
||||||
let mut save_file = WorldSaveFile::new({
|
let mut save_file = WorldSaveFile::new({
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
.open("world.kbi")?
|
.create(true)
|
||||||
|
.open(path)?
|
||||||
});
|
});
|
||||||
if save_file.file.metadata().unwrap().len() == 0 {
|
if save_file.file.metadata().unwrap().len() == 0 {
|
||||||
save_file.initialize()?;
|
save_file.initialize()?;
|
||||||
|
@ -25,13 +26,20 @@ fn open_local_save_file(path: &Path) -> Result<WorldSaveFile> {
|
||||||
pub fn initialize_from_args(
|
pub fn initialize_from_args(
|
||||||
all_storages: AllStoragesView,
|
all_storages: AllStoragesView,
|
||||||
) {
|
) {
|
||||||
|
// If an address is provided, we're in multiplayer mode (the first argument is the address)
|
||||||
|
// Otherwise, we're in singleplayer mode and working with local stuff
|
||||||
let args: Vec<String> = env::args().collect();
|
let args: Vec<String> = env::args().collect();
|
||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
|
// Parse the address and switch the state to connecting
|
||||||
let address = args[1].parse::<SocketAddr>().expect("invalid address");
|
let address = args[1].parse::<SocketAddr>().expect("invalid address");
|
||||||
all_storages.add_unique(GameType::Muliplayer);
|
all_storages.add_unique(GameType::Muliplayer);
|
||||||
all_storages.add_unique(ServerAddress(address));
|
all_storages.add_unique(ServerAddress(address));
|
||||||
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::Connecting);
|
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::Connecting);
|
||||||
} else {
|
} else {
|
||||||
|
// Open the local save file
|
||||||
|
let save_file = open_local_save_file(Path::new("./world.kubi")).expect("failed to open save file");
|
||||||
|
all_storages.add_unique(IOThreadManager::new(save_file));
|
||||||
|
// Switch the state and kick off the world loading
|
||||||
all_storages.add_unique(GameType::Singleplayer);
|
all_storages.add_unique(GameType::Singleplayer);
|
||||||
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::LoadingWorld);
|
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::LoadingWorld);
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ pub fn inject_network_responses_into_manager_queue(
|
||||||
let ServerToClientMessage::ChunkResponse {
|
let ServerToClientMessage::ChunkResponse {
|
||||||
chunk, data, queued
|
chunk, data, queued
|
||||||
} = packet else { unreachable!() };
|
} = packet else { unreachable!() };
|
||||||
manager.add_sussy_response(ChunkTaskResponse::LoadedChunk {
|
manager.add_sussy_response(ChunkTaskResponse::ChunkWorldgenDone {
|
||||||
position: chunk,
|
position: chunk,
|
||||||
chunk_data: data,
|
chunk_data: data,
|
||||||
queued
|
queued
|
||||||
|
|
|
@ -8,7 +8,7 @@ use wgpu::util::DeviceExt;
|
||||||
use crate::{
|
use crate::{
|
||||||
networking::UdpClient,
|
networking::UdpClient,
|
||||||
player::MainPlayer,
|
player::MainPlayer,
|
||||||
rendering::{world::ChunkVertex, BufferPair, Renderer},
|
rendering::{BufferPair, Renderer},
|
||||||
settings::GameSettings,
|
settings::GameSettings,
|
||||||
state::GameState,
|
state::GameState,
|
||||||
transform::Transform,
|
transform::Transform,
|
||||||
|
@ -185,7 +185,7 @@ fn process_state_changes(
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let atomic = Arc::new(Atomic::new(AbortState::Continue));
|
let atomic = Arc::new(Atomic::new(AbortState::Continue));
|
||||||
task_manager.spawn_task(ChunkTask::LoadChunk {
|
task_manager.spawn_task(ChunkTask::ChunkWorldgen {
|
||||||
seed: 0xbeef_face_dead_cafe,
|
seed: 0xbeef_face_dead_cafe,
|
||||||
position,
|
position,
|
||||||
abortion: Some(Arc::clone(&atomic)),
|
abortion: Some(Arc::clone(&atomic)),
|
||||||
|
@ -273,7 +273,7 @@ fn process_completed_tasks(
|
||||||
let mut ops: usize = 0;
|
let mut ops: usize = 0;
|
||||||
while let Some(res) = task_manager.receive() {
|
while let Some(res) = task_manager.receive() {
|
||||||
match res {
|
match res {
|
||||||
ChunkTaskResponse::LoadedChunk { position, chunk_data, mut queued } => {
|
ChunkTaskResponse::ChunkWorldgenDone { position, chunk_data, mut queued } => {
|
||||||
//If unwanted chunk is already loaded
|
//If unwanted chunk is already loaded
|
||||||
//It would be ~~...unethical~~ impossible to abort the operation at this point
|
//It would be ~~...unethical~~ impossible to abort the operation at this point
|
||||||
//Instead, we'll just throw it away
|
//Instead, we'll just throw it away
|
||||||
|
@ -308,7 +308,7 @@ fn process_completed_tasks(
|
||||||
//increase ops counter
|
//increase ops counter
|
||||||
ops += 1;
|
ops += 1;
|
||||||
},
|
},
|
||||||
ChunkTaskResponse::GeneratedMesh {
|
ChunkTaskResponse::GenerateMeshDone {
|
||||||
position,
|
position,
|
||||||
vertices, indices,
|
vertices, indices,
|
||||||
trans_vertices, trans_indices,
|
trans_vertices, trans_indices,
|
||||||
|
|
|
@ -13,7 +13,7 @@ use super::{
|
||||||
use crate::rendering::world::ChunkVertex;
|
use crate::rendering::world::ChunkVertex;
|
||||||
|
|
||||||
pub enum ChunkTask {
|
pub enum ChunkTask {
|
||||||
LoadChunk {
|
ChunkWorldgen {
|
||||||
seed: u64,
|
seed: u64,
|
||||||
position: IVec3,
|
position: IVec3,
|
||||||
abortion: Option<Arc<Atomic<AbortState>>>,
|
abortion: Option<Arc<Atomic<AbortState>>>,
|
||||||
|
@ -23,13 +23,14 @@ pub enum ChunkTask {
|
||||||
data: MeshGenData
|
data: MeshGenData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ChunkTaskResponse {
|
pub enum ChunkTaskResponse {
|
||||||
LoadedChunk {
|
ChunkWorldgenDone {
|
||||||
position: IVec3,
|
position: IVec3,
|
||||||
chunk_data: BlockData,
|
chunk_data: BlockData,
|
||||||
queued: Vec<QueuedBlock>
|
queued: Vec<QueuedBlock>
|
||||||
},
|
},
|
||||||
GeneratedMesh {
|
GenerateMeshDone {
|
||||||
position: IVec3,
|
position: IVec3,
|
||||||
vertices: Vec<ChunkVertex>,
|
vertices: Vec<ChunkVertex>,
|
||||||
indices: Vec<u32>,
|
indices: Vec<u32>,
|
||||||
|
@ -43,6 +44,7 @@ pub struct ChunkTaskManager {
|
||||||
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
|
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
|
||||||
pool: ThreadPool,
|
pool: ThreadPool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChunkTaskManager {
|
impl ChunkTaskManager {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -50,11 +52,17 @@ impl ChunkTaskManager {
|
||||||
pool: ThreadPoolBuilder::new().num_threads(4).build().unwrap()
|
pool: ThreadPoolBuilder::new().num_threads(4).build().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO get rid of add_sussy_response
|
||||||
|
|
||||||
|
/// Add a response to the queue, to be picked up by the main thread
|
||||||
|
/// Used by the multiplayer netcode, a huge hack
|
||||||
pub fn add_sussy_response(&self, response: ChunkTaskResponse) {
|
pub fn add_sussy_response(&self, response: ChunkTaskResponse) {
|
||||||
// this WILL get stuck if the channel is bounded
|
// this WILL get stuck if the channel is bounded
|
||||||
// don't make the channel bounded ever
|
// don't make the channel bounded ever
|
||||||
self.channel.0.send(response).unwrap()
|
self.channel.0.send(response).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_task(&self, task: ChunkTask) {
|
pub fn spawn_task(&self, task: ChunkTask) {
|
||||||
let sender = self.channel.0.clone();
|
let sender = self.channel.0.clone();
|
||||||
self.pool.spawn(move || {
|
self.pool.spawn(move || {
|
||||||
|
@ -64,22 +72,23 @@ impl ChunkTaskManager {
|
||||||
(vertices, indices),
|
(vertices, indices),
|
||||||
(trans_vertices, trans_indices),
|
(trans_vertices, trans_indices),
|
||||||
) = generate_mesh(position, data);
|
) = generate_mesh(position, data);
|
||||||
ChunkTaskResponse::GeneratedMesh {
|
ChunkTaskResponse::GenerateMeshDone {
|
||||||
position,
|
position,
|
||||||
vertices, indices,
|
vertices, indices,
|
||||||
trans_vertices, trans_indices,
|
trans_vertices, trans_indices,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ChunkTask::LoadChunk { position, seed, abortion } => {
|
ChunkTask::ChunkWorldgen { position, seed, abortion } => {
|
||||||
let Some((chunk_data, queued)) = generate_world(position, seed, abortion) else {
|
let Some((chunk_data, queued)) = generate_world(position, seed, abortion) else {
|
||||||
log::warn!("aborted operation");
|
log::warn!("aborted operation");
|
||||||
return
|
return
|
||||||
};
|
};
|
||||||
ChunkTaskResponse::LoadedChunk { position, chunk_data, queued }
|
ChunkTaskResponse::ChunkWorldgenDone { position, chunk_data, queued }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive(&self) -> Option<ChunkTaskResponse> {
|
pub fn receive(&self) -> Option<ChunkTaskResponse> {
|
||||||
self.channel.1.try_recv().ok()
|
self.channel.1.try_recv().ok()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue