1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
use specs;
use grid::{GridCoord, GridPoint3, PosInOwningRoot};
use globe::ChunkOrigin;
use globe::origin_of_chunk_owning;
use globe::chunk_pair::PointPair;
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum Material {
Air,
Dirt,
Water,
}
// TODO: we should actually have multiple different
// kinds of Voxmaps. "Chunk" should refer to the coarse
// entity that owns everything related to a conveniently
// sized partition of the world that would be loaded and
// unloaded into the world as a unit.
#[derive(Clone, Copy, Debug)]
pub struct Cell {
pub material: Material,
pub shade: f32,
}
// Stores from (0, 0) to (chunk_resolution, chunk_resolution) _inclusive_.
//
// Storage layout and cell ownership rules are mostly following:
// <http://kiwi.atmos.colostate.edu/BUGS/geodesic/text.html>.
// They seem to have a pretty good grasp on these things. :)
pub struct Chunk {
pub origin: ChunkOrigin,
pub chunk_resolution: [GridCoord; 3],
// Sorted by (z, y, x).
pub cells: Vec<Cell>,
pub view_entity: Option<specs::Entity>,
// Incremented whenever authoritative data in this chunk that is shared with other chunks is updated.
// This way even if a neighboring chunk was not loaded when we update this chunk,
// we can detect later that it is out-of-date.
//
// The first version is 1, so we can use 0 to represent "no last known version"
// of our neighboring chunks.
pub owned_edge_version: u64,
// Neighbors that are the source of truth for some of the cells on the border of this chunk.
//
// TODO: when we switch to froggy storage, wrap both of these in a struct that
// stores a weak pointer to where we think the chunk might be loaded,
// so that we don't need to look it up in a hash map.
pub upstream_neighbors: Vec<UpstreamNeighbor>,
// Neighbors that share some cells on the border of this chunk but don't own them.
pub downstream_neighbors: Vec<DownstreamNeighbor>,
pub is_view_dirty: bool,
// Chunks that are directly accessible from the given chunk via a single,
// step between cells, including this chunk itself.
//
// Note that this might not be adequate to find all chunks accessible via a single
// user action, e.g., stepping a `CellDweller`, because that action might lead
// to movement across multiple cells -- in this case, stepping up a block moves
// one space forward, and one up, so take care if using this to ensure the relevant
// chunks are loaded.
//
// TODO: unlike `authoritative_neighbors`, which tracks version numbers,
// this doesn't really belong here. I'm just storing it here for now because
// it's inefficient to compute and most of the time when you'll want it, you'll already
// have the chunk loaded.
//
// TODO: look at clients, and consider whether a separate lazily-cached
// map of these on Globe might be more appropriate.
pub accessible_chunks: Vec<ChunkOrigin>,
}
impl Chunk {
pub fn new(
origin: ChunkOrigin,
cells: Vec<Cell>,
root_resolution: [GridCoord; 2],
chunk_resolution: [GridCoord; 3],
) -> Chunk {
let mut chunk = Chunk {
origin: origin,
cells: cells,
chunk_resolution: chunk_resolution,
view_entity: None,
owned_edge_version: 1,
upstream_neighbors: Vec::new(),
downstream_neighbors: Vec::new(),
is_view_dirty: true,
accessible_chunks: Self::list_accessible_chunks(
origin,
root_resolution,
chunk_resolution,
),
};
chunk.populate_neighboring_chunks(root_resolution);
chunk
}
/// Find and store the origins of all upstream and downstream neighbor chunks.
///
/// Panics if called more than once; it is for initialization only.
fn populate_neighboring_chunks(&mut self, root_resolution: [GridCoord; 2]) {
use grid::EquivalentPoints;
use grid::semi_arbitrary_compare;
use globe::ChunksInSameRootContainingPoint;
if self.upstream_neighbors.len() > 0 || self.downstream_neighbors.len() > 0 {
panic!("Tried to initialize chunk multiple times.");
}
// Map neighbor chunk origins to neighbors for easy lookup during construction.
use std::collections::HashMap;
use globe::ChunkSharedPoints;
let mut upstream_neighbors_by_origin = HashMap::<ChunkOrigin, UpstreamNeighbor>::new();
let mut downstream_neighbors_by_origin = HashMap::<ChunkOrigin, DownstreamNeighbor>::new();
// For every point on the edge of this chunk, find all the chunks that contain it,
// taking note that some of those might be in other roots if the point is on the edge of a root.
// Then, depending on whether or not we own the point, add it to a downstream neighbor
// or an upstream neighbor.
let shared_points = ChunkSharedPoints::new(self.origin, self.chunk_resolution);
for our_point in shared_points {
// Find what chunk this belongs to.
let our_point_in_owning_root = PosInOwningRoot::new(our_point, root_resolution);
let owning_chunk_origin = origin_of_chunk_owning(
our_point_in_owning_root,
root_resolution,
self.chunk_resolution,
);
let we_own_this_point = owning_chunk_origin == self.origin;
// TODO: wrap this up as an `AllChunksContainingPoint` iterator.
// TODO: this is perfect as an "easy"-tagged github issue. Maybe try carving some of these off.
let equivalent_points = EquivalentPoints::new(our_point, root_resolution);
for equivalent_point in equivalent_points {
let containing_chunks = ChunksInSameRootContainingPoint::new(
equivalent_point,
root_resolution,
self.chunk_resolution,
);
for chunk_origin in containing_chunks {
if chunk_origin == self.origin {
// We're looking at the same chunk; we'll never need to copy to/from self!
continue;
}
if we_own_this_point {
// We own the cell; ensure there's a record for the downstream chunk,
// and then add the pair of representations of the same point to the list
// of cells that need to be synced.
let downstream_neighbor = downstream_neighbors_by_origin
.entry(chunk_origin)
.or_insert(DownstreamNeighbor {
origin: chunk_origin,
shared_cells: Vec::new(),
});
downstream_neighbor.shared_cells.push(PointPair {
source: our_point_in_owning_root,
sink: equivalent_point,
});
} else if owning_chunk_origin == chunk_origin {
// The other chunk owns the cell; ensure there's a record for the upstream chunk,
// and then add the pair of representations of the same point to the list
// of cells that need to be synced.
let upstream_neighbor = upstream_neighbors_by_origin
.entry(chunk_origin)
.or_insert(UpstreamNeighbor {
origin: chunk_origin,
shared_cells: Vec::new(),
});
let equivalent_point_in_owning_root =
PosInOwningRoot::new(equivalent_point, root_resolution);
upstream_neighbor.shared_cells.push(PointPair {
source: equivalent_point_in_owning_root,
sink: our_point,
});
}
}
}
}
// We'll usually just want to iterate over these. No need to store
// as a hash map beyond building it.
//
// Sort the points inside these for cache-friendliness.
self.upstream_neighbors = upstream_neighbors_by_origin.values().cloned().collect();
for upstream_neighbor in &mut self.upstream_neighbors {
upstream_neighbor.shared_cells.sort_by(|point_pair_a,
point_pair_b| {
// Comparing by source points should give us some tiny cache locality benefit.
// So would sorting by sink points, but hopefully better one sorted than neither.
// TODO: check whether this helps at all.
semi_arbitrary_compare(point_pair_a.source.pos(), &point_pair_b.source.pos())
});
}
self.downstream_neighbors = downstream_neighbors_by_origin.values().cloned().collect();
for downstream_neighbor in &mut self.downstream_neighbors {
downstream_neighbor.shared_cells.sort_by(|point_pair_a,
point_pair_b| {
// Comparing by sink points should give us some tiny cache locality benefit.
// So would sorting by source points, but hopefully better one sorted than neither.
// TODO: check whether this helps at all.
semi_arbitrary_compare(&point_pair_a.sink, &point_pair_b.sink)
});
}
}
// Panics or returns nonsense if given coordinates of a cell we don't have data for.
//
// TODO: _store_ more information to make lookups cheaper.
fn cell_index(&self, pos: GridPoint3) -> usize {
let local_x = pos.x - self.origin.pos().x;
let local_y = pos.y - self.origin.pos().y;
let local_z = pos.z - self.origin.pos().z;
let r = self.chunk_resolution;
let plane_offset = local_z * (r[0] + 1) * (r[1] + 1);
let row_offset = local_y * (r[0] + 1);
let cell_offset = local_x;
(plane_offset + row_offset + cell_offset) as usize
}
/// Returns `true` if the given `pos` lies within the bounds of this chunk,
/// or `false` otherwise.
///
/// Note that this does not consider whether or not this chunk _owns_ the
/// cell at `pos`.
pub fn contains_pos(&self, pos: GridPoint3) -> bool {
// Chunks don't share cells in the z-direction,
// but do in the x- and y-directions.
let end_x = self.origin.pos().x + self.chunk_resolution[0];
let end_y = self.origin.pos().y + self.chunk_resolution[1];
let end_z = self.origin.pos().z + self.chunk_resolution[2] - 1;
let contains_x = pos.x >= self.origin.pos().x && pos.x <= end_x;
let contains_y = pos.y >= self.origin.pos().y && pos.y <= end_y;
let contains_z = pos.z >= self.origin.pos().z && pos.z <= end_z;
contains_x && contains_y && contains_z
}
/// Most `Chunks`s will have an associated `ChunkView`. Indicate that the
/// chunk has been modified since the view was last updated.
pub fn mark_view_as_dirty(&mut self) {
self.is_view_dirty = true;
}
/// Most `Chunks`s will have an associated `ChunkView`. Indicate that the
/// view has been updated since the chunk was last modified.
pub fn mark_view_as_clean(&mut self) {
self.is_view_dirty = false;
}
fn list_accessible_chunks(
origin: ChunkOrigin,
root_resolution: [GridCoord; 2],
chunk_resolution: [GridCoord; 3],
) -> Vec<ChunkOrigin> {
// Keep track of which chunk origins we've seen.
use std::collections::HashSet;
let mut chunk_origins = HashSet::<ChunkOrigin>::new();
// For every cell at a corner of this chunk, find all its neighbors,
// and add the origins of their chunks.
for (x, y, z) in iproduct!(
&[origin.pos().x, origin.pos().x + chunk_resolution[0]],
&[origin.pos().y, origin.pos().y + chunk_resolution[1]],
// Chunks don't share cells in the z-direction,
// but do in the x- and y-directions.
&[origin.pos().z, origin.pos().z + chunk_resolution[2] - 1]
)
{
let corner_pos = GridPoint3::new(origin.pos().root, *x, *y, *z);
// Find all its neighbors and their chunks' origins.
//
// TODO: does Neighbors actually guarantee that we'll get chunks
// from the roots we intend? I don't think so! Maybe we should introduce
// some way to explicitly list all the equivalent representations of
// a pos and use that instead here, because looking at "neighbors"
// here is actually just a hack workaround for not having that.
//
// TODO: that actually exists now! See `EquivalentPoints`.
use grid::Neighbors;
use super::origin_of_chunk_in_same_root_containing;
let neighbors = Neighbors::new(corner_pos, root_resolution);
for neighbor in neighbors {
let neighbor_chunk_origin = origin_of_chunk_in_same_root_containing(
neighbor,
root_resolution,
chunk_resolution,
);
chunk_origins.insert(neighbor_chunk_origin);
}
}
// We'll usually just want to iterate over these. No need to store
// as a hash map beyond building it.
chunk_origins.iter().cloned().collect()
}
}
impl<'a> Chunk {
// TODO: replace these with two variants:
// - one that clearly is asking for authoritative data,
// and requires a PosInOwningChunk (does not yet exist)
// - one that is happy to get any old version of the cell.
// Panics if given coordinates of a cell we don't have data for.
pub fn cell(&'a self, pos: GridPoint3) -> &'a Cell {
let cell_i = self.cell_index(pos);
&self.cells[cell_i]
}
// Panics if given coordinates of a cell we don't have data for.
pub fn cell_mut(&'a mut self, pos: GridPoint3) -> &'a mut Cell {
let cell_i = self.cell_index(pos);
&mut self.cells[cell_i]
}
}
#[derive(Clone)]
pub struct UpstreamNeighbor {
// TODO: visibility? pub(::globe)?
pub origin: ChunkOrigin,
// List of positions owned by the neighbor (source), specified in both source and sink.
pub shared_cells: Vec<PointPair>,
}
#[derive(Clone)]
pub struct DownstreamNeighbor {
// TODO: visibility? pub(::globe)?
pub origin: ChunkOrigin,
// List of positions owned by this chunk (source), specified in both source and sink.
pub shared_cells: Vec<PointPair>,
}