Spread chunk updates over several frames

- At most 1 chunk is updated (previously known as 'buildModel') every
frame
- 3x3x3 chunks closest to the player are updated in real-time without
the aforementioned limit
This commit is contained in:
OLEGSHA 2020-12-30 00:29:25 +03:00
parent 5d570a810b
commit c9087e7215
10 changed files with 336 additions and 71 deletions

View File

@ -19,7 +19,7 @@ public class Client {
private final ServerCommsChannel comms;
public Client(WorldData world, ServerCommsChannel comms) {
this.world = new WorldRender(world);
this.world = new WorldRender(world, this);
this.comms = comms;
comms.addListener(new DefaultClientCommsListener(this));

View File

@ -59,7 +59,6 @@ implements GenericChunk<
private final WorldRender world;
private final ChunkData data;
private boolean needsUpdate;
private Model model = null;
private final Map<TileDataStack, TileRenderStackImpl> tileRenderLists =
@ -108,16 +107,12 @@ implements GenericChunk<
}
public synchronized void markForUpdate() {
this.needsUpdate = true;
}
public synchronized boolean needsUpdate() {
return needsUpdate;
getWorld().markChunkForUpdate(getPosition());
}
public synchronized void render(ShapeRenderHelper renderer) {
if (model == null || needsUpdate()) {
buildModel();
if (model == null) {
return;
}
renderer.pushTransform().translate(
@ -131,7 +126,7 @@ implements GenericChunk<
renderer.popTransform();
}
private void buildModel() {
public synchronized void update() {
Collection<ChunkRenderOptimizer> optimizers =
ChunkRenderOptimizers.getAllSuppliers().stream()
.map(ChunkRenderOptimizerSupplier::createOptimizer)
@ -159,7 +154,6 @@ implements GenericChunk<
.forEach(builder::addPart);
model = new StaticModel(builder);
needsUpdate = false;
}
private void buildBlock(

View File

@ -20,10 +20,12 @@ package ru.windcorp.progressia.client.world;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.WeakHashMap;
import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.client.Client;
import ru.windcorp.progressia.client.graphics.backend.FaceCulling;
import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
import ru.windcorp.progressia.client.world.block.BlockRender;
@ -31,11 +33,14 @@ import ru.windcorp.progressia.client.world.entity.EntityRenderRegistry;
import ru.windcorp.progressia.client.world.entity.EntityRenderable;
import ru.windcorp.progressia.client.world.tile.TileRender;
import ru.windcorp.progressia.client.world.tile.TileRenderStack;
import ru.windcorp.progressia.common.util.VectorUtil;
import ru.windcorp.progressia.common.world.ChunkData;
import ru.windcorp.progressia.common.world.ChunkDataListeners;
import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.WorldDataListener;
import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.generic.ChunkSet;
import ru.windcorp.progressia.common.world.generic.ChunkSets;
import ru.windcorp.progressia.common.world.generic.GenericWorld;
public class WorldRender
@ -46,35 +51,52 @@ implements GenericWorld<
ChunkRender,
EntityRenderable
> {
private final WorldData data;
private final Client client;
private final Map<ChunkData, ChunkRender> chunks =
Collections.synchronizedMap(new HashMap<>());
private final Map<EntityData, EntityRenderable> entityModels =
Collections.synchronizedMap(new WeakHashMap<>());
public WorldRender(WorldData data) {
private final ChunkSet chunksToUpdate = ChunkSets.newSyncHashSet();
public WorldRender(WorldData data, Client client) {
this.data = data;
this.client = client;
data.addListener(ChunkDataListeners.createAdder(new ChunkUpdateListener(this)));
data.addListener(new WorldDataListener() {
@Override
public void onChunkLoaded(WorldData world, ChunkData chunk) {
chunks.put(chunk, new ChunkRender(WorldRender.this, chunk));
addChunk(chunk);
}
@Override
public void beforeChunkUnloaded(WorldData world, ChunkData chunk) {
chunks.remove(chunk);
removeChunk(chunk);
}
});
}
protected void addChunk(ChunkData chunk) {
chunks.put(chunk, new ChunkRender(WorldRender.this, chunk));
markChunkForUpdate(chunk.getPosition());
}
protected void removeChunk(ChunkData chunk) {
chunks.remove(chunk);
}
public WorldData getData() {
return data;
}
public Client getClient() {
return client;
}
public ChunkRender getChunk(ChunkData chunkData) {
return chunks.get(chunkData);
}
@ -95,10 +117,67 @@ implements GenericWorld<
}
public void render(ShapeRenderHelper renderer) {
updateChunks();
getChunks().forEach(chunk -> chunk.render(renderer));
renderEntities(renderer);
}
private void updateChunks() {
synchronized (chunksToUpdate) {
if (chunksToUpdate.isEmpty()) return;
int updates = updateChunksNearLocalPlayer();
int maximumUpdates = getMaximumChunkUpdatesPerFrame();
updateRandomChunks(maximumUpdates - updates);
}
}
private int updateChunksNearLocalPlayer() {
EntityData entity = getClient().getLocalPlayer().getEntity();
if (entity == null) return 0;
int[] updates = new int[] { 0 };
VectorUtil.iterateCuboidAround(entity.getChunkCoords(null), 3, chunkPos -> {
if (chunksToUpdate.contains(chunkPos)) {
getChunk(chunkPos).update();
chunksToUpdate.remove(chunkPos);
updates[0]++;
}
});
return updates[0];
}
private void updateRandomChunks(int allowedUpdates) {
if (allowedUpdates <= 0) return;
for (Iterator<Vec3i> it = chunksToUpdate.iterator(); it.hasNext();) {
Vec3i chunkPos = it.next();
ChunkRender chunk = getChunk(chunkPos);
if (chunk != null) {
chunk.update();
allowedUpdates--;
}
it.remove();
if (allowedUpdates <= 0) return;
}
}
private int getMaximumChunkUpdatesPerFrame() {
return 1;
}
public int getPendingChunkUpdates() {
return chunksToUpdate.size();
}
private void renderEntities(ShapeRenderHelper renderer) {
FaceCulling.push(false);
@ -122,5 +201,11 @@ implements GenericWorld<
return EntityRenderRegistry.getInstance().get(entity.getId())
.createRenderable(entity);
}
public void markChunkForUpdate(Vec3i chunkPos) {
if (getData().getChunk(chunkPos) != null) {
chunksToUpdate.add(chunkPos);
}
}
}

View File

@ -19,7 +19,7 @@ public class VectorUtil {
X, Y, Z, W;
}
public static void forEachVectorInCuboid(
public static void iterateCuboid(
int x0, int y0, int z0,
int x1, int y1, int z1,
Consumer<? super Vec3i> action
@ -38,6 +38,57 @@ public class VectorUtil {
Vectors.release(cursor);
}
public static void iterateCuboid(
Vec3i vMin, Vec3i vMax,
Consumer<? super Vec3i> action
) {
iterateCuboid(vMin.x, vMin.y, vMin.z, vMax.x, vMax.y, vMax.z, action);
}
public static void iterateCuboidAround(
int cx, int cy, int cz,
int dx, int dy, int dz,
Consumer<? super Vec3i> action
) {
if (dx < 0) throw new IllegalArgumentException("dx " + dx + " is negative");
if (dy < 0) throw new IllegalArgumentException("dy " + dx + " is negative");
if (dz < 0) throw new IllegalArgumentException("dz " + dx + " is negative");
if (dx % 2 == 0) throw new IllegalArgumentException("dx " + dx + " is even, only odd accepted");
if (dy % 2 == 0) throw new IllegalArgumentException("dy " + dy + " is even, only odd accepted");
if (dz % 2 == 0) throw new IllegalArgumentException("dz " + dz + " is even, only odd accepted");
dx /= 2;
dy /= 2;
dz /= 2;
iterateCuboid(cx - dx, cy - dy, cz - dz, cx + dx + 1, cy + dy + 1, cz + dz + 1, action);
}
public static void iterateCuboidAround(
Vec3i center,
Vec3i diameters,
Consumer<? super Vec3i> action
) {
iterateCuboidAround(center.x, center.y, center.z, diameters.x, diameters.y, diameters.z, action);
}
public static void iterateCuboidAround(
int cx, int cy, int cz,
int diameter,
Consumer<? super Vec3i> action
) {
iterateCuboidAround(cx, cy, cz, diameter, diameter, diameter, action);
}
public static void iterateCuboidAround(
Vec3i center,
int diameter,
Consumer<? super Vec3i> action
) {
iterateCuboidAround(center.x, center.y, center.z, diameter, action);
}
public static void applyMat4(Vec3 in, Mat4 mat, Vec3 out) {
Vec4 vec4 = Vectors.grab4();
vec4.set(in, 1f);

View File

@ -183,7 +183,7 @@ implements GenericChunk<
}
public void forEachBlock(Consumer<Vec3i> action) {
VectorUtil.forEachVectorInCuboid(
VectorUtil.iterateCuboid(
0, 0, 0,
BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK,
action

View File

@ -2,9 +2,11 @@ package ru.windcorp.progressia.common.world.generic;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import glm.vec._3.i.Vec3i;
import gnu.trove.impl.sync.TSynchronizedLongSet;
import gnu.trove.set.hash.TLongHashSet;
public class ChunkSets {
@ -14,13 +16,19 @@ public class ChunkSets {
}
public static ChunkSet newSyncHashSet(Object mutex) {
return new LongBasedChunkSet(new TSynchronizedLongSet(new TLongHashSet(), mutex));
return new SynchronizedChunkSet(new LongBasedChunkSet(new TLongHashSet()), mutex);
}
public static ChunkSet newSyncHashSet() {
return new LongBasedChunkSet(new TSynchronizedLongSet(new TLongHashSet()));
return newSyncHashSet(null);
}
public static ChunkSet empty() {
return EMPTY_SET;
}
private ChunkSets() {}
private final static ChunkSet EMPTY_SET = new ChunkSet() {
@Override
@ -89,10 +97,174 @@ public class ChunkSets {
};
public static ChunkSet empty() {
return EMPTY_SET;
private static class SynchronizedChunkSet implements ChunkSet {
private final ChunkSet parent;
private final Object mutex;
public SynchronizedChunkSet(ChunkSet parent, Object mutex) {
Objects.requireNonNull(parent, "parent");
this.parent = parent;
this.mutex = mutex == null ? this : mutex;
}
@Override
public Iterator<Vec3i> iterator() {
return parent.iterator(); // Must be synchronized manually by user!
}
@Override
public void forEach(Consumer<? super Vec3i> action) {
synchronized (mutex) { parent.forEach(action); }
}
@Override
public int size() {
synchronized (mutex) { return parent.size(); }
}
@Override
public boolean isEmpty() {
synchronized (mutex) { return parent.isEmpty(); }
}
@Override
public boolean contains(Vec3i pos) {
synchronized (mutex) { return parent.contains(pos); }
}
@Override
public boolean add(Vec3i pos) {
synchronized (mutex) { return parent.add(pos); }
}
@Override
public boolean remove(Vec3i pos) {
synchronized (mutex) { return parent.remove(pos); }
}
@Override
public boolean contains(int x, int y, int z) {
synchronized (mutex) { return parent.contains(x, y, z); }
}
@Override
public boolean add(int x, int y, int z) {
synchronized (mutex) { return parent.add(x, y, z); }
}
@Override
public boolean remove(int x, int y, int z) {
synchronized (mutex) { return parent.remove(x, y, z); }
}
@Override
public boolean contains(GenericChunk<?, ?, ?, ?> chunk) {
synchronized (mutex) { return parent.contains(chunk); }
}
@Override
public boolean add(GenericChunk<?, ?, ?, ?> chunk) {
synchronized (mutex) { return parent.add(chunk); }
}
@Override
public boolean remove(GenericChunk<?, ?, ?, ?> chunk) {
synchronized (mutex) { return parent.remove(chunk); }
}
@Override
public <C extends GenericChunk<C, ?, ?, ?>> void forEachIn(GenericWorld<?, ?, ?, C, ?> world,
Consumer<? super C> action) {
synchronized (mutex) { parent.forEachIn(world, action); }
}
@Override
public boolean containsAll(ChunkSet other) {
synchronized (mutex) { return parent.containsAll(other); }
}
@Override
public boolean containsAny(ChunkSet other) {
synchronized (mutex) { return parent.containsAny(other); }
}
@Override
public void addAll(ChunkSet other) {
synchronized (mutex) { parent.addAll(other); }
}
@Override
public void removeAll(ChunkSet other) {
synchronized (mutex) { parent.removeAll(other); }
}
@Override
public void retainAll(ChunkSet other) {
synchronized (mutex) { parent.retainAll(other); }
}
@Override
public void clear() {
synchronized (mutex) { parent.clear(); }
}
@Override
public boolean containsAll(Iterable<? extends Vec3i> other) {
synchronized (mutex) { return parent.containsAll(other); }
}
@Override
public boolean containsAny(Iterable<? extends Vec3i> other) {
synchronized (mutex) { return parent.containsAny(other); }
}
@Override
public void addAll(Iterable<? extends Vec3i> other) {
synchronized (mutex) { parent.addAll(other); }
}
@Override
public void removeAll(Iterable<? extends Vec3i> other) {
synchronized (mutex) { parent.removeAll(other); }
}
@Override
public void retainAll(Iterable<? extends Vec3i> other) {
synchronized (mutex) { parent.retainAll(other); }
}
@Override
public void removeIf(Predicate<? super Vec3i> condition) {
synchronized (mutex) { parent.removeIf(condition); }
}
@Override
public void retainIf(Predicate<? super Vec3i> condition) {
synchronized (mutex) { parent.retainIf(condition); }
}
@Override
public boolean containsAllChunks(Iterable<? extends GenericChunk<?, ?, ?, ?>> chunks) {
synchronized (mutex) { return parent.containsAllChunks(chunks); }
}
@Override
public boolean containsAnyChunks(Iterable<? extends GenericChunk<?, ?, ?, ?>> chunks) {
synchronized (mutex) { return parent.containsAnyChunks(chunks); }
}
@Override
public void addAllChunks(Iterable<? extends GenericChunk<?, ?, ?, ?>> chunks) {
synchronized (mutex) { parent.addAllChunks(chunks); }
}
@Override
public void removeAllChunks(Iterable<? extends GenericChunk<?, ?, ?, ?>> chunks) {
synchronized (mutex) { parent.removeAllChunks(chunks); }
}
}
private ChunkSets() {}
}

View File

@ -55,7 +55,7 @@ public interface GenericChunk<
}
default void forEachBiC(Consumer<? super Vec3i> action) {
VectorUtil.forEachVectorInCuboid(
VectorUtil.iterateCuboid(
0, 0, 0,
BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK,
action
@ -63,7 +63,7 @@ public interface GenericChunk<
}
default void forEachBiW(Consumer<? super Vec3i> action) {
VectorUtil.forEachVectorInCuboid(
VectorUtil.iterateCuboid(
Coordinates.getInWorld(getX(), 0),
Coordinates.getInWorld(getY(), 0),
Coordinates.getInWorld(getZ(), 0),

View File

@ -112,49 +112,6 @@ public class TickAndUpdateUtil {
tickEntity(EntityLogicRegistry.getInstance().get(data.getId()), data, TickContextMutable.start().withServer(server).build());
}
// public static BlockTickContext getBlockTickContext(
// Server server,
// Vec3i blockInWorld
// ) {
// MutableBlockTickContext result = new MutableBlockTickContext();
// result.init(server, blockInWorld);
// return result;
// }
//
// public static TileTickContext getTileTickContext(
// Server server,
// Vec3i blockInWorld,
// BlockFace face,
// int layer
// ) {
// MutableTileTickContext result = new MutableTileTickContext();
// result.init(server, blockInWorld, face, layer);
// return result;
// }
//
// public static TileTickContext getTileTickContext(
// Server server,
// TileDataStack stack,
// int index
// ) {
// MutableTileTickContext result = new MutableTileTickContext();
// result.init(server, stack, index);
// return result;
// }
//
// public static TileTickContext getTileTickContext(
// Server server,
// TileReference ref
// ) {
// MutableTileTickContext result = new MutableTileTickContext();
// result.init(server, ref);
// return result;
// }
//
// public static TickContext getTickContext(Server server) {
// return getBlockTickContext(server, null);
// }
private TickAndUpdateUtil() {}
}

View File

@ -73,6 +73,12 @@ public class LayerTestGUI extends GUILayer {
128
));
panel.addChild(new DynamicLabel(
"ChunkUpdatesDisplay", new Font().withColor(0x37A3E6).deriveShadow(),
() -> "Pending updates: " + Integer.toString(ClientState.getInstance().getWorld().getPendingChunkUpdates()),
128
));
panel.getChildren().forEach(c -> {
if (c instanceof Label) {
labels.add((Label) c);

View File

@ -29,8 +29,8 @@ public class TestPlayerControls {
private TestPlayerControls() {}
private static final double MODE_SWITCH_MAX_DELAY = 100 * Units.MILLISECONDS;
private static final double MIN_JUMP_DELAY = 200 * Units.MILLISECONDS;
private static final double MODE_SWITCH_MAX_DELAY = 300 * Units.MILLISECONDS;
private static final double MIN_JUMP_DELAY = 400 * Units.MILLISECONDS;
// Horizontal and vertical max control speed when flying
private static final float FLYING_SPEED = 6.0f * Units.METERS_PER_SECOND;