Changed core
This commit is contained in:
@@ -14,39 +14,38 @@ import net.minecraft.client.MinecraftClient;
|
|||||||
// make the rebuild range like +-5 chunks along each axis (that means at higher levels, should only need to rebuild like)
|
// make the rebuild range like +-5 chunks along each axis (that means at higher levels, should only need to rebuild like)
|
||||||
// 4 sections or something
|
// 4 sections or something
|
||||||
public class DistanceTracker {
|
public class DistanceTracker {
|
||||||
private final TransitionRing2D[] rings;
|
private final TransitionRing2D[] loDRings;
|
||||||
|
private final TransitionRing2D[] cacheLoadRings;
|
||||||
|
private final TransitionRing2D[] cacheUnloadRings;
|
||||||
private final RenderTracker tracker;
|
private final RenderTracker tracker;
|
||||||
private final int scale;
|
|
||||||
private final int minYSection;
|
private final int minYSection;
|
||||||
private final int maxYSection;
|
private final int maxYSection;
|
||||||
|
|
||||||
public DistanceTracker(RenderTracker tracker, int rings, int scale) {
|
public DistanceTracker(RenderTracker tracker, int[] lodRingScales, int cacheLoadDistance, int cacheUnloadDistance) {
|
||||||
this.rings = new TransitionRing2D[rings];
|
this.loDRings = new TransitionRing2D[lodRingScales.length];
|
||||||
|
this.cacheLoadRings = new TransitionRing2D[lodRingScales.length];
|
||||||
|
this.cacheUnloadRings = new TransitionRing2D[lodRingScales.length];
|
||||||
this.tracker = tracker;
|
this.tracker = tracker;
|
||||||
this.scale = scale;
|
|
||||||
|
|
||||||
this.minYSection = MinecraftClient.getInstance().world.getBottomSectionCoord()/2;
|
this.minYSection = MinecraftClient.getInstance().world.getBottomSectionCoord()/2;
|
||||||
this.maxYSection = MinecraftClient.getInstance().world.getTopSectionCoord()/2;
|
this.maxYSection = MinecraftClient.getInstance().world.getTopSectionCoord()/2;
|
||||||
|
|
||||||
int radius = (MinecraftClient.getInstance().options.getViewDistance().getValue() / 2) - 4;
|
|
||||||
if (radius > 0 && false) {
|
|
||||||
this.rings[0] = new TransitionRing2D(5, radius, (x, z) -> {
|
|
||||||
for (int y = this.minYSection; y <= this.maxYSection; y++) {
|
|
||||||
this.tracker.remLvl0(x, y, z);
|
|
||||||
}
|
|
||||||
}, (x, z) -> {
|
|
||||||
for (int y = this.minYSection; y <= this.maxYSection; y++) {
|
|
||||||
this.tracker.addLvl0(x, y, z);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//The rings 0+ start at 64 vanilla rd, no matter what the game is set at, that is if the game is set to 32 rd
|
//The rings 0+ start at 64 vanilla rd, no matter what the game is set at, that is if the game is set to 32 rd
|
||||||
// there will still be 32 chunks untill the first lod drop
|
// there will still be 32 chunks untill the first lod drop
|
||||||
// if the game is set to 16, then there will be 48 chunks until the drop
|
// if the game is set to 16, then there will be 48 chunks until the drop
|
||||||
for (int i = 1; i < rings; i++) {
|
for (int i = 0; i < this.loDRings.length; i++) {
|
||||||
int capRing = i;
|
int capRing = i;
|
||||||
this.rings[i] = new TransitionRing2D(5+i, scale, (x, z) -> this.dec(capRing, x, z), (x, z) -> this.inc(capRing, x, z));
|
this.loDRings[i] = new TransitionRing2D(6+i, lodRingScales[i], (x, z) -> this.dec(capRing+1, x, z), (x, z) -> this.inc(capRing+1, x, z));
|
||||||
|
|
||||||
|
//TODO:FIXME i think the radius is wrong and (lodRingScales[i]) needs to be (lodRingScales[i]<<1) since the transition ring (the thing above)
|
||||||
|
// acts on LoD level + 1
|
||||||
|
this.cacheLoadRings[i] = new TransitionRing2D(5+i, lodRingScales[i] + cacheLoadDistance, (x, z) -> {
|
||||||
|
//When entering a cache ring, trigger a mesh op and inject into cache
|
||||||
|
|
||||||
|
}, (x, z) -> {});
|
||||||
|
this.cacheUnloadRings[i] = new TransitionRing2D(5+i, lodRingScales[i] + cacheUnloadDistance, (x, z) -> {}, (x, z) -> {
|
||||||
|
//When exiting the cache unload ring, tell the cache to dump whatever mesh it has cached and not add any mesh from that position
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,10 +68,14 @@ public class DistanceTracker {
|
|||||||
//if the center suddenly changes (say more than 1<<(7+lodlvl) block) then invalidate the entire ring and recompute
|
//if the center suddenly changes (say more than 1<<(7+lodlvl) block) then invalidate the entire ring and recompute
|
||||||
// the lod sections
|
// the lod sections
|
||||||
public void setCenter(int x, int y, int z) {
|
public void setCenter(int x, int y, int z) {
|
||||||
for (var ring : this.rings) {
|
for (var ring : this.cacheLoadRings) {
|
||||||
if (ring != null) {
|
ring.update(x, z);
|
||||||
ring.update(x, z);
|
}
|
||||||
}
|
for (var ring : this.loDRings) {
|
||||||
|
ring.update(x, z);
|
||||||
|
}
|
||||||
|
for (var ring : this.cacheUnloadRings) {
|
||||||
|
ring.update(x, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +85,18 @@ public class DistanceTracker {
|
|||||||
//Insert highest LOD level
|
//Insert highest LOD level
|
||||||
for (int ox = -SIZE; ox <= SIZE; ox++) {
|
for (int ox = -SIZE; ox <= SIZE; ox++) {
|
||||||
for (int oz = -SIZE; oz <= SIZE; oz++) {
|
for (int oz = -SIZE; oz <= SIZE; oz++) {
|
||||||
this.inc(4, (x>>(5+this.rings.length-1)) + ox, (z>>(5+this.rings.length-1)) + oz);
|
this.inc(4, (x>>(5+this.loDRings.length)) + ox, (z>>(5+this.loDRings.length)) + oz);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (int i = this.rings.length-1; 0 <= i; i--) {
|
for (var ring : this.cacheLoadRings) {
|
||||||
if (this.rings[i] != null) {
|
ring.fill(x, z);
|
||||||
this.rings[i].fill(x, z);
|
}
|
||||||
|
|
||||||
|
for (int i = this.loDRings.length-1; 0 <= i; i--) {
|
||||||
|
if (this.loDRings[i] != null) {
|
||||||
|
this.loDRings[i].fill(x, z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ public class VoxelCore {
|
|||||||
System.out.println("Render tracker and generator initialized");
|
System.out.println("Render tracker and generator initialized");
|
||||||
|
|
||||||
//To get to chunk scale multiply the scale by 2, the scale is after how many chunks does the lods halve
|
//To get to chunk scale multiply the scale by 2, the scale is after how many chunks does the lods halve
|
||||||
this.distanceTracker = new DistanceTracker(this.renderTracker, 5, VoxyConfig.CONFIG.qualityScale);
|
int q = VoxyConfig.CONFIG.qualityScale;
|
||||||
|
this.distanceTracker = new DistanceTracker(this.renderTracker, new int[]{q,q,q,q}, 2, 4);
|
||||||
System.out.println("Distance tracker initialized");
|
System.out.println("Distance tracker initialized");
|
||||||
|
|
||||||
this.postProcessing = new PostProcessing();
|
this.postProcessing = new PostProcessing();
|
||||||
|
|||||||
@@ -490,18 +490,31 @@ public class ModelManager {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:FIXME: if the model is not already in the cache for some reason it renders black, need to figure out why
|
//TODO:FIXME: DONT DO SPIN LOCKS :WAA:
|
||||||
public long getModelMetadata(int blockId) {
|
public long getModelMetadata(int blockId) {
|
||||||
int map = 0;
|
int map = this.idMappings[blockId];
|
||||||
while ((map = this.idMappings[blockId]) == -1) {
|
if (map == -1) {
|
||||||
Thread.onSpinWait();
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
map = this.idMappings[blockId];
|
||||||
}
|
}
|
||||||
|
if (map == -1) {
|
||||||
|
throw new IllegalArgumentException("Id hasnt been computed yet: " + blockId);
|
||||||
|
}
|
||||||
|
return this.metadataCache[map];
|
||||||
|
//int map = 0;
|
||||||
|
//int i = 10;
|
||||||
|
//while ((map = this.idMappings[blockId]) == -1) {
|
||||||
|
// Thread.onSpinWait();
|
||||||
|
//}
|
||||||
|
|
||||||
long meta = 0;
|
//long meta = 0;
|
||||||
while ((meta = this.metadataCache[map]) == 0) {
|
//while ((meta = this.metadataCache[map]) == 0) {
|
||||||
Thread.onSpinWait();
|
// Thread.onSpinWait();
|
||||||
}
|
//}
|
||||||
return meta;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getModelId(int blockId) {
|
public int getModelId(int blockId) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package me.cortex.voxy.client.core.rendering;
|
|||||||
import me.cortex.voxy.client.core.gl.GlBuffer;
|
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||||
import me.cortex.voxy.client.core.model.ModelManager;
|
import me.cortex.voxy.client.core.model.ModelManager;
|
||||||
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
import me.cortex.voxy.client.core.rendering.building.BuiltSection;
|
||||||
|
import me.cortex.voxy.client.core.rendering.util.DownloadStream;
|
||||||
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
import me.cortex.voxy.client.core.rendering.util.UploadStream;
|
||||||
import me.cortex.voxy.common.world.other.Mapper;
|
import me.cortex.voxy.common.world.other.Mapper;
|
||||||
import net.minecraft.client.MinecraftClient;
|
import net.minecraft.client.MinecraftClient;
|
||||||
@@ -66,6 +67,7 @@ public abstract class AbstractFarWorldRenderer {
|
|||||||
// once per frame when using multi viewport mods
|
// once per frame when using multi viewport mods
|
||||||
//it shouldent matter if its called multiple times a frame however, as its synced with fences
|
//it shouldent matter if its called multiple times a frame however, as its synced with fences
|
||||||
UploadStream.INSTANCE.tick();
|
UploadStream.INSTANCE.tick();
|
||||||
|
DownloadStream.INSTANCE.tick();
|
||||||
|
|
||||||
//Update the lightmap
|
//Update the lightmap
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class RenderTracker {
|
|||||||
//Adds a lvl 0 section into the world renderer
|
//Adds a lvl 0 section into the world renderer
|
||||||
public void addLvl0(int x, int y, int z) {
|
public void addLvl0(int x, int y, int z) {
|
||||||
this.activeSections.put(WorldEngine.getWorldSectionId(0, x, y, z), O);
|
this.activeSections.put(WorldEngine.getWorldSectionId(0, x, y, z), O);
|
||||||
this.renderGen.enqueueTask(0, x, y, z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(0, x, y, z, this::shouldStillBuild);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Removes a lvl 0 section from the world renderer
|
//Removes a lvl 0 section from the world renderer
|
||||||
@@ -61,7 +61,7 @@ public class RenderTracker {
|
|||||||
// concurrent hashmap or something, this is so that e.g. the build data position
|
// concurrent hashmap or something, this is so that e.g. the build data position
|
||||||
// can be updated
|
// can be updated
|
||||||
|
|
||||||
this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild);
|
||||||
|
|
||||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1))));
|
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1))));
|
||||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1)));
|
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1)));
|
||||||
@@ -98,35 +98,35 @@ public class RenderTracker {
|
|||||||
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z)));
|
this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z)));
|
||||||
this.renderGen.removeTask(lvl, x, y, z);
|
this.renderGen.removeTask(lvl, x, y, z);
|
||||||
|
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1), this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1), this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1)+1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1)+1, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1), this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1), this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1)+1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1)+1, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1), this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1), this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1)+1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1)+1, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1), this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1), this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1)+1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1)+1, this::shouldStillBuild);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Enqueues a renderTask for a section to cache the result
|
||||||
|
public void addCache() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Updates a sections direction mask (e.g. if the player goes past the axis, the chunk must be updated)
|
|
||||||
public void updateDirMask(int lvl, int x, int y, int z, int newMask) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Called by the world engine when a section gets dirtied
|
//Called by the world engine when a section gets dirtied
|
||||||
public void sectionUpdated(WorldSection section) {
|
public void sectionUpdated(WorldSection section) {
|
||||||
if (this.activeSections.containsKey(section.getKey())) {
|
if (this.activeSections.containsKey(section.key)) {
|
||||||
//TODO:FIXME: if the section gets updated, that means that its neighbors might need to be updated aswell
|
//TODO:FIXME: if the section gets updated, that means that its neighbors might need to be updated aswell
|
||||||
// (due to block occlusion)
|
// (due to block occlusion)
|
||||||
|
|
||||||
//TODO: FIXME: REBUILDING THE ENTIRE NEIGHBORS when probably only the internal layout changed is NOT SMART
|
//TODO: FIXME: REBUILDING THE ENTIRE NEIGHBORS when probably only the internal layout changed is NOT SMART
|
||||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(section.lvl, section.x-1, section.y, section.z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(section.lvl, section.x-1, section.y, section.z, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(section.lvl, section.x+1, section.y, section.z, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(section.lvl, section.x+1, section.y, section.z, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z-1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z-1, this::shouldStillBuild);
|
||||||
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z+1, this::shouldStillBuild, this::getBuildFlagsOrAbort);
|
this.renderGen.enqueueTask(section.lvl, section.x, section.y, section.z+1, this::shouldStillBuild);
|
||||||
}
|
}
|
||||||
//this.renderGen.enqueueTask(section);
|
//this.renderGen.enqueueTask(section);
|
||||||
}
|
}
|
||||||
@@ -143,32 +143,6 @@ public class RenderTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getBuildFlagsOrAbort(WorldSection section) {
|
|
||||||
var cam = MinecraftClient.getInstance().cameraEntity;
|
|
||||||
if (cam == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
var holder = this.activeSections.get(section.getKey());
|
|
||||||
int buildMask = 0;
|
|
||||||
if (holder != null) {
|
|
||||||
if (section.z<(((int)cam.getPos().z)>>(5+section.lvl))+1) {
|
|
||||||
buildMask |= 1<< Direction.SOUTH.getId();
|
|
||||||
}
|
|
||||||
if (section.z>(((int)cam.getPos().z)>>(5+section.lvl))-1) {
|
|
||||||
buildMask |= 1<<Direction.NORTH.getId();
|
|
||||||
}
|
|
||||||
if (section.x<(((int)cam.getPos().x)>>(5+section.lvl))+1) {
|
|
||||||
buildMask |= 1<<Direction.EAST.getId();
|
|
||||||
}
|
|
||||||
if (section.x>(((int)cam.getPos().x)>>(5+section.lvl))-1) {
|
|
||||||
buildMask |= 1<<Direction.WEST.getId();
|
|
||||||
}
|
|
||||||
buildMask |= 1<<Direction.UP.getId();
|
|
||||||
buildMask |= ((1<<6)-1)^(1);
|
|
||||||
}
|
|
||||||
return buildMask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean shouldStillBuild(int lvl, int x, int y, int z) {
|
public boolean shouldStillBuild(int lvl, int x, int y, int z) {
|
||||||
return this.activeSections.containsKey(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
return this.activeSections.containsKey(WorldEngine.getWorldSectionId(lvl, x, y, z));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package me.cortex.voxy.client.core.rendering.building;
|
||||||
|
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
//TODO: instead of storing duplicate render geometry between here and gpu memory
|
||||||
|
// when a section is unloaded from the gpu, put it into a download stream and recover the BuiltSection
|
||||||
|
// and put that into the cache, then remove the uploaded mesh from the cache
|
||||||
|
public class BuiltSectionMeshCache {
|
||||||
|
private final ConcurrentHashMap<Long, BuiltSection> renderCache = new ConcurrentHashMap<>(1000,0.75f,10);
|
||||||
|
|
||||||
|
public BuiltSection getMesh(long key) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Returns true if the mesh was used, (this is so the parent method can free mesh object)
|
||||||
|
public boolean putMesh(BuiltSection mesh) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearMesh(long key) {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void free() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ public class RenderDataFactory {
|
|||||||
|
|
||||||
//buildMask in the lower 6 bits contains the faces to build, the next 6 bits are whether the edge face builds against
|
//buildMask in the lower 6 bits contains the faces to build, the next 6 bits are whether the edge face builds against
|
||||||
// its neigbor or not (0 if it does 1 if it doesnt (0 is default behavior))
|
// its neigbor or not (0 if it does 1 if it doesnt (0 is default behavior))
|
||||||
public BuiltSection generateMesh(WorldSection section, int buildMask) {
|
public BuiltSection generateMesh(WorldSection section) {
|
||||||
section.copyDataTo(this.sectionCache);
|
section.copyDataTo(this.sectionCache);
|
||||||
this.translucentQuadCollector.clear();
|
this.translucentQuadCollector.clear();
|
||||||
this.doubleSidedQuadCollector.clear();
|
this.doubleSidedQuadCollector.clear();
|
||||||
@@ -75,7 +75,7 @@ public class RenderDataFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (quadCount == 0) {
|
if (quadCount == 0) {
|
||||||
return new BuiltSection(section.getKey());
|
return new BuiltSection(section.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var buff = new MemoryBuffer(quadCount*8L);
|
var buff = new MemoryBuffer(quadCount*8L);
|
||||||
@@ -109,7 +109,7 @@ public class RenderDataFactory {
|
|||||||
aabb |= (this.maxY-this.minY)<<20;
|
aabb |= (this.maxY-this.minY)<<20;
|
||||||
aabb |= (this.maxZ-this.minZ)<<25;
|
aabb |= (this.maxZ-this.minZ)<<25;
|
||||||
|
|
||||||
return new BuiltSection(section.getKey(), aabb, buff, offsets);
|
return new BuiltSection(section.key, aabb, buff, offsets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.function.ToIntFunction;
|
|||||||
//TODO: Add a render cache
|
//TODO: Add a render cache
|
||||||
public class RenderGenerationService {
|
public class RenderGenerationService {
|
||||||
public interface TaskChecker {boolean check(int lvl, int x, int y, int z);}
|
public interface TaskChecker {boolean check(int lvl, int x, int y, int z);}
|
||||||
private record BuildTask(Supplier<WorldSection> sectionSupplier, ToIntFunction<WorldSection> flagSupplier) {}
|
private record BuildTask(Supplier<WorldSection> sectionSupplier) {}
|
||||||
|
|
||||||
private volatile boolean running = true;
|
private volatile boolean running = true;
|
||||||
private final Thread[] workers;
|
private final Thread[] workers;
|
||||||
@@ -25,6 +25,7 @@ public class RenderGenerationService {
|
|||||||
private final WorldEngine world;
|
private final WorldEngine world;
|
||||||
private final ModelManager modelManager;
|
private final ModelManager modelManager;
|
||||||
private final Consumer<BuiltSection> resultConsumer;
|
private final Consumer<BuiltSection> resultConsumer;
|
||||||
|
private final BuiltSectionMeshCache meshCache = new BuiltSectionMeshCache();
|
||||||
|
|
||||||
public RenderGenerationService(WorldEngine world, ModelManager modelManager, int workers, Consumer<BuiltSection> consumer) {
|
public RenderGenerationService(WorldEngine world, ModelManager modelManager, int workers, Consumer<BuiltSection> consumer) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
@@ -39,8 +40,6 @@ public class RenderGenerationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ConcurrentHashMap<Long, BuiltSection> renderCache = new ConcurrentHashMap<>(1000,0.75f,10);
|
|
||||||
|
|
||||||
//TODO: add a generated render data cache
|
//TODO: add a generated render data cache
|
||||||
private void renderWorker() {
|
private void renderWorker() {
|
||||||
//Thread local instance of the factory
|
//Thread local instance of the factory
|
||||||
@@ -57,23 +56,12 @@ public class RenderGenerationService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
section.assertNotFree();
|
section.assertNotFree();
|
||||||
int buildFlags = task.flagSupplier.applyAsInt(section);
|
var mesh = factory.generateMesh(section);
|
||||||
if (buildFlags != 0) {
|
section.release();
|
||||||
var mesh = factory.generateMesh(section, buildFlags);
|
|
||||||
section.release();
|
|
||||||
|
|
||||||
this.resultConsumer.accept(mesh.clone());
|
this.resultConsumer.accept(mesh.clone());
|
||||||
|
if (!this.meshCache.putMesh(mesh)) {
|
||||||
if (false) {
|
mesh.free();
|
||||||
var prevCache = this.renderCache.put(mesh.position, mesh);
|
|
||||||
if (prevCache != null) {
|
|
||||||
prevCache.free();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mesh.free();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
section.release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,14 +82,14 @@ public class RenderGenerationService {
|
|||||||
// like if its in the render queue and if we should abort building the render data
|
// like if its in the render queue and if we should abort building the render data
|
||||||
//1 proposal fix is a Long2ObjectLinkedOpenHashMap<WorldSection> which means we can abort if needed,
|
//1 proposal fix is a Long2ObjectLinkedOpenHashMap<WorldSection> which means we can abort if needed,
|
||||||
// also gets rid of dependency on a WorldSection (kinda)
|
// also gets rid of dependency on a WorldSection (kinda)
|
||||||
public void enqueueTask(int lvl, int x, int y, int z, ToIntFunction<WorldSection> flagSupplier) {
|
public void enqueueTask(int lvl, int x, int y, int z) {
|
||||||
this.enqueueTask(lvl, x, y, z, (l,x1,y1,z1)->true, flagSupplier);
|
this.enqueueTask(lvl, x, y, z, (l,x1,y1,z1)->true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void enqueueTask(int lvl, int x, int y, int z, TaskChecker checker, ToIntFunction<WorldSection> flagSupplier) {
|
public void enqueueTask(int lvl, int x, int y, int z, TaskChecker checker) {
|
||||||
long ikey = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
long ikey = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
||||||
{
|
{
|
||||||
var cache = this.renderCache.get(ikey);
|
var cache = this.meshCache.getMesh(ikey);
|
||||||
if (cache != null) {
|
if (cache != null) {
|
||||||
this.resultConsumer.accept(cache.clone());
|
this.resultConsumer.accept(cache.clone());
|
||||||
return;
|
return;
|
||||||
@@ -116,7 +104,7 @@ public class RenderGenerationService {
|
|||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, flagSupplier);
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +147,6 @@ public class RenderGenerationService {
|
|||||||
while (!this.taskQueue.isEmpty()) {
|
while (!this.taskQueue.isEmpty()) {
|
||||||
this.taskQueue.removeFirst();
|
this.taskQueue.removeFirst();
|
||||||
}
|
}
|
||||||
this.renderCache.values().forEach(BuiltSection::free);
|
this.meshCache.free();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package me.cortex.voxy.client.core.rendering.util;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.longs.LongConsumer;
|
||||||
|
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||||
|
import me.cortex.voxy.client.core.gl.GlBuffer;
|
||||||
|
import me.cortex.voxy.client.core.gl.GlFence;
|
||||||
|
import me.cortex.voxy.client.core.gl.GlPersistentMappedBuffer;
|
||||||
|
import me.cortex.voxy.client.core.util.AllocationArena;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
|
||||||
|
import static me.cortex.voxy.client.core.util.AllocationArena.SIZE_LIMIT;
|
||||||
|
import static org.lwjgl.opengl.ARBDirectStateAccess.glCopyNamedBufferSubData;
|
||||||
|
import static org.lwjgl.opengl.ARBDirectStateAccess.glFlushMappedNamedBufferRange;
|
||||||
|
import static org.lwjgl.opengl.ARBMapBufferRange.*;
|
||||||
|
import static org.lwjgl.opengl.GL11.glFinish;
|
||||||
|
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
|
||||||
|
import static org.lwjgl.opengl.GL42C.GL_BUFFER_UPDATE_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
|
||||||
|
import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT;
|
||||||
|
|
||||||
|
public class DownloadStream {
|
||||||
|
public interface DownloadResultConsumer {
|
||||||
|
void consume(long ptr, long size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final AllocationArena allocationArena = new AllocationArena();
|
||||||
|
private final GlPersistentMappedBuffer downloadBuffer;
|
||||||
|
|
||||||
|
private final Deque<DownloadFrame> frames = new ArrayDeque<>();
|
||||||
|
private final LongArrayList thisFrameAllocations = new LongArrayList();
|
||||||
|
private final Deque<DownloadData> downloadList = new ArrayDeque<>();
|
||||||
|
private final ArrayList<DownloadData> thisFrameDownloadList = new ArrayList<>();
|
||||||
|
|
||||||
|
public DownloadStream(long size) {
|
||||||
|
this.downloadBuffer = new GlPersistentMappedBuffer(size, GL_MAP_READ_BIT);
|
||||||
|
this.allocationArena.setLimit(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long caddr = -1;
|
||||||
|
private long offset = 0;
|
||||||
|
public void download(GlBuffer buffer, long destOffset, long size, DownloadResultConsumer resultConsumer) {
|
||||||
|
if (size > Integer.MAX_VALUE) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
long addr;
|
||||||
|
if (this.caddr == -1 || !this.allocationArena.expand(this.caddr, (int) size)) {
|
||||||
|
this.caddr = this.allocationArena.alloc((int) size);//TODO: replace with allocFromLargest
|
||||||
|
if (this.caddr == SIZE_LIMIT) {
|
||||||
|
this.commit();
|
||||||
|
int attempts = 10;
|
||||||
|
while (--attempts != 0 && this.caddr == SIZE_LIMIT) {
|
||||||
|
glFinish();
|
||||||
|
this.tick();
|
||||||
|
this.caddr = this.allocationArena.alloc((int) size);
|
||||||
|
}
|
||||||
|
if (this.caddr == SIZE_LIMIT) {
|
||||||
|
throw new IllegalStateException("Could not allocate memory segment big enough for upload even after force flush");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.thisFrameAllocations.add(this.caddr);
|
||||||
|
this.offset = size;
|
||||||
|
addr = this.caddr;
|
||||||
|
} else {//Could expand the allocation so just update it
|
||||||
|
addr = this.caddr + this.offset;
|
||||||
|
this.offset += size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.caddr + size > this.downloadBuffer.size()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadList.add(new DownloadData(buffer, addr, destOffset, size, resultConsumer));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void commit() {
|
||||||
|
//Copies all the data from target buffers into the download stream
|
||||||
|
for (var entry : this.downloadList) {
|
||||||
|
glCopyNamedBufferSubData(entry.target.id, this.downloadBuffer.id, entry.downloadOffset, entry.targetOffset, entry.size);
|
||||||
|
}
|
||||||
|
thisFrameDownloadList.addAll(this.downloadList);
|
||||||
|
this.downloadList.clear();
|
||||||
|
|
||||||
|
this.caddr = -1;
|
||||||
|
this.offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick() {
|
||||||
|
this.commit();
|
||||||
|
if (!this.thisFrameAllocations.isEmpty()) {
|
||||||
|
glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
|
||||||
|
this.frames.add(new DownloadFrame(new GlFence(), new LongArrayList(this.thisFrameAllocations), new ArrayList<>(this.thisFrameDownloadList)));
|
||||||
|
this.thisFrameAllocations.clear();
|
||||||
|
this.thisFrameDownloadList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!this.frames.isEmpty()) {
|
||||||
|
//Since the ordering of frames is the ordering of the gl commands if we encounter an unsignaled fence
|
||||||
|
// all the other fences should also be unsignaled
|
||||||
|
if (!this.frames.peek().fence.signaled()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
//Release all the allocations from the frame
|
||||||
|
var frame = this.frames.pop();
|
||||||
|
|
||||||
|
//Apply all the callbacks
|
||||||
|
for (var data : frame.data) {
|
||||||
|
data.resultConsumer.consume(this.downloadBuffer.addr() + data.downloadOffset, data.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
frame.allocations.forEach(this.allocationArena::free);
|
||||||
|
frame.fence.free();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record DownloadFrame(GlFence fence, LongArrayList allocations, ArrayList<DownloadData> data) {}
|
||||||
|
private record DownloadData(GlBuffer target, long downloadOffset, long targetOffset, long size, DownloadResultConsumer resultConsumer) {}
|
||||||
|
|
||||||
|
|
||||||
|
// Global download stream
|
||||||
|
public static final DownloadStream INSTANCE = new DownloadStream(1<<25);//32 mb download buffer
|
||||||
|
}
|
||||||
@@ -111,7 +111,7 @@ public class UploadStream {
|
|||||||
}
|
}
|
||||||
//Release all the allocations from the frame
|
//Release all the allocations from the frame
|
||||||
var frame = this.frames.pop();
|
var frame = this.frames.pop();
|
||||||
frame.allocations.forEach(allocationArena::free);
|
frame.allocations.forEach(this.allocationArena::free);
|
||||||
frame.fence.free();
|
frame.fence.free();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ public class ActiveSectionTracker {
|
|||||||
private final SectionLoader loader;
|
private final SectionLoader loader;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public ActiveSectionTracker(int layers, SectionLoader loader) {
|
public ActiveSectionTracker(int cacheSizeBits, SectionLoader loader) {
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.loadedSectionCache = new Long2ObjectOpenHashMap[layers];
|
this.loadedSectionCache = new Long2ObjectOpenHashMap[1<<cacheSizeBits];
|
||||||
for (int i = 0; i < layers; i++) {
|
for (int i = 0; i < this.loadedSectionCache.length; i++) {
|
||||||
this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1<<(16-i));
|
this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1024);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldSection acquire(int lvl, int x, int y, int z, boolean nullOnEmpty) {
|
public WorldSection acquire(int lvl, int x, int y, int z, boolean nullOnEmpty) {
|
||||||
long key = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
long key = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
||||||
var cache = this.loadedSectionCache[lvl];
|
var cache = this.loadedSectionCache[this.getCacheArrayIndex(key)];
|
||||||
VolatileHolder<WorldSection> holder = null;
|
VolatileHolder<WorldSection> holder = null;
|
||||||
boolean isLoader = false;
|
boolean isLoader = false;
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
@@ -69,16 +69,25 @@ public class ActiveSectionTracker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void tryUnload(WorldSection section) {
|
void tryUnload(WorldSection section) {
|
||||||
var cache = this.loadedSectionCache[section.lvl];
|
var cache = this.loadedSectionCache[this.getCacheArrayIndex(section.key)];
|
||||||
synchronized (cache) {
|
synchronized (cache) {
|
||||||
if (section.trySetFreed()) {
|
if (section.trySetFreed()) {
|
||||||
if (cache.remove(section.getKey()).obj != section) {
|
if (cache.remove(section.key).obj != section) {
|
||||||
throw new IllegalStateException("Removed section not the same as the referenced section in the cache");
|
throw new IllegalStateException("Removed section not the same as the referenced section in the cache");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int getCacheArrayIndex(long pos) {
|
||||||
|
return (int) (mixStafford13(pos) & (this.loadedSectionCache.length-1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long mixStafford13(long seed) {
|
||||||
|
seed = (seed ^ seed >>> 30) * -4658895280553007687L;
|
||||||
|
seed = (seed ^ seed >>> 27) * -7723592293110705685L;
|
||||||
|
return seed ^ seed >>> 31;
|
||||||
|
}
|
||||||
|
|
||||||
public int[] getCacheCounts() {
|
public int[] getCacheCounts() {
|
||||||
int[] res = new int[this.loadedSectionCache.length];
|
int[] res = new int[this.loadedSectionCache.length];
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ public class SaveLoadSystem {
|
|||||||
long[] lut = LUTVAL.toLongArray();
|
long[] lut = LUTVAL.toLongArray();
|
||||||
ByteBuffer raw = MemoryUtil.memAlloc(compressed.length*2+lut.length*8+512);
|
ByteBuffer raw = MemoryUtil.memAlloc(compressed.length*2+lut.length*8+512);
|
||||||
|
|
||||||
long hash = section.getKey()^(lut.length*1293481298141L);
|
long hash = section.key^(lut.length*1293481298141L);
|
||||||
raw.putLong(section.getKey());
|
raw.putLong(section.key);
|
||||||
raw.putInt(lut.length);
|
raw.putInt(lut.length);
|
||||||
for (long id : lut) {
|
for (long id : lut) {
|
||||||
raw.putLong(id);
|
raw.putLong(id);
|
||||||
@@ -74,8 +74,8 @@ public class SaveLoadSystem {
|
|||||||
hash ^= lut[i];
|
hash ^= lut[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section.getKey() != key) {
|
if (section.key != key) {
|
||||||
throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.getKey());
|
throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < section.data.length; i++) {
|
for (int i = 0; i < section.data.length; i++) {
|
||||||
|
|||||||
@@ -33,18 +33,19 @@ public class WorldEngine {
|
|||||||
this.maxMipLevels = maxMipLayers;
|
this.maxMipLevels = maxMipLayers;
|
||||||
this.storage = storageBackend;
|
this.storage = storageBackend;
|
||||||
this.mapper = new Mapper(this.storage);
|
this.mapper = new Mapper(this.storage);
|
||||||
this.sectionTracker = new ActiveSectionTracker(maxMipLayers, this::unsafeLoadSection);
|
//4 cache size bits means that the section tracker has 16 separate maps that it uses
|
||||||
|
this.sectionTracker = new ActiveSectionTracker(4, this::unsafeLoadSection);
|
||||||
|
|
||||||
this.savingService = new SectionSavingService(this, savingServiceWorkers, compressionLevel);
|
this.savingService = new SectionSavingService(this, savingServiceWorkers, compressionLevel);
|
||||||
this.ingestService = new VoxelIngestService(this, ingestWorkers);
|
this.ingestService = new VoxelIngestService(this, ingestWorkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int unsafeLoadSection(WorldSection into) {
|
private int unsafeLoadSection(WorldSection into) {
|
||||||
var data = this.storage.getSectionData(into.getKey());
|
var data = this.storage.getSectionData(into.key);
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
try {
|
try {
|
||||||
if (!SaveLoadSystem.deserialize(into, data)) {
|
if (!SaveLoadSystem.deserialize(into, data)) {
|
||||||
this.storage.deleteSectionData(into.getKey());
|
this.storage.deleteSectionData(into.key);
|
||||||
//TODO: regenerate the section from children
|
//TODO: regenerate the section from children
|
||||||
Arrays.fill(into.data, Mapper.AIR);
|
Arrays.fill(into.data, Mapper.AIR);
|
||||||
System.err.println("Section " + into.lvl + ", " + into.x + ", " + into.y + ", " + into.z + " was unable to load, setting to air");
|
System.err.println("Section " + into.lvl + ", " + into.x + ", " + into.y + ", " + into.z + " was unable to load, setting to air");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public final class WorldSection {
|
|||||||
public final int x;
|
public final int x;
|
||||||
public final int y;
|
public final int y;
|
||||||
public final int z;
|
public final int z;
|
||||||
|
public final long key;
|
||||||
|
|
||||||
long[] data;
|
long[] data;
|
||||||
private final ActiveSectionTracker tracker;
|
private final ActiveSectionTracker tracker;
|
||||||
@@ -25,6 +26,7 @@ public final class WorldSection {
|
|||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.z = z;
|
this.z = z;
|
||||||
|
this.key = WorldEngine.getWorldSectionId(lvl, x, y, z);
|
||||||
this.tracker = tracker;
|
this.tracker = tracker;
|
||||||
|
|
||||||
this.data = new long[32*32*32];
|
this.data = new long[32*32*32];
|
||||||
@@ -78,10 +80,6 @@ public final class WorldSection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getKey() {
|
|
||||||
return WorldEngine.getWorldSectionId(this.lvl, this.x, this.y, this.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getIndex(int x, int y, int z) {
|
public static int getIndex(int x, int y, int z) {
|
||||||
int M = (1<<5)-1;
|
int M = (1<<5)-1;
|
||||||
if (x<0||x>M||y<0||y>M||z<0||z>M) {
|
if (x<0||x>M||y<0||y>M||z<0||z>M) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class SectionSavingService {
|
|||||||
section.inSaveQueue.set(false);
|
section.inSaveQueue.set(false);
|
||||||
|
|
||||||
var saveData = SaveLoadSystem.serialize(section, this.compressionLevel);
|
var saveData = SaveLoadSystem.serialize(section, this.compressionLevel);
|
||||||
this.world.storage.setSectionData(section.getKey(), saveData);
|
this.world.storage.setSectionData(section.key, saveData);
|
||||||
MemoryUtil.memFree(saveData);
|
MemoryUtil.memFree(saveData);
|
||||||
|
|
||||||
section.release();
|
section.release();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ uint extractFace(ivec2 quad) {
|
|||||||
|
|
||||||
uint extractStateId(ivec2 quad) {
|
uint extractStateId(ivec2 quad) {
|
||||||
//Eu32(quad, 20, 26);
|
//Eu32(quad, 20, 26);
|
||||||
return 1;
|
return Eu32v(quad, 6, 26)|(Eu32v(quad, 14, 32)<<6);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint extractBiomeId(ivec2 quad) {
|
uint extractBiomeId(ivec2 quad) {
|
||||||
|
|||||||
Reference in New Issue
Block a user