Skip to content

Refactor graph expansion to remove the concept of "fresh" nodes #453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: master
Choose a base branch
from

Conversation

patowen
Copy link
Collaborator

@patowen patowen commented Apr 13, 2025

Note: The first few (6 as of the time I'm writing this) commits match #452, so this PR effectively branches off of #452, and only the later commits should be reviewed.

Since keeping the server and client graphs entirely in sync is one of the main reasons Graph needed to keep track of its freshly created nodes, and now they no longer need to be in sync this way, this PR removes this functionality to simplify Graph's usage. Specifically, each Node is initialized immediately when it's added to the graph instead of being initialized to None. The NodeState field, which is used in fewer places, is still kept optional, and instead of being populated for every fresh node, it is populated for a given NodeId with an ensure_node_state function.

Part of why NodeState is kept optional is that its initialization order will need to be more complicated for megastructures, although there may be a way in the future to make things simpler on that front as well.

Since this PR makes heavy changes to graph expansion, it also addresses most of #50 by making the "distance" computations correct. However, with the current level of optimization, it is impossible to avoid the following three problems: unacceptably thick fog, pop-in, and unacceptable performance issues. Therefore, we still allow pop-in by making fog distance, view distance, and chunk generation distance separate settings with different defaults in the config.

patowen added 8 commits April 10, 2025 22:27
This is instead of waiting for the modified chunk in question to be
added to the server's Graph data structure
With the current level of optimization in Hypermine, it is not possible
to simultaneously avoid pop-in, avoid unacceptable lag, and avoid
unacceptably thick fog. To get around this issue, we make allowing
pop-in explicit in the configuration
Switch from an `Option<Node>` with a `NodeState` to a `Node` with an
`Option<NodeState>`. This reduces the number of unwraps or error
handling needed by code that doesn't care about `NodeState`.
@patowen patowen force-pushed the refactor-graph-expansion branch from a5e2e92 to 891e2cc Compare April 13, 2025 19:03
@Ralith
Copy link
Owner

Ralith commented Apr 15, 2025

unacceptably thick fog

Tangential, but I wonder if we can find a better balance by using nonphysical fog that thickens rapidly but has a distant start point...

@@ -39,7 +39,7 @@ fn build_graph(c: &mut Criterion) {
c.bench_function("worldgen", |b| {
b.iter(|| {
let mut graph = Graph::new(12);
ensure_nearby(&mut graph, &Position::origin(), 3.0);
ensure_nearby(&mut graph, &Position::origin(), 2.25);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intent of this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure_nearby was modified to return more nodes for a given distance, so this benchmark needed to be adjusted accordingly.

pub view_distance: Option<f32>,
/// Maximum distance at which new chunks will be generated in meters
pub chunk_generation_distance: Option<f32>,
/// Maximum distance at which fog becomes completely opaque in meters
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/Maximum //. The distance that fog becomes opaque at is a single value, not a range.


/// Initializes the NodeState for the given node and all its ancestors if not
/// already initialized.
pub fn ensure_node_state(&mut self, node_id: NodeId) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We often seem to need to access the state immediately after ensuring it. Would that code be more concise if this function returned the &mut NodeState directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should only need to return a &NodeState, but otherwise, that seems like a good suggestion to me.

) {
graph.ensure_node_state(node_id);
}
let local_to_view = view.local.inverse();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: let's try to adopt a convention of x_from_y when naming transforms. This makes chains much easier to read and sanity-check because it matches the multiplication order: x_from_y * y_from_z is a lot more obviously correct than y_to_x * z_to_y.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that's an interesting approach. Maybe in a separate PR? The dodeca module has a lot of functions we would also want to change.

let Some(params) =
common::worldgen::ChunkParams::new(graph.layout().dimension(), graph, chunk_id)
else {
// Skip chunks beyond the chunk generation distance
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does nearby_nodes(..., chunk_generation_distance) already accomplish this? If it's not intended to, explain in a comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nearby_nodes only returns nodes. We would need a nearby_chunks function to be as precise as this.

let state = graph[chunk.node].state.as_ref()?;
Some(Self {
/// Extract data necessary to generate a chunk, generating new graph nodes if necessary
pub fn new(dimension: u8, graph: &mut Graph, chunk: ChunkId) -> Self {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the dimension argument in favor of fetching it from the graph directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought of that but also thought about leaving it for a future refactor. However, since I'm touching this code anyway, might as well.

@patowen
Copy link
Collaborator Author

patowen commented Apr 16, 2025

unacceptably thick fog

Tangential, but I wonder if we can find a better balance by using nonphysical fog that thickens rapidly but has a distant start point...

Possibly. I'd have to try it out. I do plan on trying to better optimize worldgen in the future, and I'll have to double-check that we're not including too many chunks the user wouldn't see past the fog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants