diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/column/ColumnRenderSource.java b/src/main/java/com/seibel/lod/core/a7/datatype/column/ColumnRenderSource.java index a25868561..c95c018bd 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/column/ColumnRenderSource.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/column/ColumnRenderSource.java @@ -6,6 +6,7 @@ import com.seibel.lod.core.a7.datatype.column.accessor.ColumnQuadView; import com.seibel.lod.core.a7.datatype.column.accessor.IColumnDatatype; import com.seibel.lod.core.a7.datatype.column.render.ColumnRenderBuffer; import com.seibel.lod.core.a7.datatype.full.ChunkSizedData; +import com.seibel.lod.core.a7.datatype.full.FullDataSource; import com.seibel.lod.core.a7.datatype.transform.FullToColumnTransformer; import com.seibel.lod.core.a7.level.IClientLevel; import com.seibel.lod.core.a7.pos.DhSectionPos; @@ -47,6 +48,8 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { public final long[] dataContainer; public final int[] airDataContainer; + public boolean isEmpty = true; + /** * Constructor of the ColumnDataType * @param maxVerticalSize the maximum vertical size of the container @@ -69,8 +72,12 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { } private long[] readDataV1(DataInputStream inputData, int tempMaxVerticalData) throws IOException { int x = SECTION_SIZE * SECTION_SIZE * tempMaxVerticalData; - byte[] data = new byte[x * Long.BYTES]; short tempMinHeight = Short.reverseBytes(inputData.readShort()); + if (tempMinHeight == Short.MAX_VALUE) { //FIXME: Temp hack flag for marking a empty section + return new long[x]; + } + isEmpty = false; + byte[] data = new byte[x * Long.BYTES]; ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); inputData.readFully(data); long[] result = new long[x]; @@ -82,6 +89,7 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { } return result; } + // Load from data stream with maxVerticalSize loaded from the data stream public ColumnRenderSource(DhSectionPos sectionPos, DataInputStream inputData, int version, ILevel level) throws IOException { this.sectionPos = sectionPos; @@ -186,6 +194,11 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { output.writeByte(getDataDetail()); output.writeByte((byte) verticalSize); // FIXME: yOffset is a int, but we only are writing a short. + if (isEmpty) { + output.writeByte(Short.MAX_VALUE & 0xFF); + output.writeByte((Short.MAX_VALUE >> 8) & 0xFF); + return false; + } output.writeByte((byte) (yOffset & 0xFF)); output.writeByte((byte) ((yOffset >> 8) & 0xFF)); boolean allGenerated = true; @@ -258,7 +271,7 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { private void tryBuildBuffer(IClientLevel level, LodQuadTree quadTree) { - if (inBuildRenderBuffer == null && !ColumnRenderBuffer.isBusy()) { + if (inBuildRenderBuffer == null && !ColumnRenderBuffer.isBusy() && !isEmpty) { ColumnRenderSource[] data = new ColumnRenderSource[ELodDirection.ADJ_DIRECTIONS.length]; for (ELodDirection direction : ELodDirection.ADJ_DIRECTIONS) { LodRenderSection section = quadTree.getSection(sectionPos.getAdjacent(direction)); //FIXME: Handle traveling through different detail levels @@ -271,7 +284,7 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { } private void cancelBuildBuffer() { if (inBuildRenderBuffer != null) { - LOGGER.info("Cancelling build of render buffer for {}", sectionPos); + //LOGGER.info("Cancelling build of render buffer for {}", sectionPos); inBuildRenderBuffer.cancel(true); inBuildRenderBuffer = null; } @@ -302,21 +315,28 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { //FIXME: Temp Hack private long lastNs = -1; private static final long SWAP_TIMEOUT = /* 10 sec */ 10_000_000_000L; + private static final long SWAP_BUSY_COLLISION_TIMEOUT = /* 1 sec */ 1_000_000_000L; @Override public boolean trySwapRenderBuffer(LodQuadTree quadTree, AtomicReference referenceSlot) { if (lastNs != -1 && System.nanoTime() - lastNs < SWAP_TIMEOUT) { return false; } - if (inBuildRenderBuffer != null && inBuildRenderBuffer.isDone()) { - lastNs = System.nanoTime(); - LOGGER.info("Swapping render buffer for {}", sectionPos); - RenderBuffer oldBuffer = referenceSlot.getAndSet(inBuildRenderBuffer.join()); - if (oldBuffer instanceof ColumnRenderBuffer) usedBuffer = (ColumnRenderBuffer) oldBuffer; - inBuildRenderBuffer = null; - return true; - } else if (inBuildRenderBuffer == null && !ColumnRenderBuffer.isBusy()) { - tryBuildBuffer(level, quadTree); + if (inBuildRenderBuffer != null) { + if (inBuildRenderBuffer.isDone()) { + lastNs = System.nanoTime(); + //LOGGER.info("Swapping render buffer for {}", sectionPos); + RenderBuffer oldBuffer = referenceSlot.getAndSet(inBuildRenderBuffer.join()); + if (oldBuffer instanceof ColumnRenderBuffer) usedBuffer = (ColumnRenderBuffer) oldBuffer; + inBuildRenderBuffer = null; + return true; + } + } else { + if (!isEmpty) { + if (ColumnRenderBuffer.isBusy()) { + lastNs += (long) (SWAP_BUSY_COLLISION_TIMEOUT * Math.random()); + } else tryBuildBuffer(level, quadTree); + } } return false; } @@ -339,11 +359,14 @@ public class ColumnRenderSource implements LodRenderSource, IColumnDatatype { public void flushWrites(IClientLevel level) { boolean didSomething = false; while (!writeRequest.isEmpty()) { + isEmpty = false; ChunkSizedData chunkData = writeRequest.poll(); FullToColumnTransformer.writeFullDataChunkToColumnData(this, level, chunkData); didSomething = true; } - if (didSomething) lastNs = -1; // Reset the timeout to allow rebuilding the buffer again + if (didSomething) { + lastNs = -1; // Reset the timeout to allow rebuilding the buffer again + } } @Override diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/column/render/ColumnRenderBuffer.java b/src/main/java/com/seibel/lod/core/a7/datatype/column/render/ColumnRenderBuffer.java index a95ee2a68..67609bc20 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/column/render/ColumnRenderBuffer.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/column/render/ColumnRenderBuffer.java @@ -191,7 +191,7 @@ public class ColumnRenderBuffer extends RenderBuffer { public static CompletableFuture build(IClientLevel clientLevel, ColumnRenderBuffer usedBuffer, ColumnRenderSource data, ColumnRenderSource[] adjData) { if (isBusy()) return null; - LOGGER.info("RenderRegion startBuild @ {}", data.sectionPos); + //LOGGER.info("RenderRegion startBuild @ {}", data.sectionPos); return CompletableFuture.supplyAsync(() -> { try { EVENT_LOGGER.trace("RenderRegion start QuadBuild @ {}", data.sectionPos); @@ -242,7 +242,7 @@ public class ColumnRenderBuffer extends RenderBuffer { throw e3; } }, BUFFER_UPLOADER).handle((v, e) -> { - LOGGER.info("RenderRegion endBuild @ {}", data.sectionPos); + //LOGGER.info("RenderRegion endBuild @ {}", data.sectionPos); if (e != null) { usedBuffer.close(); return null; diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/full/ChunkSizedData.java b/src/main/java/com/seibel/lod/core/a7/datatype/full/ChunkSizedData.java index 4872b4aae..8bc0da235 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/full/ChunkSizedData.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/full/ChunkSizedData.java @@ -1,6 +1,7 @@ package com.seibel.lod.core.a7.datatype.full; import com.seibel.lod.core.a7.datatype.full.accessor.FullArrayView; +import com.seibel.lod.core.a7.pos.DhLodPos; public class ChunkSizedData extends FullArrayView { public final byte dataDetail; @@ -28,4 +29,8 @@ public class ChunkSizedData extends FullArrayView { public long emptyCount() { return 16*16 - nonEmptyCount(); } + + public DhLodPos getBBoxLodPos() { + return new DhLodPos((byte) (dataDetail+4), x, z); + } } \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/full/FullDataSource.java b/src/main/java/com/seibel/lod/core/a7/datatype/full/FullDataSource.java index d8722a996..e5509d673 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/full/FullDataSource.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/full/FullDataSource.java @@ -5,6 +5,7 @@ import com.seibel.lod.core.a7.datatype.full.accessor.FullArrayView; import com.seibel.lod.core.a7.datatype.full.accessor.SingleFullArrayView; import com.seibel.lod.core.a7.level.ILevel; import com.seibel.lod.core.a7.pos.DhBlockPos2D; +import com.seibel.lod.core.a7.pos.DhLodPos; import com.seibel.lod.core.a7.save.io.file.DataMetaFile; import com.seibel.lod.core.a7.datatype.LodDataSource; import com.seibel.lod.core.a7.util.IdMappingUtil; @@ -27,6 +28,7 @@ public class FullDataSource extends FullArrayView implements LodDataSource { // public static final long TYPE_ID = "FullDataSource".hashCode(); private final DhSectionPos sectionPos; private int localVersion = 0; + public boolean isEmpty = true; protected FullDataSource(DhSectionPos sectionPos) { super(new IdBiomeBlockStateMap(), new long[SECTION_SIZE*SECTION_SIZE][0], SECTION_SIZE); this.sectionPos = sectionPos; @@ -54,22 +56,47 @@ public class FullDataSource extends FullArrayView implements LodDataSource { // @Override public void update(ChunkSizedData data) { - if (getDataDetail() == 0 && data.dataDetail == 0) { + LodUtil.assertTrue(sectionPos.getSectionBBoxPos().overlaps(data.getBBoxLodPos())); + if (data.dataDetail == 0 && getDataDetail() == 0) { DhBlockPos2D chunkBlockPos = new DhBlockPos2D(data.x * 16, data.z * 16); DhBlockPos2D blockOffset = chunkBlockPos.subtract(sectionPos.getCorner().getCorner()); - LodUtil.assertTrue(blockOffset.x >= 0 && blockOffset.x < SECTION_SIZE && blockOffset.z >= 0 && blockOffset.z < SECTION_SIZE, - "ChunkWrite of {} outside section {}. (cal offset {} larger than {})", - new DHChunkPos(data.x, data.z), sectionPos, blockOffset, SECTION_SIZE); + LodUtil.assertTrue(blockOffset.x >= 0 && blockOffset.x < SECTION_SIZE && blockOffset.z >= 0 && blockOffset.z < SECTION_SIZE); + isEmpty = false; data.shadowCopyTo(this.subView(16, blockOffset.x, blockOffset.z)); - - for (int x = 0; x < 16; x++) { - for (int z = 0; z < 16; z++) { - SingleFullArrayView column = this.get(x+ blockOffset.x, z+ blockOffset.z); - LodUtil.assertTrue(column.doesItExist()); + { // DEBUG ASSERTION + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + SingleFullArrayView column = this.get(x + blockOffset.x, z + blockOffset.z); + LodUtil.assertTrue(column.doesItExist()); + } } } - - + } else if (data.dataDetail == 0 && getDataDetail() < 4) { + int dataPerFull = 1 << getDataDetail(); + int fullSize = 16 / dataPerFull; + DhLodPos dataOffset = data.getBBoxLodPos().getCorner(getDataDetail()); + DhLodPos baseOffset = sectionPos.getCorner(getDataDetail()); + int offsetX = dataOffset.x - baseOffset.x; + int offsetZ = dataOffset.z - baseOffset.z; + LodUtil.assertTrue(offsetX >= 0 && offsetX < SECTION_SIZE && offsetZ >= 0 && offsetZ < SECTION_SIZE); + isEmpty = false; + for (int ox = 0; ox < fullSize; ox++) { + for (int oz = 0; oz < fullSize; oz++) { + SingleFullArrayView column = this.get(ox + offsetX, oz + offsetZ); + column.downsampleFrom(data.subView(dataPerFull, ox * dataPerFull, oz * dataPerFull)); + } + } + } else if (data.dataDetail == 0 && getDataDetail() >= 4) { + //FIXME: TEMPORARY + int chunkPerFull = 1 << (getDataDetail() - 4); + if (data.x % chunkPerFull != 0 || data.z % chunkPerFull != 0) return; + DhLodPos baseOffset = sectionPos.getCorner(getDataDetail()); + DhLodPos dataOffset = data.getBBoxLodPos().convertUpwardsTo(getDataDetail()); + int offsetX = dataOffset.x - baseOffset.x; + int offsetZ = dataOffset.z - baseOffset.z; + LodUtil.assertTrue(offsetX >= 0 && offsetX < SECTION_SIZE && offsetZ >= 0 && offsetZ < SECTION_SIZE); + isEmpty = false; + data.get(0,0).deepCopyTo(get(offsetX, offsetZ)); } else { LodUtil.assertNotReach(); //TODO; @@ -83,6 +110,10 @@ public class FullDataSource extends FullArrayView implements LodDataSource { // dos.writeInt(getDataDetail()); dos.writeInt(size); dos.writeInt(level.getMinY()); + if (isEmpty) { + dos.writeInt(0x00000001); + return; + } dos.writeInt(0xFFFFFFFF); // Data array length for (int x = 0; x < size; x++) { @@ -124,6 +155,11 @@ public class FullDataSource extends FullArrayView implements LodDataSource { // LOGGER.warn("Data minY mismatch: {} != {}. Will ignore data's y level", minY, level.getMinY()); int end = dos.readInt(); // Data array length + if (end == 0x00000001) { + // Section is empty + return new FullDataSource(dataFile.pos); + } + // Non-empty section if (end != 0xFFFFFFFF) throw new IOException("invalid header end guard"); long[][] data = new long[size*size][]; for (int x = 0; x < size; x++) { @@ -154,9 +190,32 @@ public class FullDataSource extends FullArrayView implements LodDataSource { // super(mapping, data, SECTION_SIZE); LodUtil.assertTrue(data.length == SECTION_SIZE*SECTION_SIZE); this.sectionPos = pos; + isEmpty = false; } public static FullDataSource createEmpty(DhSectionPos pos) { return new FullDataSource(pos); } + + + public static FullDataSource createFromLower(DhSectionPos pos, FullDataSource[] lower) { + FullDataSource newData = new FullDataSource(pos); + LodUtil.assertTrue(lower.length == 4, "Non direct convert from lower is not yet implemented"); + int halfSize = SECTION_SIZE/2; + DhSectionPos cornerPos = pos.getChild(0); + for (FullDataSource lowerSection : lower) { + if (lowerSection==null || lowerSection.isEmpty) continue; + int offsetX = lowerSection.sectionPos.sectionX == cornerPos.sectionX ? 0 : halfSize; + int offsetZ = lowerSection.sectionPos.sectionZ == cornerPos.sectionZ ? 0 : halfSize; + newData.isEmpty = false; + for (int ox = 0; ox < halfSize; ox++) { + for (int oz = 0; oz < halfSize; oz++) { + FullArrayView quadView = lowerSection.subView(2, ox*2, oz*2); + SingleFullArrayView target = newData.get(ox+offsetX, oz+offsetZ); + target.downsampleFrom(quadView); + } + } + } + return newData; + } } diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/FullArrayView.java b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/FullArrayView.java index 59b1d3baf..b4bcf5eab 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/FullArrayView.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/FullArrayView.java @@ -38,7 +38,7 @@ public class FullArrayView implements IFullDataView { @Override public SingleFullArrayView get(int index) { - return new SingleFullArrayView(mapping, dataArrays, index + offset); + return get(index/size, index%size); } @Override diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/IFullDataView.java b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/IFullDataView.java index bbe4f1596..a8f310028 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/IFullDataView.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/IFullDataView.java @@ -1,6 +1,7 @@ package com.seibel.lod.core.a7.datatype.full.accessor; import com.seibel.lod.core.a7.datatype.full.IdBiomeBlockStateMap; +import com.seibel.lod.core.util.LodUtil; import java.util.Iterator; @@ -13,7 +14,7 @@ public interface IFullDataView { default Iterator iterator() { return new Iterator() { private int index = 0; - private final int size = width(); + private final int size = width()*width(); @Override public boolean hasNext() { return index < size; @@ -21,6 +22,7 @@ public interface IFullDataView { @Override public SingleFullArrayView next() { + LodUtil.assertTrue(hasNext(), "No more data to iterate!"); return get(index++); } }; diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/SingleFullArrayView.java b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/SingleFullArrayView.java index d1bf9fe2a..66ae6bd68 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/SingleFullArrayView.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/full/accessor/SingleFullArrayView.java @@ -61,7 +61,7 @@ public class SingleFullArrayView implements IFullDataView { return this; } - //WARNING: It will potentially share the underlying array object! + //WARNING: It may potentially share the underlying array object! public void shadowCopyTo(SingleFullArrayView target) { if (target.mapping.equals(mapping)) { target.dataArrays[target.offset] = dataArrays[offset]; @@ -76,4 +76,24 @@ public class SingleFullArrayView implements IFullDataView { target.dataArrays[target.offset] = newData; } } + public void deepCopyTo(SingleFullArrayView target) { + if (target.mapping.equals(mapping)) { + target.dataArrays[target.offset] = dataArrays[offset].clone(); + } + else { + int[] map = target.mapping.computeAndMergeMapFrom(mapping); + long[] sourceData = dataArrays[offset]; + long[] newData = new long[sourceData.length]; + for (int i = 0; i < newData.length; i++) { + newData[i] = FullFormat.remap(map, sourceData[i]); + } + target.dataArrays[target.offset] = newData; + } + } + + public void downsampleFrom(IFullDataView source) { + //TODO: Temp downsample method + SingleFullArrayView firstColumn = source.get(0); + firstColumn.deepCopyTo(this); + } } diff --git a/src/main/java/com/seibel/lod/core/a7/datatype/transform/FullToColumnTransformer.java b/src/main/java/com/seibel/lod/core/a7/datatype/transform/FullToColumnTransformer.java index 01c85d986..90b56f5d0 100644 --- a/src/main/java/com/seibel/lod/core/a7/datatype/transform/FullToColumnTransformer.java +++ b/src/main/java/com/seibel/lod/core/a7/datatype/transform/FullToColumnTransformer.java @@ -33,6 +33,8 @@ public class FullToColumnTransformer { final byte dataDetail = data.getDataDetail(); final int vertSize = Config.Client.Graphics.Quality.verticalQuality.get().calculateMaxVerticalData(data.getDataDetail()); final ColumnRenderSource columnSource = new ColumnRenderSource(pos, vertSize, level.getMinY()); + if (data.isEmpty) return columnSource; + columnSource.isEmpty = false; if (dataDetail == columnSource.getDataDetail()) { int baseX = pos.getCorner().getCorner().x; diff --git a/src/main/java/com/seibel/lod/core/a7/generation/GenerationQueue.java b/src/main/java/com/seibel/lod/core/a7/generation/GenerationQueue.java index aeb29927d..a66d1adc5 100644 --- a/src/main/java/com/seibel/lod/core/a7/generation/GenerationQueue.java +++ b/src/main/java/com/seibel/lod/core/a7/generation/GenerationQueue.java @@ -149,12 +149,13 @@ public class GenerationQueue implements PlaceHolderQueue { data.forEachPos((x,z) -> { ChunkSizedData chunkData = data.get(x,z); - DhLodPos chunkDataPos = new DhLodPos((byte)(chunkData.dataDetail + 4), chunkData.x, chunkData.z).convertUpwardsTo(sectionDetail); - DhSectionPos sectionPos = new DhSectionPos(chunkDataPos.detail, chunkDataPos.x, chunkDataPos.z); + DhLodPos chunkDataPos = new DhLodPos((byte)(chunkData.dataDetail + 4), chunkData.x, chunkData.z); + if (pos.getSectionBBoxPos().overlaps(chunkDataPos)) + writeConsumer.accept(pos, chunkData); + //DhSectionPos sectionPos = new DhSectionPos(chunkDataPos.detail, chunkDataPos.x, chunkDataPos.z); //logger.info("Writing chunk {} with data detail {} to section {}", // new DhLodPos((byte)(chunkData.dataDetail + 4), chunkData.x, chunkData.z), // dataDetail, sectionPos); - writeConsumer.accept(sectionPos, chunkData); }); // // for (int dsx = 0; dsx < sectionCount; dsx++) { diff --git a/src/main/java/com/seibel/lod/core/a7/pos/DhLodPos.java b/src/main/java/com/seibel/lod/core/a7/pos/DhLodPos.java index 1abb54dfa..946ab09e4 100644 --- a/src/main/java/com/seibel/lod/core/a7/pos/DhLodPos.java +++ b/src/main/java/com/seibel/lod/core/a7/pos/DhLodPos.java @@ -41,6 +41,11 @@ public class DhLodPos { return new DhBlockPos2D(getX().toBlock(), getZ().toBlock()); } + public DhLodPos getCorner(byte newDetail) { + LodUtil.assertTrue(newDetail <= detail); + return new DhLodPos(newDetail, x << (detail-newDetail), z << (detail-newDetail)); + } + public DhLodPos convertUpwardsTo(byte newDetail) { LodUtil.assertTrue(newDetail >= detail); return new DhLodPos(newDetail, Math.floorDiv(x, 1<<(newDetail-detail)), Math.floorDiv(z, 1<<(newDetail-detail))); diff --git a/src/main/java/com/seibel/lod/core/a7/render/LodRenderSection.java b/src/main/java/com/seibel/lod/core/a7/render/LodRenderSection.java index 36d16b025..5d684c9a5 100644 --- a/src/main/java/com/seibel/lod/core/a7/render/LodRenderSection.java +++ b/src/main/java/com/seibel/lod/core/a7/render/LodRenderSection.java @@ -19,6 +19,7 @@ public class LodRenderSection { private LodRenderSource lodRenderSource; private CompletableFuture loadFuture; private boolean isRenderEnabled = false; + private IRenderSourceProvider provider = null; // Create sub region public LodRenderSection(DhSectionPos pos) { @@ -27,28 +28,35 @@ public class LodRenderSection { public void enableRender(IClientLevel level, LodQuadTree quadTree) { if (isRenderEnabled) return; - if (lodRenderSource != null) { - lodRenderSource.enableRender(level, quadTree); - } + loadFuture = provider.read(pos); isRenderEnabled = true; } public void disableRender() { if (!isRenderEnabled) return; if (lodRenderSource != null) { lodRenderSource.disableRender(); + lodRenderSource.dispose(); + lodRenderSource = null; + } + if (loadFuture != null) { + loadFuture.cancel(true); + loadFuture = null; } isRenderEnabled = false; } public void load(IRenderSourceProvider renderDataProvider) { - if (loadFuture != null || lodRenderSource != null) throw new IllegalStateException("Reloading is not supported!"); - loadFuture = renderDataProvider.read(pos); + provider = renderDataProvider; } public void reload(IRenderSourceProvider renderDataProvider) { - if (loadFuture != null) throw new IllegalStateException("This section is already loading!"); - if (lodRenderSource == null) throw new IllegalStateException("This section is not loaded!"); - lodRenderSource.dispose(); - lodRenderSource = null; + if (loadFuture != null) { + loadFuture.cancel(true); + loadFuture = null; + } + if (lodRenderSource != null) { + lodRenderSource.dispose(); + lodRenderSource = null; + } loadFuture = renderDataProvider.read(pos); } @@ -59,7 +67,8 @@ public class LodRenderSection { if (isRenderEnabled) { lodRenderSource.enableRender(level, quadTree); } - } else if (lodRenderSource != null) { + } + if (lodRenderSource != null) { lodRenderSource.flushWrites(level); } } @@ -73,11 +82,11 @@ public class LodRenderSection { } public boolean canRender() { - return isLoaded() && isRenderEnabled && lodRenderSource.isRenderReady(); + return isLoaded() && isRenderEnabled && lodRenderSource != null && lodRenderSource.isRenderReady(); } public boolean isLoaded() { - return lodRenderSource != null; + return provider != null; } //FIXME: Used by RenderBufferHandler @@ -86,7 +95,7 @@ public class LodRenderSection { } public boolean isLoading() { - return loadFuture != null; + return false; } public boolean isOutdated() { diff --git a/src/main/java/com/seibel/lod/core/a7/save/io/file/DataMetaFile.java b/src/main/java/com/seibel/lod/core/a7/save/io/file/DataMetaFile.java index a03a2e937..e1ebb1451 100644 --- a/src/main/java/com/seibel/lod/core/a7/save/io/file/DataMetaFile.java +++ b/src/main/java/com/seibel/lod/core/a7/save/io/file/DataMetaFile.java @@ -1,8 +1,11 @@ package com.seibel.lod.core.a7.save.io.file; +import java.awt.*; import java.io.*; import java.lang.ref.*; +import java.security.Provider; import java.sql.Ref; +import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -13,6 +16,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; +import java.util.function.Supplier; import com.seibel.lod.core.a7.datatype.LodDataSource; import com.seibel.lod.core.a7.datatype.DataSourceLoader; @@ -107,10 +112,26 @@ public class DataMetaFile extends MetaFile { } // Make a new MetaFile. It doesn't load or write any metadata itself. - public DataMetaFile(ILevel level, File path, DhSectionPos pos) { + public DataMetaFile(ILevel level, File path, DhSectionPos pos, CompletableFuture creator) { super(path, pos); debugCheck(); this.level = level; + CompletableFuture future = new CompletableFuture<>(); + data.set(future); + creator.thenApply((f) -> { + applyWriteQueue(f); + return f; + }).whenComplete((f, e) -> { + if (e != null) { + LOGGER.error("Uncaught error on creation {}: ", path, e); + future.complete(null); + data.set(null); + } else { + future.complete(f); + new DataObjTracker(f); + data.set(new SoftReference<>(f)); + } + }); } public boolean isValid(int version) { diff --git a/src/main/java/com/seibel/lod/core/a7/save/io/file/LocalDataFileHandler.java b/src/main/java/com/seibel/lod/core/a7/save/io/file/LocalDataFileHandler.java index cff9bc97e..f3e1d4880 100644 --- a/src/main/java/com/seibel/lod/core/a7/save/io/file/LocalDataFileHandler.java +++ b/src/main/java/com/seibel/lod/core/a7/save/io/file/LocalDataFileHandler.java @@ -7,6 +7,7 @@ import com.seibel.lod.core.a7.datatype.full.ChunkSizedData; import com.seibel.lod.core.a7.datatype.full.FullDataSource; import com.seibel.lod.core.a7.datatype.full.FullFormat; import com.seibel.lod.core.a7.level.IServerLevel; +import com.seibel.lod.core.a7.pos.DhLodPos; import com.seibel.lod.core.a7.pos.DhSectionPos; import com.seibel.lod.core.logging.DhLoggerBuilder; import com.seibel.lod.core.objects.DHChunkPos; @@ -22,14 +23,17 @@ import java.util.Comparator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicInteger; public class LocalDataFileHandler implements IDataSourceProvider { // Note: Single main thread only for now. May make it multi-thread later, depending on the usage. private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - final ExecutorService fileReaderThread = LodUtil.makeSingleThreadPool("FileReaderThread"); + final ExecutorService fileReaderThread = LodUtil.makeThreadPool(4, "FileReaderThread"); final ConcurrentHashMap files = new ConcurrentHashMap<>(); final IServerLevel level; final File saveDir; + AtomicInteger topDetailLevel = new AtomicInteger(-1); + final int minDetailLevel = FullDataSource.SECTION_SIZE_OFFSET; public LocalDataFileHandler(IServerLevel level, File saveRootDir) { @@ -97,6 +101,7 @@ public class LocalDataFileHandler implements IDataSourceProvider { fileToUse = metaFiles.iterator().next(); } // Add file to the list of files. + topDetailLevel.updateAndGet(v -> Math.max(v, fileToUse.pos.sectionDetail)); files.put(pos, fileToUse); } } @@ -106,6 +111,16 @@ public class LocalDataFileHandler implements IDataSourceProvider { */ @Override public CompletableFuture read(DhSectionPos pos) { + topDetailLevel.updateAndGet(v -> Math.max(v, pos.sectionDetail)); + DataMetaFile metaFile = files.get(pos); + if (metaFile == null) { + return CompletableFuture.completedFuture(null); + } + return metaFile.loadOrGetCached(fileReaderThread); + } + + // This prevents new higher detail levels from being created by not updating the topDetailLevel. + private CompletableFuture readWithoutUpdate(DhSectionPos pos) { DataMetaFile metaFile = files.get(pos); if (metaFile == null) { return CompletableFuture.completedFuture(null); @@ -118,25 +133,66 @@ public class LocalDataFileHandler implements IDataSourceProvider { */ @Override public void write(DhSectionPos sectionPos, ChunkSizedData chunkData) { + DhLodPos chunkPos = new DhLodPos((byte) (chunkData.dataDetail+4), chunkData.x, chunkData.z); + LodUtil.assertTrue(chunkPos.overlaps(sectionPos.getSectionBBoxPos()), "Chunk {} does not overlap section {}", chunkPos, sectionPos); + chunkPos = chunkPos.convertUpwardsTo((byte) minDetailLevel); // TODO: Handle if chunkData has higher detail than lowestDetail. + recursiveWrite(new DhSectionPos(chunkPos.detail, chunkPos.x, chunkPos.z), chunkData); + } + private void recursiveWrite(DhSectionPos sectionPos, ChunkSizedData chunkData) { DataMetaFile metaFile = files.get(sectionPos); if (metaFile != null) { // Fast path: if there is a file for this section, just write to it. metaFile.addToWriteQueue(chunkData); - return; + } else if (sectionPos.sectionDetail <= minDetailLevel) { + File file = computeDefaultFilePath(sectionPos); + //FIXME: Handle file already exists issue. Possibly by renaming the file. + LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, sectionPos); + CompletableFuture gen = new CompletableFuture<>(); + DataMetaFile newMetaFile = new DataMetaFile(level, file, sectionPos, gen); + metaFile = files.putIfAbsent(sectionPos, newMetaFile); // This is a CAS with expected null value. + if (metaFile == null) { + newMetaFile.addToWriteQueue(chunkData); + CompletableFuture.runAsync(() -> gen.complete(FullDataSource.createEmpty(sectionPos)), fileReaderThread) + .exceptionally((e) -> { + gen.completeExceptionally(e); + return null; + }); + } else { + metaFile.addToWriteQueue(chunkData); + gen.cancel(true); + } + } else { + File file = computeDefaultFilePath(sectionPos); + //FIXME: Handle file already exists issue. Possibly by renaming the file. + LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, sectionPos); + CompletableFuture gen = new CompletableFuture<>(); + DataMetaFile newMetaFile = new DataMetaFile(level, file, sectionPos, gen); + metaFile = files.putIfAbsent(sectionPos, newMetaFile); // This is a CAS with expected null value. + if (metaFile == null) { + newMetaFile.addToWriteQueue(chunkData); + // Create future that generate downsized file + CompletableFuture subChunk0 = readWithoutUpdate(sectionPos.getChild(0)); + CompletableFuture subChunk1 = readWithoutUpdate(sectionPos.getChild(1)); + CompletableFuture subChunk2 = readWithoutUpdate(sectionPos.getChild(2)); + CompletableFuture subChunk3 = readWithoutUpdate(sectionPos.getChild(3)); + CompletableFuture.allOf(subChunk0, subChunk1, subChunk2, subChunk3) + .thenAccept(v -> + gen.complete(FullDataSource.createFromLower(sectionPos, new FullDataSource[]{ + (FullDataSource) subChunk0.join(), + (FullDataSource) subChunk1.join(), + (FullDataSource) subChunk2.join(), + (FullDataSource) subChunk3.join() + })) + ).exceptionally((e) -> { + gen.completeExceptionally(e); + return null; + }); + } else { + metaFile.addToWriteQueue(chunkData); + gen.cancel(true); + } } - // Slow path: if there is no file for this section, create one. - File file = computeDefaultFilePath(sectionPos); - //FIXME: Handle file already exists issue. Possibly by renaming the file. - LodUtil.assertTrue(!file.exists(), "File {} already exist for path {}", file, sectionPos); - DataMetaFile newMetaFile = new DataMetaFile(level, file, sectionPos); - //LOGGER.info("Created new Data file at {} for sect {}", newMetaFile.path, sectionPos); - - // We add to the queue first so on CAS onto the map, no other thread - // will see the new file without our write entry. - newMetaFile.addToWriteQueue(chunkData); - DataMetaFile casResult = files.putIfAbsent(sectionPos, newMetaFile); // This is a CAS with expected null value. - if (casResult != null) { // another thread already created the file. CAS failed. - // Drop our version and use the cas result. - casResult.addToWriteQueue(chunkData); + if (sectionPos.sectionDetail <= topDetailLevel.get()) { + recursiveWrite(sectionPos.getParent(), chunkData); } } diff --git a/src/main/java/com/seibel/lod/core/a7/save/io/render/RenderFileHandler.java b/src/main/java/com/seibel/lod/core/a7/save/io/render/RenderFileHandler.java index cc02be987..fa8c53e6f 100644 --- a/src/main/java/com/seibel/lod/core/a7/save/io/render/RenderFileHandler.java +++ b/src/main/java/com/seibel/lod/core/a7/save/io/render/RenderFileHandler.java @@ -4,8 +4,10 @@ import com.google.common.collect.HashMultimap; import com.seibel.lod.core.a7.PlaceHolderQueue; import com.seibel.lod.core.a7.datatype.PlaceHolderRenderSource; import com.seibel.lod.core.a7.datatype.LodRenderSource; +import com.seibel.lod.core.a7.datatype.column.ColumnRenderSource; import com.seibel.lod.core.a7.datatype.full.ChunkSizedData; import com.seibel.lod.core.a7.level.IClientLevel; +import com.seibel.lod.core.a7.pos.DhLodPos; import com.seibel.lod.core.a7.save.io.file.IDataSourceProvider; import com.seibel.lod.core.a7.pos.DhSectionPos; import com.seibel.lod.core.logging.DhLoggerBuilder; @@ -132,10 +134,22 @@ public class RenderFileHandler implements IRenderSourceProvider { @Override public void write(DhSectionPos sectionPos, ChunkSizedData chunkData) { dataSourceProvider.write(sectionPos, chunkData); - RenderMetaFile metaFile = files.get(sectionPos); + recursive_write(sectionPos,chunkData); + } + + private void recursive_write(DhSectionPos sectPos, ChunkSizedData chunkData) { + if (!sectPos.getSectionBBoxPos().overlaps(new DhLodPos((byte) (4 + chunkData.dataDetail), chunkData.x, chunkData.z))) return; + if (sectPos.sectionDetail > ColumnRenderSource.SECTION_SIZE_OFFSET) { + recursive_write(sectPos.getChild(0), chunkData); + recursive_write(sectPos.getChild(1), chunkData); + recursive_write(sectPos.getChild(2), chunkData); + recursive_write(sectPos.getChild(3), chunkData); + } + RenderMetaFile metaFile = files.get(sectPos); if (metaFile != null) { // Fast path: if there is a file for this section, just write to it. metaFile.updateChunkIfNeeded(chunkData); } + } /* diff --git a/src/main/java/com/seibel/lod/core/builders/worldGeneration/BatchGenerator.java b/src/main/java/com/seibel/lod/core/builders/worldGeneration/BatchGenerator.java index 9750a51f3..32f098cd9 100644 --- a/src/main/java/com/seibel/lod/core/builders/worldGeneration/BatchGenerator.java +++ b/src/main/java/com/seibel/lod/core/builders/worldGeneration/BatchGenerator.java @@ -307,7 +307,7 @@ public class BatchGenerator implements IChunkGenerator @Override public byte getMaxGenerationGranularity() { - return 16; + return 8; } @Override diff --git a/src/main/java/com/seibel/lod/core/util/DetailDistanceUtil.java b/src/main/java/com/seibel/lod/core/util/DetailDistanceUtil.java index 956d21b78..25f03cff1 100644 --- a/src/main/java/com/seibel/lod/core/util/DetailDistanceUtil.java +++ b/src/main/java/com/seibel/lod/core/util/DetailDistanceUtil.java @@ -30,7 +30,7 @@ import com.seibel.lod.core.enums.config.EHorizontalQuality; public class DetailDistanceUtil { private static byte minDetail = Config.Client.Graphics.Quality.drawResolution.get().detailLevel; - private static final byte maxDetail = LodUtil.DETAIL_OPTIONS; + private static final byte maxDetail = Byte.MAX_VALUE; private static final double minDistance = 0; private static double distanceUnit = 16 * Config.Client.Graphics.Quality.horizontalScale.get(); private static double maxDistance = Config.Client.Graphics.Quality.lodChunkRenderDistance.get() * 16 * 2;