diff --git a/engine-tests/build.gradle b/engine-tests/build.gradle index 0f6de9e0d88..41a61f513bf 100644 --- a/engine-tests/build.gradle +++ b/engine-tests/build.gradle @@ -76,6 +76,8 @@ dependencies { testImplementation('com.google.truth:truth:1.1.3') testImplementation('com.google.truth.extensions:truth-java8-extension:1.1.3') + + implementation("io.projectreactor:reactor-test:3.4.14") } task copyResourcesToClasses(type:Copy) { diff --git a/engine-tests/src/test/java/org/terasology/engine/rendering/world/ChunkMeshWorkerTest.java b/engine-tests/src/test/java/org/terasology/engine/rendering/world/ChunkMeshWorkerTest.java new file mode 100644 index 00000000000..4423f502c61 --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/rendering/world/ChunkMeshWorkerTest.java @@ -0,0 +1,246 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.world; + +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.terasology.engine.rendering.primitives.ChunkMesh; +import org.terasology.engine.world.chunks.Chunk; +import org.terasology.engine.world.chunks.RenderableChunk; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; +import reactor.test.subscriber.TestSubscriber; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.time.Duration; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +public class ChunkMeshWorkerTest { + static final Duration EXPECTED_DURATION = Duration.ofSeconds(4); + + static Vector3ic position0 = new Vector3i(123, 456, 789); + + final Vector3i currentPosition = new Vector3i(position0); + + Comparator comparator = Comparator.comparingDouble(chunk -> + chunk.getRenderPosition().distanceSquared(currentPosition.x, currentPosition.y, currentPosition.z) + ); + ChunkMeshWorker worker; + + /** Creates a new mock ChunkMesh. + *

+ * A simple work function for {@link ChunkMeshWorker}. + */ + static Mono> alwaysCreateMesh(Chunk chunk) { + chunk.setDirty(false); + return Mono.just(Tuples.of(chunk, mock(ChunkMesh.class))); + } + + /** + * Create a new Chunk at this position. + *

+ * The {@link DummyChunk} is marked {@code ready} and {@code dirty}. + */ + static Chunk newDirtyChunk(Vector3ic position) { + var chunk = new DummyChunk(position); + chunk.markReady(); + chunk.setDirty(true); + return chunk; + } + + /** + * Creates a new ChunkMeshWorker with a StepVerifier on its output. + *

+ * Sets {@link #worker} to a new {@link ChunkMeshWorker}. + * + * @return A verifier for {@link ChunkMeshWorker#getCompletedChunks()}. + */ + protected StepVerifier.Step completedChunksStepVerifier() { + StepVerifier.setDefaultTimeout(EXPECTED_DURATION); + + // Use virtual time so we don't have to wait around in real time + // to see whether there are more events pending. + // Requires that the schedulers be created _inside_ the withVirtualTime supplier. + return StepVerifier.withVirtualTime(() -> { + worker = new ChunkMeshWorker( + ChunkMeshWorkerTest::alwaysCreateMesh, + comparator, + Schedulers.parallel(), + Schedulers.single() + ); + return worker.getCompletedChunks(); + }); + } + + /** + * Get completed Chunks as a list. + *

+ * Applies the given function to a new {@link ChunkMeshWorker}, and returns the list of completed + * {@link Chunk Chunks}. + *

+ * Assumes the work will not be delayed by more than {@link #EXPECTED_DURATION}. + */ + protected List getChunksThatResultFrom(Consumer withWorker) { + // TODO: Make a VirtualTimeScheduler JUnit Extension so that we don't have these + // two different ways of creating schedulers for the ChunkMeshWorker. + var scheduler = VirtualTimeScheduler.create(); + + var workerB = new ChunkMeshWorker( + ChunkMeshWorkerTest::alwaysCreateMesh, + comparator, + scheduler, + scheduler + ); + + var completed = workerB.getCompletedChunks() + .subscribeWith(TestSubscriber.create()); + + withWorker.accept(workerB); + + // The Worker doesn't mark the flux as complete; it expects it'll still get more work. + // That means we can't collect the the complete flux in to a list. + // Instead, we use TestSubscriber's methods to see what it has output so far. + // + // Other things I have tried here: + // * Adding `.timeout(EXPECTED_DURATION)` to the flux, and waiting for the TimeoutError. + // That works, and perhaps allows for less ambiguity about what is happening, but it + // doesn't seem to be necessary. + // + // * Using `.buffer(EXPECTED_DURATION).next()` instead of TestSubscriber.getReceived. + // Did not work; instead of giving me a buffer containing everything from that window, + // waiting on the result just timed out. + // + // See https://stackoverflow.com/a/72116182/9585 for some notes from the reactor-test author + // about this test scenario. + scheduler.advanceTimeBy(EXPECTED_DURATION); + return completed.getReceivedOnNext(); + } + + @Test + void testMultipleChunks() { + var chunk1 = newDirtyChunk(position0); + var chunk2 = newDirtyChunk(new Vector3i(position0).add(1, 0, 0)); + + var resultingChunks = getChunksThatResultFrom(worker -> { + worker.add(chunk1); + worker.add(chunk2); + worker.update(); + }); + + assertThat(resultingChunks).containsExactly(chunk1, chunk2); + } + + @Test + void testChunkIsNotProcessedTwice() { + var chunk1 = newDirtyChunk(position0); + + completedChunksStepVerifier().then(() -> { + worker.add(chunk1); + worker.add(chunk1); // added twice + worker.update(); + }) + .expectNextCount(1).as("expect only one result") + .then(() -> { + // adding it again and doing another update should still not change + worker.add(chunk1); + worker.update(); + }) + .verifyTimeout(EXPECTED_DURATION); + } + + @Test + void testChunkIsRegeneratedIfDirty() { + var chunk1 = newDirtyChunk(position0); + + completedChunksStepVerifier().then(() -> { + worker.add(chunk1); + worker.update(); + }) + .expectNext(chunk1).as("initial generation") + .then(() -> { + chunk1.setDirty(true); + worker.update(); + }) + .expectNext(chunk1).as("regenerating after dirty") + .verifyTimeout(EXPECTED_DURATION); + } + + @Test + void testChunkCanBeRemovedBeforeMeshGeneration() { + var chunk = newDirtyChunk(position0); + completedChunksStepVerifier().then(() -> { + worker.add(chunk); + worker.remove(chunk); + worker.update(); + }) + // chunk was removed, no events expected + .verifyTimeout(EXPECTED_DURATION); + } + + @Test + void testDoubleRemoveIsNoProblem() { + var chunk = newDirtyChunk(position0); + completedChunksStepVerifier().then(() -> { + worker.add(chunk); + worker.remove(chunk); + worker.update(); + }) + .then(() -> { + worker.remove(chunk); // second time calling remove on the same chunk + worker.update(); + }) + // chunk was removed, no events expected + .verifyTimeout(EXPECTED_DURATION); + } + + @Test + void testChunkCanBeRemovedByPosition() { + var chunk = newDirtyChunk(position0); + completedChunksStepVerifier().then(() -> { + worker.add(chunk); + worker.remove(position0); + worker.update(); + }) + // chunk was removed, no events expected + .verifyTimeout(EXPECTED_DURATION); + } + + @Test + void testWorkIsPrioritized() { + var nearChunk = newDirtyChunk(position0); + var farChunk = newDirtyChunk(new Vector3i(position0).add(100, 0, 0)); + + var completed = getChunksThatResultFrom(worker -> { + worker.add(farChunk); + worker.add(nearChunk); + worker.update(); + }); + // TODO: this may be flaky due to parallelization. + // Given a scheduler with N threads, we should test it with more than N chunks. + assertThat(completed).containsExactly(nearChunk, farChunk).inOrder(); + + // TODO: change the state of the comparator + // assert the next one through the gate is the one closest *now* + } + + @Test + @Disabled("TODO") + void testWorkerStopsWhenShutDown() { + fail("TODO: add shutdown method"); + } +} diff --git a/engine-tests/src/test/java/org/terasology/engine/rendering/world/DummyChunk.java b/engine-tests/src/test/java/org/terasology/engine/rendering/world/DummyChunk.java new file mode 100644 index 00000000000..433ed56f61e --- /dev/null +++ b/engine-tests/src/test/java/org/terasology/engine/rendering/world/DummyChunk.java @@ -0,0 +1,236 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.world; + +import com.google.common.base.MoreObjects; +import org.joml.Vector3i; +import org.joml.Vector3ic; +import org.terasology.engine.rendering.primitives.ChunkMesh; +import org.terasology.engine.world.block.Block; +import org.terasology.engine.world.block.BlockRegionc; +import org.terasology.engine.world.chunks.Chunk; +import org.terasology.engine.world.chunks.ChunkBlockIterator; +import org.terasology.joml.geom.AABBfc; +import org.terasology.protobuf.EntityData; + +import java.text.NumberFormat; + +public class DummyChunk implements Chunk { + private final Vector3ic chunkPos; + private boolean dirty; + private ChunkMesh mesh; + private boolean ready; + + public DummyChunk(Vector3ic position) { + this.chunkPos = position; + } + + @Override + public Vector3ic getPosition() { + return chunkPos; + } + + @Override + public Block getBlock(int x, int y, int z) { + return null; + } + + @Override + public Block setBlock(int x, int y, int z, Block block) { + return null; + } + + @Override + public void setExtraData(int index, int x, int y, int z, int value) { + + } + + @Override + public int getExtraData(int index, int x, int y, int z) { + return 0; + } + + @Override + public Vector3i getChunkWorldOffset(Vector3i pos) { + return null; + } + + @Override + public int getChunkWorldOffsetX() { + return chunkPos.x() * getChunkSizeX(); + } + + @Override + public int getChunkWorldOffsetY() { + return chunkPos.y() * getChunkSizeY(); + } + + @Override + public int getChunkWorldOffsetZ() { + return chunkPos.z() * getChunkSizeZ(); + } + + @Override + public Vector3i chunkToWorldPosition(int x, int y, int z, Vector3i dest) { + return null; + } + + @Override + public int chunkToWorldPositionX(int x) { + return 0; + } + + @Override + public int chunkToWorldPositionY(int y) { + return 0; + } + + @Override + public int chunkToWorldPositionZ(int z) { + return 0; + } + + @Override + public BlockRegionc getRegion() { + return null; + } + + @Override + public byte getSunlight(int x, int y, int z) { + return 0; + } + + @Override + public boolean setSunlight(int x, int y, int z, byte amount) { + return false; + } + + @Override + public byte getSunlightRegen(int x, int y, int z) { + return 0; + } + + @Override + public boolean setSunlightRegen(int x, int y, int z, byte amount) { + return false; + } + + @Override + public byte getLight(int x, int y, int z) { + return 0; + } + + @Override + public boolean setLight(int x, int y, int z, byte amount) { + return false; + } + + @Override + public int getEstimatedMemoryConsumptionInBytes() { + return 0; + } + + @Override + public ChunkBlockIterator getBlockIterator() { + return null; + } + + @Override + public void markReady() { + ready = true; + } + + @Override + public AABBfc getAABB() { + return null; + } + + @Override + public void setMesh(ChunkMesh newMesh) { + this.mesh = newMesh; + } + + @Override + public void setAnimated(boolean animated) { + + } + + @Override + public boolean isAnimated() { + return false; + } + + @Override + public boolean hasMesh() { + return mesh != null; + } + + @Override + public ChunkMesh getMesh() { + return mesh; + } + + @Override + public void disposeMesh() { + mesh = null; + } + + @Override + public boolean isReady() { + return ready; + } + + @Override + public void deflate() { + + } + + @Override + public void deflateSunlight() { + + } + + @Override + public void dispose() { + + } + + @Override + public boolean isDisposed() { + return false; + } + + @Override + public void prepareForReactivation() { + + } + + @Override + public EntityData.ChunkStore.Builder encode() { + return null; + } + + @Override + public boolean isDirty() { + return dirty; + } + + @Override + public void setDirty(boolean dirty) { + this.dirty = dirty; + } + + @Override + public String toString() { + // I think using scientific notation for small integers adds a lot of noise. + // Should we set `joml.format` to false? + String pos = ((Vector3i) chunkPos).toString(NumberFormat.getIntegerInstance()); + return MoreObjects.toStringHelper(this) + .addValue(pos) + .add("dirty", dirty) + .add("ready", ready) + .add("mesh", mesh) + .toString(); + } +} diff --git a/engine/build.gradle b/engine/build.gradle index 2bad356f42d..a219b9875b3 100644 --- a/engine/build.gradle +++ b/engine/build.gradle @@ -102,8 +102,9 @@ dependencies { implementation 'io.micrometer:micrometer-core:1.8.0' implementation 'io.micrometer:micrometer-registry-jmx:1.8.0' - api group: 'io.projectreactor', name: 'reactor-core', version: '3.4.11' - api group: 'io.projectreactor.addons', name: 'reactor-extra', version: '3.4.5' + api group: 'io.projectreactor', name: 'reactor-core', version: '3.4.14' + api group: 'io.projectreactor.addons', name: 'reactor-extra', version: '3.4.6' + api group: 'org.joml', name: 'joml', version: '1.10.0' api group: 'org.terasology.joml-ext', name: 'joml-geometry', version: '0.1.0' diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/ChunkMeshComponent.java b/engine/src/main/java/org/terasology/engine/rendering/logic/ChunkMeshComponent.java index cf404ed5103..7492301e928 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/logic/ChunkMeshComponent.java +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/ChunkMeshComponent.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.logic; @@ -26,6 +26,13 @@ public ChunkMeshComponent(ChunkMesh mesh, AABBf aabb) { this.aabb = aabb; } + public synchronized void setMesh(ChunkMesh mesh) { + if (this.mesh != null) { + this.mesh.dispose(); + } + this.mesh = mesh; + } + @Override public void copyFrom(ChunkMeshComponent other) { this.mesh = other.mesh; // TODO deep or shallow copy? diff --git a/engine/src/main/java/org/terasology/engine/rendering/logic/EntityBasedRenderableChunk.java b/engine/src/main/java/org/terasology/engine/rendering/logic/EntityBasedRenderableChunk.java index 8605045ca31..6761e3dbbc8 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/logic/EntityBasedRenderableChunk.java +++ b/engine/src/main/java/org/terasology/engine/rendering/logic/EntityBasedRenderableChunk.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.logic; @@ -34,9 +34,10 @@ public AABBfc getAABB() { @Override public void setMesh(ChunkMesh newMesh) { - ChunkMeshComponent component = entity.getComponent(ChunkMeshComponent.class); - component.mesh = newMesh; - entity.saveComponent(component); + entity.updateComponent(ChunkMeshComponent.class, c -> { + c.setMesh(newMesh); + return c; + }); } @Override @@ -65,10 +66,10 @@ public ChunkMesh getMesh() { @Override public void disposeMesh() { - ChunkMeshComponent component = entity.getComponent(ChunkMeshComponent.class); - if (component != null && component.mesh != null) { - component.mesh.dispose(); - } + entity.updateComponent(ChunkMeshComponent.class, c -> { + c.setMesh(null); + return c; + }); } @Override diff --git a/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkMesh.java b/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkMesh.java index 0ed52c6dd78..c4a36c154af 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkMesh.java +++ b/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkMesh.java @@ -53,8 +53,6 @@ public interface ChunkMesh { int render(ChunkMesh.RenderPhase type); - - /** * Possible rendering types. */ diff --git a/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkTessellator.java b/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkTessellator.java index b920d3134cb..c6ca1835c58 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkTessellator.java +++ b/engine/src/main/java/org/terasology/engine/rendering/primitives/ChunkTessellator.java @@ -33,15 +33,11 @@ public ChunkMesh generateMesh(ChunkView chunkView, float scale, int border) { final Stopwatch watch = Stopwatch.createStarted(); - watch.stop(); - mesh.setTimeToGenerateBlockVertices((int) watch.elapsed(TimeUnit.MILLISECONDS)); - watch.reset().start(); - // The mesh extends into the borders in the horizontal directions, but not vertically upwards, in order to cover // gaps between LOD chunks of different scales, but also avoid multiple overlapping ocean surfaces. - for (int x = 0; x < Chunks.SIZE_X; x++) { + for (int y = 0; y < Chunks.SIZE_Y - border * 2; y++) { for (int z = 0; z < Chunks.SIZE_Z; z++) { - for (int y = 0; y < Chunks.SIZE_Y - border * 2; y++) { + for (int x = 0; x < Chunks.SIZE_X; x++) { Block block = chunkView.getBlock(x, y, z); block.getMeshGenerator().generateChunkMesh(chunkView, mesh, x, y, z); } @@ -61,7 +57,7 @@ public ChunkMesh generateMesh(ChunkView chunkView, float scale, int border) { } watch.stop(); - mesh.setTimeToGenerateOptimizedBuffers((int) watch.elapsed(TimeUnit.MILLISECONDS)); + mesh.setTimeToGenerateBlockVertices((int) watch.elapsed(TimeUnit.MILLISECONDS)); statVertexArrayUpdateCount++; PerformanceMonitor.endActivity(); diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshUpdateManager.java b/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshUpdateManager.java deleted file mode 100644 index 2e2342ae3a5..00000000000 --- a/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshUpdateManager.java +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 -package org.terasology.engine.rendering.world; - -import com.google.common.collect.Lists; -import com.google.common.collect.Queues; -import org.joml.RoundingMode; -import org.joml.Vector3f; -import org.joml.Vector3i; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.engine.monitoring.chunk.ChunkMonitor; -import org.terasology.engine.rendering.primitives.ChunkMesh; -import org.terasology.engine.rendering.primitives.ChunkTessellator; -import org.terasology.engine.utilities.concurrency.TaskMaster; -import org.terasology.engine.world.ChunkView; -import org.terasology.engine.world.WorldProvider; -import org.terasology.engine.world.chunks.Chunk; -import org.terasology.engine.world.chunks.Chunks; -import org.terasology.engine.world.chunks.pipeline.ChunkTask; -import org.terasology.engine.world.chunks.pipeline.ShutdownChunkTask; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.BlockingDeque; -import java.util.concurrent.ConcurrentHashMap; - -/** - * Provides the mechanism for updating and generating chunk meshes. - * - */ -public final class ChunkMeshUpdateManager { - private static final int NUM_TASK_THREADS = 8; - - private static final Logger logger = LoggerFactory.getLogger(ChunkMeshUpdateManager.class); - - /* CHUNK UPDATES */ - private final Set chunksProcessing = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - private final BlockingDeque chunksComplete = Queues.newLinkedBlockingDeque(); - - private TaskMaster chunkUpdater; - - private final ChunkTessellator tessellator; - private final WorldProvider worldProvider; - /** - * This variable is volatile, so that it's value is visible to worker thread that calculates the best task to - * process - */ - private volatile float cameraChunkPosX; - private volatile float cameraChunkPosY; - private volatile float cameraChunkPosZ; - - public ChunkMeshUpdateManager(ChunkTessellator tessellator, WorldProvider worldProvider) { - this.tessellator = tessellator; - this.worldProvider = worldProvider; - - chunkUpdater = TaskMaster.createPriorityTaskMaster("Chunk-Updater", NUM_TASK_THREADS, 100, new ChunkUpdaterComparator()); - } - - /** - * Updates the given chunk using a new thread from the thread pool. If the maximum amount of chunk updates - * is reached, the chunk update is ignored. Chunk updates can be forced though. - * - * @param chunk The chunk to update - * @return True if a chunk update was executed - */ - // TODO: Review this system - public boolean queueChunkUpdate(Chunk chunk) { - - if (!chunksProcessing.contains(chunk)) { - executeChunkUpdate(chunk); - return true; - } - - return false; - } - - /** - * The method tells the chunk mesh update manager where the camera is, so that is able to prioritize chunks near the - * camera. It stores the values in volatile variables so that the change is visible to the chunk updating threads - * immediately. - */ - public void setCameraPosition(Vector3f cameraPosition) { - Vector3i chunkPos = Chunks.toChunkPos(new Vector3i(cameraPosition, RoundingMode.FLOOR)); - cameraChunkPosX = chunkPos.x; - cameraChunkPosY = chunkPos.y; - cameraChunkPosZ = chunkPos.z; - } - - public List availableChunksForUpdate() { - List result = Lists.newArrayListWithExpectedSize(chunksComplete.size()); - chunksComplete.drainTo(result); - chunksProcessing.removeAll(result); - return result; - } - - private void executeChunkUpdate(final Chunk c) { - chunksProcessing.add(c); - - ChunkUpdateTask task = new ChunkUpdateTask(c, tessellator, worldProvider, this); - try { - chunkUpdater.put(task); - } catch (InterruptedException e) { - logger.error("Failed to enqueue task {}", task, e); - } - } - - private void finishedProcessing(Chunk c) { - chunksComplete.add(c); - } - - public void shutdown() { - chunkUpdater.shutdown(new ShutdownChunkTask(), false); - } - - - private static class ChunkUpdateTask implements ChunkTask { - - private Chunk c; - private ChunkTessellator tessellator; - private WorldProvider worldProvider; - private ChunkMeshUpdateManager chunkMeshUpdateManager; - - ChunkUpdateTask(Chunk chunk, ChunkTessellator tessellator, WorldProvider worldProvider, ChunkMeshUpdateManager chunkMeshUpdateManager) { - this.chunkMeshUpdateManager = chunkMeshUpdateManager; - this.c = chunk; - this.tessellator = tessellator; - this.worldProvider = worldProvider; - } - - - @Override - public String getName() { - return "Update chunk"; - } - - @Override - public boolean isTerminateSignal() { - return false; - } - - @Override - public void run() { - ChunkMesh newMesh; - ChunkView chunkView = worldProvider.getLocalView(c.getPosition()); - if (chunkView != null) { - /* - * Important set dirty flag first, so that a concurrent modification of the chunk in the mean time we - * will end up with a dirty chunk. - */ - c.setDirty(false); - if (chunkView.isValidView()) { - newMesh = tessellator.generateMesh(chunkView); - - c.setPendingMesh(newMesh); - ChunkMonitor.fireChunkTessellated(c, newMesh); - } - - } - chunkMeshUpdateManager.finishedProcessing(c); - // Clean these up because the task executor holds the object in memory. - c = null; - tessellator = null; - worldProvider = null; - } - - @Override - public Chunk getChunk() { - return (Chunk) c; - } - } - - private class ChunkUpdaterComparator implements Comparator { - @Override - public int compare(ChunkTask o1, ChunkTask o2) { - return score(o1) - score(o2); - } - - private int score(ChunkTask task) { - if (task.isTerminateSignal()) { - return -1; - } - return distFromRegion(task.getPosition(), new Vector3i(cameraChunkPosX, cameraChunkPosY, cameraChunkPosZ, RoundingMode.FLOOR)); - } - - private int distFromRegion(Vector3i pos, Vector3i regionCenter) { - return (int) pos.gridDistance(regionCenter); - } - } -} diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java b/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java new file mode 100644 index 00000000000..20ba3ba9f8a --- /dev/null +++ b/engine/src/main/java/org/terasology/engine/rendering/world/ChunkMeshWorker.java @@ -0,0 +1,176 @@ +// Copyright 2022 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.engine.rendering.world; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.joml.Vector3ic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.engine.core.GameScheduler; +import org.terasology.engine.monitoring.chunk.ChunkMonitor; +import org.terasology.engine.rendering.primitives.ChunkMesh; +import org.terasology.engine.rendering.primitives.ChunkTessellator; +import org.terasology.engine.rendering.world.viewDistance.ViewDistance; +import org.terasology.engine.world.ChunkView; +import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.chunks.Chunk; +import org.terasology.engine.world.chunks.RenderableChunk; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.core.scheduler.Scheduler; +import reactor.function.TupleUtils; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * Receives RenderableChunks, works to make sure their Mesh is up-to-date. + *

+ * Prioritizes work according to the given comparator function. + *

+ * TODO: + *

+ */ +public final class ChunkMeshWorker { + private static final Logger logger = LoggerFactory.getLogger(ChunkMeshWorker.class); + + private static final int MAX_LOADABLE_CHUNKS = + ViewDistance.MEGA.getChunkDistance().x() * ViewDistance.MEGA.getChunkDistance().y() * ViewDistance.MEGA.getChunkDistance().z(); + + private final Comparator frontToBackComparator; + private final Set chunkMeshProcessing = Sets.newConcurrentHashSet(); + + private final Sinks.Many chunkMeshPublisher = Sinks.many().unicast().onBackpressureBuffer(); + private final List chunksInProximityOfCamera = Lists.newArrayListWithCapacity(MAX_LOADABLE_CHUNKS); + private final Flux> chunksAndNewMeshes; + private final Flux completedChunks; + + ChunkMeshWorker(Function>> workFunction, + Comparator frontToBackComparator, Scheduler parallelScheduler, Scheduler graphicsScheduler) { + this.frontToBackComparator = frontToBackComparator; + + chunksAndNewMeshes = chunkMeshPublisher.asFlux() + .distinct(Chunk::getPosition, () -> chunkMeshProcessing) + .parallel().runOn(parallelScheduler) + .flatMap(workFunction) + .sequential(); + + completedChunks = chunksAndNewMeshes + .publishOn(graphicsScheduler) + .map(TupleUtils.function(ChunkMeshWorker::uploadNewMesh)) + .doOnNext(chunk -> chunkMeshProcessing.remove(chunk.getPosition())); + + // FIXME: error handling??? + // throwable -> logger.error("Failed to build mesh {}", throwable); + } + + public static ChunkMeshWorker create(ChunkTessellator chunkTessellator, + WorldProvider worldProvider, + Comparator frontToBackComparator) { + ChunkMeshWorker worker = new ChunkMeshWorker(generateMeshFunc(chunkTessellator, worldProvider), + frontToBackComparator, + GameScheduler.parallel(), GameScheduler.gameMain()); + worker.completedChunks.subscribe(); + return worker; + } + + public void add(Chunk chunk) { + // TODO: avoid adding duplicates + chunksInProximityOfCamera.add(chunk); + } + + public void remove(Chunk chunk) { + chunkMeshProcessing.remove(chunk.getPosition()); + + chunksInProximityOfCamera.remove(chunk); + chunk.disposeMesh(); + } + + public void remove(Vector3ic coord) { + chunkMeshProcessing.remove(coord); + + Iterator iterator = chunksInProximityOfCamera.iterator(); + while (iterator.hasNext()) { + Chunk chunk = iterator.next(); + if (chunk.getPosition().equals(coord)) { + chunk.disposeMesh(); + iterator.remove(); + break; + } + } + } + + /** + * Queue all dirty items in our collection, in priority order. + * + * @return the number of dirty chunks added to the queue + */ + public int update() { + int statDirtyChunks = 0; + chunksInProximityOfCamera.sort(frontToBackComparator); + for (Chunk chunk : chunksInProximityOfCamera) { + if (!chunk.isReady()) { + // Chunk was added as part of some region, but not yet ready. + // Leave it here with the expectation that it will be ready later. + continue; + } + if (!chunk.isDirty()) { + // Chunk is in proximity list, but is no longer dirty. Probably already processed. + // Will poll it again next tick to see if it got dirty since then. + continue; + } + statDirtyChunks++; + Sinks.EmitResult result = chunkMeshPublisher.tryEmitNext(chunk); + if (result.isFailure()) { + logger.error("failed to process chunk {} : {}", chunk, result); + } + } + return statDirtyChunks; + } + + public int numberChunkMeshProcessing() { + return chunkMeshProcessing.size(); + } + + public Collection chunks() { + return chunksInProximityOfCamera; + } + + Flux getCompletedChunks() { + return completedChunks; + } + + private static Chunk uploadNewMesh(Chunk chunk, ChunkMesh chunkMesh) { + chunkMesh.updateMesh(); // Does GL stuff, must be on main thread! + chunkMesh.discardData(); + chunk.setMesh(chunkMesh); + return chunk; + } + + private static Function>> generateMeshFunc(ChunkTessellator chunkTessellator, WorldProvider worldProvider) { + return (chunk -> { + chunk.setDirty(false); + ChunkView chunkView = worldProvider.getLocalView(chunk.getPosition()); + if (chunkView != null && chunkView.isValidView()) { + ChunkMesh newMesh = chunkTessellator.generateMesh(chunkView); + ChunkMonitor.fireChunkTessellated(chunk, newMesh); + return Mono.just(Tuples.of(chunk, newMesh)); + } + return Mono.empty(); + }); + } +} diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorld.java b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorld.java index 9ad1e31d45e..975bc06e5b1 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorld.java +++ b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorld.java @@ -24,8 +24,6 @@ public interface RenderableWorld { boolean updateChunksInProximity(ViewDistance viewDistance, int chunkLods); - void generateVBOs(); - int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame); void dispose(); diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java index 446eaa74847..e513ac59243 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java +++ b/engine/src/main/java/org/terasology/engine/rendering/world/RenderableWorldImpl.java @@ -1,9 +1,9 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.world; import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; +import org.joml.Math; import org.joml.Vector3f; import org.joml.Vector3fc; import org.joml.Vector3i; @@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory; import org.terasology.engine.config.Config; import org.terasology.engine.config.RenderingConfig; -import org.terasology.engine.context.Context; import org.terasology.engine.monitoring.PerformanceMonitor; import org.terasology.engine.registry.CoreRegistry; import org.terasology.engine.rendering.cameras.Camera; @@ -28,14 +27,10 @@ import org.terasology.engine.world.chunks.Chunks; import org.terasology.engine.world.chunks.LodChunkProvider; import org.terasology.engine.world.chunks.RenderableChunk; -import org.terasology.engine.world.generator.ScalableWorldGenerator; -import org.terasology.engine.world.generator.WorldGenerator; import org.terasology.joml.geom.AABBfc; -import org.terasology.math.TeraMath; import java.util.ArrayList; import java.util.Comparator; -import java.util.Iterator; import java.util.List; import java.util.PriorityQueue; @@ -50,16 +45,13 @@ class RenderableWorldImpl implements RenderableWorld { ViewDistance.MEGA.getChunkDistance().x() * ViewDistance.MEGA.getChunkDistance().y() * ViewDistance.MEGA.getChunkDistance().z(); private static final Vector3fc CHUNK_CENTER_OFFSET = new Vector3f(Chunks.CHUNK_SIZE).div(2); - private final int maxChunksForShadows = - TeraMath.clamp(CoreRegistry.get(Config.class).getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024); + private final int maxChunksForShadows; private final WorldProvider worldProvider; private final ChunkProvider chunkProvider; private final LodChunkProvider lodChunkProvider; private final ChunkTessellator chunkTessellator; - private final ChunkMeshUpdateManager chunkMeshUpdateManager; - private final List chunksInProximityOfCamera = Lists.newArrayListWithCapacity(MAX_LOADABLE_CHUNKS); private BlockRegion renderableRegion = new BlockRegion(BlockRegion.INVALID); private ViewDistance currentViewDistance; private final RenderQueuesHelper renderQueues; @@ -68,37 +60,39 @@ class RenderableWorldImpl implements RenderableWorld { private final Camera playerCamera; private Camera shadowMapCamera; - private final Config config = CoreRegistry.get(Config.class); - private final RenderingConfig renderingConfig = config.getRendering(); + private final RenderingConfig renderingConfig; private int statDirtyChunks; private int statVisibleChunks; private int statIgnoredPhases; + private final RenderableWorldImpl.ChunkFrontToBackComparator frontToBackComparator; + private final RenderableWorldImpl.ChunkBackToFrontComparator backToFrontComparator; - RenderableWorldImpl(Context context, Camera playerCamera) { + private final ChunkMeshWorker chunkWorker; - worldProvider = context.get(WorldProvider.class); - chunkProvider = context.get(ChunkProvider.class); - chunkTessellator = context.get(ChunkTessellator.class); - chunkMeshUpdateManager = new ChunkMeshUpdateManager(chunkTessellator, worldProvider); + RenderableWorldImpl(WorldRenderer worldRenderer, LodChunkProvider lodChunkProvider, ChunkProvider chunkProvider, + ChunkTessellator chunkTessellator, WorldProvider worldProvider, Config config, Camera playerCamera) { + frontToBackComparator = new RenderableWorldImpl.ChunkFrontToBackComparator(worldRenderer); + backToFrontComparator = new RenderableWorldImpl.ChunkBackToFrontComparator(worldRenderer); + this.worldProvider = worldProvider; + this.chunkProvider = chunkProvider; this.playerCamera = playerCamera; - WorldGenerator worldGenerator = context.get(WorldGenerator.class); - if (worldGenerator instanceof ScalableWorldGenerator) { - lodChunkProvider = new LodChunkProvider(context, (ScalableWorldGenerator) worldGenerator, - chunkTessellator, renderingConfig.getViewDistance(), (int) renderingConfig.getChunkLods(), - calcCameraCoordinatesInChunkUnits()); - } else { - lodChunkProvider = null; - } + this.lodChunkProvider = lodChunkProvider; + this.chunkTessellator = chunkTessellator; + this.renderingConfig = config.getRendering(); + this.maxChunksForShadows = Math.clamp(config.getRendering().getMaxChunksUsedForShadowMapping(), 64, 1024); + this.chunkWorker = ChunkMeshWorker.create(chunkTessellator, worldProvider, frontToBackComparator); renderQueues = new RenderQueuesHelper(new PriorityQueue<>(MAX_LOADABLE_CHUNKS, - new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkFrontToBackComparator()), - new PriorityQueue<>(MAX_LOADABLE_CHUNKS, new ChunkBackToFrontComparator())); + frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, frontToBackComparator), + new PriorityQueue<>(MAX_LOADABLE_CHUNKS, backToFrontComparator)); + + } @Override @@ -106,8 +100,7 @@ public void onChunkLoaded(Vector3ic chunkCoordinates) { if (renderableRegion.contains(chunkCoordinates)) { Chunk chunk = chunkProvider.getChunk(chunkCoordinates); if (chunk != null) { - chunksInProximityOfCamera.add(chunk); - chunksInProximityOfCamera.sort(new ChunkFrontToBackComparator()); + chunkWorker.add(chunk); if (lodChunkProvider != null) { lodChunkProvider.onRealChunkLoaded(chunkCoordinates); } @@ -126,16 +119,7 @@ public void onChunkLoaded(Vector3ic chunkCoordinates) { @Override public void onChunkUnloaded(Vector3ic chunkCoordinates) { if (renderableRegion.contains(chunkCoordinates)) { - Chunk chunk; - Iterator iterator = chunksInProximityOfCamera.iterator(); - while (iterator.hasNext()) { - chunk = iterator.next(); - if (chunk.getPosition().equals(chunkCoordinates)) { - chunk.disposeMesh(); - iterator.remove(); - break; - } - } + chunkWorker.remove(chunkCoordinates); } if (lodChunkProvider != null) { lodChunkProvider.onRealChunkUnloaded(chunkCoordinates); @@ -210,36 +194,22 @@ public void update() { */ @Override public boolean updateChunksInProximity(BlockRegion newRenderableRegion) { + if (!newRenderableRegion.equals(renderableRegion)) { - Chunk chunk; for (Vector3ic chunkPositionToRemove : renderableRegion) { if (!newRenderableRegion.contains(chunkPositionToRemove)) { - Iterator nearbyChunks = chunksInProximityOfCamera.iterator(); - while (nearbyChunks.hasNext()) { - chunk = nearbyChunks.next(); - if (chunk.getPosition().equals(chunkPositionToRemove)) { - chunk.disposeMesh(); - nearbyChunks.remove(); - break; - } - - } + chunkWorker.remove(chunkPositionToRemove); } } - boolean chunksHaveBeenAdded = false; for (Vector3ic chunkPositionToAdd : newRenderableRegion) { if (!renderableRegion.contains(chunkPositionToAdd)) { - chunk = chunkProvider.getChunk(chunkPositionToAdd); + Chunk chunk = chunkProvider.getChunk(chunkPositionToAdd); if (chunk != null) { - chunksInProximityOfCamera.add(chunk); - chunksHaveBeenAdded = true; + chunkWorker.add(chunk); } } } - if (chunksHaveBeenAdded) { - chunksInProximityOfCamera.sort(new ChunkFrontToBackComparator()); - } renderableRegion = newRenderableRegion; return true; } @@ -280,33 +250,6 @@ private Vector3i calcCameraCoordinatesInChunkUnits() { return Chunks.toChunkPos(cameraCoordinates, new Vector3i()); } - @Override - public void generateVBOs() { - PerformanceMonitor.startActivity("Building Mesh VBOs"); - ChunkMesh pendingMesh; - chunkMeshUpdateManager.setCameraPosition(playerCamera.getPosition()); - for (Chunk chunk : chunkMeshUpdateManager.availableChunksForUpdate()) { - - if (chunk.hasPendingMesh() && chunksInProximityOfCamera.contains(chunk)) { - pendingMesh = chunk.getPendingMesh(); - pendingMesh.updateMesh(); - pendingMesh.discardData(); - if (chunk.hasMesh()) { - chunk.getMesh().dispose(); - } - chunk.setMesh(pendingMesh); - chunk.setPendingMesh(null); - - } else { - if (chunk.hasPendingMesh()) { - chunk.getPendingMesh().dispose(); - chunk.setPendingMesh(null); - } - } - } - PerformanceMonitor.endActivity(); - } - /** * Updates the currently visible chunks (in sight of the player). */ @@ -317,7 +260,6 @@ public int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame) { statVisibleChunks = 0; statIgnoredPhases = 0; - int processedChunks = 0; int chunkCounter = 0; renderQueues.clear(); @@ -326,7 +268,7 @@ public int queueVisibleChunks(boolean isFirstRenderingStageForCurrentFrame) { boolean isDynamicShadows = renderingConfig.isDynamicShadows(); int billboardLimit = (int) renderingConfig.getBillboardLimit(); - List allChunks = new ArrayList<>(chunksInProximityOfCamera); + List allChunks = new ArrayList<>(chunkWorker.chunks()); allChunks.addAll(chunkMeshRenderer.getRenderableChunks()); if (lodChunkProvider != null) { lodChunkProvider.addAllChunks(allChunks); @@ -380,17 +322,11 @@ && isChunkVisibleFromMainLight(chunk)) { } if (isFirstRenderingStageForCurrentFrame) { - for (Chunk chunk : chunksInProximityOfCamera) { - if (isChunkValidForRender(chunk) && (chunk.isDirty() || !chunk.hasMesh())) { - statDirtyChunks++; - chunkMeshUpdateManager.queueChunkUpdate(chunk); - processedChunks++; - } - } + statDirtyChunks = chunkWorker.update(); } PerformanceMonitor.endActivity(); - return processedChunks; + return chunkWorker.numberChunkMeshProcessing(); } private int triangleCount(ChunkMesh mesh, ChunkMesh.RenderPhase renderPhase) { @@ -403,7 +339,6 @@ private int triangleCount(ChunkMesh mesh, ChunkMesh.RenderPhase renderPhase) { @Override public void dispose() { - chunkMeshUpdateManager.shutdown(); if (lodChunkProvider != null) { lodChunkProvider.shutdown(); } @@ -484,25 +419,33 @@ private static float squaredDistanceToCamera(RenderableChunk chunk, Vector3f cam // TODO: find the right place to check if the activeCamera has changed, // TODO: so that the comparators can hold an up-to-date reference to it // TODO: and avoid having to find it on a per-comparison basis. - private static class ChunkFrontToBackComparator implements Comparator { + public static class ChunkFrontToBackComparator implements Comparator { + + private final WorldRenderer worldRenderer; + ChunkFrontToBackComparator(WorldRenderer worldRenderer) { + this.worldRenderer = worldRenderer; + } @Override public int compare(RenderableChunk chunk1, RenderableChunk chunk2) { Preconditions.checkNotNull(chunk1); Preconditions.checkNotNull(chunk2); - Vector3f cameraPosition = CoreRegistry.get(WorldRenderer.class).getActiveCamera().getPosition(); + Vector3f cameraPosition = worldRenderer.getActiveCamera().getPosition(); double distance1 = squaredDistanceToCamera(chunk1, cameraPosition); double distance2 = squaredDistanceToCamera(chunk2, cameraPosition); // Using Double.compare as simple d1 < d2 comparison is flagged as problematic by Jenkins // On the other hand Double.compare can return any positive/negative value apparently, // hence the need for Math.signum(). - return (int) Math.signum(Double.compare(distance1, distance2)); + return Math.signum(Double.compare(distance1, distance2)); } } - private static class ChunkBackToFrontComparator implements Comparator { - + public static class ChunkBackToFrontComparator implements Comparator { + private final WorldRenderer worldRenderer; + ChunkBackToFrontComparator(WorldRenderer worldRenderer) { + this.worldRenderer = worldRenderer; + } @Override public int compare(RenderableChunk chunk1, RenderableChunk chunk2) { Preconditions.checkNotNull(chunk1); @@ -514,5 +457,4 @@ public int compare(RenderableChunk chunk1, RenderableChunk chunk2) { return Double.compare(distance2, distance1); } } - } diff --git a/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java b/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java index b9ea118c7d0..cfe1713bfe5 100644 --- a/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java +++ b/engine/src/main/java/org/terasology/engine/rendering/world/WorldRendererImpl.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.rendering.world; @@ -42,6 +42,12 @@ import org.terasology.engine.rendering.world.viewDistance.ViewDistance; import org.terasology.engine.utilities.Assets; import org.terasology.engine.world.WorldProvider; +import org.terasology.engine.world.block.BlockManager; +import org.terasology.engine.world.chunks.ChunkProvider; +import org.terasology.engine.world.chunks.LodChunkProvider; +import org.terasology.engine.world.chunks.blockdata.ExtraBlockDataManager; +import org.terasology.engine.world.generator.ScalableWorldGenerator; +import org.terasology.engine.world.generator.WorldGenerator; import org.terasology.math.TeraMath; import java.util.List; @@ -156,7 +162,20 @@ public WorldRendererImpl(Context context) { context.put(ChunkTessellator.class, new ChunkTessellator()); - renderableWorld = new RenderableWorldImpl(context, playerCamera); + ChunkProvider chunkProvider = context.get(ChunkProvider.class); + ChunkTessellator chunkTessellator = context.get(ChunkTessellator.class); + BlockManager blockManager = context.get(BlockManager.class); + ExtraBlockDataManager extraDataManager = context.get(ExtraBlockDataManager.class); + Config config = context.get(Config.class); + + + WorldGenerator worldGenerator = context.get(WorldGenerator.class); + LodChunkProvider lodChunkProvider = null; + if (worldGenerator instanceof ScalableWorldGenerator) { + lodChunkProvider = new LodChunkProvider(chunkProvider, blockManager, extraDataManager, + (ScalableWorldGenerator) worldGenerator, chunkTessellator); + } + this.renderableWorld = new RenderableWorldImpl(this, lodChunkProvider, chunkProvider, chunkTessellator, worldProvider, config, playerCamera); renderQueues = renderableWorld.getRenderQueues(); initRenderingSupport(); @@ -295,7 +314,6 @@ private void preRenderUpdate(RenderingStage renderingStage) { playerCamera.update(secondsSinceLastFrame); renderableWorld.update(); - renderableWorld.generateVBOs(); secondsSinceLastFrame = 0; displayResolutionDependentFbo.update(); diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/Chunk.java b/engine/src/main/java/org/terasology/engine/world/chunks/Chunk.java index c2049c49874..e173844eef8 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/Chunk.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/Chunk.java @@ -5,7 +5,6 @@ import org.joml.Vector3f; import org.joml.Vector3i; import org.joml.Vector3ic; -import org.terasology.engine.rendering.primitives.ChunkMesh; import org.terasology.engine.world.block.Block; import org.terasology.engine.world.block.BlockRegionc; import org.terasology.gestalt.module.sandbox.API; @@ -13,6 +12,7 @@ /** * Chunks are a box-shaped logical grouping of Terasology's blocks, for performance reasons. + * *

* For example the renderer renders a single mesh for all opaque blocks in a chunk rather * than rendering each block as a separate mesh. @@ -386,10 +386,4 @@ default boolean setLight(Vector3ic pos, byte amount) { boolean isDirty(); void setDirty(boolean dirty); - - void setPendingMesh(ChunkMesh newPendingMesh); - - boolean hasPendingMesh(); - - ChunkMesh getPendingMesh(); } diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunk.java b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunk.java index 8d883f8df3d..ce5bdb0cdf8 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunk.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunk.java @@ -1,4 +1,4 @@ -// Copyright 2021 The Terasology Foundation +// Copyright 2022 The Terasology Foundation // SPDX-License-Identifier: Apache-2.0 package org.terasology.engine.world.chunks; @@ -6,9 +6,11 @@ import org.joml.Vector3f; import org.joml.Vector3i; import org.joml.Vector3ic; +import org.terasology.engine.rendering.primitives.ChunkMesh; import org.terasology.joml.geom.AABBf; import org.terasology.joml.geom.AABBfc; -import org.terasology.engine.rendering.primitives.ChunkMesh; + +import java.util.concurrent.atomic.AtomicReference; /** * A static, far away chunk that has only the data needed for rendering. @@ -18,11 +20,11 @@ public class LodChunk implements RenderableChunk { public int hiddenness; //The number of LOD chunks of the next level of fineness covering this one. public Chunk realVersion; //The real chunk hiding this one. private Vector3ic position; - private ChunkMesh mesh; + private final AtomicReference mesh = new AtomicReference<>(); public LodChunk(Vector3ic pos, ChunkMesh mesh, int scale) { position = pos; - this.mesh = mesh; + this.mesh.set(mesh); this.scale = scale; } @@ -58,24 +60,27 @@ public boolean hasMesh() { @Override public ChunkMesh getMesh() { - return mesh; + return mesh.get(); } @Override public void setMesh(ChunkMesh newMesh) { - mesh = newMesh; + var oldMesh = mesh.getAndSet(newMesh); + if (oldMesh != null) { + oldMesh.dispose(); + } } @Override public void disposeMesh() { - if (mesh != null) { - mesh.dispose(); - mesh = null; + var oldMesh = mesh.getAndSet(null); + if (oldMesh != null) { + oldMesh.dispose(); } } @Override public boolean isReady() { - return mesh != null && hiddenness < 8 && (realVersion == null || !realVersion.isReady() || !realVersion.hasMesh()); + return mesh.get() != null && hiddenness < 8 && (realVersion == null || !realVersion.isReady() || !realVersion.hasMesh()); } } diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java index 910fde4fa24..dc61fa4a5e8 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/LodChunkProvider.java @@ -6,9 +6,6 @@ import com.google.common.collect.Queues; import org.joml.Vector3i; import org.joml.Vector3ic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.terasology.engine.context.Context; import org.terasology.engine.rendering.primitives.ChunkMesh; import org.terasology.engine.rendering.primitives.ChunkTessellator; import org.terasology.engine.rendering.world.viewDistance.ViewDistance; @@ -34,17 +31,15 @@ import java.util.concurrent.PriorityBlockingQueue; public class LodChunkProvider { - private static final Logger logger = LoggerFactory.getLogger(LodChunkProvider.class); + private final ChunkProvider chunkProvider; + private final BlockManager blockManager; + private final ExtraBlockDataManager extraDataManager; + private final ChunkTessellator tessellator; + private final ScalableWorldGenerator generator; - private ChunkProvider chunkProvider; - private BlockManager blockManager; - private ExtraBlockDataManager extraDataManager; - private ChunkTessellator tessellator; - private ScalableWorldGenerator generator; - - private Vector3i center; - private ViewDistance viewDistanceSetting; - private int chunkLods; + private Vector3i center = new Vector3i(); + private ViewDistance viewDistanceSetting = ViewDistance.MODERATE; + private int chunkLods = 0; // The chunks that may be actually loaded. private BlockRegion possiblyLoadedRegion = new BlockRegion(BlockRegion.INVALID); // The chunks that should be visible, and therefore shouldn't have LOD chunks even if the chunk there hasn't @@ -53,28 +48,25 @@ public class LodChunkProvider { private BlockRegion[] lodRegions = new BlockRegion[0]; // The sizes of all of the LOD chunks that are meant to exist. All the chunks at the same positions with larger // sizes also may exist, but don't always. - private Map requiredChunks; - private ArrayList> chunks = new ArrayList<>(); - private ClosenessComparator nearby; + private final Map requiredChunks; + private final ArrayList> chunks = new ArrayList<>(); + private final ClosenessComparator nearby; // Communication with the generation threads. - private PriorityBlockingQueue neededChunks; - private BlockingQueue readyChunks = Queues.newLinkedBlockingQueue(); - private List generationThreads = new ArrayList<>(); + private final PriorityBlockingQueue neededChunks; + private final BlockingQueue readyChunks = Queues.newLinkedBlockingQueue(); + private final List generationThreads = new ArrayList<>(); - public LodChunkProvider(Context context, ScalableWorldGenerator generator, ChunkTessellator tessellator, - ViewDistance viewDistance, int chunkLods, Vector3i center) { - chunkProvider = context.get(ChunkProvider.class); - blockManager = context.get(BlockManager.class); - extraDataManager = context.get(ExtraBlockDataManager.class); + public LodChunkProvider(ChunkProvider chunkProvider, BlockManager blockManager, ExtraBlockDataManager extraDataManager, + ScalableWorldGenerator generator, ChunkTessellator tessellator) { + this.chunkProvider = chunkProvider; + this.blockManager = blockManager; + this.extraDataManager = extraDataManager; this.generator = generator; this.tessellator = tessellator; - viewDistanceSetting = viewDistance; - this.chunkLods = chunkLods; - this.center = center; - requiredChunks = new ConcurrentHashMap<>(); - nearby = new ClosenessComparator(center); - neededChunks = new PriorityBlockingQueue<>(11, nearby); + this.requiredChunks = new ConcurrentHashMap<>(); + this.nearby = new ClosenessComparator(center); + this.neededChunks = new PriorityBlockingQueue<>(11, nearby); for (int i = 0; i < 4; i++) { Thread thread = new Thread(this::createChunks, "LOD Chunk Generation " + i); thread.start(); @@ -82,6 +74,7 @@ public LodChunkProvider(Context context, ScalableWorldGenerator generator, Chunk } } + private void createChunks() { Block unloaded = blockManager.getBlock(BlockManager.UNLOADED_ID); try { @@ -150,10 +143,10 @@ public void update(Vector3i newCenter) { } public void updateRenderableRegion(ViewDistance newViewDistance, int newChunkLods, Vector3i newCenter) { - viewDistanceSetting = newViewDistance; - center = new Vector3i(delay(center.x, newCenter.x), delay(center.y, newCenter.y), delay(center.z, newCenter.z)); - chunkLods = newChunkLods; - nearby.pos = center; + this.viewDistanceSetting = newViewDistance; + this.center = new Vector3i(delay(center.x, newCenter.x), delay(center.y, newCenter.y), delay(center.z, newCenter.z)); + this.chunkLods = newChunkLods; + this.nearby.pos = center; Vector3i viewDistance = new Vector3i(newViewDistance.getChunkDistance()).div(2); Vector3i altViewDistance = viewDistance.add(1 - Math.abs(viewDistance.x % 2), 1 - Math.abs(viewDistance.y % 2), 1 - Math.abs(viewDistance.z % 2), new Vector3i()); diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/internal/ChunkImpl.java b/engine/src/main/java/org/terasology/engine/world/chunks/internal/ChunkImpl.java index e67c3bc6a20..eca80808323 100644 --- a/engine/src/main/java/org/terasology/engine/world/chunks/internal/ChunkImpl.java +++ b/engine/src/main/java/org/terasology/engine/world/chunks/internal/ChunkImpl.java @@ -28,6 +28,7 @@ import org.terasology.protobuf.EntityData; import java.text.DecimalFormat; +import java.util.concurrent.atomic.AtomicReference; /** * Chunks are the basic components of the world. Each chunk contains a fixed amount of blocks determined by its @@ -65,8 +66,7 @@ public class ChunkImpl implements Chunk { private boolean animated; // Rendering - private ChunkMesh activeMesh; - private ChunkMesh pendingMesh; + private final AtomicReference activeMesh = new AtomicReference<>(); public ChunkImpl(int x, int y, int z, BlockManager blockManager, ExtraBlockDataManager extraDataManager) { this(new Vector3i(x, y, z), blockManager, extraDataManager); @@ -358,12 +358,10 @@ public boolean equals(Object obj) { @Override public void setMesh(ChunkMesh mesh) { - this.activeMesh = mesh; - } - - @Override - public void setPendingMesh(ChunkMesh mesh) { - this.pendingMesh = mesh; + var oldMesh = activeMesh.getAndSet(mesh); + if (oldMesh != null) { + oldMesh.dispose(); + } } @Override @@ -378,22 +376,13 @@ public boolean isAnimated() { @Override public boolean hasMesh() { - return activeMesh != null; + return activeMesh.get() != null; } - @Override - public boolean hasPendingMesh() { - return pendingMesh != null; - } @Override public ChunkMesh getMesh() { - return activeMesh; - } - - @Override - public ChunkMesh getPendingMesh() { - return pendingMesh; + return activeMesh.get(); } @Override @@ -416,6 +405,7 @@ public void prepareForReactivation() { public void dispose() { disposed = true; ready = false; + dirty = true; disposeMesh(); /* * Explicitly do not clear data, so that background threads that work with the chunk can finish. @@ -425,9 +415,9 @@ public void dispose() { @Override public void disposeMesh() { - if (activeMesh != null) { - activeMesh.dispose(); - activeMesh = null; + var oldMesh = activeMesh.getAndSet(null); + if (oldMesh != null) { + oldMesh.dispose(); } } diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkTask.java b/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkTask.java deleted file mode 100644 index 775effe3c28..00000000000 --- a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ChunkTask.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.engine.world.chunks.pipeline; - -import org.joml.Vector3i; -import org.terasology.engine.utilities.concurrency.Task; -import org.terasology.engine.world.chunks.Chunk; - -/** - * @deprecated Use {@link org.terasology.engine.world.chunks.pipeline.stages.ChunkTask} instead - */ -@Deprecated -public interface ChunkTask extends Task { - - Chunk getChunk(); - - default Vector3i getPosition() { - return new Vector3i(); - } -} diff --git a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ShutdownChunkTask.java b/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ShutdownChunkTask.java deleted file mode 100644 index 2c79e26a72a..00000000000 --- a/engine/src/main/java/org/terasology/engine/world/chunks/pipeline/ShutdownChunkTask.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2021 The Terasology Foundation -// SPDX-License-Identifier: Apache-2.0 - -package org.terasology.engine.world.chunks.pipeline; - -import org.joml.Vector3i; -import org.terasology.engine.utilities.concurrency.TaskMaster; -import org.terasology.engine.world.chunks.Chunk; - -/** - * Special Chunk task for shutdown {@link ChunkProcessingPipeline} and it's {@link TaskMaster} - */ -public final class ShutdownChunkTask implements ChunkTask { - - @Override - public String getName() { - return "Shutdown"; - } - - @Override - public void run() { - } - - @Override - public boolean isTerminateSignal() { - return true; - } - - @Override - public Chunk getChunk() { - return null; - } - - @Override - public Vector3i getPosition() { - return new Vector3i(); - } -}