From a222ea8f67ffd57db294dc70c115f8fddf579732 Mon Sep 17 00:00:00 2001 From: OLEGSHA Date: Sat, 28 Aug 2021 21:14:35 +0300 Subject: [PATCH] Fixed multithread chunk IO --- .../common/world/DefaultChunkData.java | 38 ++-- .../ru/windcorp/progressia/server/Server.java | 12 +- .../progressia/server/ServerState.java | 2 +- .../management/load/ChunkRequestDaemon.java | 191 ++++++++++++------ .../server/world/DefaultChunkLogic.java | 4 +- .../server/world/DefaultWorldLogic.java | 3 +- .../test/gen/TestGenerationConfig.java | 45 +++-- 7 files changed, 191 insertions(+), 104 deletions(-) diff --git a/src/main/java/ru/windcorp/progressia/common/world/DefaultChunkData.java b/src/main/java/ru/windcorp/progressia/common/world/DefaultChunkData.java index 967fe4e..730580c 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/DefaultChunkData.java +++ b/src/main/java/ru/windcorp/progressia/common/world/DefaultChunkData.java @@ -25,8 +25,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.Objects; +import java.util.Set; + import glm.vec._3.i.Vec3i; import ru.windcorp.progressia.common.world.block.BlockData; @@ -49,7 +50,7 @@ public class DefaultChunkData implements ChunkData { public boolean isEmpty = false; public boolean isOpaque = false; - public static HashSet transparent; + public static Set transparent = Collections.emptySet(); private final Vec3i position = new Vec3i(); private final DefaultWorldData world; @@ -204,45 +205,36 @@ public class DefaultChunkData implements ChunkData { public void setGenerationHint(Object generationHint) { this.generationHint = generationHint; } - - public void computeOpaque() - { - for (int xyz=0;xyz buffer = new ArrayList<>(); - + private static class ChunkUnloadRequest { private final Vec3i chunkPos; private final long unloadAt; - + public ChunkUnloadRequest(Vec3i chunkPos, long unloadAt) { this.chunkPos = chunkPos; this.unloadAt = unloadAt; } - + /** * @return the chunk position */ public Vec3i getChunkPos() { return chunkPos; } - + /** * @return the moment when the chunks becomes eligible for unloading */ @@ -72,19 +72,42 @@ public class ChunkRequestDaemon { return unloadAt; } } - + private final ChunkMap unloadSchedule = ChunkMaps.newHashMap(); + private Thread thread = null; + private final AtomicBoolean shouldRun = new AtomicBoolean(false); + public ChunkRequestDaemon(ChunkManager chunkManager) { this.chunkManager = chunkManager; this.loaded = getServer().getWorld().getData().getLoadedChunks(); } - - public void tick() { + + public synchronized void start() { + if (this.thread != null) { + throw new IllegalStateException("Already running"); + } + + this.thread = new Thread(this::runOffthread, getClass().getSimpleName()); + this.shouldRun.set(true); + this.thread.start(); + } + + public synchronized void stop() { + this.shouldRun.set(false); + + synchronized (this) { + notify(); + } + } + + public synchronized void tick() { synchronized (getServer().getWorld().getData()) { synchronized (getServer().getPlayerManager().getMutex()) { loadAndUnloadChunks(); sendAndRevokeChunks(); + + notify(); } } } @@ -116,18 +139,13 @@ public class ChunkRequestDaemon { } private void processLoadQueues() { - toRequestUnload.forEach((pos) -> executor.submit(() -> scheduleUnload(pos))); - toRequestUnload.clear(); - - toLoad.forEach((pos) -> executor.submit(() -> getChunkManager().loadOrGenerateChunk(pos))); - toLoad.clear(); - - toGenerate.forEach((pos) -> executor.submit(() -> getChunkManager().loadOrGenerateChunk(pos))); + toGenerate.forEach(getChunkManager()::loadOrGenerateChunk); toGenerate.clear(); - - executor.submit(() -> unloadScheduledChunks()); + + toRequestUnload.forEach(this::scheduleUnload); + toRequestUnload.clear(); } - + private void scheduleUnload(Vec3i chunkPos) { if (unloadSchedule.containsKey(chunkPos)) { // Unload already requested, skip @@ -136,27 +154,8 @@ public class ChunkRequestDaemon { long unloadAt = System.currentTimeMillis() + (long) (getUnloadDelay() * 1000); Vec3i chunkPosCopy = new Vec3i(chunkPos); - - unloadSchedule.put(chunkPosCopy, new ChunkUnloadRequest(chunkPosCopy, unloadAt)); - } - - private void unloadScheduledChunks() { - long now = System.currentTimeMillis(); - - for (Iterator it = unloadSchedule.values().iterator(); it.hasNext();) { - ChunkUnloadRequest request = it.next(); - - if (request.getUnloadAt() < now) { - it.remove(); - - if (requested.contains(request.getChunkPos())) { - continue; // do not unload chunks that became requested - } - - getChunkManager().unloadChunk(request.getChunkPos()); - } - } + unloadSchedule.put(chunkPosCopy, new ChunkUnloadRequest(chunkPosCopy, unloadAt)); } private void sendAndRevokeChunks() { @@ -172,51 +171,129 @@ public class ChunkRequestDaemon { toGenerate.add(chunk); return; } - + if (vision.isChunkVisible(chunk.getPosition())) { return; } - + buffer.add(chunk.getPosition()); }); - - if (buffer.isEmpty()) return; + + if (buffer.isEmpty()) + return; for (Vec3i chunkPos : buffer) { getChunkManager().sendChunk(vision.getPlayer(), chunkPos); } - + buffer.clear(); } - + private void revokeChunks(PlayerVision vision) { vision.getVisibleChunks().forEach(chunkPos -> { if (getChunkManager().isChunkLoaded(chunkPos) && vision.getRequestedChunks().contains(chunkPos)) return; buffer.add(new Vec3i(chunkPos)); }); - - if (buffer.isEmpty()) return; + + if (buffer.isEmpty()) + return; for (Vec3i chunkPos : buffer) { getChunkManager().revokeChunk(vision.getPlayer(), chunkPos); } - + buffer.clear(); } + /* + * Off-thread activity + */ + + private void runOffthread() { + while (true) { + + processLoadQueue(); + processUnloadQueue(); + + synchronized (this) { + try { + // We're not afraid of spurious wakeups + wait(); + } catch (InterruptedException e) { + // Pretend nothing happened + } + + if (!shouldRun.get()) { + return; + } + } + + } + } + + private void processQueueOffthread(ChunkSet queue, Consumer action) { + while (true) { + synchronized (this) { + Iterator iterator = toLoad.iterator(); + if (!iterator.hasNext()) { + return; + } + Vec3i position = iterator.next(); + iterator.remove(); + + action.accept(position); + } + } + } + + private void processLoadQueue() { + processQueueOffthread(toLoad, getChunkManager()::loadOrGenerateChunk); + } + + private void processUnloadQueue() { + long now = System.currentTimeMillis(); + + Collection toUnload = null; + + synchronized (this) { + for (Iterator it = unloadSchedule.values().iterator(); it.hasNext();) { + ChunkUnloadRequest request = it.next(); + + if (request.getUnloadAt() < now) { + it.remove(); + + if (requested.contains(request.getChunkPos())) { + continue; // do not unload chunks that became requested + } + + if (toUnload == null) { + toUnload = new ArrayList<>(); + } + toUnload.add(request.getChunkPos()); + } + } + } + + if (toUnload == null) { + return; + } + + toUnload.forEach(getChunkManager()::unloadChunk); + } + /** * @return the minimum amount of time a chunk will spend in the unload queue */ public float getUnloadDelay() { return CHUNK_UNLOAD_DELAY; } - + /** * @return the manager */ public ChunkManager getChunkManager() { return chunkManager; } - + public Server getServer() { return getChunkManager().getServer(); } diff --git a/src/main/java/ru/windcorp/progressia/server/world/DefaultChunkLogic.java b/src/main/java/ru/windcorp/progressia/server/world/DefaultChunkLogic.java index d18ff11..7a05adf 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/DefaultChunkLogic.java +++ b/src/main/java/ru/windcorp/progressia/server/world/DefaultChunkLogic.java @@ -34,7 +34,7 @@ import ru.windcorp.progressia.common.world.rels.RelFace; import ru.windcorp.progressia.common.world.TileDataStack; import ru.windcorp.progressia.common.world.generic.GenericChunks; import ru.windcorp.progressia.common.world.TileDataReference; -import ru.windcorp.progressia.server.Server; +import ru.windcorp.progressia.server.ServerState; import ru.windcorp.progressia.server.world.block.BlockLogic; import ru.windcorp.progressia.server.world.block.BlockLogicRegistry; import ru.windcorp.progressia.server.world.block.TickableBlock; @@ -226,7 +226,7 @@ public class DefaultChunkLogic implements ChunkLogic { } private void tmp_generateTickLists() { - ServerWorldContextRO context = Server.getCurrentServer().createContext(getUp()); + ServerWorldContextRO context = ServerState.getInstance().createContext(getUp()); GenericChunks.forEachBiC(blockInChunk -> { diff --git a/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java b/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java index 3e654b6..5c4e75d 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java +++ b/src/main/java/ru/windcorp/progressia/server/world/DefaultWorldLogic.java @@ -19,6 +19,7 @@ package ru.windcorp.progressia.server.world; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -43,7 +44,7 @@ public class DefaultWorldLogic implements WorldLogic { private final WorldGenerator generator; - private final Map chunks = new HashMap<>(); + private final Map chunks = Collections.synchronizedMap(new HashMap<>()); private final Evaluation tickEntitiesTask = new TickEntitiesTask(); diff --git a/src/main/java/ru/windcorp/progressia/test/gen/TestGenerationConfig.java b/src/main/java/ru/windcorp/progressia/test/gen/TestGenerationConfig.java index ee0f312..76090d6 100644 --- a/src/main/java/ru/windcorp/progressia/test/gen/TestGenerationConfig.java +++ b/src/main/java/ru/windcorp/progressia/test/gen/TestGenerationConfig.java @@ -49,9 +49,18 @@ public class TestGenerationConfig { private static final float CURVATURE = Units.get("100 m"); private static final float INNER_RADIUS = Units.get("200 m"); - private static final Fields FIELDS = new Fields(SEED); + private final Fields fields = new Fields(SEED); + private final Function generator; + + public TestGenerationConfig() { + this.generator = createGenerator(); + } + + public Function getGenerator() { + return generator; + } - public static Function createGenerator() { + private Function createGenerator() { Planet planet = new Planet( ((int) PLANET_RADIUS) / Coordinates.CHUNK_SIZE, @@ -60,7 +69,7 @@ public class TestGenerationConfig { INNER_RADIUS ); - TestHeightMap heightMap = new TestHeightMap(planet, planet.getRadius() / 4, FIELDS); + TestHeightMap heightMap = new TestHeightMap(planet, planet.getRadius() / 4, fields); FloatRangeMap layers = new ArrayFloatRangeMap<>(); registerTerrainLayers(layers); @@ -72,11 +81,11 @@ public class TestGenerationConfig { } - private static void registerTerrainLayers(FloatRangeMap layers) { + private void registerTerrainLayers(FloatRangeMap layers) { BlockData dirt = BlockDataRegistry.getInstance().get("Test:Dirt"); BlockData air = BlockDataRegistry.getInstance().get("Test:Air"); - SurfaceFloatField cliffs = FIELDS.get("Test:Cliff"); + SurfaceFloatField cliffs = fields.get("Test:Cliff"); WorleyProceduralNoise.Builder builder = WorleyProceduralNoise.builder(); TestContent.ROCKS.getRocks().forEach(rock -> { @@ -88,9 +97,9 @@ public class TestGenerationConfig { } }, 1); }); - SurfaceFloatField rockDepthOffsets = FIELDS.register( + SurfaceFloatField rockDepthOffsets = fields.register( "Test:RockDepthOffsets", - () -> tweak(FIELDS.primitive(), 40, 5) + () -> tweak(fields.primitive(), 40, 5) ); RockLayer rockLayer = new RockLayer(builder.build(SEED), rockDepthOffsets); @@ -105,28 +114,28 @@ public class TestGenerationConfig { layers.put(4, Float.POSITIVE_INFINITY, rockLayer); } - private static void registerFeatures(List features) { + private void registerFeatures(List features) { - SurfaceFloatField forestiness = FIELDS.register( + SurfaceFloatField forestiness = fields.register( "Test:Forest", - () -> squash(scale(FIELDS.primitive(), 200), 5) + () -> squash(scale(fields.primitive(), 200), 5) ); - SurfaceFloatField grassiness = FIELDS.register( + SurfaceFloatField grassiness = fields.register( "Test:Grass", f -> multiply( - tweak(octaves(FIELDS.primitive(), 2, 2), 40, 0.5, 1.2), - squash(tweak(FIELDS.get("Test:Forest", f), 1, -1, 1), 10), - anti(squash(FIELDS.get("Test:Cliff", f), 10)) + tweak(octaves(fields.primitive(), 2, 2), 40, 0.5, 1.2), + squash(tweak(fields.get("Test:Forest", f), 1, -1, 1), 10), + anti(squash(fields.get("Test:Cliff", f), 10)) ) ); - Function floweriness = flowerName -> FIELDS.register( + Function floweriness = flowerName -> fields.register( "Test:Flower" + flowerName, f -> multiply( - selectPositive(squash(scale(octaves(FIELDS.primitive(), 2, 3), 100), 2), 1, 0.5), - tweak(FIELDS.get("Test:Forest", f), 1, -1, 1.1), - anti(squash(FIELDS.get("Test:Cliff", f), 10)) + selectPositive(squash(scale(octaves(fields.primitive(), 2, 3), 100), 2), 1, 0.5), + tweak(fields.get("Test:Forest", f), 1, -1, 1.1), + anti(squash(fields.get("Test:Cliff", f), 10)) ) );