Grid based compile error for surrounding neighbours

Hi I have been strugging with this simple issue for over a week and I can't get anywhere. Please could you help.

So to give some context: I am making a minecraft clone for learning. I have chunks of blocks that are in a hashmap. For a block, the light level needs to be calculated by looking at the neighbouring blocks, which may be in other chunks. I want to have a reference to the main chunk for speed, and then only look in the hashmap when the surrounding block falls off the current chunk.

**The problem is that I can't have access to the main chunk (that I'm writing to) at the same time as looking at surrounding chunks, because I'm accessing the same hashmap with two references to it **

image

You could just say, why don't you treat the middle chunk like the surrounding chunks and always look up the chunk from the dictionary to get to the block: But that is very slow.

pub fn calculate_chunk_light_level(mut chunks: &mut std::collections::HashMap<String, Box<Chunk>>, chunk_pos: IVec3, block_metadata: &HashMap<BlockType, BlockMetadata>) {

    let mut air_blocks_in_darkness: Vec<[usize; 3]> = Vec::new();

    let chunk_corner_world_pos = Chunk::get_chunk_pos_as_i_world_pos(chunk_pos);


    let main_chunk: &mut Chunk = get_chunk_from_chunk_pos_mut(&mut chunks, chunk_pos);
    
    // Go from top to bottom, and illuminate
    for x in 0..CHUNK_WIDTH_X {
        for z in 0..CHUNK_DEPTH_Z {
            let mut in_darkness = false;
            for inv_y in 0..CHUNK_HEIGHT_Y {
                let y = CHUNK_HEIGHT_Y - inv_y - 1;

                let block: &mut Block = &mut main_chunk.blocks[x][y][z];

                if block_metadata[&block.block_type].is_see_through == false {
                    in_darkness = true;
                }

                if !in_darkness {
                    block.light_evaluated = true;
                    block.light_level = 1.0;
                } else if block_metadata[&block.block_type].is_see_through { // In darkness and air then we can set it to be iterated
                    air_blocks_in_darkness.push([x, y, z]);
                }
            }
        }  
    }

    let mut next_light_levels: Vec<f32> = Vec::with_capacity(air_blocks_in_darkness.len());

    // Cellular automata, for 30 iterations
    for i in 0..30 {
        for air_block in air_blocks_in_darkness.iter() {

            let mut highest_surrounding_light_level: f32 = 0.0;
            for dir in NEIGHBOUR_ITERATOR { // This just iterates through [(-1,-1), (-1, 0), (1, 0) etc... ]
                let other_local_block_pos = IVec3::new(
                    air_block[0] as i32 + dir[0],
                    air_block[1] as i32 + dir[1],
                    air_block[2] as i32 + dir[2]);

                if is_vec_on_chunk(other_local_block_pos){
                    let other_block: &Block = &main_chunk.blocks[other_local_block_pos.x as usize][other_local_block_pos.z as usize][other_local_block_pos.y as usize];
                    if block_metadata[&other_block.block_type].is_see_through {
                        if other_block.light_level >  highest_surrounding_light_level {
                            highest_surrounding_light_level = other_block.light_level;
                        }
                    }
                } else { // Not on chunk so must be from surrounding
// THE BELOW LINE IS THE PROBLEM, BECAUSE IM ACCESSING CHUNKS AGAIN
                    let other_block_option = get_block_from_world_pos(chunks, chunk_corner_world_pos + other_local_block_pos);
                    match other_block_option {
                        Some(other_block) =>{
                            if block_metadata[&other_block.block_type].is_see_through {
                                if other_block.light_level >  highest_surrounding_light_level {
                                    highest_surrounding_light_level = other_block.light_level;
                                }
                            }
                        },
                        None =>{ }
                    }
                }            
            }

            // Move in the direction
            let next =  highest_surrounding_light_level - 0.05;// TODO: The darker it gets the less it takes off      
            next_light_levels.push(next);
        }
    
        // transfer next to current
        for (index, air_block) in air_blocks_in_darkness.iter().enumerate() {
            main_chunk.blocks[air_block[0]][air_block[1]][air_block[2]].light_level = next_light_levels[index];
        }


        next_light_levels.clear();
    }

    
}

If you need mut:

hashbrown has a method for querying multiple keys especially for this purpose:

Otherwise you'll probably need unsafe here to override this (cast hashmap Tile reference to one with a new lifetime). Borrow checking can't be based on values, so the borrow checker won't know that you have two non-overlapping elements at run time.

https://blog.rom1v.com/2019/04/implementing-tile-encoding-in-rav1e/

If you don't use mut:

Alternative solution is to use AtomicBool or RwLock in each cell, so that you get shared references (& intead of &mut) and then you'll be able to mutate atomic or locked data at any time.

4 Likes

That's unsound (run with Miri).

1 Like

This seems to be okay with miri:

fn main() {
    let mut hm: HashMap<Tile, Tile> = [(Tile(0), Tile(0)), (Tile(1), Tile(1))].into();
    let a = hm.get_mut(&Tile(0)).unwrap() as *mut Tile;
    let b = hm.get_mut(&Tile(1)).unwrap();
    let a = unsafe { a.as_mut() }.unwrap();
    println!("{a:?}{b:?}");
}
1 Like

BTW, if instead of a HashMap you will use Vec to store all tiles row by row, then you can use chunks_mut or split_at_mut to have safe mutable access to separate rows.

1 Like

Pedantically, HashMap::get_mut() doesn't promise that it won't use its &mut Self access to move items (some sort of incremental rebucketing, perhaps?) before returning a reference, so the *mut Tile you saved isn't guaranteed to stay valid.

2 Likes