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; private final ServerCommsChannel comms;
public Client(WorldData world, ServerCommsChannel comms) { public Client(WorldData world, ServerCommsChannel comms) {
this.world = new WorldRender(world); this.world = new WorldRender(world, this);
this.comms = comms; this.comms = comms;
comms.addListener(new DefaultClientCommsListener(this)); comms.addListener(new DefaultClientCommsListener(this));

View File

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

View File

@ -20,10 +20,12 @@ package ru.windcorp.progressia.client.world;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import glm.vec._3.i.Vec3i; 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.backend.FaceCulling;
import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper; import ru.windcorp.progressia.client.graphics.model.ShapeRenderHelper;
import ru.windcorp.progressia.client.world.block.BlockRender; 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.entity.EntityRenderable;
import ru.windcorp.progressia.client.world.tile.TileRender; import ru.windcorp.progressia.client.world.tile.TileRender;
import ru.windcorp.progressia.client.world.tile.TileRenderStack; 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.ChunkData;
import ru.windcorp.progressia.common.world.ChunkDataListeners; import ru.windcorp.progressia.common.world.ChunkDataListeners;
import ru.windcorp.progressia.common.world.WorldData; import ru.windcorp.progressia.common.world.WorldData;
import ru.windcorp.progressia.common.world.WorldDataListener; import ru.windcorp.progressia.common.world.WorldDataListener;
import ru.windcorp.progressia.common.world.entity.EntityData; 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; import ru.windcorp.progressia.common.world.generic.GenericWorld;
public class WorldRender public class WorldRender
@ -46,35 +51,52 @@ implements GenericWorld<
ChunkRender, ChunkRender,
EntityRenderable EntityRenderable
> { > {
private final WorldData data; private final WorldData data;
private final Client client;
private final Map<ChunkData, ChunkRender> chunks = private final Map<ChunkData, ChunkRender> chunks =
Collections.synchronizedMap(new HashMap<>()); Collections.synchronizedMap(new HashMap<>());
private final Map<EntityData, EntityRenderable> entityModels = private final Map<EntityData, EntityRenderable> entityModels =
Collections.synchronizedMap(new WeakHashMap<>()); Collections.synchronizedMap(new WeakHashMap<>());
public WorldRender(WorldData data) { private final ChunkSet chunksToUpdate = ChunkSets.newSyncHashSet();
public WorldRender(WorldData data, Client client) {
this.data = data; this.data = data;
this.client = client;
data.addListener(ChunkDataListeners.createAdder(new ChunkUpdateListener(this))); data.addListener(ChunkDataListeners.createAdder(new ChunkUpdateListener(this)));
data.addListener(new WorldDataListener() { data.addListener(new WorldDataListener() {
@Override @Override
public void onChunkLoaded(WorldData world, ChunkData chunk) { public void onChunkLoaded(WorldData world, ChunkData chunk) {
chunks.put(chunk, new ChunkRender(WorldRender.this, chunk)); addChunk(chunk);
} }
@Override @Override
public void beforeChunkUnloaded(WorldData world, ChunkData chunk) { 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() { public WorldData getData() {
return data; return data;
} }
public Client getClient() {
return client;
}
public ChunkRender getChunk(ChunkData chunkData) { public ChunkRender getChunk(ChunkData chunkData) {
return chunks.get(chunkData); return chunks.get(chunkData);
} }
@ -95,10 +117,67 @@ implements GenericWorld<
} }
public void render(ShapeRenderHelper renderer) { public void render(ShapeRenderHelper renderer) {
updateChunks();
getChunks().forEach(chunk -> chunk.render(renderer)); getChunks().forEach(chunk -> chunk.render(renderer));
renderEntities(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) { private void renderEntities(ShapeRenderHelper renderer) {
FaceCulling.push(false); FaceCulling.push(false);
@ -122,5 +201,11 @@ implements GenericWorld<
return EntityRenderRegistry.getInstance().get(entity.getId()) return EntityRenderRegistry.getInstance().get(entity.getId())
.createRenderable(entity); .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; X, Y, Z, W;
} }
public static void forEachVectorInCuboid( public static void iterateCuboid(
int x0, int y0, int z0, int x0, int y0, int z0,
int x1, int y1, int z1, int x1, int y1, int z1,
Consumer<? super Vec3i> action Consumer<? super Vec3i> action
@ -38,6 +38,57 @@ public class VectorUtil {
Vectors.release(cursor); 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) { public static void applyMat4(Vec3 in, Mat4 mat, Vec3 out) {
Vec4 vec4 = Vectors.grab4(); Vec4 vec4 = Vectors.grab4();
vec4.set(in, 1f); vec4.set(in, 1f);

View File

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

View File

@ -2,9 +2,11 @@ package ru.windcorp.progressia.common.world.generic;
import java.util.Iterator; import java.util.Iterator;
import java.util.NoSuchElementException; 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 glm.vec._3.i.Vec3i;
import gnu.trove.impl.sync.TSynchronizedLongSet;
import gnu.trove.set.hash.TLongHashSet; import gnu.trove.set.hash.TLongHashSet;
public class ChunkSets { public class ChunkSets {
@ -14,13 +16,19 @@ public class ChunkSets {
} }
public static ChunkSet newSyncHashSet(Object mutex) { 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() { 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() { private final static ChunkSet EMPTY_SET = new ChunkSet() {
@Override @Override
@ -89,10 +97,174 @@ public class ChunkSets {
}; };
public static ChunkSet empty() { private static class SynchronizedChunkSet implements ChunkSet {
return EMPTY_SET;
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) { default void forEachBiC(Consumer<? super Vec3i> action) {
VectorUtil.forEachVectorInCuboid( VectorUtil.iterateCuboid(
0, 0, 0, 0, 0, 0,
BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK, BLOCKS_PER_CHUNK,
action action
@ -63,7 +63,7 @@ public interface GenericChunk<
} }
default void forEachBiW(Consumer<? super Vec3i> action) { default void forEachBiW(Consumer<? super Vec3i> action) {
VectorUtil.forEachVectorInCuboid( VectorUtil.iterateCuboid(
Coordinates.getInWorld(getX(), 0), Coordinates.getInWorld(getX(), 0),
Coordinates.getInWorld(getY(), 0), Coordinates.getInWorld(getY(), 0),
Coordinates.getInWorld(getZ(), 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()); 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() {} private TickAndUpdateUtil() {}
} }

View File

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

View File

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