-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Fix Y-slices with num_emerge_threads > 1 #16224
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
base: master
Are you sure you want to change the base?
Conversation
I had been considering something similar, but only setting these back to IGNORE if they have (A) not been generated by the other chunk, (B) not been changed after initial generation. Consider tall grass, two nodes high, placed at the top "valid" slice. It will write the upper half into this overgenerated area. Unconditionally setting that back to IGNORE might clip this. This might also apply to trees. |
To me this looks like a bad hack. If we're placing too much stone, then removing it after the fact, why not fix the placement in the first place? |
Overgeneration is necessary for some decoration placement, such as dust. Dust placement depends on the node below, hence if the chunk below is not yet generated, one y slice needs to be overgenerated. However, I do not think overgenerated base terrain should be written. Minecraft may (judging by some youtube videos on MC terrain generation) be doing a multi-step emerge to avoid such reasons. For this, every block would have an integer emerge state phase. To emerge a block to a state, all neighboring blocks must at least be in the previous state. Phases could be: base terrain, then cavegen, then dust, then decorations. Or maybe even dust after decorations to add snow cover? This will then allow such phases to reliably touch neighboring chunks. |
kno10: very good point, had not considered the tall grass and trees being why we probably do this Y slice overgeneration in the first place. Will take a look tonight, but you are probably correct and the fix is simply to check if the node is c_stone, and if so, set it back to content ignore. Sfan5: don't think we can simply get rid of overgeneration in general, it's required for our algorithms to know if the topmost node is covered or exposed, so things like dust and top layer fills work correctly. After those pieces of generation are complete, there's no reason to keep the left over c_stone, so removing it eliminates that complexity. Haven't confirmed yet, but our mapblock writing must skip Content ignores, because I expected that doing this would have caused an entire empty layer. |
kno10: very good point, had not considered the tall grass and trees being why we probably do this Y slice overgeneration in the first place. Will take a look tonight, but you are probably correct and the fix is simply to check if the node is c_stone, and if so, set it back to content ignore. Sfan5: don't think we can simply get rid of overgeneration in general, it's required for our algorithms to know if the topmost node is covered or exposed, so things like dust and top layer fills work correctly. After those pieces of generation are complete, there's no reason to keep the left over c_stone nodes, so removing them eliminates that complexity. Haven't confirmed yet, but our mapblock writing must skip Content ignores, because I expected that doing this would have caused an entire empty layer. Edit: I'm also personally against the stages thing short term. |
Right, that makes sense. |
We want some* over generated nodes (decor, dust, structures, etc.), just not the default terrain perlin noise generation step nodes. Is the vmanip able to store more information than just content_id? I know we are calculating lighting too, and that depends on those overgenerated nodes as well, so we can't use paramtype1. Perhaps we could use paramtype2 in combination with what node type it is (c_stone, c_water, etc.) to flag for deletion. Of course, structures often use stone, so not a perfect solution. |
param0, param1 and param2 all need to be preserved but like I said we have a flag array. Lines 350 to 356 in fde6384
Lines 508 to 511 in fde6384
|
Will have to look at it later, but my first thought with marking "over-generated" is how will we know lua callbacks during mapgen have changed the vmanip and ensure we remove the flag. For example, say my lua callbacks set that layer to stone on purpose, then I would want to remove that flag - but we won't be able to tell an edit has been made if the whole layer was previously stone. I.e. if paramtype0, 1, 2 don't change after lua mapgen, how will we know NOT to remove that node if it was intentionally meant to be that paramtype 0,1,2? |
True, no idea. Would removing overgenerated stuff before handing things to Lua work? |
Just took a look: Lines 723 to 736 in fde6384
Yep, that totally works: lua comes into the picture after we have completed C++ mapgen |
Instead of removing the nodes there, what about not iterating one too much: luanti/src/mapgen/mapgen_v7.cpp Line 540 in fde6384
|
See above reasons:
To walk you through a full example, imagine we're doing normal MTG mapgen_v7. I have a big mountain, and I'm generating a middle chunk of that mountain as my very first mapcunk for the entire map. The topmost layer will be generating below an entire layer of CONTENT_IGNORE. For biome generation to work, it would check the node above, see if it's AIR or IGNORE, and if so - assume that this is the top of the mountain, and set all those nodes to grass nodes. But that would be wrong, it should still be stone because the next mapchunk above (that hasn't been generated yet) will be covering that layer. So to make each mapchunk aware of what is above and below it, we overgenerate +-1 Y direction with the base terrain (stone + water). Now biome and dust and decorations actually know if there is AIR above a node or not. Since we must keep the overgeneration to keep everything else aware of what is going to be above or below, this solution thread aims to remove those crutches afterwards to clean up what is effectively the "worksite scrap material" The complexity we're trying to address is to ensure that we only remove scrap and not the nodes we meant to actually put in that layer (decor, structures, trees) |
Not going to help in the mapgen world, in mapgen world we only have a voxelarea, which only has a vector of mapnodes, which only have param0, param1, and param2. Will look at what methods are available to us to flag such nodes, hopefully it's glaringly obvious how to accomplish such a feat in a bit. |
Update 1:
As suspected, just setting all overgenerated layers to content ignore results in bad things for decor: void MapgenBasic::removeOvergeneratedCStone()
{
for (s16 z = node_min.Z; z <= node_max.Z; z++)
for (s16 x = node_min.X; x <= node_max.X; x++) {
u32 vi = vm->m_area.index(x, node_max.Y + 1, z); // top
vm->m_data[vi].setContent(CONTENT_IGNORE);
vi = vm->m_area.index(x, node_min.Y - 1, z); // bottom
vm->m_data[vi].setContent(CONTENT_IGNORE);
}
} And as proposed, only removing the overgenerated, leftover c_stone resolves that particular issue: void MapgenBasic::removeOvergeneratedCStone()
{
for (s16 z = node_min.Z; z <= node_max.Z; z++)
for (s16 x = node_min.X; x <= node_max.X; x++) {
u32 vi = vm->m_area.index(x, node_max.Y + 1, z); // top
if (vm->m_data[vi].getContent() == c_stone) {
vm->m_data[vi].setContent(CONTENT_IGNORE);
}
vi = vm->m_area.index(x, node_min.Y - 1, z); // bottom
if (vm->m_data[vi].getContent() == c_stone) {
vm->m_data[vi].setContent(CONTENT_IGNORE);
}
}
} Next steps
|
Update 2:New failure mode: when overlapping mapchunks are both generating at the same time, and the one below makes big jungle trees, and the one above is not aware of those nodes so it just places air: So wait, I understand overlapping at the +-1 boundary, WHY do we overlap entire chunk generation along x/z axes? For example, top down view really brings this into focus: |
Update 3:Dungeons are fine, mostly because they use cobble, don't have a good test setup yet for finding the fallback stone-only dungeons, will try to get that going now. Edit:Confirmed with the mapgen "game" that was made, enabled dungeons, floated around at +48, -48 in flat mapgen with the fixes: seeing no issues (pink or yellow nodes are issues in this mapgen): Currently testing with caves, nothing to report there yet. |
Update 4:Caves seem absolutely fine, confirmed ores and air and water are all showing up correctly. So with that, the +-y overgeneration issue seems resolved, but not the X/Z full mapblock overlap I'm seeing?Any tips out there about where to look for that bad boy? |
Will address tomorrow I guess, out of time for now. Thinking I'll make changes to BlitBackAll(), so that instead of just taking the received mapgen blocks as gospel: Lines 875 to 880 in fde6384
I would do a "mergeFrom" instead of "copyFrom": block->mergeFrom(*this);
block->raiseModified(MOD_STATE_WRITE_NEEDED, MOD_REASON_VMANIP);
block->expireIsAirCache(); Which this mergeFrom would not allow AIR or CONTENT_IGNORE to overwrite other terrain is my thinking. This way existing decor/map cannot be overwritten by air. And I would only use mergeFrom if BlitBackAll is provided some boolean flag like it is for overwrite_generated. My immediate concern with such a change is not performance but actually wondering how this would affect re-generation requests from lua-side - would require resetting that chunk to content_ignore, or something. |
❓
I hope I'm not misunderstanding the problem but this again sounds like bandaid to me.
for a PoC you could implement this solution the way @kno10 said:
|
I do not think you can rely on stone being the only thing overgenerated. What if, e.g., a schematic places additional stone intentionally? |
I think you are correct, I would assume c_water would also have an effect, I just haven't found a good way to induce that to be an issue.
Exactly my concern, and why I was originally excited about what sfan was talking about with a flag per node outside param0,1,2
I had hoped you or another core dev could shed some light on it too. I don't understand yet if/why we would have mapchunks offset slightly to overlap generation. Hopefully I'm wrong about that. If I'm wrong about that guess, the answer lies in "not only is overgeneration an issue" but also "order of mapblock write-back matters with multi threaded mapgen". In my screenshots, that would mean the lower with jungle trees wrote first, and then the other mapchunk that was doing generation simultaneously wrote 2nd, which it thought all that space was air (correct for it's algorithm), since it started before the mapchunk below was done and had written back. In that scenario, my proposed solution of not just blindly blitbackall() every mapblock from mapchunk generation when num_emerge_threads > 1
Tree is made in one mapchunk at the very top, Y=46 in my pictures. It needs to extend far into the next mapchunk above. That's not overgeneration, per se, but same effect and issue with multi threaded mapgen. In single threaded mapgen, those tree nodes are copied into the next mapchunk's starting vmanip, and our mapgen correctly knows to ignore and leave that alone. "Leave alone" is effectively saying that those nodes, whatever they are, should trump the air that was going to be written there.
Edit: After finally understanding I have the voxelManip flags available during all this, we may be able to pull this off correctly with flags alone like you both keep proposing:
Issue still lies in "what if they want to re-emerge a mapchunk" - which I would have to look into |
And for the others following along since the beginning... Yes using flags to say what gets priority is exactly the same thing as a layering system, so you were right, whoever kept proposing that, I'm now oboard for that same solution, using the vmanip flags to create a 2-level priority system. |
Holy moly, our mapgen decoration placement is a wild thing. So, from what I understand after 2 more hours, besides getting flagging to work properly, is that a tree placed at the top of a mapchunk, will only generate within our maps limits. What i don't understand yet, is how the heck that information is passed to the mapchunk above for generation. Like, does the mapgen above just look and see that there's some tree stumps sitting there at the bottom, and keep on generating an L-tree based on that? If so... How does it know how far along in the turtle axiom it was? I.e. if a tree generates near such a boundary, does it just get taller than normal since it might have a base that is 3 nodes high already or something... For multi threaded mapgen, if we need to know where those tree stumps were to keep on generating, I can see how we have a mess on our hands. We'd have to overgenerate to the full height of the tallest tree, which is game specific, to pull that off without bugs. I guess the only alternative is to just not emerge any mapchunk that is adjacent to another that is currently generating... Which would really cut down on parallelism, and result in a sort of checkerboard emerge pattern. For example, say we're generating 27 mapchunks in a 3x3x3 cube. The maximum simultaneous mapchunks that can be generated would be 14, followed by 13. Which, I mean 13 is a lot more than 1. Oh wait, even that is not safe, because the mapchunk that fills in the middle, could make trees, that the mapchunk above wouldn't have known about.... We must, even with single threaded mapgen have a method to deal with this madness? Like if I have 2 players, teleport far apart veritcally, and they then get close together in ungenerated space, I assume our algorithm would eventually be forced to make a middle mapchunk, that may have trees that never finish generating.... Does this happen with single threaded mapgen?? Thoughts from the smarter people than me? |
I don't know how it works under the hood but I suppose it could simply place a decoration several times in, once in each adjacent mapchunk? At least that's how I do overgeneration in my game's road generator - simply mirror all node placements for adjacent map division units. But idk how it is implemented in Luanti. Either way simply redoing node placements in each mapchunk is a simple alternative to actual overgeneration. Then instead of a shell of 1 mapblock around each mapchunk there would be only mapchunks and nodes written to areas outside of the mapchunk would get ignored. |
Goal
Allow reliable multi-threaded mapgen (outside pure lua mapgen) by removing issues arising from race conditions related to Y direction overgeneration and more than one emerge thread, Solved in this PR by always setting those two overgenerated layers back to
CONTENT_IGNORE
after using it to generate dust/biome/etc.Resolves #9357
To do
This PR is Ready for Review.
How to test
Load up a new world, with num_emerge_threads > 1 in your favorite game (mtg or devtest, or otherwise), toggle on fly,fast and start flying around. Look for bad y-slices in master vs this branch.
Other ways to test:
You can use this especially made "game" just for hunting this bug:
https://codeberg.org/perfect_city_game_studio/mapgen_test
I'm not the author, this is from the original issue thread
OR
You can load up any game, with a world seed of "2" and select mapgen v7, have a high num emerge threads, and teleport to 50,50,500.