Compare commits

...

93 Commits

Author SHA1 Message Date
James Seibel e701c0e5ea remove dev from version number 2025-03-06 07:40:49 -06:00
James Seibel ebc1114a51 temporarily disable sqlite tests for release 2025-03-06 07:40:24 -06:00
s809 eb8563482e Replace chunk counts with speed in pregen 2025-02-27 21:08:27 +05:00
s809 b53c33e454 Make generation info text a bit clearer 2025-02-27 21:08:02 +05:00
s809 2483671e5e Should be division instead of multiplication 2025-02-26 23:16:58 +05:00
s809 cc4733b052 Offset generation bounds by teleportation scale 2025-02-26 22:13:38 +05:00
James Seibel 34e5463718 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-02-25 07:25:49 -06:00
James Seibel 53011a13be duct tape fix to reduce chance of LOD uploading requiring MC reboot 2025-02-25 07:25:46 -06:00
s809 1c579675a2 Remember split section responses temporarily 2025-02-22 20:55:10 +05:00
James Seibel 69a4e6b27e Add TODO about why LODs sometimes fail to load 2025-02-22 08:55:41 -06:00
s809 b05eb78f3a Fix foreground thread sometimes blocking server shutdown 2025-02-19 21:17:38 +05:00
s809 83fabe3ee8 Show section numbers in pregen 2025-02-19 20:37:26 +05:00
s809 fdfab2b3a8 Use another method for enforcing non nsized generation 2025-02-19 20:15:34 +05:00
James Seibel 45c67d057a Fix IDhApiConfigValue.clearValue() failing for some deprecated functions 2025-02-17 21:16:22 -06:00
James Seibel c296795280 Fix DB leaks in FullDataV2Repo 2025-02-16 20:07:00 -06:00
James Seibel 2deb24ec1e Add javadocs to ClientWrapper getPlayer Pos methods 2025-02-16 19:53:44 -06:00
James Seibel 5ab7a3030a Fix DB leaks in FullDataV2Repo 2025-02-16 19:52:48 -06:00
James Seibel 1af4d23c14 improve DB leak tracking exception handling 2025-02-16 19:52:37 -06:00
James Seibel 977204abf0 Add DB leak tracking 2025-02-16 19:34:13 -06:00
James Seibel 276f2adf00 Revert 10 minute memoization for world gen
I thought this was only an issue for N-sized generation, but in testing found it to still be an issue for max-detail retrieval as well.
This will have to be looked into more another time
2025-02-15 11:56:21 -06:00
James Seibel 1b3c9e1a89 Fix beacon culling with auto overdraw prevention 2025-02-15 11:12:46 -06:00
James Seibel 6fbe0a9e72 Add missing cave blocks for cave culling 2025-02-15 11:06:43 -06:00
James Seibel 11a2b8bf5b Add TODO to PriorityTaskPicker about VisualVM 2025-02-15 11:06:30 -06:00
James Seibel 99f2d2f844 Add TODO comment about Immersive Portals only rendering 1 level 2025-02-14 07:48:05 -06:00
s809 a5c029203c Invert generateOnlyInHighestDetail and rename to enableNSizedGeneration 2025-02-11 22:08:25 +05:00
James Seibel 84015e4a40 Put N-sized generation and upsampling behind experimental configs 2025-02-11 07:47:24 -06:00
James Seibel 08f63470a5 Fix auto updater failing for nightly builds 2025-02-10 07:46:43 -06:00
James Seibel f2404b6455 remove unneeded IVersionConstant methods 2025-02-08 11:39:18 -06:00
James Seibel f20231ccbc fix rare null pointer in sharedApi 2025-02-08 11:38:36 -06:00
s809 3a94bbe804 Reduce queue size back to improve responsiveness 2025-02-07 23:23:15 +05:00
s809 15f1754922 Improve ordering of reading positions to update a bit 2025-02-07 23:21:41 +05:00
s809 28448941e1 Keep update propagation queue filled 2025-02-07 23:20:24 +05:00
James Seibel 18c29b9810 Attempt to fix threadpool shutdown rejection exception 2025-02-07 07:25:57 -06:00
James Seibel fa66cefbe2 Add comments to LodRenderSection memoized gen positions
also increase timeout from 15 sec -> 10 minutes
 - done to test if memoization is actually needed
2025-02-07 07:14:50 -06:00
James Seibel f7dc46cb55 Increase full data update task count to reduce down time 2025-02-06 20:10:30 -06:00
James Seibel 5cebee3be4 Flush world gen memory cache when full 2025-02-06 20:08:28 -06:00
s809 532ac8fe01 Fix incorrect distance being used in update propagation SQL and reduce queue size 2025-02-07 01:05:14 +05:00
James Seibel 8385eeb62c Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-02-05 17:31:26 -06:00
James Seibel 95db6885e7 add error logging to FullDataRequestHandler 2025-02-05 17:31:13 -06:00
James Seibel 10a3840373 Fix empty data sources when moving in multiplayer or with N-sized world gen
Increases Protocol version 9 -> 10
2025-02-05 17:30:59 -06:00
James Seibel cedaaa8a2e replace a few implicit datasource V2 repo statement setters 2025-02-04 19:29:39 -06:00
James Seibel 2c7f11c722 Improve DhApiBeforeRenderEvent javadocs 2025-02-03 20:30:28 -06:00
James Seibel 4fbda8f02b Fix render enabled config getting set by world gen progress config 2025-02-02 19:52:31 -06:00
James Seibel b0bd536248 Fix compiling with missing "E" 2025-02-02 15:52:17 -06:00
s809 a3ed0012e3 Balance tasks in thread pool using elapsed time instead of priorities 2025-02-02 20:30:35 +05:00
s809 9952481d77 Do not request already fulfilled sections again until some time passes 2025-02-02 20:30:35 +05:00
s809 5e137ee10d Auto-move old save data to new location 2025-02-02 20:30:35 +05:00
James Seibel f02ea68b6f Add missing Enum prefix to RequestResult -> ERequestResult 2025-02-01 16:08:15 -06:00
s809 1041e0a4dd Remove generationProgressDisableMessageDisplayTimeInSeconds from server config command 2025-02-01 19:38:43 +05:00
s809 6fb862ecfe Add GUI description for generateOnlyInHighestDetail 2025-02-01 19:38:25 +05:00
s809 1f8013c1cf Use generateOnlyInHighestDetail client-side 2025-02-01 18:30:07 +05:00
s809 157d72d8dc Decrease delay between missing generation rechecks 2025-01-31 14:54:18 +05:00
James Seibel 2c077f5224 Fix a null pointer in the chunk update queue 2025-01-30 20:13:53 -06:00
s809 6e5bd02ae0 Fix beacon beams flickering 2025-01-30 22:30:02 +05:00
s809 a7578b2a72 Process chunks only once with real-time updates enabled 2025-01-30 21:47:31 +05:00
s809 041cf4e0d4 Fix nightly self-updater after moving jars into zip root 2025-01-30 18:11:34 +05:00
s809 bb1154b036 Revert "Improve chunk processing throughput"
This reverts commit dd3903f66e.
2025-01-28 20:05:59 +05:00
James Seibel 9c9c90e786 Improve world gen import hiding message 2025-01-26 17:47:28 -06:00
James Seibel 3dbd05a4ae minor beacon beam height merge cleanup 2025-01-26 17:41:46 -06:00
James Seibel 042a0b6853 Merge branch 'distant-horizons-core-main' 2025-01-26 17:40:15 -06:00
James Seibel 39c621b8d9 Remove locks from LodRenderSection uploading 2025-01-26 17:12:39 -06:00
James Seibel dd3903f66e Improve chunk processing throughput 2025-01-26 17:05:37 -06:00
James Seibel 2d1859c77d change low memory warning to 4GB or more 2025-01-26 16:53:11 -06:00
James Seibel d62a801c43 Reduce locking in SharedApi.UpdateChunkPosManager 2025-01-25 10:10:10 -06:00
James Seibel cb40336fda Increase rolling average window for world gen queue
This should reduce fluctuations a bit
2025-01-24 21:53:13 -06:00
James Seibel 766c831af0 fix recalculate heightmap breaking stairs, slabs, and glass 2025-01-24 07:22:11 -06:00
s809 736df9f848 Check if session is ready before ignoring local chunks 2025-01-24 11:20:37 +05:00
James Seibel a347caafed Fix holes when moving with N-sized world gen/server side support 2025-01-23 19:44:58 -06:00
James Seibel 2d5902df28 Fix data source leaks for custom world generators 2025-01-23 19:38:01 -06:00
James Seibel 29e496757a Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-01-23 19:16:33 -06:00
James Seibel 7cf05ed31d Fix rare concurrency error on world gen shutdown 2025-01-23 19:15:55 -06:00
James Seibel e7eb8e24ae Speed up PhantomArrayListPool for large checkouts 2025-01-23 19:15:42 -06:00
s809 cdca7723a7 Ignore local chunks if realtime updates are enabled 2025-01-23 23:21:13 +05:00
s809 e0a0ba5222 Fix full data source being released too early 2025-01-23 22:06:20 +05:00
James Seibel 0f88c7c231 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-01-22 21:31:20 -06:00
James Seibel d9911f64b9 Simplify Full data hash logic to speed up saving 2025-01-22 21:31:07 -06:00
s809 8bddd6d503 Fix column order check breaking on tiny columns 2 2025-01-23 00:18:33 +05:00
s809 9b261f6472 Fix column order check breaking on tiny columns 2025-01-23 00:15:12 +05:00
James Seibel 00559b5d34 Remove unneeded locks and speed up FullDataId Entry retrieval 2025-01-22 07:16:04 -06:00
James Seibel 9cae54a079 Show instructions to disable world gen progress message for short time 2025-01-21 07:49:30 -06:00
James Seibel 363ec76450 fix isClosedException name 2025-01-21 07:07:33 -06:00
s809 ebd00df388 Fix task splitting causing generation of already generated sections 2025-01-21 17:26:54 +05:00
James Seibel 13882f44ce minor LodRenderSection rename 2025-01-20 21:51:04 -06:00
James Seibel fce1fa3f41 Fix cached RenderSource closing while in use 2025-01-20 21:50:33 -06:00
James Seibel fab8191ddd remove unneeded wal flush logic 2025-01-20 07:39:04 -06:00
James Seibel 582541d240 handle additional DB closed message on DTO get 2025-01-20 07:38:40 -06:00
s809 f609dcb468 Merge remote-tracking branch 'origin/main' 2025-01-20 11:00:13 +05:00
s809 a69936ca69 Merge branch 'feature/generation-bounds' 2025-01-20 10:59:11 +05:00
James Seibel 8c81c867b6 merge 2025-01-19 17:42:45 -06:00
James Seibel 995f80d553 Fix beacons disappearing and not updating correctly 2025-01-19 17:42:14 -06:00
s809 08f36b4371 Lower the log level of rate limit hits 2025-01-18 17:02:35 +05:00
Jan Trummer 9dcc7e1ad2 Replace set with setMinDefaultMax 2025-01-11 16:05:58 +01:00
Jan Trummer 72139f1f59 Add config to set max beacon render height 2025-01-11 13:38:44 +01:00
65 changed files with 2534 additions and 1402 deletions
@@ -20,6 +20,8 @@
package com.seibel.distanthorizons.api.enums.worldGeneration; package com.seibel.distanthorizons.api.enums.worldGeneration;
/** /**
* DOWN_SAMPLED, <br>
*
* EMPTY, <br> * EMPTY, <br>
* STRUCTURE_START, <br> * STRUCTURE_START, <br>
* STRUCTURE_REFERENCE, <br> * STRUCTURE_REFERENCE, <br>
@@ -37,6 +39,14 @@ package com.seibel.distanthorizons.api.enums.worldGeneration;
*/ */
public enum EDhApiWorldGenerationStep public enum EDhApiWorldGenerationStep
{ {
/**
* Only used when using N-sized world generators or server-side retrieval.
* This denotes that the given datasource was created using lower quality LOD data from above it in the quad tree. <br>
*
* This isn't a valid option for queuing world generation.
*/
DOWN_SAMPLED(-1, "down_sampled"),
EMPTY(0, "empty"), EMPTY(0, "empty"),
STRUCTURE_START(1, "structure_start"), STRUCTURE_START(1, "structure_start"),
STRUCTURE_REFERENCE(2, "structure_reference"), STRUCTURE_REFERENCE(2, "structure_reference"),
@@ -25,11 +25,16 @@ import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhAp
/** /**
* Called before Distant Horizons starts rendering a frame. <br> * Called before Distant Horizons starts rendering a frame. <br>
* Canceling the event will prevent DH from rendering that frame. * Canceling the event will prevent DH from rendering that frame. <br> <br>
*
* This is called before DH starts modifying the GL state.
* If you want to inject into DH's rendering pass, use {@link DhApiBeforeRenderPassEvent} instead.
* *
* @author James Seibel * @author James Seibel
* @version 2023-6-23 * @version 2023-6-23
* @since API 1.0.0 * @since API 1.0.0
*
* @see DhApiBeforeRenderPassEvent
*/ */
public abstract class DhApiBeforeRenderEvent implements IDhApiCancelableEvent<DhApiRenderParam> public abstract class DhApiBeforeRenderEvent implements IDhApiCancelableEvent<DhApiRenderParam>
{ {
@@ -87,7 +87,20 @@ public class DhApiConfigValue<coreType, apiType> implements IDhApiConfigValue<ap
} }
} }
public boolean clearValue() { return this.setValue(null); } public boolean clearValue()
{
if (this.configEntry.getAllowApiOverride())
{
// no converter should be used here since null objects may need to be handled differently
// TODO the API should just have a bool to keep track of whether the API value is in use instead of using NULL
this.configEntry.setApiValue(null);
return true;
}
else
{
return false;
}
}
public boolean getCanBeOverrodeByApi() { return this.configEntry.getAllowApiOverride(); } public boolean getCanBeOverrodeByApi() { return this.configEntry.getAllowApiOverride(); }
@@ -31,14 +31,14 @@ public final class ModInfo
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial"; public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */ /** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 9; public static final int PROTOCOL_VERSION = 10;
public static final String WRAPPER_PACKET_PATH = "message"; public static final String WRAPPER_PACKET_PATH = "message";
/** The internal mod name */ /** The internal mod name */
public static final String NAME = "DistantHorizons"; public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */ /** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons"; public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "2.3.0-b-dev"; public static final String VERSION = "2.3.0-b";
/** Returns true if the current build is an unstable developer build, false otherwise. */ /** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
@@ -685,7 +685,7 @@ public class ClientApi
// orange text // orange text
"\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" + "\u00A76" + "Distant Horizons: Low memory detected." + "\u00A7r \n" +
"Stuttering or low FPS may occur. \n" + "Stuttering or low FPS may occur. \n" +
"Please increase Minecraft's available memory to 4 gigabytes. \n" + "Please increase Minecraft's available memory to 4 GB or more. \n" +
"This warning can be disabled in DH's config under Advanced -> Logging. \n"; "This warning can be disabled in DH's config under Advanced -> Logging. \n";
MC_CLIENT.sendChatMessage(message); MC_CLIENT.sendChatMessage(message);
} }
@@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
@@ -49,7 +50,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
/** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */ /** Contains code and variables used by both {@link ClientApi} and {@link ServerApi} */
public class SharedApi public class SharedApi
@@ -231,6 +231,14 @@ public class SharedApi
return; return;
} }
if (dhLevel instanceof DhClientLevel)
{
if (!((DhClientLevel) dhLevel).shouldProcessChunkUpdate(chunkWrapper.getChunkPos()))
{
return;
}
}
// shoudln't normally happen, but just in case // shoudln't normally happen, but just in case
if (UPDATE_POS_MANAGER.contains(chunkWrapper.getChunkPos())) if (UPDATE_POS_MANAGER.contains(chunkWrapper.getChunkPos()))
{ {
@@ -491,11 +499,9 @@ public class SharedApi
/** keeps track of which chunks need to be updated */ /** keeps track of which chunks need to be updated */
private static class UpdateChunkPosManager private static class UpdateChunkPosManager
{ {
private final PriorityQueue<DhChunkPos> closestQueue; private final PriorityBlockingQueue<DhChunkPos> closestQueue;
private final PriorityQueue<DhChunkPos> furthestQueue; private final PriorityBlockingQueue<DhChunkPos> furthestQueue;
private final HashMap<DhChunkPos, UpdateChunkData> updateDataByChunkPos; private final ConcurrentHashMap<DhChunkPos, UpdateChunkData> updateDataByChunkPos;
private final ReentrantLock lock = new ReentrantLock();
private DhChunkPos center; private DhChunkPos center;
private int maxSize = 500; private int maxSize = 500;
@@ -508,9 +514,9 @@ public class SharedApi
public UpdateChunkPosManager() public UpdateChunkPosManager()
{ {
this.closestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> pos.squaredDistance(this.center))); this.closestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> pos.squaredDistance(this.center)));
this.furthestQueue = new PriorityQueue<>(Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed()); this.furthestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed());
this.updateDataByChunkPos = new HashMap<>(); this.updateDataByChunkPos = new ConcurrentHashMap<>();
// defaulting to 0,0 is fine since it'll be updated once we start adding items // defaulting to 0,0 is fine since it'll be updated once we start adding items
this.center = new DhChunkPos(0, 0); this.center = new DhChunkPos(0, 0);
} }
@@ -521,50 +527,20 @@ public class SharedApi
// list/set methods // // list/set methods //
//==================// //==================//
public boolean contains(DhChunkPos pos) public boolean contains(DhChunkPos pos) { return this.updateDataByChunkPos.containsKey(pos); }
{
try
{
this.lock.lock();
return this.updateDataByChunkPos.containsKey(pos);
}
finally
{
this.lock.unlock();
}
}
public void clear() public void clear()
{ {
try this.updateDataByChunkPos.clear();
{ this.closestQueue.clear();
this.lock.lock(); this.furthestQueue.clear();
this.updateDataByChunkPos.clear();
this.closestQueue.clear();
this.furthestQueue.clear();
}
finally
{
this.lock.unlock();
}
} }
public void removeItem(DhChunkPos pos) public void removeItem(DhChunkPos pos)
{ {
try this.updateDataByChunkPos.remove(pos);
{ this.closestQueue.remove(pos);
this.lock.lock(); this.furthestQueue.remove(pos);
this.updateDataByChunkPos.remove(pos);
this.closestQueue.remove(pos);
this.furthestQueue.remove(pos);
}
finally
{
this.lock.unlock();
}
} }
/** /**
@@ -575,35 +551,29 @@ public class SharedApi
*/ */
public int addItem(DhChunkPos pos, UpdateChunkData updateData) public int addItem(DhChunkPos pos, UpdateChunkData updateData)
{ {
try int remainingSlots = this.maxSize - this.updateDataByChunkPos.size();
if (this.updateDataByChunkPos.containsKey(pos))
{ {
this.lock.lock(); // Chunk is already present in queue, no need to insert
return remainingSlots;
}
int remainingSlots = this.maxSize - this.updateDataByChunkPos.size(); // If no slots are left, get one by removing the item furthest from the center
if (this.updateDataByChunkPos.containsKey(pos)) if (remainingSlots <= 0)
{
DhChunkPos furthest = this.furthestQueue.poll();
if (furthest != null)
{ {
// Chunk is already present in queue, no need to insert
return remainingSlots;
}
// If no slots are left, get one by removing the item furthest from the center
if (remainingSlots <= 0)
{
DhChunkPos furthest = this.furthestQueue.poll();
this.closestQueue.remove(furthest); this.closestQueue.remove(furthest);
this.updateDataByChunkPos.remove(furthest); this.updateDataByChunkPos.remove(furthest);
} }
this.updateDataByChunkPos.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
return remainingSlots;
}
finally
{
this.lock.unlock();
} }
this.updateDataByChunkPos.put(pos, updateData);
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
return remainingSlots;
} }
@@ -622,46 +592,33 @@ public class SharedApi
return; return;
} }
try this.center = newCenter;
{
this.lock.lock();
this.center = newCenter; // rebuild the priority queues to match the new center
this.closestQueue.clear();
// rebuild the priority queues to match the new center this.furthestQueue.clear();
this.closestQueue.clear(); for (DhChunkPos pos : this.updateDataByChunkPos.keySet())
this.furthestQueue.clear();
for (DhChunkPos pos : this.updateDataByChunkPos.keySet())
{
this.closestQueue.add(pos);
this.furthestQueue.add(pos);
}
}
finally
{ {
this.lock.unlock(); this.closestQueue.add(pos);
this.furthestQueue.add(pos);
} }
} }
public UpdateChunkData popClosest() public UpdateChunkData popClosest()
{ {
try if (this.closestQueue.isEmpty())
{ {
this.lock.lock(); return null;
if (this.closestQueue.isEmpty())
{
return null;
}
DhChunkPos closest = this.closestQueue.poll();
this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest);
} }
finally
DhChunkPos closest = this.closestQueue.poll();
if (closest == null)
{ {
this.lock.unlock(); return null;
} }
this.furthestQueue.remove(closest);
return this.updateDataByChunkPos.remove(closest);
} }
} }
@@ -427,6 +427,15 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> beaconRenderHeight = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(1, 6000, 6_000_000)
.comment(""
+ "Sets the maximum height at which beacons will render."
+ "This will only affect new beacons coming into LOD render distance."
+ "Beacons currently visible in LOD chunks will not be affected."
+ "")
.build();
public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCloudRendering = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.comment("" .comment(""
@@ -699,16 +708,15 @@ public class Config
public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> enableCaveCulling = new ConfigEntry.Builder<Boolean>()
.set(true) .set(true)
.setPerformance(EConfigEntryPerformance.HIGH)
.comment("" .comment(""
+ "If enabled caves will be culled \n" + "If enabled caves won't be rendered. \n"
+ "\n" + "\n"
+ "NOTE: This feature is under development and \n" + " Note: for some world types this can cause \n"
+ " it is VERY experimental! Please don't report \n" + " overhangs or walls for floating objects. \n"
+ " any issues related to this feature. \n" + " Tweaking the caveCullingHeight, can resolve some \n"
+ "\n" + " of those issues. \n"
+ "Additional Info: Currently this cull all faces \n" + "")
+ " with skylight value of 0 in dimensions that \n"
+ " does not have a ceiling.")
.addListener(ReloadLodsConfigEventHandler.INSTANCE) .addListener(ReloadLodsConfigEventHandler.INSTANCE)
.build(); .build();
@@ -753,20 +761,26 @@ public class Config
+ "Disable this if shadows render incorrectly.") + "Disable this if shadows render incorrectly.")
.build(); .build();
public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() public static ConfigEntry<String> ignoredRenderBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards
.set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom") .set("minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only shown in file since the UI has a character limit
.comment("" .comment(""
+ "A comma separated list of block resource locations that won't be rendered by DH. \n" + "A comma separated list of block resource locations that won't be rendered by DH. \n"
+ "Note: air is always included in this list. \n" + "Air is always included in this list. \n"
+ "Requires a restart to change. \n"
+ "") + "")
.build(); .build();
public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>() public static ConfigEntry<String> ignoredRenderCaveBlockCsv = new ConfigEntry.Builder<String>() // TODO accept wildcards
.set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column") .set("minecraft:glow_lichen,minecraft:rail,minecraft:water,minecraft:lava,minecraft:bubble_column," +
"minecraft:cave_vines_plant,minecraft:vine,minecraft:cave_vines,minecraft:short_grass,minecraft:tall_grass," +
"minecraft:small_dripleaf,minecraft:big_dripleaf,minecraft:big_dripleaf_stem,minecraft:sculk_vein")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) // only shown in file since the UI has a character limit
.comment("" .comment(""
+ "A comma separated list of block resource locations that shouldn't be rendered \n" + "A comma separated list of block resource locations that shouldn't be rendered \n"
+ "if they are in a 0 sky light underground area. \n" + "if they are in a 0 sky light underground area. \n"
+ "Note: air is always included in this list. \n" + "Air is always included in this list. \n"
+ "Requires a restart to change. \n"
+ "") + "")
.build(); .build();
@@ -1273,6 +1287,14 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> generationProgressDisableMessageDisplayTimeInSeconds = new ConfigEntry.Builder<Integer>()
.setMinDefaultMax(0, 20, 60 * 60) // max = 1 hour
.comment(""
+ "For how many seconds should instructions for disabling the distant generator progress be displayed? \n"
+ "Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts. \n"
+ "")
.build();
} }
public static class LodBuilding public static class LodBuilding
@@ -1386,6 +1408,30 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
public static class Experimental
{
public static ConfigEntry<Boolean> upsampleLowerDetailLodsToFillHoles = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "When active DH will attempt to fill missing LOD data \n"
+ "with any data that is present in the tree, preventing holes when moving \n"
+ "when a N-sized generator (or server) is active. \n"
+ "\n"
+ "This is only used when N-sized world generation is available \n"
+ "and/or when on a server where [generateOnlyInHighestDetail] is false. \n"
+ "\n"
+ "Experimental:\n"
+ "Enabling this option will increase CPU and harddrive use\n"
+ "and may cause rendering bugs.\n"
+ "\n"
+ "")
.build();
}
} }
public static class MultiThreading public static class MultiThreading
@@ -1569,14 +1615,6 @@ public class Config
.setPerformance(EConfigEntryPerformance.HIGH) .setPerformance(EConfigEntryPerformance.HIGH)
.build(); .build();
public static ConfigEntry<Boolean> generateOnlyInHighestDetail = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.highestDetailOnly")
.set(false)
.comment(""
+ "Makes the server reject all generation requests for detail levels below the highest one.\n"
+ "")
.build();
public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> generationBoundsX = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.bounds.x") .setChatCommandName("generation.bounds.x")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE) .setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
@@ -1662,6 +1700,23 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
public static class Experimental
{
public static ConfigEntry<Boolean> enableNSizedGeneration = new ConfigEntry.Builder<Boolean>()
.setChatCommandName("generation.nSized")
.set(false)
.comment(""
+ "When enabled on the client, this allows loading lower detail levels as needed to speed up terrain generation.\n"
+ "This must also be enabled on the server; otherwise, it will have no effect.\n"
+ "For better performance when switching LOD detail levels, enabling [upsampleLowerDetailLodsToFillHoles] is recommended.\n"
+ "")
.build();
}
} }
@@ -35,8 +35,17 @@ public class QuickRenderToggleConfigEventHandler
/** private since we only ever need one handler at a time */ /** private since we only ever need one handler at a time */
private QuickRenderToggleConfigEventHandler() private QuickRenderToggleConfigEventHandler()
{ {
this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering, (val) -> { Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get() ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED); }); this.quickRenderChangeListener = new ConfigChangeListener<>(Config.Client.quickEnableRendering,
this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode, (val) -> { Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED); }); (val) -> {
Config.Client.Advanced.Debugging.rendererMode.set(Config.Client.quickEnableRendering.get()
? EDhApiRendererMode.DEFAULT
: EDhApiRendererMode.DISABLED);
});
this.rendererModeChangeListener = new ConfigChangeListener<>(Config.Client.Advanced.Debugging.rendererMode,
(val) -> {
Config.Client.quickEnableRendering.set(
Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED);
});
} }
/** /**
@@ -45,7 +54,8 @@ public class QuickRenderToggleConfigEventHandler
*/ */
public void setUiOnlyConfigValues() public void setUiOnlyConfigValues()
{ {
Config.Client.quickEnableRendering.set(Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED); boolean enableRendering = Config.Client.Advanced.Debugging.rendererMode.get() != EDhApiRendererMode.DISABLED;
Config.Client.quickEnableRendering.set(enableRendering);
} }
} }
@@ -56,7 +56,8 @@ public class QuickShowWorldGenProgressConfigEventHandler
*/ */
public void setUiOnlyConfigValues() public void setUiOnlyConfigValues()
{ {
Config.Client.quickEnableRendering.set(Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED); boolean showProgress = Config.Common.WorldGenerator.showGenerationProgress.get() != EDhApiDistantGeneratorProgressDisplayLocation.DISABLED;
Config.Client.quickShowWorldGenProgress.set(showProgress);
} }
} }
@@ -26,6 +26,7 @@ import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryAppearance;
import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance; import com.seibel.distanthorizons.core.config.types.enums.EConfigEntryPerformance;
import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry; import com.seibel.distanthorizons.coreapi.interfaces.config.IConfigEntry;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -54,6 +55,8 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
* and any get() method calls will return the apiValue if it is set. * and any get() method calls will return the apiValue if it is set.
*/ */
public final boolean allowApiOverride; public final boolean allowApiOverride;
/** Will be null if un-set */
@Nullable
private T apiValue; private T apiValue;
@@ -132,9 +135,9 @@ public class ConfigEntry<T> extends AbstractConfigType<T, ConfigEntry<T>> implem
@Override @Override
public T get() public T get()
{ {
if (allowApiOverride && apiValue != null) if (this.allowApiOverride && this.apiValue != null)
{ {
return apiValue; return this.apiValue;
} }
return super.get(); return super.get();
@@ -35,6 +35,7 @@ import org.apache.logging.log4j.Logger;
import java.io.*; import java.io.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
@@ -63,15 +64,12 @@ public class FullDataPointIdMap
private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_"; private static final String BLOCK_STATE_SEPARATOR_STRING = "_DH-BSW_";
/** used when the data point map is running normally */
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/** should only be used for debugging */ /** should only be used for debugging */
private long pos; private long pos;
/** The index should be the same as the Entry's ID */ /** The index should be the same as the Entry's ID */
private final ArrayList<Entry> entryList = new ArrayList<>(); private final ArrayList<Entry> entryList = new ArrayList<>();
private final HashMap<Entry, Integer> idMap = new HashMap<>(); private final ConcurrentHashMap<Entry, Integer> idMap = new ConcurrentHashMap<>();
private int cachedHashCode = 0; private int cachedHashCode = 0;
@@ -89,34 +87,25 @@ public class FullDataPointIdMap
// getters // // getters //
//=========// //=========//
/** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#entryList} */
private Entry getEntry(int id) throws IndexOutOfBoundsException
{
try
{
this.readWriteLock.readLock().lock();
Entry entry;
try
{
entry = this.entryList.get(id);
}
catch (IndexOutOfBoundsException e)
{
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+this.pos+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"].");
}
return entry;
}
finally
{
this.readWriteLock.readLock().unlock();
}
}
/** @see FullDataPointIdMap#getEntry(int) */ /** @see FullDataPointIdMap#getEntry(int) */
public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; } public IBiomeWrapper getBiomeWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).biome; }
/** @see FullDataPointIdMap#getEntry(int) */ /** @see FullDataPointIdMap#getEntry(int) */
public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; } public IBlockStateWrapper getBlockStateWrapper(int id) throws IndexOutOfBoundsException { return this.getEntry(id).blockState; }
/** @throws IndexOutOfBoundsException if the given ID isn't in the {@link FullDataPointIdMap#entryList} */
private Entry getEntry(int id) throws IndexOutOfBoundsException
{
Entry entry;
try
{
entry = this.entryList.get(id);
}
catch (IndexOutOfBoundsException e)
{
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+this.pos+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"].");
}
return entry;
}
/** @return -1 if the list is empty */ /** @return -1 if the list is empty */
@@ -137,73 +126,36 @@ public class FullDataPointIdMap
* If an entry with the given values already exists nothing will * If an entry with the given values already exists nothing will
* be added but the existing item's ID will still be returned. * be added but the existing item's ID will still be returned.
*/ */
public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState), true); } public int addIfNotPresentAndGetId(IBiomeWrapper biome, IBlockStateWrapper blockState) { return this.addIfNotPresentAndGetId(Entry.getEntry(biome, blockState)); }
/** @param useWriteLocks should only be false if this method is already in a write lock to prevent unlocking at the wrong time */ private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry)
private int addIfNotPresentAndGetId(Entry biomeBlockStateEntry, boolean useWriteLocks)
{ {
try // try getting the existing ID
Integer nullableId = this.idMap.get(biomeBlockStateEntry);
if (nullableId != null)
{ {
if (useWriteLocks) return nullableId;
{
this.readWriteLock.writeLock().lock();
}
int id;
if (this.idMap.containsKey(biomeBlockStateEntry))
{
// use the existing ID
id = this.idMap.get(biomeBlockStateEntry);
}
else
{
// Add the new ID
id = this.entryList.size();
this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
// invalidate the cached hash code
this.cachedHashCode = 0;
}
return id;
} }
finally
{
if (useWriteLocks)
{
this.readWriteLock.writeLock().unlock();
}
}
}
/** allows for adding duplicate {@link Entry} */
private void add(Entry biomeBlockStateEntry, boolean useWriteLocks) // create the new ID
{ return this.idMap.compute(biomeBlockStateEntry, (Entry newBiomeBlockStateEntry, Integer currentId) ->
try
{ {
if (useWriteLocks) if (currentId != null)
{ {
this.readWriteLock.writeLock().lock(); return currentId;
} }
int id = this.entryList.size(); // Add the new ID
currentId = this.entryList.size();
this.entryList.add(biomeBlockStateEntry); this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
// invalidate the cached hash code // invalidate the cached hash code
this.cachedHashCode = 0; this.cachedHashCode = 0;
}
finally
{
if (useWriteLocks)
{
this.readWriteLock.writeLock().unlock();
}
}
}
return currentId;
});
}
/** /**
* Adds every {@link Entry} from inputMap into this map. <br> * Adds every {@link Entry} from inputMap into this map. <br>
@@ -216,27 +168,22 @@ public class FullDataPointIdMap
*/ */
public void addAll(FullDataPointIdMap inputMap) public void addAll(FullDataPointIdMap inputMap)
{ {
try ArrayList<Entry> entriesToMerge = inputMap.entryList;
for (int i = 0; i < entriesToMerge.size(); i++)
{ {
//LOGGER.trace("adding {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); Entry entity = entriesToMerge.get(i);
this.add(entity);
inputMap.readWriteLock.readLock().lock();
this.readWriteLock.writeLock().lock();
ArrayList<Entry> entriesToMerge = inputMap.entryList;
for (int i = 0; i < entriesToMerge.size(); i++)
{
Entry entity = entriesToMerge.get(i);
this.add(entity, false);
}
} }
finally }
{ /** allows for adding duplicate {@link Entry} */
this.readWriteLock.writeLock().unlock(); private void add(Entry biomeBlockStateEntry)
inputMap.readWriteLock.readLock().unlock(); {
int id = this.entryList.size();
this.entryList.add(biomeBlockStateEntry);
this.idMap.put(biomeBlockStateEntry, id);
//LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); // invalidate the cached hash code
} this.cachedHashCode = 0;
} }
/** /**
@@ -250,31 +197,16 @@ public class FullDataPointIdMap
*/ */
public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap) public int[] mergeAndReturnRemappedEntityIds(FullDataPointIdMap inputMap)
{ {
try ArrayList<Entry> entriesToMerge = inputMap.entryList;
int[] remappedEntryIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++)
{ {
//LOGGER.trace("merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); Entry entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity);
inputMap.readWriteLock.readLock().lock(); remappedEntryIds[i] = id;
this.readWriteLock.writeLock().lock();
ArrayList<Entry> entriesToMerge = inputMap.entryList;
int[] remappedEntryIds = new int[entriesToMerge.size()];
for (int i = 0; i < entriesToMerge.size(); i++)
{
Entry entity = entriesToMerge.get(i);
int id = this.addIfNotPresentAndGetId(entity, false);
remappedEntryIds[i] = id;
}
return remappedEntryIds;
} }
finally
{
this.readWriteLock.writeLock().unlock();
inputMap.readWriteLock.readLock().unlock();
//LOGGER.trace("finished merging {" + this.pos + ", " + this.entryList.size() + "} and {" + inputMap.pos + ", " + inputMap.entryList.size() + "}"); return remappedEntryIds;
}
} }
/** Should only be used if this map is going to be reused, otherwise bad things will happen. */ /** Should only be used if this map is going to be reused, otherwise bad things will happen. */
@@ -295,38 +227,29 @@ public class FullDataPointIdMap
/** Serializes all contained entries into the given stream, formatted in UTF */ /** Serializes all contained entries into the given stream, formatted in UTF */
public void serialize(DhDataOutputStream outputStream) throws IOException public void serialize(DhDataOutputStream outputStream) throws IOException
{ {
try outputStream.writeInt(this.entryList.size());
// only used when debugging
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList)
{ {
this.readWriteLock.readLock().lock(); String entryString = entry.serialize();
outputStream.writeInt(this.entryList.size()); outputStream.writeUTF(entryString);
// only used when debugging if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
HashMap<String, FullDataPointIdMap.Entry> dataPointEntryBySerialization = new HashMap<>();
for (Entry entry : this.entryList)
{ {
String entryString = entry.serialize(); if (dataPointEntryBySerialization.containsKey(entryString))
outputStream.writeUTF(entryString);
if (RUN_SERIALIZATION_DUPLICATE_VALIDATION)
{ {
if (dataPointEntryBySerialization.containsKey(entryString)) LOGGER.error("Duplicate serialized entry found with serial: " + entryString);
{
LOGGER.error("Duplicate serialized entry found with serial: " + entryString);
}
if (dataPointEntryBySerialization.containsValue(entry))
{
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize());
}
dataPointEntryBySerialization.put(entryString, entry);
} }
if (dataPointEntryBySerialization.containsValue(entry))
{
LOGGER.error("Duplicate serialized entry found with value: " + entry.serialize());
}
dataPointEntryBySerialization.put(entryString, entry);
} }
} }
finally
{
this.readWriteLock.readLock().unlock();
//LOGGER.trace("serialize " + this.pos + " " + this.entryList.size());
}
} }
/** Creates a new IdBiomeBlockStateMap from the given UTF formatted stream */ /** Creates a new IdBiomeBlockStateMap from the given UTF formatted stream */
@@ -429,7 +352,7 @@ public class FullDataPointIdMap
{ {
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final Int2ReferenceOpenHashMap<ArrayList<Entry>> ENTRY_POOL = new Int2ReferenceOpenHashMap<>(); private static final ConcurrentHashMap<Integer, Entry> ENTRY_BY_HASH = new ConcurrentHashMap<>();
/** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */ /** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */
private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock(); private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock();
@@ -437,6 +360,7 @@ public class FullDataPointIdMap
public final IBlockStateWrapper blockState; public final IBlockStateWrapper blockState;
private Integer hashCode = null; private Integer hashCode = null;
private String serialString = null;
@@ -446,62 +370,25 @@ public class FullDataPointIdMap
public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState) public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{ {
int entryHash = getHashCode(biome, blockState); int entryHash = generateHashCode(biome, blockState);
// try getting the existing entry // try getting the existing Entry
try Entry entry = ENTRY_BY_HASH.get(entryHash);
if (entry != null)
{ {
ENTRY_POOL_LOCK.readLock().lock(); return entry;
// check if an entry already exists
ArrayList<Entry> entryList = ENTRY_POOL.get(entryHash);
if (entryList != null)
{
// at least one entry exists with this hash code
for (int i = 0; i < entryList.size(); i++)
{
Entry entry = entryList.get(i);
if (entry.biome.equals(biome) && entry.blockState.equals(blockState))
{
return entry;
}
}
// if we got here, then there was a hash collision and this entry wasn't present in the array
}
}
finally
{
ENTRY_POOL_LOCK.readLock().unlock();
} }
// create the missing entry
// no entry exists, return ENTRY_BY_HASH.compute(entryHash, (Integer newHash, Entry currentEntry) ->
// create a new one
try
{ {
ENTRY_POOL_LOCK.writeLock().lock(); if (currentEntry != null)
ArrayList<Entry> entryList = ENTRY_POOL.get(entryHash);
if (entryList == null)
{ {
// no entries exist for this hash code return currentEntry;
// we assume that hash collisions should basically never happen,
// so the array starts with an initial capacity of 1.
// However, since collisions will eventually happen, using an arrayList prevents unexpected bugs caused by collisions.
entryList = new ArrayList<>(1);
ENTRY_POOL.put(entryHash, entryList);
} }
Entry newEntry = new Entry(biome, blockState); return new Entry(biome, blockState);
entryList.add(newEntry); });
return newEntry;
}
finally
{
ENTRY_POOL_LOCK.writeLock().unlock();
}
} }
private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState) private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{ {
@@ -515,8 +402,19 @@ public class FullDataPointIdMap
// overrides // // overrides //
//===========// //===========//
public static int getHashCode(Entry entry) { return getHashCode(entry.biome, entry.blockState); } @Override
public static int getHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState) public int hashCode()
{
// cache the hash code to improve speed
if (this.hashCode == null)
{
this.hashCode = generateHashCode(this);
}
return this.hashCode;
}
private static int generateHashCode(Entry entry) { return generateHashCode(entry.biome, entry.blockState); }
private static int generateHashCode(IBiomeWrapper biome, IBlockStateWrapper blockState)
{ {
final int prime = 31; final int prime = 31;
@@ -527,17 +425,6 @@ public class FullDataPointIdMap
result = prime * result + (blockState == null ? 0 : blockState.hashCode()); result = prime * result + (blockState == null ? 0 : blockState.hashCode());
return result; return result;
} }
@Override
public int hashCode()
{
// cache the hash code to improve speed
if (this.hashCode == null)
{
this.hashCode = getHashCode(this);
}
return this.hashCode;
}
@Override @Override
public boolean equals(Object otherObj) public boolean equals(Object otherObj)
@@ -562,7 +449,15 @@ public class FullDataPointIdMap
// (de)serializing // // (de)serializing //
//=================// //=================//
public String serialize() { return this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString(); } public String serialize()
{
if (this.serialString == null)
{
this.serialString = this.biome.getSerialString() + BLOCK_STATE_SEPARATOR_STRING + this.blockState.getSerialString();
}
return this.serialString;
}
public static Entry deserialize(String str, ILevelWrapper levelWrapper) throws DataCorruptedException public static Entry deserialize(String str, ILevelWrapper levelWrapper) throws DataCorruptedException
{ {
@@ -73,7 +73,7 @@ public class FullDataSourceV2
public static final byte DATA_FORMAT_VERSION = 1; public static final byte DATA_FORMAT_VERSION = 1;
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2", false); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("FullDataV2");
@@ -116,7 +116,12 @@ public class FullDataSourceV2
public final LongArrayList[] dataPoints; public final LongArrayList[] dataPoints;
public boolean isEmpty; public boolean isEmpty;
public boolean applyToParent = false; /** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToParent = null;
/** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToChildren = null;
/** should only be used by methods exposed via the DH API */ /** should only be used by methods exposed via the DH API */
private boolean runApiChunkValidation = false; private boolean runApiChunkValidation = false;
@@ -269,11 +274,6 @@ public class FullDataSourceV2
{ {
ListUtil.clearAndSetSize(this.columnWorldCompressionMode, WIDTH * WIDTH); ListUtil.clearAndSetSize(this.columnWorldCompressionMode, WIDTH * WIDTH);
} }
// the pooled arrays have all been set,
// the checkout object is no longer needed
this.pooledArraysCheckout = null;
} }
@@ -306,10 +306,47 @@ public class FullDataSourceV2
if (inputDetailLevel == thisDetailLevel) if (inputDetailLevel == thisDetailLevel)
{ {
dataChanged = this.updateFromSameDetailLevel(inputDataSource, remappedIds); dataChanged = this.updateFromSameDetailLevel(inputDataSource, remappedIds);
// same detail level, propagate parent/children update flags from input
if (this.applyToParent != null || inputDataSource.applyToParent != null)
{
this.applyToParent =
// copy over application flag if either are set to continue propagating
(BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
// don't propagate past the top of the tree
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
}
// null check to prevent setting a flag we don't want to save in the DB
if (this.applyToChildren != null || inputDataSource.applyToChildren != null)
{
this.applyToChildren =
(BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
// don't propagate past the bottom of the tree
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL);
}
} }
else if (inputDetailLevel + 1 == thisDetailLevel) else if (inputDetailLevel + 1 == thisDetailLevel)
{ {
dataChanged = this.updateFromOneBelowDetailLevel(inputDataSource, remappedIds); dataChanged = this.updateFromOneBelowDetailLevel(inputDataSource, remappedIds);
// propagating up, parent will need changes
this.applyToParent =
dataChanged
&& (BoolUtil.falseIfNull(this.applyToParent) || BoolUtil.falseIfNull(inputDataSource.applyToParent))
&& (DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
}
else if (inputDetailLevel - 1 == thisDetailLevel)
{
dataChanged = this.downsampleFromOneAboveDetailLevel(inputDataSource, remappedIds);
// propagating down, children will need changes
this.applyToChildren =
dataChanged
&& (BoolUtil.falseIfNull(this.applyToChildren) || BoolUtil.falseIfNull(inputDataSource.applyToChildren))
&& (DhSectionPos.getDetailLevel(this.pos) > AbstractDataSourceHandler.MIN_SECTION_DETAIL_LEVEL);
} }
else else
{ {
@@ -317,12 +354,9 @@ public class FullDataSourceV2
// and would lead to edge cases that don't necessarily need to be supported // and would lead to edge cases that don't necessarily need to be supported
// (IE what do you do when the input is smaller than a single datapoint in the receiving data source?) // (IE what do you do when the input is smaller than a single datapoint in the receiving data source?)
// instead it's better to just percolate the updates up // instead it's better to just percolate the updates up
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+thisDetailLevel+"] or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"]."); throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
} }
// determine if this data source should be applied to its parent
this.applyToParent = (dataChanged && DhSectionPos.getDetailLevel(this.pos) < AbstractDataSourceHandler.TOP_SECTION_DETAIL_LEVEL);
if (dataChanged) if (dataChanged)
{ {
// update the hash code // update the hash code
@@ -331,6 +365,7 @@ public class FullDataSourceV2
return dataChanged; return dataChanged;
} }
public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds) public boolean updateFromSameDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{ {
// both data sources should have the same detail level // both data sources should have the same detail level
@@ -354,8 +389,30 @@ public class FullDataSourceV2
byte thisGenState = this.columnGenerationSteps.getByte(index); byte thisGenState = this.columnGenerationSteps.getByte(index);
byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index); byte inputGenState = inputDataSource.columnGenerationSteps.getByte(index);
if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value
&& thisGenState <= inputGenState) // determine if this column should be updated
boolean genStateAllowsUpdating = false;
// if the input is downsampled, we only want to replace empty or downsampled values
if (inputGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value
&&
(
thisGenState == EDhApiWorldGenerationStep.EMPTY.value
|| thisGenState == EDhApiWorldGenerationStep.DOWN_SAMPLED.value
))
{
genStateAllowsUpdating = true;
}
// if the input is any other non-empty value,
// replace anything that is less-complete
else if (inputGenState != EDhApiWorldGenerationStep.EMPTY.value
&& thisGenState <= inputGenState)
{
// don't apply less-complete generation data
genStateAllowsUpdating = true;
}
if (genStateAllowsUpdating)
{ {
// check if the data changed // check if the data changed
if (this.dataPoints[index] == null) if (this.dataPoints[index] == null)
@@ -835,6 +892,101 @@ public class FullDataSourceV2
return value; return value;
} }
/**
* Only downsamples into a given column if this data source doesn't
* already contain data in that column.
* This is done to prevent accidentally downsampling onto already present higher-detail data.
*/
public boolean downsampleFromOneAboveDetailLevel(FullDataSourceV2 inputDataSource, int[] remappedIds)
{
if (DhSectionPos.getDetailLevel(inputDataSource.pos) - 1 != DhSectionPos.getDetailLevel(this.pos))
{
throw new IllegalArgumentException("Input data source must be exactly 1 detail level above this data source. Expected [" + (DhSectionPos.getDetailLevel(this.pos) - 1) + "], received [" + DhSectionPos.getDetailLevel(inputDataSource.pos) + "].");
}
// input is one detail level higher (lower detail)
// so 1x1 input data points will be converted into 2x2 recipient data point
// determine where in this data source should be read from
// since the input is one detail level above this will be one of input position's 4 children
int minParentXPos = DhSectionPos.getX(DhSectionPos.getChildByIndex(inputDataSource.pos, 0));
int inputOffsetX = (DhSectionPos.getX(this.pos) == minParentXPos) ? 0 : (WIDTH / 2);
int minParentZPos = DhSectionPos.getZ(DhSectionPos.getChildByIndex(inputDataSource.pos, 0));
int inputOffsetZ = (DhSectionPos.getZ(this.pos) == minParentZPos) ? 0 : (WIDTH / 2);
// merge the input's data points
// into this data source's
boolean dataChanged = false;
for (int x = 0; x < WIDTH; x++)
{
for (int z = 0; z < WIDTH; z++)
{
// recipient index is 1-to-1
int recipientIndex = relativePosToIndex(x, z);
int inputX = (x / 2) + inputOffsetX;
int inputZ = (z / 2) + inputOffsetZ;
int inputIndex = relativePosToIndex(inputX, inputZ);
// world gen //
// a separate generation step needs to be used so can replace
// this data with higher-quality data when it is available
byte inputGenStep = EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
this.columnGenerationSteps.set(recipientIndex, inputGenStep);
// world compression //
byte worldCompressionMode = inputDataSource.columnWorldCompressionMode.getByte(recipientIndex);
this.columnWorldCompressionMode.set(recipientIndex, worldCompressionMode);
// data points //
// check if this column should be downsampled
boolean downSampleColumn;
if (this.dataPoints[recipientIndex] == null)
{
downSampleColumn = true;
}
else
{
downSampleColumn = true; // assume empty until we find non-empty data
for (long dataPoint : this.dataPoints[recipientIndex])
{
if (dataPoint != FullDataPointUtil.EMPTY_DATA_POINT)
{
downSampleColumn = false;
break;
}
}
}
if (downSampleColumn)
{
LongArrayList inputDataArray = inputDataSource.dataPoints[inputIndex];
this.dataPoints[recipientIndex] = inputDataArray;
this.remapDataColumn(recipientIndex, remappedIds);
if (RUN_DATA_ORDER_VALIDATION)
{
throwIfDataColumnInWrongOrder(inputDataSource.pos, this.dataPoints[recipientIndex]);
}
dataChanged = true;
}
this.isEmpty = false;
}
}
return dataChanged;
}
//================// //================//
@@ -866,6 +1018,11 @@ public class FullDataSourceV2
*/ */
public static void throwIfDataColumnInWrongOrder(long pos, LongArrayList dataArray) throws IllegalStateException public static void throwIfDataColumnInWrongOrder(long pos, LongArrayList dataArray) throws IllegalStateException
{ {
if (dataArray.size() < 2)
{
return;
}
long firstDataPoint = dataArray.getLong(0); long firstDataPoint = dataArray.getLong(0);
int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint); int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint);
@@ -884,6 +1041,11 @@ public class FullDataSourceV2
*/ */
private static void ensureDataColumnOrder(LongArrayList dataColumn) private static void ensureDataColumnOrder(LongArrayList dataColumn)
{ {
if (dataColumn.size() < 2)
{
return;
}
long firstDataPoint = dataColumn.getLong(0); long firstDataPoint = dataColumn.getLong(0);
int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint); int firstBottomY = FullDataPointUtil.getBottomY(firstDataPoint);
@@ -972,7 +1134,7 @@ public class FullDataSourceV2
LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0); LongArrayList packedDataPoints = LodDataBuilder.convertApiDataPointListToPackedLongArray(columnDataPoints, this, 0);
// TODO there should be an "unknown" compression and generation step, or be defined via the datapoints // TODO there should be an "unknown" compression and generation step, or be defined via the datapoints
this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.LIGHT, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS); this.setSingleColumn(packedDataPoints, relX, relZ, EDhApiWorldGenerationStep.SURFACE, EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
return columnDataPoints; return columnDataPoints;
} }
@@ -0,0 +1,101 @@
package com.seibel.distanthorizons.core.dataObjects.render;
import com.google.common.cache.Cache;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
/**
* Wrapper for {@link ColumnRenderSource} that handles reference counting
* and cache tracking.
*/
public class CachedColumnRenderSource implements AutoCloseable
{
/** an externally handled future that will complete once the {@link CachedColumnRenderSource#columnRenderSource} has finished loading */
public final CompletableFuture<CachedColumnRenderSource> loadFuture;
/** will be null initially, should be non-null once the corresponding load future is done */
@Nullable
public ColumnRenderSource columnRenderSource = null;
private final AtomicInteger referenceCount;
private final Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos;
private final ReentrantLock getterLock;
//=============//
// constructor //
//=============//
public CachedColumnRenderSource(
@NotNull CompletableFuture<CachedColumnRenderSource> loadFuture,
@NotNull ReentrantLock getterLock,
@NotNull Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos)
{
this.loadFuture = loadFuture;
this.getterLock = getterLock;
this.referenceCount = new AtomicInteger(1);
this.cachedRenderSourceByPos = cachedRenderSourceByPos;
}
//====================//
// reference counting //
//====================//
public void markInUse() { this.referenceCount.getAndIncrement(); }
//================//
// base overrides //
//================//
/**
* Will be called multiple times,
* however it will only close the underlying data once
* all references have closed.
*/
@Override
public void close() throws IllegalStateException
{
try
{
// lock to prevent other threads for accessing the cache if we invalidate it
this.getterLock.lock();
// should only happen if something goes wrong up-stream
if (this.columnRenderSource == null)
{
return;
}
// only close once everyone is done with this datasource
int refCount = this.referenceCount.decrementAndGet();
if (refCount == 0)
{
this.cachedRenderSourceByPos.invalidate(this.columnRenderSource.pos);
this.columnRenderSource.close();
}
else if (refCount < 0)
{
throw new IllegalStateException("Render source ["+this.columnRenderSource.pos+"] reference count incorrect. Object already closed.");
}
}
finally
{
this.getterLock.unlock();
}
}
}
@@ -41,8 +41,6 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
/** /**
* Used to populate the buffers in a {@link ColumnRenderSource} object. * Used to populate the buffers in a {@link ColumnRenderSource} object.
@@ -270,7 +270,7 @@ public class FullDataToRenderDataTransformer
//====================// //====================//
boolean ignoreBlock = blockStatesToIgnore.contains(block); boolean ignoreBlock = blockStatesToIgnore.contains(block);
boolean caveBlock = caveBlockStatesToIgnore.contains(block); boolean caveBlock = caveBlockStatesToIgnore.contains(block); // TODO caves should also ignore transparent/non-solid blocks (IE grass and plants) wthout each being defined
if (caveBlock) if (caveBlock)
{ {
if (caveCullingEnabled if (caveCullingEnabled
@@ -73,6 +73,8 @@ public class LodDataBuilder
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
dataSource.isEmpty = false; dataSource.isEmpty = false;
// chunk updates always propagate up
dataSource.applyToParent = true;
@@ -175,7 +177,12 @@ public class LodDataBuilder
// determine the starting Y Pos // determine the starting Y Pos
int y = chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ); int y = Math.max(
// max between both heightmaps to account for solid invisible blocks (glass)
// and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now)
chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ),
chunkWrapper.getSolidHeightMapValue(relBlockX, relBlockZ)
);
// go up until we reach open air or the world limit // go up until we reach open air or the world limit
IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState); IBlockStateWrapper topBlockState = previousBlockState = chunkWrapper.getBlockState(relBlockX, y, relBlockZ, mcBlockPos, previousBlockState);
while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight()) while (!topBlockState.isAir() && y < chunkWrapper.getExclusiveMaxBuildHeight())
@@ -1,177 +0,0 @@
package com.seibel.distanthorizons.core.file.beacon;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.LodUtil;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class BeaconBeamDataHandler
{
private final BeaconBeamRepo beaconBeamRepo;
@Nullable
private BeaconRenderHandler beaconRenderHandler;
private final KeyedLockContainer<Long> updateLockContainer = new KeyedLockContainer<>();
//=============//
// constructor //
//=============//
public BeaconBeamDataHandler(@NotNull BeaconBeamRepo beaconBeamRepo, @Nullable GenericObjectRenderer renderer)
{
this.beaconBeamRepo = beaconBeamRepo;
if (renderer != null)
{
this.beaconRenderHandler = new BeaconRenderHandler(renderer);
}
}
//==========//
// updating //
//==========//
public void setBeaconBeamsForChunk(DhChunkPos chunkPos, List<BeaconBeamDTO> activeBeamList)
{
long sectionPos = DhSectionPos.encode(LodUtil.CHUNK_DETAIL_LEVEL, chunkPos.getX(), chunkPos.getZ());
this.setBeaconBeamsForPos(sectionPos, activeBeamList);
}
public void setBeaconBeamsForPos(long sectionPos, List<BeaconBeamDTO> activeBeamList)
{
// locked to prevent two threads from updating the same section at the same time
ReentrantLock lock = this.updateLockContainer.getLockForPos(sectionPos);
try
{
lock.lock();
HashSet<DhBlockPos> allPosSet = new HashSet<>();
// sort new beams
HashMap<DhBlockPos, BeaconBeamDTO> activeBeamByPos = new HashMap<>(activeBeamList.size());
for (BeaconBeamDTO beam : activeBeamList)
{
activeBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
// get existing beams
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(sectionPos);
HashMap<DhBlockPos, BeaconBeamDTO> existingBeamByPos = new HashMap<>(existingBeamList.size());
for (BeaconBeamDTO beam : existingBeamList)
{
existingBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
for (DhBlockPos beaconPos : allPosSet)
{
if (!DhSectionPos.contains(sectionPos, beaconPos))
{
// don't update beacons outside the updated chunk
continue;
}
BeaconBeamDTO existingBeam = existingBeamByPos.get(beaconPos);
BeaconBeamDTO activeBeam = activeBeamByPos.get(beaconPos);
if (activeBeam != null)
{
if (existingBeam == null)
{
// new beam found, add to DB
this.beaconBeamRepo.save(activeBeam);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.startRenderingBeacon(activeBeam);
}
}
else
{
// beam still exists in chunk
if (!existingBeam.color.equals(activeBeam.color))
{
// beam colors were changed
this.beaconBeamRepo.save(activeBeam);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.updateBeaconColor(activeBeam);
}
}
}
}
else if (existingBeam != null)
{
// beam no longer exists at position, remove from DB
this.beaconBeamRepo.deleteWithKey(beaconPos);
if (this.beaconRenderHandler != null)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beaconPos);
}
}
}
}
finally
{
lock.unlock();
}
}
//===================//
// loading/unloading //
//===================//
public void loadBeaconBeamsInPos(long pos)
{
if (this.beaconRenderHandler == null)
{
return;
}
// get all beams in pos
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(pos);
for (BeaconBeamDTO newBeam : existingBeamList)
{
this.beaconRenderHandler.startRenderingBeacon(newBeam);
}
}
public void unloadBeaconBeamsInPos(long pos)
{
if (this.beaconRenderHandler == null)
{
return;
}
// get all beams in pos
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsForPos(pos);
for (BeaconBeamDTO beam : existingBeamList)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
}
}
@@ -106,14 +106,12 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
} }
} }
public int getUnsavedCount() { return (int)this.dataSourceByPosition.size(); }
public void handleDataSourceRemoval(RemovalNotification<Long, FullDataSourceV2> removalNotification) public void handleDataSourceRemoval(RemovalNotification<Long, FullDataSourceV2> removalNotification)
{ {
RemovalCause cause = removalNotification.getCause(); RemovalCause cause = removalNotification.getCause();
if (cause == RemovalCause.EXPIRED if (cause == RemovalCause.EXPIRED
|| cause == RemovalCause.COLLECTED || cause == RemovalCause.COLLECTED
|| cause == RemovalCause.EXPLICIT
|| cause == RemovalCause.SIZE) || cause == RemovalCause.SIZE)
{ {
// close the data source after it has expired from the cache // close the data source after it has expired from the cache
@@ -144,6 +142,35 @@ public class DelayedFullDataSourceSaveCache implements AutoCloseable
//==============//
// List methods //
//==============//
public int getUnsavedCount() { return (int)this.dataSourceByPosition.size(); }
/** Removes everything from the memory cache and fires the {@link DelayedFullDataSourceSaveCache#onSaveTimeoutAsyncFunc} for each. */
public void flush()
{
Set<Long> keySet = this.dataSourceByPosition.asMap().keySet();
for (Long pos : keySet)
{
ReentrantLock lock = this.saveLockContainer.getLockForPos(pos);
try
{
lock.lock();
this.dataSourceByPosition.invalidate(pos);
}
finally
{
lock.unlock();
}
}
}
//================// //================//
// static cleanup // // static cleanup //
//================// //================//
@@ -68,9 +68,9 @@ public class FullDataSourceProviderV2
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
protected static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 50; protected static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 5;
/** how many parent update tasks can be in the queue at once */ /** how many parent update tasks can be in the queue at once */
protected static final int MAX_UPDATE_TASK_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD * Config.Common.MultiThreading.numberOfThreads.get(); protected static int getMaxUpdateTaskCount() { return NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD* Config.Common.MultiThreading.numberOfThreads.get(); }
/** indicates how long the update queue thread should wait between queuing ticks */ /** indicates how long the update queue thread should wait between queuing ticks */
protected static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 250; protected static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 250;
@@ -103,7 +103,7 @@ public class FullDataSourceProviderV2
* Tracks which positions are currently being updated * Tracks which positions are currently being updated
* to prevent duplicate concurrent updates. * to prevent duplicate concurrent updates.
*/ */
public final Set<Long> parentUpdatingPosSet = ConcurrentHashMap.newKeySet(); public final Set<Long> updatingPosSet = ConcurrentHashMap.newKeySet();
// TODO only run thread if modifications happened recently // TODO only run thread if modifications happened recently
/** /**
@@ -225,106 +225,11 @@ public class FullDataSourceProviderV2
targetBlockPos = MC_CLIENT.getPlayerBlockPos(); targetBlockPos = MC_CLIENT.getPlayerBlockPos();
} }
// queue parent updates this.runParentUpdates(executor, targetBlockPos);
if (executor.getQueueSize() < MAX_UPDATE_TASK_COUNT
&& this.parentUpdatingPosSet.size() < MAX_UPDATE_TASK_COUNT) if (Config.Common.LodBuilding.Experimental.upsampleLowerDetailLodsToFillHoles.get())
{ {
// get the positions that need to be applied to their parents this.runChildUpdates(executor, targetBlockPos);
LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), MAX_UPDATE_TASK_COUNT);
// combine updates together based on their parent
HashMap<Long, HashSet<Long>> updatePosByParentPos = new HashMap<>();
for (Long pos : parentUpdatePosList)
{
updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) ->
{
if (updatePosSet == null)
{
updatePosSet = new HashSet<>();
}
updatePosSet.add(pos);
return updatePosSet;
});
}
// queue the updates
for (Long parentUpdatePos : updatePosByParentPos.keySet())
{
// stop if there are already a bunch of updates queued
if (this.parentUpdatingPosSet.size() > MAX_UPDATE_TASK_COUNT
|| !this.parentUpdatingPosSet.add(parentUpdatePos))
{
break;
}
try
{
executor.execute(() ->
{
ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentWriteLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
// apply each child pos to the parent
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos);
try
{
childReadLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 dataSource = this.get(childPos))
{
// can return null when the file handler is being shut down
if (dataSource != null)
{
this.updateDataSourceAtPos(parentUpdatePos, dataSource, false);
this.repo.setApplyToParent(childPos, false);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in update for parent pos: [" + DhSectionPos.toString(parentUpdatePos) + "] Error: [" + e.getMessage() + "].", e);
}
finally
{
childReadLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
}
}
finally
{
if (parentLocked)
{
parentWriteLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.parentUpdatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.parentUpdatingPosSet.remove(parentUpdatePos);
throw e;
}
}
} }
} }
@@ -340,6 +245,248 @@ public class FullDataSourceProviderV2
LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated."); LOGGER.info("Update thread ["+Thread.currentThread().getName()+"] terminated.");
} }
/** will always apply updates */
private void runParentUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos)
{
int maxUpdateTaskCount = getMaxUpdateTaskCount();
// queue parent updates
if (executor.getQueueSize() < maxUpdateTaskCount
&& this.updatingPosSet.size() < maxUpdateTaskCount)
{
// get the positions that need to be applied to their parents
LongArrayList parentUpdatePosList = this.repo.getPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount);
// combine updates together based on their parent
HashMap<Long, HashSet<Long>> updatePosByParentPos = new HashMap<>();
for (Long pos : parentUpdatePosList)
{
updatePosByParentPos.compute(DhSectionPos.getParentPos(pos), (parentPos, updatePosSet) ->
{
if (updatePosSet == null)
{
updatePosSet = new HashSet<>();
}
updatePosSet.add(pos);
return updatePosSet;
});
}
// queue the updates
for (Long parentUpdatePos : updatePosByParentPos.keySet())
{
// stop if there are already a bunch of updates queued
if (this.updatingPosSet.size() > maxUpdateTaskCount
|| executor.getQueueSize() > maxUpdateTaskCount
|| !this.updatingPosSet.add(parentUpdatePos))
{
break;
}
try
{
executor.execute(() ->
{
ReentrantLock parentWriteLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentWriteLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos))
{
// will return null if the file handler is shutting down
if (parentDataSource != null)
{
// apply each child pos to the parent
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
ReentrantLock childReadLock = this.updateLockProvider.getLock(childPos);
try
{
childReadLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 childDataSource = this.get(childPos))
{
// can return null when the file handler is being shut down
if (childDataSource != null)
{
parentDataSource.update(childDataSource);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in parent update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e);
}
finally
{
childReadLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
if (DhSectionPos.getDetailLevel(parentUpdatePos) < TOP_SECTION_DETAIL_LEVEL)
{
parentDataSource.applyToParent = true;
}
this.updateDataSourceAtPos(parentUpdatePos, parentDataSource, false);
for (Long childPos : updatePosByParentPos.get(parentUpdatePos))
{
this.repo.setApplyToParent(childPos, false);
}
}
}
}
}
finally
{
if (parentLocked)
{
parentWriteLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.updatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.updatingPosSet.remove(parentUpdatePos);
throw e;
}
}
}
}
/** stops if it finds any LOD data */
private void runChildUpdates(PriorityTaskPicker.Executor executor, DhBlockPos targetBlockPos)
{
int maxUpdateTaskCount = getMaxUpdateTaskCount();
// queue child updates
if (executor.getQueueSize() < maxUpdateTaskCount
&& this.updatingPosSet.size() < maxUpdateTaskCount)
{
// get the positions that need to be applied to their children
LongArrayList childUpdatePosList = this.repo.getChildPositionsToUpdate(targetBlockPos.getX(), targetBlockPos.getZ(), maxUpdateTaskCount);
// queue the updates
for (long parentUpdatePos : childUpdatePosList)
{
// stop if there are already a bunch of updates queued
if (this.updatingPosSet.size() > maxUpdateTaskCount
|| executor.getQueueSize() > maxUpdateTaskCount)
{
break;
}
// skip already updating positions
if (!this.updatingPosSet.add(parentUpdatePos))
{
continue;
}
try
{
executor.execute(() ->
{
ReentrantLock parentReadLock = this.updateLockProvider.getLock(parentUpdatePos);
boolean parentLocked = false;
try
{
//LOGGER.info("updating parent: "+parentUpdatePos);
// Locking the parent before the children should prevent deadlocks.
// TryLock is used instead of lock so this thread can handle a different update.
if (parentReadLock.tryLock())
{
parentLocked = true;
this.lockedPosSet.add(parentUpdatePos);
try (FullDataSourceV2 parentDataSource = this.get(parentUpdatePos))
{
// will return null if the file handler is shutting down
if (parentDataSource != null)
{
// apply parent to each child
for (int i = 0; i < 4; i++)
{
long childPos = DhSectionPos.getChildByIndex(parentUpdatePos, i);
ReentrantLock childWriteLock = this.updateLockProvider.getLock(childPos);
try
{
childWriteLock.lock();
this.lockedPosSet.add(childPos);
try (FullDataSourceV2 childDataSource = this.get(childPos))
{
// will return null if the file handler is shutting down
if (childDataSource != null)
{
childDataSource.update(parentDataSource);
// don't propagate child updates past the bottom of the tree
if (DhSectionPos.getDetailLevel(childPos) != DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL)
{
childDataSource.applyToChildren = true;
}
this.updateDataSourceAtPos(childPos, childDataSource, false);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected in child update propagation for parent pos: ["+DhSectionPos.toString(parentUpdatePos)+"], child pos: [" + DhSectionPos.toString(parentUpdatePos) + "], Error: [" + e.getMessage() + "].", e);
}
finally
{
childWriteLock.unlock();
this.lockedPosSet.remove(childPos);
}
}
this.repo.setApplyToChild(parentUpdatePos, false);
}
}
}
}
finally
{
if (parentLocked)
{
parentReadLock.unlock();
this.lockedPosSet.remove(parentUpdatePos);
}
this.updatingPosSet.remove(parentUpdatePos);
}
});
}
catch (RejectedExecutionException ignore)
{ /* the executor was shut down, it should be back up shortly and able to accept new jobs */ }
catch (Exception e)
{
this.updatingPosSet.remove(parentUpdatePos);
throw e;
}
}
}
}
@@ -645,7 +792,7 @@ public class FullDataSourceProviderV2
this.queuedUpdateCountsByPos this.queuedUpdateCountsByPos
.forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); }); .forEach((pos, updateCountRef) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f + (updateCountRef.get() * 16f), 0.20f, Color.WHITE)); });
this.parentUpdatingPosSet this.updatingPosSet
.forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); }); .forEach((pos) -> { renderer.renderBox(new DebugRenderer.Box(pos, -32f, 80f, 0.20f, Color.MAGENTA)); });
} }
@@ -34,7 +34,6 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
@@ -88,8 +87,20 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// event listeners // // event listeners //
//=================// //=================//
public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.add(listener); } public void addWorldGenCompleteListener(IOnWorldGenCompleteListener listener)
public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener) { this.onWorldGenTaskCompleteListeners.remove(listener); } {
synchronized (this.onWorldGenTaskCompleteListeners)
{
this.onWorldGenTaskCompleteListeners.add(listener);
}
}
public void removeWorldGenCompleteListener(IOnWorldGenCompleteListener listener)
{
synchronized (this.onWorldGenTaskCompleteListeners)
{
this.onWorldGenTaskCompleteListeners.remove(listener);
}
}
@@ -129,10 +140,14 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// TODO only fire after the section has finished generated or once every X seconds // TODO only fire after the section has finished generated or once every X seconds
private void fireOnGenPosSuccessListeners(long pos) private void fireOnGenPosSuccessListeners(long pos)
{ {
// fire the event listeners // synchronized to prevent a rare issue where the world generator is being shut down while this listener is firing
for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners) synchronized (this.onWorldGenTaskCompleteListeners)
{ {
listener.onWorldGenTaskComplete(pos); // fire the event listeners
for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners)
{
listener.onWorldGenTaskComplete(pos);
}
} }
} }
@@ -195,16 +210,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
PriorityTaskPicker.Executor updateExecutor = ThreadPoolUtil.getUpdatePropagatorExecutor();
if (updateExecutor == null || updateExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2)
{
// don't queue additional world gen requests if the updater is behind
return false;
}
PriorityTaskPicker.Executor fileExecutor = ThreadPoolUtil.getFileHandlerExecutor(); PriorityTaskPicker.Executor fileExecutor = ThreadPoolUtil.getFileHandlerExecutor();
if (fileExecutor == null || fileExecutor.getQueueSize() >= MAX_UPDATE_TASK_COUNT / 2) if (fileExecutor == null || fileExecutor.getQueueSize() >= getMaxUpdateTaskCount() / 2)
{ {
// don't queue additional world gen requests if the file handler is overwhelmed, // don't queue additional world gen requests if the file handler is overwhelmed,
// otherwise LODs may not load in properly // otherwise LODs may not load in properly
@@ -229,6 +236,11 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// don't queue additional world gen requests if there are // don't queue additional world gen requests if there are
// a lot of data sources in memory // a lot of data sources in memory
// (this is done to prevent infinite memory growth) // (this is done to prevent infinite memory growth)
// clear out the data sources that are in memory so
// we can start queuing new world gen tasks
this.delayedFullDataSourceSaveCache.flush();
return false; return false;
} }
@@ -298,7 +310,12 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
public boolean isFullyGenerated(ByteArrayList columnGenerationSteps) public boolean isFullyGenerated(ByteArrayList columnGenerationSteps)
{ {
return IntStream.range(0, columnGenerationSteps.size()) return IntStream.range(0, columnGenerationSteps.size())
.noneMatch(i -> columnGenerationSteps.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value); .noneMatch(i ->
{
byte value = columnGenerationSteps.getByte(i);
return value == EDhApiWorldGenerationStep.EMPTY.value
|| value == EDhApiWorldGenerationStep.DOWN_SAMPLED.value;
});
} }
public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider"); public static final PhantomArrayListPool ARRAY_LIST_POOL = new PhantomArrayListPool("Generated Provider");
@@ -328,7 +345,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
// check if any positions are ungenerated // check if any positions are ungenerated
for (int i = 0; i < columnGenStepArray.size(); i++) for (int i = 0; i < columnGenStepArray.size(); i++)
{ {
if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value) if (columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.EMPTY.value
|| columnGenStepArray.getByte(i) == EDhApiWorldGenerationStep.DOWN_SAMPLED.value)
{ {
positionFullyGenerated = false; positionFullyGenerated = false;
break; break;
@@ -393,7 +411,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
} }
if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY) if (currentMinWorldGenStep == EDhApiWorldGenerationStep.EMPTY
|| currentMinWorldGenStep == EDhApiWorldGenerationStep.DOWN_SAMPLED)
{ {
// queue the task // queue the task
break checkWorldGenLoop; break checkWorldGenLoop;
@@ -402,7 +421,8 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
} }
} }
if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY) if (currentMinWorldGenStep != EDhApiWorldGenerationStep.EMPTY
&& currentMinWorldGenStep != EDhApiWorldGenerationStep.DOWN_SAMPLED)
{ {
// no world gen needed for this position // no world gen needed for this position
return; return;
@@ -455,6 +475,23 @@ public class GeneratedFullDataSourceProvider extends FullDataSourceProviderV2 im
}; };
} }
@Override
public CompletableFuture<Boolean> shouldGenerateSplitChild(long pos)
{
return GeneratedFullDataSourceProvider.this.getAsync(pos).thenApply(fullDataSource ->
{
//noinspection TryFinallyCanBeTryWithResources
try
{
return !GeneratedFullDataSourceProvider.this.isFullyGenerated(fullDataSource.columnGenerationSteps);
}
finally
{
fullDataSource.close();
}
});
}
} }
private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource) private CompletableFuture<Void> onDataSourceSaveAsync(FullDataSourceV2 fullDataSource)
{ {
@@ -99,7 +99,10 @@ public class RemoteFullDataSourceProvider extends GeneratedFullDataSourceProvide
Long timestamp = this.getTimestampForPos(pos); Long timestamp = this.getTimestampForPos(pos);
if (timestamp != null) if (timestamp != null)
{ {
this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, this::updateDataSource); this.syncOnLoadRequestQueue.submitRequest(pos, timestamp, fullDataSource ->
{
this.updateDataSourceAsync(fullDataSource).whenComplete((result, throwable) -> fullDataSource.close());
});
} }
return super.get(pos); return super.get(pos);
@@ -106,6 +106,19 @@ public class ClientOnlySaveStructure implements ISaveStructure
}); });
} }
@Override
public File getPre23SaveFolder(ILevelWrapper levelWrapper)
{
// Allow API users to override the save folder
IDhApiSaveStructure saveStructureOverride = OverrideInjector.INSTANCE.get(IDhApiSaveStructure.class);
if (saveStructureOverride != null)
{
return this.getSaveFolder(levelWrapper);
}
return getSaveFolderByLevelId(levelWrapper.getDimensionType().getName());
}
//================// //================//
@@ -34,5 +34,7 @@ public interface ISaveStructure extends AutoCloseable
*/ */
File getSaveFolder(ILevelWrapper levelWrapper); File getSaveFolder(ILevelWrapper levelWrapper);
File getPre23SaveFolder(ILevelWrapper levelWrapper);
} }
@@ -75,6 +75,8 @@ public class LocalSaveStructure implements ISaveStructure
}); });
} }
@Override
public File getPre23SaveFolder(ILevelWrapper levelWrapper) { return this.getSaveFolder(levelWrapper); }
//==================// //==================//
@@ -110,7 +110,7 @@ public class BatchGenerator implements IDhApiWorldGenerator
targetStep = EDhApiWorldGenerationStep.FEATURES; targetStep = EDhApiWorldGenerationStep.FEATURES;
break; break;
case INTERNAL_SERVER: case INTERNAL_SERVER:
targetStep = EDhApiWorldGenerationStep.LIGHT; // TODO using something other than LIGHT would be good for clarity targetStep = EDhApiWorldGenerationStep.LIGHT;
break; break;
} }
@@ -9,6 +9,7 @@ import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSource
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
@@ -167,17 +168,15 @@ public class PregenManager
this.generatedPercentage.update((double) this.nextSectionSpiralIndex.get() / this.sectionsToGenerate); this.generatedPercentage.update((double) this.nextSectionSpiralIndex.get() / this.sectionsToGenerate);
double chunksToGenerate = Math.ceil(Math.sqrt(this.sectionsToGenerate) / 2 * 4 * 10) / 10; // ceil to nearest 0.1 double chunksToGenerate = Math.ceil(Math.sqrt(this.sectionsToGenerate) / 2 * 4 * 10) / 10; // ceil to nearest 0.1
int chunkRatePerSecond = (int) (1000 / this.averageTaskCompletionIntervalMs.getAverage() * 4 * 4);
double etaMs = this.averageTaskCompletionIntervalMs.getAverage() * (this.sectionsToGenerate - this.nextSectionSpiralIndex.get()); double etaMs = this.averageTaskCompletionIntervalMs.getAverage() * (this.sectionsToGenerate - this.nextSectionSpiralIndex.get());
return MessageFormat.format("Generated radius: {0,number,#.###} / {1,number,#.#} chunks ({2,number,#.###%}), ETA: {3}", return MessageFormat.format("Generated radius: {0,number,#.###} / {1,number,#.#} chunks ({2} cps, {3,number,#.###%}), ETA: {4}",
this.generatedRadius.getValue(), this.generatedRadius.getValue(),
chunksToGenerate, chunksToGenerate,
chunkRatePerSecond,
this.generatedPercentage.getValue(), this.generatedPercentage.getValue(),
Duration.ofMillis((long) etaMs).toString() FormatUtil.formatEta(Duration.ofMillis((long) etaMs))
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.replaceAll("\\.\\d+", "")
.toLowerCase()
); );
} }
@@ -16,6 +16,7 @@ import org.apache.logging.log4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQueue implements IFullDataSourceRetrievalQueue, IDebugRenderable
@@ -47,7 +48,7 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
public void startAndSetTargetPos(DhBlockPos2D targetPos) { super.tick(targetPos); } public void startAndSetTargetPos(DhBlockPos2D targetPos) { super.tick(targetPos); }
@Override @Override
public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL + 12; } // TODO should be the same as what the server's update propgator can provide public byte lowestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL + 12; } // TODO should be the same as what the server's update propagator can provide
@Override @Override
public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; } public byte highestDataDetail() { return LodUtil.BLOCK_DETAIL_LEVEL; }
@@ -56,7 +57,11 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
{ {
long generationStartMsTime = System.currentTimeMillis(); long generationStartMsTime = System.currentTimeMillis();
return super.submitRequest(sectionPos, tracker.getDataSourceConsumer())
return super.submitRequest(sectionPos, fullDataSource -> {
Objects.requireNonNull(tracker.getDataSourceConsumer()).accept(fullDataSource);
fullDataSource.close();
})
.thenApply(requestResult -> .thenApply(requestResult ->
{ {
long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime; long totalGenTimeInMs = System.currentTimeMillis() - generationStartMsTime;
@@ -74,7 +79,14 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return WorldGenResult.CreateFail(); return WorldGenResult.CreateFail();
case REQUIRES_SPLITTING: case REQUIRES_SPLITTING:
List<CompletableFuture<WorldGenResult>> childFutures = new ArrayList<>(4); List<CompletableFuture<WorldGenResult>> childFutures = new ArrayList<>(4);
DhSectionPos.forEachChild(sectionPos, childPos -> childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker))); DhSectionPos.forEachChild(sectionPos, childPos -> {
tracker.shouldGenerateSplitChild(childPos).thenAccept(shouldGenerate -> {
if (shouldGenerate)
{
childFutures.add(this.submitRetrievalTask(childPos, requiredDataDetail, tracker));
}
});
});
return WorldGenResult.CreateSplit(childFutures); return WorldGenResult.CreateSplit(childFutures);
} }
@@ -101,8 +113,8 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
if (this.networkState.sessionConfig.getGenerationBoundsRadius() > 0) if (this.networkState.sessionConfig.getGenerationBoundsRadius() > 0)
{ {
if (DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, new DhBlockPos2D( if (DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, new DhBlockPos2D(
this.networkState.sessionConfig.getGenerationBoundsX(), (int) (this.networkState.sessionConfig.getGenerationBoundsX() / this.level.levelWrapper.getDimensionType().getCoordinateScale()),
this.networkState.sessionConfig.getGenerationBoundsZ() (int) (this.networkState.sessionConfig.getGenerationBoundsZ() / this.level.levelWrapper.getDimensionType().getCoordinateScale())
)) > this.networkState.sessionConfig.getGenerationBoundsRadius()) )) > this.networkState.sessionConfig.getGenerationBoundsRadius())
{ {
return false; return false;
@@ -111,6 +123,18 @@ public class RemoteWorldRetrievalQueue extends AbstractFullDataNetworkRequestQue
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxGenerationRequestDistance() * 16;
} }
@Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future)
{
if (DhSectionPos.getDetailLevel(sectionPos) > DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL
&& !Config.Server.Experimental.enableNSizedGeneration.get())
{
future.complete(ERequestResult.REQUIRES_SPLITTING);
return false;
}
return true;
}
@Override @Override
protected String getQueueName() { return "World Remote Generation Queue"; } protected String getQueueName() { return "World Remote Generation Queue"; }
@@ -26,6 +26,7 @@ import com.seibel.distanthorizons.api.objects.data.DhApiChunk;
import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource; import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker; import com.seibel.distanthorizons.core.generation.tasks.IWorldGenTaskTracker;
import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGroup; import com.seibel.distanthorizons.core.generation.tasks.InProgressWorldGenTaskGroup;
import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult; import com.seibel.distanthorizons.core.generation.tasks.WorldGenResult;
@@ -103,7 +104,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private int estimatedRemainingTaskCount = 0; private int estimatedRemainingTaskCount = 0;
private int estimatedRemainingChunkCount = 0; private int estimatedRemainingChunkCount = 0;
private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(1_000); private final RollingAverage rollingAverageChunkGenTimeInMs = new RollingAverage(Runtime.getRuntime().availableProcessors() * 500);
public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; } public RollingAverage getRollingAverageChunkGenTimeInMs() { return this.rollingAverageChunkGenTimeInMs; }
@@ -439,9 +440,8 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
ThreadPoolUtil.getWorldGenExecutor(), ThreadPoolUtil.getWorldGenExecutor(),
(DhApiChunk dataPoints) -> (DhApiChunk dataPoints) ->
{ {
try try(FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation()))
{ {
FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiValidation());
dataSourceConsumer.accept(dataSource); dataSourceConsumer.accept(dataSource);
} }
catch (DataCorruptedException | IllegalArgumentException e) catch (DataCorruptedException | IllegalArgumentException e)
@@ -464,6 +464,11 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
// set here so the API user doesn't have to pass in this value anywhere themselves // set here so the API user doesn't have to pass in this value anywhere themselves
pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation()); pooledDataSource.setRunApiChunkValidation(this.generator.runApiValidation());
// only apply to children if we aren't at the bottom of the tree
pooledDataSource.applyToChildren = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
pooledDataSource.applyToParent = DhSectionPos.getDetailLevel(pooledDataSource.getPos()) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
return this.generator.generateLod( return this.generator.generateLod(
chunkPosMin.getX(), chunkPosMin.getZ(), chunkPosMin.getX(), chunkPosMin.getZ(),
DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos), DhSectionPos.getX(requestPos), DhSectionPos.getZ(requestPos),
@@ -471,11 +476,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
pooledDataSource, pooledDataSource,
generatorMode, generatorMode,
ThreadPoolUtil.getWorldGenExecutor(), ThreadPoolUtil.getWorldGenExecutor(),
(IDhApiFullDataSource dataSource) -> (IDhApiFullDataSource apiDataSource) ->
{ {
try try
{ {
dataSourceConsumer.accept((FullDataSourceV2)dataSource); FullDataSourceV2 fullDataSource = (FullDataSourceV2) apiDataSource;
try
{
dataSourceConsumer.accept(fullDataSource);
}
finally
{
fullDataSource.close();
}
} }
catch (IllegalArgumentException e) catch (IllegalArgumentException e)
{ {
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.generation.tasks;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -33,4 +34,6 @@ public interface IWorldGenTaskTracker
@Nullable @Nullable
Consumer<FullDataSourceV2> getDataSourceConsumer(); Consumer<FullDataSourceV2> getDataSourceConsumer();
CompletableFuture<Boolean> shouldGenerateSplitChild(long pos);
} }
@@ -28,6 +28,7 @@ import com.seibel.distanthorizons.core.jar.installer.GitlabGetter;
import com.seibel.distanthorizons.core.jar.installer.ModrinthGetter; import com.seibel.distanthorizons.core.jar.installer.ModrinthGetter;
import com.seibel.distanthorizons.core.jar.installer.WebDownloader; import com.seibel.distanthorizons.core.jar.installer.WebDownloader;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants; import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
@@ -42,8 +43,12 @@ import java.net.URLEncoder;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.text.NumberFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipFile;
/** /**
* Used to update the mod automatically * Used to update the mod automatically
@@ -253,94 +258,177 @@ public class SelfUpdater
deleteOldJarOnJvmShutdown = true; deleteOldJarOnJvmShutdown = true;
LOGGER.info("Distant Horizons successfully updated. It will apply on game's relaunch"); String message = "Distant Horizons successfully updated. It will apply on game's relaunch";
LOGGER.info(message);
new Thread(() -> new Thread(() ->
{ {
String message = "Distant Horizons updated, this will be applied on game restart."; try
if (!GraphicsEnvironment.isHeadless())
{ {
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false); TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false);
} }
else catch (Exception ignore) { }
{
LOGGER.info(message);
}
}).start(); }).start();
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.warn("Failed to update Distant Horizons to version [" + ModrinthGetter.getLatestNameForVersion(minecraftVersion) + "], error: ["+e.getMessage()+"].", e); // delete the update file to prevent issues with a corrupt jar floating around
try
{
Files.deleteIfExists(file.toPath());
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update file at ["+file.toPath()+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
String message = "Failed to update Distant Horizons to version [" + ModrinthGetter.getLatestNameForVersion(minecraftVersion) + "], error: ["+e.getMessage()+"].";
LOGGER.error(message, e);
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "error", false);
}
catch (Exception ignore) { }
return false; return false;
} }
} }
public static boolean updateNightlyMod(String minecraftVersion, File file) public static boolean updateNightlyMod(String minecraftVersion, File file)
{ {
if (GitlabGetter.INSTANCE.projectPipelines.size() == 0) if (GitlabGetter.INSTANCE.projectPipelines.isEmpty())
{ {
LOGGER.warn("Failed to find any nightly builds for the minecraft version ["+minecraftVersion+"] update canceled."); LOGGER.warn("Failed to find any nightly builds for the minecraft version ["+minecraftVersion+"] update canceled.");
return false; return false;
} }
Path mergedZipPath = null;
try try
{ {
LOGGER.info("Attempting to auto update Distant Horizons."); LOGGER.info("Attempting to auto update Distant Horizons.");
Files.createDirectories(file.getParentFile().toPath()); Files.createDirectories(file.getParentFile().toPath());
File mergedZip = file.getParentFile().toPath().resolve("merged.zip").toFile(); mergedZipPath = file.getParentFile().toPath().resolve("merged.zip");
WebDownloader.downloadAsFile(GitlabGetter.INSTANCE.getDownloads(GitlabGetter.INSTANCE.projectPipelines.get(0).get("id")).get(minecraftVersion), mergedZipPath.toFile());
WebDownloader.downloadAsFile(GitlabGetter.INSTANCE.getDownloads(GitlabGetter.INSTANCE.projectPipelines.get(0).get("id")).get(minecraftVersion), mergedZip); try (ZipFile zipFile = new ZipFile(mergedZipPath.toFile()))
ZipInputStream zis = new ZipInputStream(new FileInputStream(mergedZip));
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null)
{ {
if (!zipEntry.isDirectory() && zipEntry.getName().contains("Merged")) // Look until the merged jar is found ZipEntry zipEntry =
Collections.list(zipFile.entries()).stream()
.max(Comparator.comparingInt(entry -> entry.getName().length()))
// shouldn't happen, but just in case
.orElseThrow(() -> new Exception("Unable to find jar in zip. Is the downloaded zip empty?"));
// expected values as defined by the zip
long expectedCheckSum = zipEntry.getCrc();
int expectedSize = (int)zipEntry.getSize();
// read in the file content
byte[] buffer = new byte[expectedSize];
CRC32 crcCheckSumGenerator = new CRC32();
InputStream inputStream = zipFile.getInputStream(zipEntry);
int byteReadIndex = 0;
try
{ {
// write file content NumberFormat outputFormat = NumberFormat.getNumberInstance();
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[1024];
int len; int nextByte = inputStream.read();
while ((len = zis.read(buffer)) > 0) { while (nextByte != -1)
fos.write(buffer, 0, len);
}
fos.close();
deleteOldJarOnJvmShutdown = true;
LOGGER.info("Distant Horizons successfully updated. It will apply on game's relaunch");
new Thread(() ->
{ {
String message = "Distant Horizons updated, this will be applied on game restart."; buffer[byteReadIndex] = (byte) nextByte;
if (!GraphicsEnvironment.isHeadless()) crcCheckSumGenerator.update(nextByte);
{ nextByte = inputStream.read();
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false); byteReadIndex++;
}
else
{
LOGGER.info(message);
}
}).start();
zis.close(); // TODO it would be better to change this divisor based on the expected size,
Files.deleteIfExists(newFileLocation.getParentFile().toPath().resolve("merged.zip")); // so it would always be split up into 100 1% increments
// but this will work for now when the expected size is about 17 MB, this will log about 170 times
if (byteReadIndex % 100_000 == 0)
{
LOGGER.info("Decompressing ["+outputFormat.format(((double)byteReadIndex / expectedSize)*100.0)+"]%");
}
}
}
catch (EOFException ignore) { /* shouldn't happen, but just in case */ }
return true; // confirm we read the whole file
if (byteReadIndex != expectedSize) // +1 on the index isn't necessary since the readIndex will always end +1 from where it started
{
LOGGER.warn("Distant Horizons update decompression failed, aborting install");
throw new Exception("Decompression failed");
} }
zipEntry = zis.getNextEntry(); // confirm the checksum is correct (IE we decompressed correctly)
} long actualChecksum = crcCheckSumGenerator.getValue();
zis.close(); if (actualChecksum != expectedCheckSum)
{
LOGGER.warn("Distant Horizons checksum mismatch, aborting install");
throw new Exception("Checksum Mismatch");
}
return false; Files.write(file.toPath(), buffer);
}
Files.deleteIfExists(mergedZipPath);
deleteOldJarOnJvmShutdown = true;
String message = "Distant Horizons updated, this will be applied on game restart.";
LOGGER.info(message);
new Thread(() ->
{
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "info", false);
}
catch (Exception ignore) { }
}).start();
return true;
} }
catch (Exception e) catch (Exception e)
{ {
LOGGER.warn("Failed to update [" + ModInfo.READABLE_NAME + "] to version [" + GitlabGetter.INSTANCE.projectPipelines.get(0).get("sha") + "].", e); // delete the update jar to prevent issues with a corrupt jar floating around
try
{
Files.deleteIfExists(file.toPath());
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update jar file at ["+file.toPath()+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
// delete the update zip so we can clean up
try
{
if (mergedZipPath != null)
{
Files.deleteIfExists(mergedZipPath);
}
}
catch (Exception deleteCorruptFileException)
{
LOGGER.error("Unable to delete corrupted update zip file at ["+mergedZipPath+"], error: ["+deleteCorruptFileException.getMessage()+"].", deleteCorruptFileException);
}
String message = "Failed to update [" + ModInfo.READABLE_NAME + "] to version [" + GitlabGetter.INSTANCE.projectPipelines.get(0).get("sha") + "], error: ["+e.getMessage()+"].";
LOGGER.error(message, e);
try
{
TinyFileDialogs.tinyfd_messageBox(ModInfo.READABLE_NAME, message, "ok", "error", false);
}
catch (Exception ignore) { }
return false; return false;
} }
} }
@@ -21,12 +21,12 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiChunkModifiedEvent;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.beacon.BeaconBeamDataHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache; import com.seibel.distanthorizons.core.file.fullDatafile.DelayedFullDataSourceSaveCache;
import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.renderer.generic.CloudRenderHandler; import com.seibel.distanthorizons.core.render.renderer.generic.CloudRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
@@ -34,6 +34,7 @@ import com.seibel.distanthorizons.core.sql.dto.ChunkHashDTO;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo; import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo; import com.seibel.distanthorizons.core.sql.repo.ChunkHashRepo;
import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
@@ -43,10 +44,12 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
public abstract class AbstractDhLevel implements IDhLevel public abstract class AbstractDhLevel implements IDhLevel
{ {
@@ -59,6 +62,8 @@ public abstract class AbstractDhLevel implements IDhLevel
@Nullable @Nullable
public BeaconBeamRepo beaconBeamRepo; public BeaconBeamRepo beaconBeamRepo;
protected final KeyedLockContainer<Long> beaconUpdateLockContainer = new KeyedLockContainer<>();
protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSaveAsync, 3_000); protected final DelayedFullDataSourceSaveCache delayedFullDataSourceSaveCache = new DelayedFullDataSourceSaveCache(this::onDataSourceSaveAsync, 3_000);
/** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save */ /** contains the {@link DhChunkPos} for each {@link DhSectionPos} that are queued to save */
protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>(); protected final ConcurrentHashMap<Long, HashSet<DhChunkPos>> updatedChunkPosSetBySectionPos = new ConcurrentHashMap<>();
@@ -67,7 +72,6 @@ public abstract class AbstractDhLevel implements IDhLevel
/** Will be null if clouds shouldn't be rendered for this level. */ /** Will be null if clouds shouldn't be rendered for this level. */
@Nullable @Nullable
protected CloudRenderHandler cloudRenderHandler; protected CloudRenderHandler cloudRenderHandler;
protected BeaconBeamDataHandler beaconBeamDataHandler;
@@ -126,13 +130,6 @@ public abstract class AbstractDhLevel implements IDhLevel
} }
} }
} }
// shouldn't happen, but just in case
if (this.beaconBeamRepo != null)
{
this.beaconBeamDataHandler = new BeaconBeamDataHandler(this.beaconBeamRepo, genericRenderer);
}
} }
@@ -228,31 +225,139 @@ public abstract class AbstractDhLevel implements IDhLevel
//=================// //=================//
@Override @Override
public void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList) public void updateBeaconBeamsForSectionPos(long sectionPos, List<BeaconBeamDTO> activeBeamList)
{ {
if (this.beaconBeamDataHandler != null) int minBlockX = DhSectionPos.getMinCornerBlockX(sectionPos);
int minBlockZ = DhSectionPos.getMinCornerBlockZ(sectionPos);
// TODO special logic had to be done for DhChunkPos.getMaxBlock,
// does that need to be done here?
// The DhChunkPos issue caused beacons to appear/disappear incorrectly on negative chunk borders
int maxBlockX = minBlockX + DhSectionPos.getBlockWidth(sectionPos);
int maxBlockZ = minBlockZ + DhSectionPos.getBlockWidth(sectionPos);
this.updateBeaconBeamsBetweenBlockPos(
sectionPos,
minBlockX, maxBlockX,
minBlockZ, maxBlockZ,
activeBeamList
);
}
@Override
public void updateBeaconBeamsForChunkPos(DhChunkPos chunkPos, List<BeaconBeamDTO> activeBeamList)
{
long sectionPos = DhSectionPos.encodeContaining(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, chunkPos);
int minBlockX = chunkPos.getMinBlockX();
int minBlockZ = chunkPos.getMinBlockZ();
int maxBlockX = chunkPos.getMaxBlockX();
int maxBlockZ = chunkPos.getMaxBlockZ();
//LOGGER.info("beacons ["+activeBeamList.size()+"] at ["+chunkPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
this.updateBeaconBeamsBetweenBlockPos(
sectionPos,
minBlockX, maxBlockX,
minBlockZ, maxBlockZ,
activeBeamList
);
}
private void updateBeaconBeamsBetweenBlockPos(
long sectionPosForLock,
int minBlockX, int maxBlockX,
int minBlockZ, int maxBlockZ,
List<BeaconBeamDTO> activeBeamList
) // TODO min/max block pos instead
{
if (this.beaconBeamRepo == null)
{ {
List<BeaconBeamDTO> activeBeamList = chunkToUpdate.getAllActiveBeacons(nearbyChunkList); return;
this.beaconBeamDataHandler.setBeaconBeamsForChunk(chunkToUpdate.getChunkPos(), activeBeamList); }
// locked to prevent two threads from updating the same section at the same time
ReentrantLock lock = this.beaconUpdateLockContainer.getLockForPos(sectionPosForLock);
try
{
lock.lock();
HashSet<DhBlockPos> allPosSet = new HashSet<>();
// sort new beams
HashMap<DhBlockPos, BeaconBeamDTO> activeBeamByPos = new HashMap<>(activeBeamList.size());
for (BeaconBeamDTO beam : activeBeamList)
{
activeBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
// get existing beams
List<BeaconBeamDTO> existingBeamList = this.beaconBeamRepo.getAllBeamsInBlockPosRange(
minBlockX, maxBlockX,
minBlockZ, maxBlockZ);
HashMap<DhBlockPos, BeaconBeamDTO> existingBeamByPos = new HashMap<>(existingBeamList.size());
for (BeaconBeamDTO beam : existingBeamList)
{
existingBeamByPos.put(beam.blockPos, beam);
allPosSet.add(beam.blockPos);
}
for (DhBlockPos beaconPos : allPosSet)
{
if (minBlockX <= beaconPos.getX() && beaconPos.getX() <= maxBlockX
&& minBlockZ <= beaconPos.getZ() && beaconPos.getZ() <= maxBlockZ)
{
//// don't modify beacons outside the updated range
//continue;
}
else
{
continue;
}
BeaconBeamDTO existingBeam = existingBeamByPos.get(beaconPos);
BeaconBeamDTO activeBeam = activeBeamByPos.get(beaconPos);
if (activeBeam != null)
{
//LOGGER.info("add beacon ["+activeBeam.blockPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
if (existingBeam == null)
{
// new beam found, add to DB
this.beaconBeamRepo.save(activeBeam);
}
else
{
// beam still exists in chunk
if (!existingBeam.color.equals(activeBeam.color))
{
// beam colors were changed
this.beaconBeamRepo.save(activeBeam);
}
}
}
else if (existingBeam != null)
{
// beam no longer exists at position, remove from DB
this.beaconBeamRepo.deleteWithKey(beaconPos);
//LOGGER.info("remove beacon ["+beaconPos+"] x["+minBlockX+"]-["+maxBlockX+"] z["+minBlockZ+"]-["+maxBlockZ+"].");
}
}
}
finally
{
lock.unlock();
} }
} }
@Override @Override
public void loadBeaconBeamsInPos(long pos) @Nullable
{ public BeaconBeamRepo getBeaconBeamRepo() { return this.beaconBeamRepo; }
if (this.beaconBeamDataHandler != null)
{
this.beaconBeamDataHandler.loadBeaconBeamsInPos(pos);
}
}
@Override
public void unloadBeaconBeamsInPos(long pos)
{
if (this.beaconBeamDataHandler != null)
{
this.beaconBeamDataHandler.unloadBeaconBeamsInPos(pos);
}
}
@@ -23,7 +23,6 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
@@ -31,7 +30,6 @@ import org.apache.logging.log4j.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.*; import java.util.concurrent.*;
public abstract class AbstractDhServerLevel extends AbstractDhLevel implements IDhServerLevel public abstract class AbstractDhServerLevel extends AbstractDhLevel implements IDhServerLevel
@@ -153,8 +151,10 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
if (Config.Server.generationBoundsRadius.get() > 0) if (Config.Server.generationBoundsRadius.get() > 0)
{ {
double coordinateScale = this.serverLevelWrapper.getDimensionType().getCoordinateScale();
if (DhSectionPos.getChebyshevSignedBlockDistance(message.sectionPos, new DhBlockPos2D( if (DhSectionPos.getChebyshevSignedBlockDistance(message.sectionPos, new DhBlockPos2D(
Config.Server.generationBoundsX.get(), Config.Server.generationBoundsZ.get() (int) (Config.Server.generationBoundsX.get() / coordinateScale),
(int) (Config.Server.generationBoundsZ.get() / coordinateScale)
)) > Config.Server.generationBoundsRadius.get()) )) > Config.Server.generationBoundsRadius.get())
{ {
message.sendResponse(new RequestOutOfRangeException("Section out of allowed bounds")); message.sendResponse(new RequestOutOfRangeException("Section out of allowed bounds"));
@@ -162,7 +162,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
} }
} }
if (Config.Server.generateOnlyInHighestDetail.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL) if (!Config.Server.Experimental.enableNSizedGeneration.get() && DhSectionPos.getDetailLevel(message.sectionPos) != DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
{ {
message.sendResponse(new SectionRequiresSplittingException("Only highest-detail sections are allowed")); message.sendResponse(new SectionRequiresSplittingException("Only highest-detail sections are allowed"));
return; return;
@@ -278,8 +278,7 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition(); Vec3d playerPosition = serverPlayerState.getServerPlayer().getPosition();
int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16; int distanceFromPlayer = DhSectionPos.getChebyshevSignedBlockDistance(data.getPos(), new DhBlockPos2D((int) playerPosition.x, (int) playerPosition.z)) / 16;
if (distanceFromPlayer >= serverPlayerState.getServerPlayer().getViewDistance() if (distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius())
&& distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius())
{ {
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () -> serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () ->
{ {
@@ -19,12 +19,12 @@
package com.seibel.distanthorizons.core.level; package com.seibel.distanthorizons.core.level;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.config.AppliedConfigState; import com.seibel.distanthorizons.core.config.AppliedConfigState;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.beacon.BeaconBeamDataHandler;
import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure; import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
@@ -34,6 +34,7 @@ import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource; import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
@@ -52,8 +53,11 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.CheckForNull; import javax.annotation.CheckForNull;
import java.awt.*; import java.awt.*;
import java.io.File; import java.io.File;
import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/** The level used when connected to a server */ /** The level used when connected to a server */
public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@@ -71,6 +75,13 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
@Nullable @Nullable
private final ScopedNetworkEventSource networkEventSource; private final ScopedNetworkEventSource networkEventSource;
private final Set<DhChunkPos> loadedOnceChunks = Collections.newSetFromMap(
CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.<DhChunkPos, Boolean>build()
.asMap()
);
public final WorldGenModule worldGenModule; public final WorldGenModule worldGenModule;
public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig; public final AppliedConfigState<Boolean> worldGeneratorEnabledConfig;
@@ -87,10 +98,21 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{ this(saveStructure, clientLevelWrapper, null, true, networkState); } { this(saveStructure, clientLevelWrapper, null, true, networkState); }
public DhClientLevel(ISaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState) public DhClientLevel(ISaveStructure saveStructure, IClientLevelWrapper clientLevelWrapper, @Nullable File fullDataSaveDirOverride, boolean enableRendering, @Nullable ClientNetworkState networkState)
{ {
if (saveStructure.getSaveFolder(clientLevelWrapper).mkdirs()) File saveFolder = saveStructure.getSaveFolder(clientLevelWrapper);
File pre23Folder = saveStructure.getPre23SaveFolder(clientLevelWrapper);
if (pre23Folder.exists())
{
if (!pre23Folder.renameTo(saveFolder))
{
throw new RuntimeException("Could not move old save data folder: " + pre23Folder.getAbsolutePath() + " to " + saveFolder.getAbsolutePath());
}
}
else if (saveStructure.getSaveFolder(clientLevelWrapper).mkdirs())
{ {
LOGGER.warn("unable to create data folder."); LOGGER.warn("unable to create data folder.");
} }
this.levelWrapper = clientLevelWrapper; this.levelWrapper = clientLevelWrapper;
this.levelWrapper.setParentLevel(this); this.levelWrapper.setParentLevel(this);
this.saveStructure = saveStructure; this.saveStructure = saveStructure;
@@ -142,11 +164,10 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
return; return;
} }
this.beaconBeamDataHandler.setBeaconBeamsForPos(dataSourceDto.pos, message.payload.beaconBeams); this.updateBeaconBeamsForSectionPos(dataSourceDto.pos, message.payload.beaconBeams);
try (FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper))
{ FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper);
this.updateDataSourcesAsync(fullDataSource); this.updateDataSourcesAsync(fullDataSource).whenComplete((result, e) -> fullDataSource.close());
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -284,7 +305,16 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get(); ClientLevelModule.ClientRenderState renderState = this.clientside.ClientRenderStateRef.get();
return (renderState != null) ? renderState.renderBufferHandler : null; return (renderState != null) ? renderState.renderBufferHandler : null;
} }
public BeaconBeamDataHandler getBeaconBeamDataHandler() { return this.beaconBeamDataHandler; }
public boolean shouldProcessChunkUpdate(DhChunkPos chunkPos)
{
if (this.networkState == null || !this.networkState.isReady())
{
return true;
}
return !this.networkState.sessionConfig.isRealTimeUpdatesEnabled() || this.loadedOnceChunks.add(chunkPos);
}
@@ -26,6 +26,8 @@ import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderBufferHandler; import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer; import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -50,9 +52,16 @@ public interface IDhLevel extends AutoCloseable, GeneratedFullDataSourceProvider
int getChunkHash(DhChunkPos pos); int getChunkHash(DhChunkPos pos);
void updateChunkAsync(IChunkWrapper chunk, int newChunkHash); void updateChunkAsync(IChunkWrapper chunk, int newChunkHash);
void loadBeaconBeamsInPos(long pos); default void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList)
void updateBeaconBeamsForChunk(IChunkWrapper chunkToUpdate, ArrayList<IChunkWrapper> nearbyChunkList); {
void unloadBeaconBeamsInPos(long pos); List<BeaconBeamDTO> activeBeamList = chunkToUpdate.getAllActiveBeacons(nearbyChunkList);
this.updateBeaconBeamsForChunkPos(chunkToUpdate.getChunkPos(), activeBeamList);
}
void updateBeaconBeamsForChunkPos(DhChunkPos chunkPos, List<BeaconBeamDTO> activeBeamList);
void updateBeaconBeamsForSectionPos(long sectionPos, List<BeaconBeamDTO> activeBeamList);
@Nullable
BeaconBeamRepo getBeaconBeamRepo();
FullDataSourceProviderV2 getFullDataProvider(); FullDataSourceProviderV2 getFullDataProvider();
@@ -27,6 +27,7 @@ import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
@@ -35,6 +36,7 @@ import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.Closeable; import java.io.Closeable;
import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
@@ -220,6 +222,9 @@ public class WorldGenModule implements Closeable
/** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */ /** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */
public static abstract class AbstractWorldGenState public static abstract class AbstractWorldGenState
{ {
/** static so we only send the disable message once per session */
private static long firstProgressMessageSentMs = 0;
public IFullDataSourceRetrievalQueue worldGenerationQueue; public IFullDataSourceRetrievalQueue worldGenerationQueue;
private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater"); private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater");
@@ -280,23 +285,40 @@ public class WorldGenModule implements Closeable
} }
private void sendRetrievalProgress() private void sendRetrievalProgress()
{ {
// format the remaining chunks
int remainingChunkCount = this.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount(); int remainingChunkCount = this.worldGenerationQueue.getRetrievalEstimatedRemainingChunkCount();
remainingChunkCount += this.worldGenerationQueue.getQueuedChunkCount(); remainingChunkCount += this.worldGenerationQueue.getQueuedChunkCount();
String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount); String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount);
String message = "DH Gen/Import: " + remainingChunkCountStr + " chunks"; String message = "DH Gen/Import: " + remainingChunkCountStr + " chunks left.";
// show a message about how to disable progress logging if requested
int msToShowDisableInstructions = Config.Common.WorldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.get() * 1_000;
if (msToShowDisableInstructions > 0)
{
long timeSinceFirstMessageInMs = (System.currentTimeMillis() - firstProgressMessageSentMs);
// always show this message for the first tick
if (firstProgressMessageSentMs == 0
// show this message if there is still time
|| timeSinceFirstMessageInMs < msToShowDisableInstructions)
{
// append to the current message
message += " This message can be hidden in the DH config.";
}
}
// add the remaining time estimate if available
double chunksPerSec = this.getEstimatedChunksPerSecond(); double chunksPerSec = this.getEstimatedChunksPerSecond();
if (chunksPerSec > 0) if (chunksPerSec > 0)
{ {
long estimatedRemainingTime = (long) (remainingChunkCount / chunksPerSec); long estimatedRemainingTime = (long) (remainingChunkCount / chunksPerSec);
message += " Estimated Time: " + formatSeconds(estimatedRemainingTime);//+ " at " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec"; message += " ETA: " + FormatUtil.formatEta(Duration.ofSeconds(estimatedRemainingTime));//+ " at " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec";
} }
// only log if there are chunks needing to be generated
if (remainingChunkCount != 0) if (remainingChunkCount != 0)
{ {
// determine where to log
EDhApiDistantGeneratorProgressDisplayLocation displayLocation = Config.Common.WorldGenerator.showGenerationProgress.get(); EDhApiDistantGeneratorProgressDisplayLocation displayLocation = Config.Common.WorldGenerator.showGenerationProgress.get();
if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY) if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY)
{ {
@@ -311,32 +333,13 @@ public class WorldGenModule implements Closeable
LOGGER.info(message); LOGGER.info(message);
} }
}
}
private static String formatSeconds(long totalSeconds)
{
long days = totalSeconds / (24 * 3600); // 24 hours in a day
long hours = (totalSeconds % (24 * 3600)) / 3600; // Hours
long minutes = (totalSeconds % 3600) / 60; // Minutes
long seconds = totalSeconds % 60; // Seconds
// mark when the first message was sent
String timeString = ""; if (firstProgressMessageSentMs == 0)
if (days > 0) {
{ firstProgressMessageSentMs = System.currentTimeMillis();
timeString += days+" "; }
} }
if (hours > 0)
{
timeString += hours+":";
}
if (minutes > 0)
{
timeString += String.format("%02d", minutes)+":";
}
timeString += String.format("%02d", seconds);
return timeString;
} }
/** @return -1 if this method isn't supported or available */ /** @return -1 if this method isn't supported or available */
@@ -1,6 +1,7 @@
package com.seibel.distanthorizons.core.multiplayer.client; package com.seibel.distanthorizons.core.multiplayer.client;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
@@ -71,6 +72,16 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit); private final SupplierBasedRateLimiter<Void> rateLimiter = new SupplierBasedRateLimiter<>(this::getRequestRateLimit);
private final Set<Long> succeededPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
private final Set<Long> requiresSplittingPositions = Collections.newSetFromMap(CacheBuilder.newBuilder()
.expireAfterWrite(20, TimeUnit.MINUTES)
.<Long, Boolean>build()
.asMap());
//=============// //=============//
@@ -96,6 +107,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
protected abstract int getRequestRateLimit(); protected abstract int getRequestRateLimit();
protected abstract boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos); protected abstract boolean isSectionAllowedToGenerate(long sectionPos, DhBlockPos2D targetPos);
protected abstract boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future);
protected abstract String getQueueName(); protected abstract String getQueueName();
@@ -105,12 +117,22 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
// request submitting // // request submitting //
//====================// //====================//
public CompletableFuture<RequestResult> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer) public CompletableFuture<ERequestResult> submitRequest(long sectionPos, Consumer<FullDataSourceV2> dataSourceConsumer)
{ return this.submitRequest(sectionPos, null, dataSourceConsumer); } { return this.submitRequest(sectionPos, null, dataSourceConsumer); }
public CompletableFuture<RequestResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer) public CompletableFuture<ERequestResult> submitRequest(long sectionPos, @Nullable Long clientTimestamp, Consumer<FullDataSourceV2> dataSourceConsumer)
{ {
if (this.succeededPositions.contains(sectionPos))
{
return CompletableFuture.completedFuture(ERequestResult.FAILED);
}
if (this.requiresSplittingPositions.contains(sectionPos))
{
return CompletableFuture.completedFuture(ERequestResult.REQUIRES_SPLITTING);
}
AtomicBoolean added = new AtomicBoolean(false); AtomicBoolean added = new AtomicBoolean(false);
RequestQueueEntry entry = this.waitingTasksBySectionPos.compute(sectionPos, (k, existingQueueEntry) -> RequestQueueEntry entry = this.waitingTasksBySectionPos.compute(sectionPos, (pos, existingQueueEntry) ->
{ {
if (existingQueueEntry != null) if (existingQueueEntry != null)
{ {
@@ -126,8 +148,10 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
{ {
case SUCCEEDED: case SUCCEEDED:
this.finishedRequests.incrementAndGet(); this.finishedRequests.incrementAndGet();
this.succeededPositions.add(pos);
return; return;
case REQUIRES_SPLITTING: case REQUIRES_SPLITTING:
this.requiresSplittingPositions.add(sectionPos);
return; return;
case FAILED: case FAILED:
this.failedRequests.incrementAndGet(); this.failedRequests.incrementAndGet();
@@ -147,7 +171,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
if (!added.get()) if (!added.get())
{ {
return CompletableFuture.completedFuture(RequestResult.FAILED); return CompletableFuture.completedFuture(ERequestResult.FAILED);
} }
return entry.future; return entry.future;
@@ -204,6 +228,12 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
return; return;
} }
if (!this.onBeforeRequest(sectionPos, entry.future))
{
this.pendingTasksSemaphore.release();
return;
}
Long offsetEntryTimestamp = entry.updateTimestamp != null Long offsetEntryTimestamp = entry.updateTimestamp != null
? entry.updateTimestamp + this.networkState.getServerTimeOffset() ? entry.updateTimestamp + this.networkState.getServerTimeOffset()
: null; : null;
@@ -228,6 +258,11 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
{ {
FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(response.payload); FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(response.payload);
// set application flags based on the received detail level,
// this is needed so the data sources propagate correctly
dataSourceDto.applyToChildren = DhSectionPos.getDetailLevel(dataSourceDto.pos) > DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
dataSourceDto.applyToParent = DhSectionPos.getDetailLevel(dataSourceDto.pos) < DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL + 12;
AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor(); AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor();
if (executor == null) if (executor == null)
{ {
@@ -240,11 +275,10 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
{ {
try try
{ {
this.level.getBeaconBeamDataHandler().setBeaconBeamsForPos(dataSourceDto.pos, response.payload.beaconBeams); this.level.updateBeaconBeamsForSectionPos(dataSourceDto.pos, response.payload.beaconBeams);
try (FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper()))
{ FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.level.getLevelWrapper());
entry.dataSourceConsumer.accept(fullDataSource); entry.dataSourceConsumer.accept(fullDataSource);
}
} }
catch (Exception e) catch (Exception e)
{ {
@@ -263,7 +297,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
} }
catch (SectionRequiresSplittingException ignored) catch (SectionRequiresSplittingException ignored)
{ {
return entry.future.complete(RequestResult.REQUIRES_SPLITTING); return entry.future.complete(ERequestResult.REQUIRES_SPLITTING);
} }
catch (SessionClosedException | CancellationException ignored) catch (SessionClosedException | CancellationException ignored)
{ {
@@ -272,11 +306,11 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
catch (RequestRejectedException e) catch (RequestRejectedException e)
{ {
LOGGER.info("Request rejected by the server: " + e.getMessage()); LOGGER.info("Request rejected by the server: " + e.getMessage());
return entry.future.complete(RequestResult.FAILED); return entry.future.complete(ERequestResult.FAILED);
} }
catch (RateLimitedException e) catch (RateLimitedException e)
{ {
LOGGER.warn("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage()); LOGGER.info("Rate limited by server, re-queueing task [" + DhSectionPos.toString(sectionPos) + "]: " + e.getMessage());
// Skip all requests for 1 second // Skip all requests for 1 second
this.rateLimiter.acquireAll(); this.rateLimiter.acquireAll();
@@ -304,11 +338,11 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
} }
else else
{ {
return entry.future.complete(RequestResult.FAILED); return entry.future.complete(ERequestResult.FAILED);
} }
} }
return entry.future.complete(RequestResult.SUCCEEDED); return entry.future.complete(ERequestResult.SUCCEEDED);
}); });
} }
@@ -421,7 +455,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
protected static class RequestQueueEntry protected static class RequestQueueEntry
{ {
/** encapsulates the entire request, including client side queuing and the actual server request */ /** encapsulates the entire request, including client side queuing and the actual server request */
public final CompletableFuture<RequestResult> future = new CompletableFuture<>(); public final CompletableFuture<ERequestResult> future = new CompletableFuture<>();
public final Consumer<FullDataSourceV2> dataSourceConsumer; public final Consumer<FullDataSourceV2> dataSourceConsumer;
/** will be null if we want to retrieve the LOD regardless of when it was last updated */ /** will be null if we want to retrieve the LOD regardless of when it was last updated */
@Nullable @Nullable
@@ -451,7 +485,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
} }
public enum RequestResult public enum ERequestResult
{ {
SUCCEEDED, SUCCEEDED,
REQUIRES_SPLITTING, REQUIRES_SPLITTING,
@@ -6,6 +6,8 @@ import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import java.util.concurrent.CompletableFuture;
/** /**
* This queue only handles LOD updates for * This queue only handles LOD updates for
* LODs that were changed when the player wasn't online * LODs that were changed when the player wasn't online
@@ -37,6 +39,8 @@ public class SyncOnLoadRequestQueue extends AbstractFullDataNetworkRequestQueue
{ {
return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16; return DhSectionPos.getChebyshevSignedBlockDistance(sectionPos, targetPos) <= this.networkState.sessionConfig.getMaxSyncOnLoadDistance() * 16;
} }
@Override
protected boolean onBeforeRequest(long sectionPos, CompletableFuture<ERequestResult> future) { return true; }
@Override @Override
protected String getQueueName() { return "Sync On Login Queue"; } protected String getQueueName() { return "Sync On Login Queue"; }
@@ -64,7 +64,7 @@ public class FullDataSourceRequestHandler
// the server timestamp will be null if no LOD data exists for this position // the server timestamp will be null if no LOD data exists for this position
Long serverTimestamp = this.fullDataSourceProvider().getTimestampForPos(message.sectionPos); Long serverTimestamp = this.fullDataSourceProvider().getTimestampForPos(message.sectionPos);
if (serverTimestamp == null if (serverTimestamp == null
|| serverTimestamp <= clientTimestamp) || serverTimestamp <= clientTimestamp)
{ {
// either no data exists to sync, or the client is already up to date // either no data exists to sync, or the client is already up to date
rateLimiterSet.syncOnLoginRateLimiter.release(); rateLimiterSet.syncOnLoginRateLimiter.release();
@@ -93,6 +93,10 @@ public class FullDataSourceRequestHandler
rateLimiterSet.syncOnLoginRateLimiter.release(); rateLimiterSet.syncOnLoginRateLimiter.release();
}); });
} }
catch (Exception e)
{
LOGGER.error("Unexpected issue getting request for pos ["+DhSectionPos.toString(message.sectionPos)+"], error: ["+e.getMessage()+"].", e);
}
}, executor); }, executor);
} }
@@ -5,10 +5,12 @@ import com.seibel.distanthorizons.coreapi.util.StringUtil;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.shorts.ShortArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* This keeps track of all the poolable * This keeps track of all the poolable
@@ -20,7 +22,16 @@ import java.util.ArrayList;
public class PhantomArrayListCheckout implements AutoCloseable public class PhantomArrayListCheckout implements AutoCloseable
{ {
/** defines which pool the arrays should be returned too */ /** defines which pool the arrays should be returned too */
@NotNull
private final PhantomArrayListPool owningPool; private final PhantomArrayListPool owningPool;
/**
* soft reference used by the {@link PhantomArrayListPool} so this checkout can be
* freed if there isn't enough memory.
*/
@NotNull
public final SoftReference<PhantomArrayListCheckout> ownerSoftReference;
/** Will be null if the parent pool doesn't want leak stack tracing */ /** Will be null if the parent pool doesn't want leak stack tracing */
@Nullable @Nullable
public final String allocationStackTrace; public final String allocationStackTrace;
@@ -28,7 +39,6 @@ public class PhantomArrayListCheckout implements AutoCloseable
private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>(); private final ArrayList<ByteArrayList> byteArrayLists = new ArrayList<>();
private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>(); private final ArrayList<ShortArrayList> shortArrayLists = new ArrayList<>();
private final ArrayList<LongArrayList> longArrayLists = new ArrayList<>(); private final ArrayList<LongArrayList> longArrayLists = new ArrayList<>();
private final ArrayList<SoftReference<LongArrayList>> longArrayRefLists = new ArrayList<>();
@@ -36,7 +46,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
// constructor // // constructor //
//=============// //=============//
public PhantomArrayListCheckout(PhantomArrayListPool owningPool) public PhantomArrayListCheckout(@NotNull PhantomArrayListPool owningPool)
{ {
if (owningPool.logGarbageCollectedStacks) if (owningPool.logGarbageCollectedStacks)
{ {
@@ -50,6 +60,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
} }
this.owningPool = owningPool; this.owningPool = owningPool;
this.ownerSoftReference = new SoftReference<>(this);
} }
@@ -60,11 +71,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
public void addByteArrayList(ByteArrayList list) { this.byteArrayLists.add(list); } public void addByteArrayList(ByteArrayList list) { this.byteArrayLists.add(list); }
public void addShortArrayList(ShortArrayList list) { this.shortArrayLists.add(list); } public void addShortArrayList(ShortArrayList list) { this.shortArrayLists.add(list); }
public void addLongArrayListRef(LongArrayList list, SoftReference<LongArrayList> listRef) public void addLongArrayListRef(LongArrayList list) { this.longArrayLists.add(list); }
{
this.longArrayLists.add(list);
this.longArrayRefLists.add(listRef);
}
@@ -100,7 +107,6 @@ public class PhantomArrayListCheckout implements AutoCloseable
public ArrayList<ByteArrayList> getAllByteArrays() { return this.byteArrayLists; } public ArrayList<ByteArrayList> getAllByteArrays() { return this.byteArrayLists; }
public ArrayList<ShortArrayList> getAllShortArrays() { return this.shortArrayLists; } public ArrayList<ShortArrayList> getAllShortArrays() { return this.shortArrayLists; }
public ArrayList<LongArrayList> getAllLongArrays() { return this.longArrayLists; } public ArrayList<LongArrayList> getAllLongArrays() { return this.longArrayLists; }
public ArrayList<SoftReference<LongArrayList>> getAllLongArrayRefs() { return this.longArrayRefLists; }
@@ -109,10 +115,7 @@ public class PhantomArrayListCheckout implements AutoCloseable
//================// //================//
@Override @Override
public void close() public void close() { this.owningPool.returnCheckout(this); }
{
this.owningPool.returnCheckout(this);
}
@@ -28,7 +28,7 @@ public abstract class PhantomArrayListParent implements AutoCloseable
* It's recommended to set this as null after the child's constructor * It's recommended to set this as null after the child's constructor
* finishes to show the pooled arrays have all been accessed * finishes to show the pooled arrays have all been accessed
*/ */
protected PhantomArrayListCheckout pooledArraysCheckout; protected final PhantomArrayListCheckout pooledArraysCheckout;
@@ -57,19 +57,7 @@ public abstract class PhantomArrayListParent implements AutoCloseable
//================// //================//
@Override @Override
public void close() //throws Exception public void close() { this.phantomArrayListPool.returnParentPhantomRef(this.phantomReference); }
{
try
{
this.phantomReference.clear();
PhantomArrayListCheckout checkout = this.phantomArrayListPool.phantomRefToCheckout.remove(this.phantomReference);
this.phantomArrayListPool.returnCheckout(checkout);
}
catch (Exception e)
{
LOGGER.error("Unable to close Phantom Array", e);
}
}
} }
@@ -4,7 +4,6 @@ import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
@@ -14,7 +13,9 @@ import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.shorts.ShortArrayList; import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference; import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue; import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference; import java.lang.ref.SoftReference;
@@ -25,8 +26,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
/** /**
* DH uses a lot of potentially large arrays of {@link Byte}s and {@link Long}s. * DH uses a lot of potentially large arrays of {@link Byte}s and {@link Long}s.
@@ -46,6 +45,12 @@ import java.util.function.Supplier;
* This is less efficient since it may allow a lot of additional arrays to * This is less efficient since it may allow a lot of additional arrays to
* be created while we wait for the garbage collector to run, but * be created while we wait for the garbage collector to run, but
* does prevent any leaks from {@link PhantomArrayListParent} that weren't closed. * does prevent any leaks from {@link PhantomArrayListParent} that weren't closed.
*
* <br><br>
* <strong>Use Notes: </strong><br>
* If possible all checkouts for a given pool should be the same size,
* since {@link PhantomArrayListCheckout}'s are shared, getting the same size checkout each time
* prevents accidentally returning a larger checkout than necessary, which wastes memory.
*/ */
public class PhantomArrayListPool public class PhantomArrayListPool
{ {
@@ -83,10 +88,7 @@ public class PhantomArrayListPool
public final ReferenceQueue<PhantomArrayListParent> phantomRefQueue = new ReferenceQueue<>(); public final ReferenceQueue<PhantomArrayListParent> phantomRefQueue = new ReferenceQueue<>();
private final ConcurrentLinkedQueue<SoftReference<PhantomArrayListCheckout>> pooledCheckoutsRefs = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<ByteArrayList> pooledByteArrays = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<ShortArrayList> pooledShortArrays = new ConcurrentLinkedQueue<>();
private final ConcurrentLinkedQueue<SoftReference<LongArrayList>> pooledLongArrays = new ConcurrentLinkedQueue<>();
/** counts how many byte arrays have been created by this pool */ /** counts how many byte arrays have been created by this pool */
private final AtomicInteger totalByteArrayCountRef = new AtomicInteger(0); private final AtomicInteger totalByteArrayCountRef = new AtomicInteger(0);
@@ -102,6 +104,15 @@ public class PhantomArrayListPool
/** used for debugging, represents an estimate for how many bytes the long[] pool contains */ /** used for debugging, represents an estimate for how many bytes the long[] pool contains */
private long lastLongPoolSizeInBytes = -1; private long lastLongPoolSizeInBytes = -1;
/** used for debugging, represents an estimate for how many byte[]'s are currently in this pool*/
private int lastBytePoolCount = 0;
/** used for debugging, represents an estimate for how many short[]'s are currently in this pool*/
private int lastShortPoolCount = 0;
/** used for debugging, represents an estimate for how many long[]'s are currently in this pool*/
private int lastLongPoolCount = 0;
/** used for debugging, represents an estimate for how many checkouts are currently in this pool*/
private int lastCheckoutPoolCount = 0;
/** For pools backed by {@link SoftReference}'s we may need to decrease the size when elements are garbage collected */ /** For pools backed by {@link SoftReference}'s we may need to decrease the size when elements are garbage collected */
private boolean clearLastRefPoolSizes = false; private boolean clearLastRefPoolSizes = false;
@@ -132,30 +143,77 @@ public class PhantomArrayListPool
// get checkout // // get checkout //
//==============// //==============//
/**
* If possible all checkouts for a given pool should be the same size,
* since {@link PhantomArrayListCheckout}'s are shared, returning the same size
* prevents accidentally returning a larger checkout than necessary, which wastes memory.
*/
public PhantomArrayListCheckout checkoutArrays(int byteArrayCount, int shortArrayCount, int longArrayCount) public PhantomArrayListCheckout checkoutArrays(int byteArrayCount, int shortArrayCount, int longArrayCount)
{ {
PhantomArrayListCheckout checkout = new PhantomArrayListCheckout(this); PhantomArrayListCheckout checkout = null;
while (checkout == null)
{
SoftReference<PhantomArrayListCheckout> checkoutRef = this.pooledCheckoutsRefs.poll();
if (checkoutRef == null)
{
// pool is empty, create new checkout
checkout = new PhantomArrayListCheckout(this);
}
else
{
checkout = checkoutRef.get();
if (checkout != null)
{
// use pooled checkout
}
else
{
// this reference is pointing to null,
// the checkout must have been garbage collected,
// that means we don't have enough memory
if (!lowMemoryWarningLogged)
{
lowMemoryWarningLogged = true;
// orange text
String message = "\u00A76" + "Distant Horizons: Insufficient memory detected." + "\u00A7r \n" +
"This may cause stuttering or crashing. \n" +
"Potential causes: \n" +
"1. your allocated memory isn't high enough \n" +
"2. your DH CPU preset is too high \n" +
"3. your DH quality preset is too high";
LOGGER.warn(message);
if (Config.Common.Logging.Warning.showPoolInsufficientMemoryWarning.get())
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
}
this.clearLastRefPoolSizes = true;
}
}
}
// get any missing arrays
// byte // byte
for (int i = 0; i < byteArrayCount; i++) for (int i = checkout.getByteArrayCount(); i < byteArrayCount; i++)
{ {
checkout.addByteArrayList(getPooledArray(this.pooledByteArrays, () -> this.createEmptyByteArrayList())); checkout.addByteArrayList(this.createEmptyByteArrayList());
} }
// short // short
for (int i = 0; i < shortArrayCount; i++) for (int i = checkout.getShortArrayCount(); i < shortArrayCount; i++)
{ {
checkout.addShortArrayList(getPooledArray(this.pooledShortArrays, () -> this.createEmptyShortArrayList())); checkout.addShortArrayList(this.createEmptyShortArrayList());
} }
// long // long
for (int i = 0; i < longArrayCount; i++) for (int i = checkout.getLongArrayCount(); i < longArrayCount; i++)
{ {
addRefPooledArray( checkout.addLongArrayListRef(this.createEmptyLongArrayList());
this.pooledLongArrays,
this::createEmptyLongArrayList,
this::onLongArrayListGarbageCollected,
checkout::addLongArrayListRef);
} }
return checkout; return checkout;
@@ -184,91 +242,6 @@ public class PhantomArrayListPool
} }
// garbage collection handlers //
/** should only happen if Java doesn't have enough memory */
private void onLongArrayListGarbageCollected()
{
this.clearLastRefPoolSizes = true;
this.totalLongArrayCountRef.getAndDecrement();
}
// internal pool handlers //
private static <T extends List<?>> T getPooledArray(ConcurrentLinkedQueue<T> pool, Supplier<T> emptyArrayCreatorFunc)
{
T array = pool.poll();
if (array != null)
{
array.clear();
return array;
}
else
{
// no pooled sources exist
return emptyArrayCreatorFunc.get();
}
}
private static <T extends List<?>> void addRefPooledArray(
ConcurrentLinkedQueue<SoftReference<T>> arrayPool,
Supplier<T> emptyArrayCreatorFunc,
Runnable arrayGarbageCollectedFunc,
BiConsumer<T, SoftReference<T>> putArrayFunc)
{
T array = null;
SoftReference<T> arrayRef = arrayPool.poll();
// find the first non-null pooled array
while (arrayRef != null && array == null)
{
array = arrayRef.get();
if (array == null)
{
// this reference is pointing to null,
// the array must have been garbage collected,
// that means we don't have enough memory
if (!lowMemoryWarningLogged)
{
lowMemoryWarningLogged = true;
// orange text
String message = "\u00A76" + "Distant Horizons: Insufficient memory detected." + "\u00A7r \n" +
"This may cause stuttering or crashing. \n" +
"Either: your allocated memory isn't high enough, \n" +
"your DH CPU preset is too high, or your DH quality preset is too high.";
LOGGER.warn(message);
if (Config.Common.Logging.Warning.showPoolInsufficientMemoryWarning.get())
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
}
arrayGarbageCollectedFunc.run();
// try the next reference
arrayRef = arrayPool.poll();
}
}
if (array != null)
{
LodUtil.assertTrue(arrayRef != null, "How did we get an array without it's reference?");
array.clear();
}
else
{
// no pooled sources exist
array = emptyArrayCreatorFunc.get();
arrayRef = new SoftReference<>(array);
}
putArrayFunc.accept(array, arrayRef);
}
//==================// //==================//
// phantom recovery // // phantom recovery //
@@ -405,21 +378,29 @@ public class PhantomArrayListPool
// return checkout // // return checkout //
//=================// //=================//
public void returnCheckout(PhantomArrayListCheckout checkout) public void returnParentPhantomRef(@NotNull PhantomReference<PhantomArrayListParent> parentRef)
{
try
{
parentRef.clear();
// will be null if the this parent has already been returned
PhantomArrayListCheckout checkout = this.phantomRefToCheckout.remove(parentRef);
this.returnCheckout(checkout);
}
catch (Exception e)
{
LOGGER.error("Unable to close Phantom Array, error: ["+e.getMessage()+"].", e);
}
}
public void returnCheckout(@Nullable PhantomArrayListCheckout checkout)
{ {
if (checkout == null) if (checkout == null)
{ {
throw new IllegalArgumentException("Null phantom checkout, object is being closed multiple times."); throw new IllegalArgumentException("Null phantom checkout, object is being closed multiple times.");
} }
SoftReference<PhantomArrayListCheckout> checkoutRef = checkout.ownerSoftReference;
// In James' testing pooling the checkout object wasn't necessary this.pooledCheckoutsRefs.add(checkoutRef);
// since it is relatively small and short lived, thus
// the GC can handle quickly discarding it.
this.pooledByteArrays.addAll(checkout.getAllByteArrays());
this.pooledShortArrays.addAll(checkout.getAllShortArrays());
this.pooledLongArrays.addAll(checkout.getAllLongArrayRefs());
//LOGGER.info("Returned ["+checkout.byteArrayLists.size()+"/"+this.pooledByteArrays.size()+"] bytes and ["+checkout.longArrayLists.size()+"/"+this.pooledLongArrays.size()+"] longs.");\ //LOGGER.info("Returned ["+checkout.byteArrayLists.size()+"/"+this.pooledByteArrays.size()+"] bytes and ["+checkout.longArrayLists.size()+"/"+this.pooledLongArrays.size()+"] longs.");\
} }
@@ -444,9 +425,9 @@ public class PhantomArrayListPool
totalShortArrayCount += pool.totalShortArrayCountRef.get(); totalShortArrayCount += pool.totalShortArrayCountRef.get();
totalLongArrayCount += pool.totalLongArrayCountRef.get(); totalLongArrayCount += pool.totalLongArrayCountRef.get();
pooledByteArraySize += pool.pooledByteArrays.size(); pooledByteArraySize += pool.lastBytePoolCount;
pooledShortArraySize += pool.pooledShortArrays.size(); pooledShortArraySize += pool.lastShortPoolCount;
pooledLongArraySize += pool.pooledLongArrays.size(); pooledLongArraySize += pool.lastLongPoolCount;
lastBytePoolSizeInBytes += pool.lastBytePoolSizeInBytes; lastBytePoolSizeInBytes += pool.lastBytePoolSizeInBytes;
lastShortPoolSizeInBytes += pool.lastShortPoolSizeInBytes; lastShortPoolSizeInBytes += pool.lastShortPoolSizeInBytes;
@@ -475,7 +456,7 @@ public class PhantomArrayListPool
addDebugMenuStringsToList(messageList, addDebugMenuStringsToList(messageList,
this.name, this.name,
this.totalByteArrayCountRef.get(), this.totalShortArrayCountRef.get(), this.totalLongArrayCountRef.get(), this.totalByteArrayCountRef.get(), this.totalShortArrayCountRef.get(), this.totalLongArrayCountRef.get(),
this.pooledByteArrays.size(), this.pooledShortArrays.size(), this.pooledLongArrays.size(), this.lastBytePoolCount, this.lastShortPoolCount, this.lastLongPoolCount,
this.lastBytePoolSizeInBytes, this.lastShortPoolSizeInBytes, this.lastLongPoolSizeInBytes this.lastBytePoolSizeInBytes, this.lastShortPoolSizeInBytes, this.lastLongPoolSizeInBytes
); );
} }
@@ -529,25 +510,60 @@ public class PhantomArrayListPool
*/ */
public void recalculateSizeForDebugging() public void recalculateSizeForDebugging()
{ {
// byte long bytePoolByteSize = 0;
long bytePoolByteSize = estimateMemoryUsage(this.pooledByteArrays, Byte.BYTES); long shortPoolByteSize = 0;
this.lastBytePoolSizeInBytes = Math.max(bytePoolByteSize, this.lastBytePoolSizeInBytes); long longPoolByteSize = 0;
// short int bytePoolCount = 0;
long shortPoolByteSize = estimateMemoryUsage(this.pooledShortArrays, Short.BYTES); int shortPoolCount = 0;
this.lastShortPoolSizeInBytes = Math.max(shortPoolByteSize, this.lastShortPoolSizeInBytes); int longPoolCount = 0;
// long
// checkouts //
for (SoftReference<PhantomArrayListCheckout> pooledCheckoutRef : this.pooledCheckoutsRefs)
{
PhantomArrayListCheckout pooledCheckout = pooledCheckoutRef.get();
if (pooledCheckout == null)
{
continue;
}
bytePoolByteSize += estimateMemoryUsage(pooledCheckout.getAllByteArrays(), Byte.BYTES);
bytePoolCount += pooledCheckout.getAllByteArrays().size();
shortPoolByteSize += estimateMemoryUsage(pooledCheckout.getAllShortArrays(), Short.BYTES);
shortPoolCount += pooledCheckout.getAllShortArrays().size();
longPoolByteSize += estimateMemoryUsage(pooledCheckout.getAllLongArrays(), Long.BYTES);
longPoolCount += pooledCheckout.getAllLongArrays().size();
}
// clear old values if something was garbage collected
if (this.clearLastRefPoolSizes) if (this.clearLastRefPoolSizes)
{ {
this.lastBytePoolSizeInBytes = 0;
this.lastShortPoolSizeInBytes = 0;
this.lastLongPoolSizeInBytes = 0; this.lastLongPoolSizeInBytes = 0;
this.clearLastRefPoolSizes = false; this.clearLastRefPoolSizes = false;
} }
long longPoolByteSize = estimateRefMemoryUsage(this.pooledLongArrays, Long.BYTES);
this.lastCheckoutPoolCount = this.pooledCheckoutsRefs.size();
// byte //
// math.max is used since the pool should only grow until a soft reference is freed,
// and it's easier to understand if this constantly grows instead of jumping around
this.lastBytePoolSizeInBytes = Math.max(bytePoolByteSize, this.lastBytePoolSizeInBytes);
this.lastBytePoolCount = bytePoolCount;
// short //
this.lastShortPoolSizeInBytes = Math.max(shortPoolByteSize, this.lastShortPoolSizeInBytes);
this.lastShortPoolCount = shortPoolCount;
// long //
this.lastLongPoolSizeInBytes = Math.max(longPoolByteSize, this.lastLongPoolSizeInBytes); this.lastLongPoolSizeInBytes = Math.max(longPoolByteSize, this.lastLongPoolSizeInBytes);
this.lastLongPoolCount = longPoolCount;
} }
private static <T extends Collection<?>> long estimateMemoryUsage(ConcurrentLinkedQueue<T> pool, long elementSizeInBytes) private static <T extends Collection<?>> long estimateMemoryUsage(Iterable<T> pool, long elementSizeInBytes)
{ {
long longByteSize = 0; long longByteSize = 0;
for (T array : pool) for (T array : pool)
@@ -83,6 +83,19 @@ public class DhChunkPos
public int getMinBlockX() { return this.x << 4; } public int getMinBlockX() { return this.x << 4; }
public int getMinBlockZ() { return this.z << 4; } public int getMinBlockZ() { return this.z << 4; }
public int getMaxBlockX()
{
int minBlockPos = this.getMinBlockX() + LodUtil.CHUNK_WIDTH;
minBlockPos += (minBlockPos < 0) ? -1 : 0;
return minBlockPos;
}
public int getMaxBlockZ()
{
int minBlockPos = this.getMinBlockZ() + LodUtil.CHUNK_WIDTH;
minBlockPos += (minBlockPos < 0) ? -1 : 0;
return minBlockPos;
}
public DhBlockPos2D getMinBlockPos() { return new DhBlockPos2D(this.x << 4, this.z << 4); } public DhBlockPos2D getMinBlockPos() { return new DhBlockPos2D(this.x << 4, this.z << 4); }
public boolean contains(DhBlockPos pos) public boolean contains(DhBlockPos pos)
@@ -92,8 +105,8 @@ public class DhChunkPos
int maxBlockX = minBlockX + LodUtil.CHUNK_WIDTH; int maxBlockX = minBlockX + LodUtil.CHUNK_WIDTH;
int maxBlockZ = minBlockZ + LodUtil.CHUNK_WIDTH; int maxBlockZ = minBlockZ + LodUtil.CHUNK_WIDTH;
return minBlockX <= pos.getX() && pos.getX() < maxBlockX return minBlockX >= pos.getX() && pos.getX() < maxBlockX
&& minBlockZ <= pos.getZ() && pos.getZ() < maxBlockZ; && minBlockZ >= pos.getZ() && pos.getZ() < maxBlockZ;
} }
public double distance(DhChunkPos other) public double distance(DhChunkPos other)
@@ -24,6 +24,7 @@ import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause; import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification; import com.google.common.cache.RemovalNotification;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.CachedColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.enums.EDhDirection;
@@ -34,6 +35,8 @@ import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRenderer;
import com.seibel.distanthorizons.core.util.KeyedLockContainer; import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil; import com.seibel.distanthorizons.core.util.ThreadUtil;
@@ -43,6 +46,7 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.coreapi.util.MathUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil;
import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongIterator;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose; import javax.annotation.WillNotClose;
import java.awt.*; import java.awt.*;
@@ -51,6 +55,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
/** /**
@@ -95,48 +100,29 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
* caching the loaded positions significantly improves initial loading performance * caching the loaded positions significantly improves initial loading performance
* since the same position doesn't need to be loaded 5 times. * since the same position doesn't need to be loaded 5 times.
*/ */
private final Cache<Long, ColumnRenderSource> cachedRenderSourceByPos private final Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos
= CacheBuilder.newBuilder() = CacheBuilder.newBuilder()
// availableProcessors() : each process may need to be loading a render source // availableProcessors() : each process may need to be loading a render source
// +1 : add 1 thread count buffer to reduce the chance of accidentally unloading a render source before it's used // +1 : add 1 thread count buffer to reduce the chance of accidentally unloading a render source before it's used
// *5 : each render source needs it's 4 adjacent sides, so a total of 5 render sources are needed per load // *5 : each render source needs it's 4 adjacent sides, so a total of 5 render sources are needed per load
.maximumSize((Runtime.getRuntime().availableProcessors() + 1) * 5L) .maximumSize((Runtime.getRuntime().availableProcessors() + 1) * 5L)
.removalListener((RemovalNotification<Long, ColumnRenderSource> removalNotification) -> // No closing logic since the CachedColumnRenderSource is in charge
{ // of freeing the underlying ColumnRenderSource.
RemovalCause cause = removalNotification.getCause(); // That way we don't have to worry about accidentally closing an in-use object.
if (cause == RemovalCause.EXPLICIT .<Long, CachedColumnRenderSource>build();
|| cause == RemovalCause.EXPIRED
|| cause == RemovalCause.COLLECTED /**
|| cause == RemovalCause.SIZE) * Used to limit how many upload tasks are queued at once.
{ * If all the upload tasks are queued at once, they will start uploading nearest
// cleanup needs to be handled on a different thread to prevent locking up the main loading threads * to the player, however if the player moves, that order is no longer valid and holes may appear
ThreadPoolExecutor executor = ThreadPoolUtil.getCleanupExecutor(); * as further sections are loaded before closer ones.
executor.execute(() -> * Only queuing a few of the sections at a time solves this problem.
{ */
// close the render source after it's been public final AtomicInteger uploadTaskCountRef = new AtomicInteger(0);
ColumnRenderSource renderSource = removalNotification.getValue();
if (renderSource != null)
{ @Nullable
ReentrantLock lock = renderLoadLockContainer.getLockForPos(renderSource.getPos()); public final BeaconRenderHandler beaconRenderHandler;
try
{
lock.lock();
renderSource.close();
}
finally
{
lock.unlock();
}
}
else
{
// shouldn't happen, but just in case
LOGGER.error("Unable to close null cached render source.");
}
});
}
})
.<Long, ColumnRenderSource>build();
/** the smallest numerical detail level number that can be rendered */ /** the smallest numerical detail level number that can be rendered */
@@ -167,6 +153,10 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
this.level = level; this.level = level;
this.fullDataSourceProvider = fullDataSourceProvider; this.fullDataSourceProvider = fullDataSourceProvider;
this.blockRenderDistanceDiameter = viewDiameterInBlocks; this.blockRenderDistanceDiameter = viewDiameterInBlocks;
GenericObjectRenderer genericObjectRenderer = this.level.getGenericRenderer();
this.beaconRenderHandler = (genericObjectRenderer != null) ? new BeaconRenderHandler(genericObjectRenderer) : null;
} }
@@ -247,7 +237,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
long rootPos = rootPosIterator.nextLong(); long rootPos = rootPosIterator.nextLong();
if (this.getNode(rootPos) == null) if (this.getNode(rootPos) == null)
{ {
this.setValue(rootPos, new LodRenderSection(rootPos, this, this.level, this.fullDataSourceProvider, this.cachedRenderSourceByPos, this.renderLoadLockContainer)); this.setValue(rootPos, new LodRenderSection(rootPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef, this.cachedRenderSourceByPos, this.renderLoadLockContainer));
} }
QuadNode<LodRenderSection> rootNode = this.getNode(rootPos); QuadNode<LodRenderSection> rootNode = this.getNode(rootPos);
@@ -286,7 +276,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// create the node // create the node
if (quadNode == null && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance if (quadNode == null && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance
{ {
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.cachedRenderSourceByPos, this.renderLoadLockContainer)); rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef, this.cachedRenderSourceByPos, this.renderLoadLockContainer));
quadNode = rootNode.getNode(sectionPos); quadNode = rootNode.getNode(sectionPos);
} }
if (quadNode == null) if (quadNode == null)
@@ -299,7 +289,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
LodRenderSection renderSection = quadNode.value; LodRenderSection renderSection = quadNode.value;
if (renderSection == null) if (renderSection == null)
{ {
renderSection = new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.cachedRenderSourceByPos, this.renderLoadLockContainer); renderSection = new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef, this.cachedRenderSourceByPos, this.renderLoadLockContainer);
quadNode.setValue(sectionPos, renderSection); quadNode.setValue(sectionPos, renderSection);
} }
@@ -340,9 +330,11 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
else else
{ {
// onRenderingDisabled() needs to be fired before the children are enabled so beacons render correctly // children are all loaded, unload this and parents
if (renderSection.getRenderingEnabled()) if (renderSection.getRenderingEnabled())
{ {
// needs to be fired before the children are enabled so beacons render correctly
renderSection.onRenderingDisabled(); renderSection.onRenderingDisabled();
@@ -407,14 +399,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// prepare this section for rendering // prepare this section for rendering
if (!renderSection.gpuUploadInProgress() if (!renderSection.gpuUploadInProgress()
&& renderSection.renderBuffer == null && renderSection.renderBuffer == null
&& // this check is specifically for N-sized world generators where the higher quality
( // data source may not exist yet, this is done to prevent holes while waiting for said generator
// this check is specifically for N-sized world generators where the higher quality && renderSection.getFullDataSourceExists()
// data source may not exist yet, this is done to prevent holes while waiting for said generator
renderSection.getFullDataSourceExists()
// if we can't request generation we don't want to check for full data existing
// since that will prevent server LODs from loading high-detail LODs where quadrants haven't been generated.
|| !this.fullDataSourceProvider.canQueueRetrieval())
) )
{ {
nodesNeedingLoading.add(renderSection); nodesNeedingLoading.add(renderSection);
@@ -463,7 +450,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
} }
}); });
// onRenderingEnabled() needs to be fired after the children are disabled so beacons render correctly // needs to be fired after the children are disabled so beacons render correctly
renderSection.onRenderingEnabled(); renderSection.onRenderingEnabled();
} }
@@ -643,7 +630,22 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
*/ */
public void reloadPos(long pos) public void reloadPos(long pos)
{ {
this.cachedRenderSourceByPos.invalidate(pos); // TODO will this cause issues? we may need to lock this invalidation if the cached data source is currently in use // clear cache //
this.clearRenderCacheForPos(pos);
for (EDhDirection direction : EDhDirection.ADJ_DIRECTIONS)
{
long adjacentPos = DhSectionPos.getAdjacentPos(pos, direction);
this.clearRenderCacheForPos(adjacentPos);
}
// queue reloads //
// only queue each section for reloading
// after the cache has been cleared,
// this is done to prevent accidentally using old cached data
this.sectionsToReload.add(pos); this.sectionsToReload.add(pos);
// the adjacent locations also need to be updated to make sure lighting // the adjacent locations also need to be updated to make sure lighting
@@ -652,10 +654,24 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
for (EDhDirection direction : EDhDirection.ADJ_DIRECTIONS) for (EDhDirection direction : EDhDirection.ADJ_DIRECTIONS)
{ {
long adjacentPos = DhSectionPos.getAdjacentPos(pos, direction); long adjacentPos = DhSectionPos.getAdjacentPos(pos, direction);
this.cachedRenderSourceByPos.invalidate(adjacentPos); // TODO will this cause issues? we may need to lock this invalidation if the cached data source is currently in use
this.sectionsToReload.add(adjacentPos); this.sectionsToReload.add(adjacentPos);
} }
} }
private void clearRenderCacheForPos(long pos)
{
// locking is needed to prevent another thread
// from accessing the cache while it's being cleared
ReentrantLock lock = this.renderLoadLockContainer.getLockForPos(pos);
try
{
lock.lock();
this.cachedRenderSourceByPos.invalidate(pos);
}
finally
{
lock.unlock();
}
}
@@ -23,6 +23,7 @@ import com.google.common.base.Suppliers;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dataObjects.render.CachedColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource; import com.seibel.distanthorizons.core.dataObjects.render.ColumnRenderSource;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBufferBuilder; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBufferBuilder;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodQuadBuilder;
@@ -38,6 +39,9 @@ import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer;
import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.KeyedLockContainer; import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -49,6 +53,7 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose; import javax.annotation.WillNotClose;
import java.awt.*; import java.awt.*;
import java.util.*; import java.util.*;
import java.util.List;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@@ -63,15 +68,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/**
* Used to limit how many upload tasks are queued at once.
* If all the upload tasks are queued at once, they will start uploading nearest
* to the player, however if the player moves, that order is no longer valid and holes may appear
* as further sections are loaded before closer ones.
* Only queuing a few of the sections at a time solves this problem.
*/
public static final AtomicInteger GLOBAL_UPLOAD_TASKS_COUNT_REF = new AtomicInteger(0);
public final long pos; public final long pos;
@@ -81,8 +77,18 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private final FullDataSourceProviderV2 fullDataSourceProvider; private final FullDataSourceProviderV2 fullDataSourceProvider;
private final LodQuadTree quadTree; private final LodQuadTree quadTree;
private final KeyedLockContainer<Long> renderLoadLockContainer; private final KeyedLockContainer<Long> renderLoadLockContainer;
private final Cache<Long, ColumnRenderSource> cachedRenderSourceByPos; private final Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos;
private final AtomicInteger uploadTaskCountRef;
/**
* contains the list of beacons currently being rendered in this section
* if this list is modified the {@link LodRenderSection#beaconRenderHandler} should be updated to match.
*/
private final List<BeaconBeamDTO> activeBeaconList = new ArrayList<>();
@Nullable
public final BeaconRenderHandler beaconRenderHandler;
@Nullable
public final BeaconBeamRepo beaconBeamRepo;
private boolean renderingEnabled = false; private boolean renderingEnabled = false;
@@ -112,10 +118,15 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
*/ */
private CompletableFuture<ColumnRenderBuffer> bufferUploadFuture = null; private CompletableFuture<ColumnRenderBuffer> bufferUploadFuture = null;
/** should be an empty array if no positions need to be generated */ /**
* should be an empty array if no positions need to be generated
*
* @deprecated see the comment where this variable is set
*/
@Nullable @Nullable
private Supplier<LongArrayList> missingGenerationPos; @Deprecated
private LongArrayList getMissingGenerationPos() { return this.missingGenerationPos != null ? this.missingGenerationPos.get() : null; } private Supplier<LongArrayList> missingGenerationPosFunc;
private LongArrayList getMissingGenerationPos() { return this.missingGenerationPosFunc != null ? this.missingGenerationPosFunc.get() : null; }
private boolean checkedIfFullDataSourceExists = false; private boolean checkedIfFullDataSourceExists = false;
private boolean fullDataSourceExists = false; private boolean fullDataSourceExists = false;
@@ -130,7 +141,8 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
long pos, long pos,
LodQuadTree quadTree, LodQuadTree quadTree,
IDhClientLevel level, FullDataSourceProviderV2 fullDataSourceProvider, IDhClientLevel level, FullDataSourceProviderV2 fullDataSourceProvider,
Cache<Long, ColumnRenderSource> cachedRenderSourceByPos, KeyedLockContainer<Long> renderLoadLockContainer) AtomicInteger uploadTaskCountRef,
Cache<Long, CachedColumnRenderSource> cachedRenderSourceByPos, KeyedLockContainer<Long> renderLoadLockContainer)
{ {
this.pos = pos; this.pos = pos;
this.quadTree = quadTree; this.quadTree = quadTree;
@@ -138,6 +150,10 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
this.renderLoadLockContainer = renderLoadLockContainer; this.renderLoadLockContainer = renderLoadLockContainer;
this.level = level; this.level = level;
this.fullDataSourceProvider = fullDataSourceProvider; this.fullDataSourceProvider = fullDataSourceProvider;
this.uploadTaskCountRef = uploadTaskCountRef;
this.beaconRenderHandler = this.quadTree.beaconRenderHandler;
this.beaconBeamRepo = this.level.getBeaconBeamRepo();
DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus); DebugRenderer.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus);
} }
@@ -174,32 +190,35 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// this means the closer (higher priority) tasks will load first. // this means the closer (higher priority) tasks will load first.
// This also prevents issues where the nearby tasks are canceled due to // This also prevents issues where the nearby tasks are canceled due to
// LOD detail level changing, and having holes in the world // LOD detail level changing, and having holes in the world
if (GLOBAL_UPLOAD_TASKS_COUNT_REF.getAndIncrement() > executor.getPoolSize()) if (this.uploadTaskCountRef.getAndIncrement() > executor.getPoolSize())
{ {
GLOBAL_UPLOAD_TASKS_COUNT_REF.decrementAndGet(); this.uploadTaskCountRef.decrementAndGet();
return false; return false;
} }
try try
{ {
CompletableFuture<Void> future = new CompletableFuture<>(); CompletableFuture<Void> future = new CompletableFuture<>();
this.getAndBuildRenderDataFuture = future; this.getAndBuildRenderDataFuture = future; // TODO should use a setter/getter to guard against replacing an incomplete future
future.handle((voidObj, throwable) -> future.handle((voidObj, throwable) ->
{ {
// this has to fire are the end of every added future, otherwise we'll lock up and nothing will load // this has to fire are the end of every added future, otherwise we'll lock up and nothing will load
GLOBAL_UPLOAD_TASKS_COUNT_REF.decrementAndGet(); this.uploadTaskCountRef.decrementAndGet(); // TODO there is an issue where this variable isn't decremented properly, preventing LODs from loading in, or loading much slower
return null; return null;
}); });
this.getAndBuildRenderDataRunnable = () -> this.getAndBuildRenderDataRunnable = () ->
{ {
this.getAndUploadRenderDataToGpu(); this.getAndRefreshRenderingBeacons();
this.getAndUploadRenderDataToGpuAsync()
// the future is passed in separate to prevent any possible race condition null pointers .thenRun(() ->
future.complete(null); {
// the task is done, we don't need to track these anymore // the future is passed in separately (IE not using the local var) to prevent any possible race condition null pointers
this.getAndBuildRenderDataFuture = null; future.complete(null);
this.getAndBuildRenderDataRunnable = null; // the task is done, we don't need to track these anymore
this.getAndBuildRenderDataFuture = null;
this.getAndBuildRenderDataRunnable = null;
});
}; };
executor.execute(this.getAndBuildRenderDataRunnable); executor.execute(this.getAndBuildRenderDataRunnable);
@@ -215,83 +234,138 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
return false; return false;
} }
} }
private void getAndUploadRenderDataToGpu() private CompletableFuture<Void> getAndUploadRenderDataToGpuAsync()
{ {
try // get the center pos data
{ return this.getRenderSourceForPosAsync(this.pos)
ColumnRenderSource renderSource = this.getRenderSourceForPos(this.pos); .thenCompose((CachedColumnRenderSource cachedRenderSource) ->
if (renderSource == null)
{ {
// nothing needs to be rendered try
// TODO how doesn't this cause infinite file handler loops? {
// to trigger an upload we check if the buffer is null, and we aren't if (cachedRenderSource == null || cachedRenderSource.columnRenderSource == null)
// setting the render buffer here {
return; // nothing needs to be rendered
} // TODO how doesn't this cause infinite file handler loops?
// to trigger an upload we check if the buffer is null, and we aren't
// setting the render buffer here
return CompletableFuture.completedFuture(null);
}
ColumnRenderSource thisRenderSource = cachedRenderSource.columnRenderSource;
boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled; boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper()); LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper());
// load adjacent render sources
{
ColumnRenderSource northRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH));
ColumnRenderSource southRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH));
ColumnRenderSource eastRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST));
ColumnRenderSource westRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST));
ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; // get the adjacent positions
adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = northRenderSource; // needs to be done async to prevent threads waiting on the same positions to be processed
adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = southRenderSource; final CompletableFuture<CachedColumnRenderSource>[] adjacentLoadFutures = new CompletableFuture[4];
adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = eastRenderSource; adjacentLoadFutures[0] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH));
adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = westRenderSource; adjacentLoadFutures[1] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH));
adjacentLoadFutures[2] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST));
adjacentLoadFutures[3] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST));
return CompletableFuture.allOf(adjacentLoadFutures).thenRun(() ->
{
try (CachedColumnRenderSource northRenderSource = adjacentLoadFutures[0].get();
CachedColumnRenderSource southRenderSource = adjacentLoadFutures[1].get();
CachedColumnRenderSource eastRenderSource = adjacentLoadFutures[2].get();
CachedColumnRenderSource westRenderSource = adjacentLoadFutures[3].get())
{
ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length];
adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = (northRenderSource != null) ? northRenderSource.columnRenderSource : null;
adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = (southRenderSource != null) ? southRenderSource.columnRenderSource : null;
adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = (eastRenderSource != null) ? eastRenderSource.columnRenderSource : null;
adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = (westRenderSource != null) ? westRenderSource.columnRenderSource : null;
boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length]; boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length];
adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH); adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH);
adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH); adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH);
adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST); adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST);
adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST); adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST);
// the render sources are only needed in this synchronous method, // the render sources are only needed by this synchronous method,
// then they can be closed // then they can be closed
ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, renderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel); ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, thisRenderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel);
} this.uploadToGpuAsync(lodQuadBuilder);
}
this.uploadToGpuAsync(lodQuadBuilder); catch (Exception e)
} {
catch (Exception e) LOGGER.error("Unexpected error while loading LodRenderSection [" + DhSectionPos.toString(this.pos) + "] adjacent data, Error: [" + e.getMessage() + "].", e);
{ }
LOGGER.error("Unexpected error while loading LodRenderSection ["+DhSectionPos.toString(this.pos)+"], Error: [" + e.getMessage() + "].", e); finally
} {
// can only be closed after the data has been processed and uploaded to the GPU
cachedRenderSource.close();
}
});
}
catch (Exception e)
{
LOGGER.error("Unexpected error while loading LodRenderSection ["+DhSectionPos.toString(this.pos)+"], Error: [" + e.getMessage() + "].", e);
return CompletableFuture.completedFuture(null);
}
});
} }
@Nullable /** async is done so each thread can run without waiting on others */
private ColumnRenderSource getRenderSourceForPos(long pos) private CompletableFuture<CachedColumnRenderSource> getRenderSourceForPosAsync(long pos)
{ {
ReentrantLock lock = this.renderLoadLockContainer.getLockForPos(pos); ReentrantLock lock = this.renderLoadLockContainer.getLockForPos(pos);
try try
{ {
// we don't want multiple threads attempting to load the same position at the same time // we don't want multiple threads attempting to load the same position at the same time,
// and we don't want to access the cache while invalidating it on a different thread
lock.lock(); lock.lock();
// use the cached data if possible // use the cached data if possible
ColumnRenderSource renderSource = this.cachedRenderSourceByPos.getIfPresent(pos); CachedColumnRenderSource existingCachedRenderSource = this.cachedRenderSourceByPos.getIfPresent(pos);
if (renderSource != null) if (existingCachedRenderSource != null)
{ {
return renderSource; existingCachedRenderSource.markInUse();
return existingCachedRenderSource.loadFuture;
} }
// generate new render source
try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos))
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor == null || executor.isTerminated())
{ {
renderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level); // should only happen if the threadpool is actively being re-sized
// only add valid data to the cache (to prevent null pointers) return CompletableFuture.completedFuture(null);
if (renderSource != null) }
// queue loading the render data
CompletableFuture<CachedColumnRenderSource> loadFuture = new CompletableFuture<>();
final CachedColumnRenderSource newCachedRenderSource = new CachedColumnRenderSource(loadFuture, lock, this.cachedRenderSourceByPos);
executor.execute(() ->
{
// generate new render source
try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos))
{ {
this.cachedRenderSourceByPos.put(pos, renderSource); newCachedRenderSource.columnRenderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level);
} }
} catch (Exception e)
{
LOGGER.error("Unexpected issue creating render data for pos: ["+DhSectionPos.toString(pos)+"], error: ["+e.getMessage()+"].", e);
}
finally
{
loadFuture.complete(newCachedRenderSource);
}
});
this.cachedRenderSourceByPos.put(pos, newCachedRenderSource);
return renderSource; return loadFuture;
}
catch (RejectedExecutionException ignore)
{
// the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
return CompletableFuture.completedFuture(null);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue getting and creating render data for pos: ["+DhSectionPos.toString(pos)+"], error: ["+e.getMessage()+"].", e);
return CompletableFuture.completedFuture(null);
} }
finally finally
{ {
@@ -350,25 +424,24 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;} public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;}
/** @see LodRenderSection#setRenderingEnabled */ /** @see LodRenderSection#setRenderingEnabled */
public void onRenderingEnabled() { this.level.loadBeaconBeamsInPos(this.pos); } public void onRenderingEnabled() { this.startRenderingBeacons(); }
/** @see LodRenderSection#setRenderingEnabled */ /** @see LodRenderSection#setRenderingEnabled */
public void onRenderingDisabled() public void onRenderingDisabled()
{ {
this.level.unloadBeaconBeamsInPos(this.pos); this.stopRenderingBeacons();
if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get()) if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get())
{ {
// show that this position has just been disabled // show that this position has just been disabled
DebugRenderer.makeParticle( DebugRenderer.makeParticle(
new DebugRenderer.BoxParticle( new DebugRenderer.BoxParticle(
new DebugRenderer.Box(this.pos, 128f, 156f, 0.09f, Color.CYAN.darker()), new DebugRenderer.Box(this.pos, 128f, 156f, 0.09f, Color.CYAN.darker()),
0.2, 32f 0.2, 32f
) )
); );
} }
} }
public boolean gpuUploadInProgress() { return this.getAndBuildRenderDataFuture != null; } public boolean gpuUploadInProgress() { return this.getAndBuildRenderDataFuture != null; }
@@ -433,10 +506,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
if (this.fullDataSourceProvider.canRetrieveMissingDataSources() && this.fullDataSourceProvider.canQueueRetrieval()) if (this.fullDataSourceProvider.canRetrieveMissingDataSources() && this.fullDataSourceProvider.canQueueRetrieval())
{ {
// calculate the missing positions if not already done // calculate the missing positions if not already done
if (this.missingGenerationPos == null) if (this.missingGenerationPosFunc == null)
{ {
//this.missingGenerationPos = Suppliers.memoize(() -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos)); // TODO memoization is needed for multiplayer, otherwise
this.missingGenerationPos = Suppliers.memoizeWithExpiration(() -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos), 1, TimeUnit.MINUTES); // new retrieval requests won't be submitted.
// TODO why is that the case? Shouldn't the missing positions be un-changing?
this.missingGenerationPosFunc = Suppliers.memoizeWithExpiration(
() -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos),
15, TimeUnit.SECONDS);
} }
LongArrayList missingGenerationPos = this.getMissingGenerationPos(); LongArrayList missingGenerationPos = this.getMissingGenerationPos();
@@ -465,6 +542,85 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================//
// beacon handling //
//=================//
/** gets the active beacon list and stops/starts beacon rendering as necessary */
private void getAndRefreshRenderingBeacons()
{
// do nothing if beacon rendering or repos are unavailable
if (this.beaconBeamRepo == null
|| this.beaconRenderHandler == null)
{
return;
}
// Synchronized to prevent two threads for starting/stopping rendering at once
// Shouldn't be necessary, but just in case.
synchronized (this.activeBeaconList)
{
List<BeaconBeamDTO> activeBeacons = this.beaconBeamRepo.getAllBeamsForPos(this.pos);
// stop rendering current beacons
for (BeaconBeamDTO beam : this.activeBeaconList)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
// swap old and new active beacon list
this.activeBeaconList.clear();
this.activeBeaconList.addAll(activeBeacons);
// start rendering new beacon list
for (BeaconBeamDTO beam : this.activeBeaconList)
{
this.beaconRenderHandler.startRenderingBeacon(beam);
}
}
}
private void stopRenderingBeacons()
{
// do nothing if beacon rendering is unavailable
if (this.beaconRenderHandler == null)
{
return;
}
synchronized (this.activeBeaconList)
{
for (BeaconBeamDTO beam : this.activeBeaconList)
{
this.beaconRenderHandler.stopRenderingBeaconAtPos(beam.blockPos);
}
}
}
private void startRenderingBeacons()
{
// do nothing if beacon rendering is unavailable
if (this.beaconRenderHandler == null)
{
return;
}
synchronized (this.activeBeaconList)
{
for (BeaconBeamDTO beam : this.activeBeaconList)
{
this.beaconRenderHandler.startRenderingBeacon(beam);
}
}
}
//==============// //==============//
// base methods // // base methods //
//==============// //==============//
@@ -514,7 +670,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
} }
this.level.unloadBeaconBeamsInPos(this.pos); this.stopRenderingBeacons();
if (this.renderBuffer != null) if (this.renderBuffer != null)
{ {
@@ -551,6 +707,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// while this should generally be a fast operation // while this should generally be a fast operation
// this is run on a separate thread to prevent lag on the render thread // this is run on a separate thread to prevent lag on the render thread
executor.execute(() -> this.fullDataSourceProvider.removeRetrievalRequestIf((genPos) -> DhSectionPos.contains(this.pos, genPos))); executor.execute(() -> this.fullDataSourceProvider.removeRetrievalRequestIf((genPos) -> DhSectionPos.contains(this.pos, genPos)));
} }
@@ -31,6 +31,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d; import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
@@ -50,8 +51,6 @@ public class BeaconRenderHandler
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final int BEAM_TOP_Y = 6_000;
/** how often should we check if a beacon should be culled? */ /** how often should we check if a beacon should be culled? */
private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000; private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000;
@@ -98,9 +97,10 @@ public class BeaconRenderHandler
if (this.beaconBlockPosSet.add(beacon.blockPos)) if (this.beaconBlockPosSet.add(beacon.blockPos))
{ {
int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get();
DhApiRenderableBox beaconBox = new DhApiRenderableBox( DhApiRenderableBox beaconBox = new DhApiRenderableBox(
new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()), new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()),
new DhApiVec3d(beacon.blockPos.getX() + 1, BEAM_TOP_Y, beacon.blockPos.getZ() + 1), new DhApiVec3d(beacon.blockPos.getX() + 1, maxBeaconBeamHeight, beacon.blockPos.getZ() + 1),
beacon.color, beacon.color,
EDhApiBlockMaterial.ILLUMINATED EDhApiBlockMaterial.ILLUMINATED
); );
@@ -215,7 +215,7 @@ public class BeaconRenderHandler
double mcRenderDistance = MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH; double mcRenderDistance = MC_RENDER.getRenderDistance() * LodUtil.CHUNK_WIDTH;
// multiplying by overdraw prevention helps reduce beacons from rendering strangely // multiplying by overdraw prevention helps reduce beacons from rendering strangely
// on the border of DH's render distance // on the border of DH's render distance
mcRenderDistance *= Config.Client.Advanced.Graphics.Culling.overdrawPrevention.get(); mcRenderDistance *= RenderUtil.getAutoOverdrawPrevention();
// Clear the existing box group so we can re-populate it. // Clear the existing box group so we can re-populate it.
@@ -16,11 +16,14 @@ public class DbConnectionClosedException extends SQLException
// helper methods // // helper methods //
public static boolean IsClosedException(SQLException e) public static boolean isClosedException(SQLException e)
{ {
// TODO long term we should prevent using repos that are closed, but for now this is the easier solution // TODO long term we should prevent using repos that are closed, but for now this is the easier solution
String message = e.getMessage().toLowerCase(); String message = e.getMessage().toLowerCase();
return message.contains("connection closed") || message.contains("pointer is closed") || message.contains("database has been closed"); return message.contains("connection closed")
|| message.contains("pointer is closed")
|| message.contains("stmt pointer is closed")
|| message.contains("database has been closed");
} }
} }
@@ -29,6 +29,7 @@ import com.seibel.distanthorizons.core.pooling.PhantomArrayListParent;
import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool; import com.seibel.distanthorizons.core.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.network.INetworkObject; import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.util.BoolUtil;
import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.ListUtil; import com.seibel.distanthorizons.core.util.ListUtil;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
@@ -40,6 +41,7 @@ import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*; import java.io.*;
@@ -70,7 +72,12 @@ public class FullDataSourceV2DTO
public byte dataFormatVersion; public byte dataFormatVersion;
public byte compressionModeValue; public byte compressionModeValue;
public boolean applyToParent; /** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToParent;
/** Will be null if we don't want to update this value in the DB */
@Nullable
public Boolean applyToChildren;
public long lastModifiedUnixDateTime; public long lastModifiedUnixDateTime;
public long createdUnixDateTime; public long createdUnixDateTime;
@@ -97,12 +104,15 @@ public class FullDataSourceV2DTO
// populate individual variables // populate individual variables
{ {
dto.pos = dataSource.getPos(); dto.pos = dataSource.getPos();
dto.dataChecksum = (dataSource.mapping.hashCode() * 4217) + dataSource.hashCode(); // the mapping hash isn't included since it takes significantly longer to calculate and
// as of the time of this comment (2025-1-22) the checksum isn't used for anything so changing it shouldn't cause any issues
dto.dataChecksum = dataSource.hashCode();
dto.dataFormatVersion = FullDataSourceV2.DATA_FORMAT_VERSION; dto.dataFormatVersion = FullDataSourceV2.DATA_FORMAT_VERSION;
dto.compressionModeValue = compressionModeEnum.value; dto.compressionModeValue = compressionModeEnum.value;
dto.lastModifiedUnixDateTime = dataSource.lastModifiedUnixDateTime; dto.lastModifiedUnixDateTime = dataSource.lastModifiedUnixDateTime;
dto.createdUnixDateTime = dataSource.createdUnixDateTime; dto.createdUnixDateTime = dataSource.createdUnixDateTime;
dto.applyToParent = dataSource.applyToParent; dto.applyToParent = dataSource.applyToParent;
dto.applyToChildren = dataSource.applyToChildren;
dto.levelMinY = dataSource.levelMinY; dto.levelMinY = dataSource.levelMinY;
} }
@@ -121,8 +131,6 @@ public class FullDataSourceV2DTO
this.compressedColumnGenStepByteArray = this.pooledArraysCheckout.getByteArray(1, 0); this.compressedColumnGenStepByteArray = this.pooledArraysCheckout.getByteArray(1, 0);
this.compressedWorldCompressionModeByteArray = this.pooledArraysCheckout.getByteArray(2, 0); this.compressedWorldCompressionModeByteArray = this.pooledArraysCheckout.getByteArray(2, 0);
this.compressedMappingByteArray = this.pooledArraysCheckout.getByteArray(3, 0); this.compressedMappingByteArray = this.pooledArraysCheckout.getByteArray(3, 0);
this.pooledArraysCheckout = null;
} }
@@ -195,6 +203,15 @@ public class FullDataSourceV2DTO
dataSource.isEmpty = false; dataSource.isEmpty = false;
if (this.applyToParent != null)
{
dataSource.applyToParent = this.applyToParent;
}
if (this.applyToChildren != null)
{
dataSource.applyToChildren = this.applyToChildren;
}
return dataSource; return dataSource;
} }
@@ -379,7 +396,8 @@ public class FullDataSourceV2DTO
out.writeByte(this.dataFormatVersion); out.writeByte(this.dataFormatVersion);
out.writeByte(this.compressionModeValue); out.writeByte(this.compressionModeValue);
out.writeBoolean(this.applyToParent); out.writeBoolean(BoolUtil.falseIfNull(this.applyToParent));
out.writeBoolean(BoolUtil.falseIfNull(this.applyToChildren));
out.writeLong(this.lastModifiedUnixDateTime); out.writeLong(this.lastModifiedUnixDateTime);
out.writeLong(this.createdUnixDateTime); out.writeLong(this.createdUnixDateTime);
@@ -406,6 +424,7 @@ public class FullDataSourceV2DTO
this.compressionModeValue = in.readByte(); this.compressionModeValue = in.readByte();
this.applyToParent = in.readBoolean(); this.applyToParent = in.readBoolean();
this.applyToChildren = in.readBoolean();
this.lastModifiedUnixDateTime = in.readLong(); this.lastModifiedUnixDateTime = in.readLong();
this.createdUnixDateTime = in.readLong(); this.createdUnixDateTime = in.readLong();
@@ -444,6 +463,7 @@ public class FullDataSourceV2DTO
.add("dataFormatVersion", this.dataFormatVersion) .add("dataFormatVersion", this.dataFormatVersion)
.add("compressionModeValue", this.compressionModeValue) .add("compressionModeValue", this.compressionModeValue)
.add("applyToParent", this.applyToParent) .add("applyToParent", this.applyToParent)
.add("applyToChildren", this.applyToChildren)
.add("lastModifiedUnixDateTime", this.lastModifiedUnixDateTime) .add("lastModifiedUnixDateTime", this.lastModifiedUnixDateTime)
.add("createdUnixDateTime", this.createdUnixDateTime) .add("createdUnixDateTime", this.createdUnixDateTime)
.toString(); .toString();
@@ -23,20 +23,17 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.sql.DbConnectionClosedException; import com.seibel.distanthorizons.core.sql.DbConnectionClosedException;
import com.seibel.distanthorizons.core.sql.dto.IBaseDTO; import com.seibel.distanthorizons.core.sql.dto.IBaseDTO;
import com.seibel.distanthorizons.core.sql.repo.phantoms.AutoClosableTrackingWrapper;
import com.seibel.distanthorizons.core.util.KeyedLockContainer; import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.swing.plaf.nimbus.State;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.sql.*; import java.sql.*;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
/** /**
@@ -55,8 +52,6 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
private static final ConcurrentHashMap<String, Connection> CONNECTIONS_BY_CONNECTION_STRING = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, Connection> CONNECTIONS_BY_CONNECTION_STRING = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<AbstractDhRepo<?, ?>, String> ACTIVE_CONNECTION_STRINGS_BY_REPO = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<AbstractDhRepo<?, ?>, String> ACTIVE_CONNECTION_STRINGS_BY_REPO = new ConcurrentHashMap<>();
private static final ThreadPoolExecutor WAL_FLUSH_THREAD = ThreadUtil.makeSingleDaemonThreadPool("Abstract Repo WAL Flush");
private static final AtomicBoolean FLUSH_THREAD_QUEUED = new AtomicBoolean(false);
private final String connectionString; private final String connectionString;
@@ -65,6 +60,8 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
public final String databaseType; public final String databaseType;
public final File databaseFile; public final File databaseFile;
public final Set<AutoClosableTrackingWrapper> openClosables = ConcurrentHashMap.newKeySet();
public final Class<? extends TDTO> dtoClass; public final Class<? extends TDTO> dtoClass;
protected final KeyedLockContainer<TKey> saveLockContainer = new KeyedLockContainer<>(); protected final KeyedLockContainer<TKey> saveLockContainer = new KeyedLockContainer<>();
@@ -189,7 +186,15 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
catch (SQLException | IOException e) catch (SQLException | IOException e)
{ {
LOGGER.warn("Unexpected issue deserializing DTO ["+this.dtoClass.getSimpleName()+"] with primary key ["+primaryKey+"]. Error: ["+e.getMessage()+"].", e); if (e instanceof SQLException
&& DbConnectionClosedException.isClosedException((SQLException)e))
{
//LOGGER.warn("Attempted to get ["+this.dtoClass.getSimpleName()+"] with primary key ["+primaryKey+"] on closed repo ["+this.connectionString+"].");
}
else
{
LOGGER.warn("Unexpected issue deserializing DTO ["+this.dtoClass.getSimpleName()+"] with primary key ["+primaryKey+"]. Error: ["+e.getMessage()+"].", e);
}
return null; return null;
} }
} }
@@ -310,65 +315,6 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
protected void tryTriggerWalFlush()
{
if (FLUSH_THREAD_QUEUED.compareAndSet(false, true))
{
WAL_FLUSH_THREAD.execute(() ->
{
try
{
Thread.sleep(10_000);
this.triggerWalFlush();
}
catch (InterruptedException ignore) { }
finally
{
FLUSH_THREAD_QUEUED.set(false);
}
});
}
}
protected void triggerWalFlush()
{
try (PreparedStatement statement = this.createPreparedStatement("PRAGMA wal_checkpoint(PASSIVE)");
ResultSet result = this.query(statement))
{
if (result == null)
{
return;
}
int busyInt = result.getInt("busy"); // usually 0 but will be 1 if a RESTART or FULL or TRUNCATE checkpoint was blocked from completing
boolean checkpointWasBlocked = (busyInt == 1);
int modifiedPageCount = result.getInt("log"); // number of modified pages that have been written to the write-ahead log file
int numberOfPagesWrittenToDb = result.getInt("checkpointed"); // number of pages in the write-ahead log file that have been successfully moved back into the database file at the conclusion of the checkpoint
if (!checkpointWasBlocked)
{
LOGGER.info("WAL flushed, modified pages: ["+modifiedPageCount+"], written pages: ["+numberOfPagesWrittenToDb+"].");
}
else
{
LOGGER.warn("WAL flush blocked, modified pages: ["+modifiedPageCount+"], written pages: ["+numberOfPagesWrittenToDb+"].");
}
}
catch (Exception e)
{
if (e instanceof SQLException && DbConnectionClosedException.IsClosedException((SQLException)e))
{
LOGGER.warn("DB closed");
}
else
{
LOGGER.error("unexpected error", e);
}
}
}
//==============// //==============//
// low level DB // // low level DB //
@@ -409,7 +355,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
// Note: this can only handle 1 command at a time // Note: this can only handle 1 command at a time
boolean resultSetPresent = statement.execute(sql); boolean resultSetPresent = statement.execute(sql);
try (ResultSet resultSet = statement.getResultSet()) try (ResultSet resultSet = AutoClosableTrackingWrapper.wrap(ResultSet.class, statement.getResultSet(), this.openClosables))
{ {
return this.convertResultSetToDictionaryList(resultSet, resultSetPresent); return this.convertResultSetToDictionaryList(resultSet, resultSetPresent);
} }
@@ -419,7 +365,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
// SQL exceptions generally only happen when something is wrong with // SQL exceptions generally only happen when something is wrong with
// the database or the query and should cause the system to blow up to notify the developer // the database or the query and should cause the system to blow up to notify the developer
if (DbConnectionClosedException.IsClosedException(e)) if (DbConnectionClosedException.isClosedException(e))
{ {
throw new DbConnectionClosedException(e); throw new DbConnectionClosedException(e);
} }
@@ -458,7 +404,8 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
boolean resultSetPresent = statement.execute(); boolean resultSetPresent = statement.execute();
if (resultSetPresent) if (resultSetPresent)
{ {
return statement.getResultSet(); ResultSet resultSet = statement.getResultSet();
return AutoClosableTrackingWrapper.wrap(ResultSet.class, resultSet, this.openClosables);
} }
else else
{ {
@@ -470,7 +417,7 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
// SQL exceptions generally only happen when something is wrong with // SQL exceptions generally only happen when something is wrong with
// the database or the query and should cause the system to blow up to notify the developer // the database or the query and should cause the system to blow up to notify the developer
if (DbConnectionClosedException.IsClosedException(e)) if (DbConnectionClosedException.isClosedException(e))
{ {
return null; return null;
} }
@@ -496,11 +443,11 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
{ {
PreparedStatement statement = this.connection.prepareStatement(sql); PreparedStatement statement = this.connection.prepareStatement(sql);
statement.setQueryTimeout(TIMEOUT_SECONDS); statement.setQueryTimeout(TIMEOUT_SECONDS);
return statement; return AutoClosableTrackingWrapper.wrap(PreparedStatement.class, statement, this.openClosables);
} }
catch(SQLException e) catch(SQLException e)
{ {
if (DbConnectionClosedException.IsClosedException(e)) if (DbConnectionClosedException.isClosedException(e))
{ {
return null; return null;
} }
@@ -581,6 +528,32 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
{ {
CONNECTIONS_BY_CONNECTION_STRING.remove(this.connectionString); CONNECTIONS_BY_CONNECTION_STRING.remove(this.connectionString);
// log any leaked objects
int openClosableCount = this.openClosables.size();
if (openClosableCount != 0)
{
LOGGER.warn("[" + openClosableCount + "] objects not closed for repo [" + this.getClass().getSimpleName() + "]-[" + this.getTableName() + "] with connection: [" + this.connectionString + "]. A memory leak may be present and closing this connection may take longer than normal.");
// header
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("Unclosed objects: \n");
// leaked objects
HashMap<String, AtomicInteger> unclosedObjectCountsByString = this.getUnclosedObjectStringsAndCounts();
for (String objString : unclosedObjectCountsByString.keySet())
{
AtomicInteger countRef = unclosedObjectCountsByString.get(objString);
if (countRef != null)
{
stringBuilder.append("[" + countRef.get() + "] - [" + objString + "] \n");
}
}
LOGGER.warn(stringBuilder.toString());
}
if (!this.connection.isClosed()) if (!this.connection.isClosed())
{ {
LOGGER.info("Closing database connection: [" + this.connectionString + "]..."); LOGGER.info("Closing database connection: [" + this.connectionString + "]...");
@@ -675,6 +648,49 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
return list; return list;
} }
/** used for logging leaked objects */
public HashMap<String, AtomicInteger> getUnclosedObjectStringsAndCounts()
{
HashMap<String, AtomicInteger> closableCountsByToString = new HashMap<>();
for (AutoClosableTrackingWrapper closableWrapper : this.openClosables)
{
// custom to-strings for better merging
String str = closableWrapper.wrappedClosable.getClass().getSimpleName();
if (closableWrapper.wrappedClosable instanceof ResultSet)
{
str += " @ " + closableWrapper.wrappedClosable.toString();
}
else if (closableWrapper.wrappedClosable instanceof PreparedStatement)
{
String sql = closableWrapper.wrappedClosable.toString();
int parametersIndex = sql.indexOf("\n parameters="); // remove Sqlite parameters so queries aren't separated by properties
if (parametersIndex != -1)
{
sql = sql.substring(0, parametersIndex);
}
str += " @ " + sql;
}
else
{
str += " @ " + closableWrapper.wrappedClosable.toString();
}
closableCountsByToString.compute(str, (stringVal, countRef) ->
{
if (countRef == null)
{
countRef = new AtomicInteger(0);
}
countRef.incrementAndGet();
return countRef;
});
}
return closableCountsByToString;
}
//==================// //==================//
@@ -174,8 +174,8 @@ public class BeaconBeamRepo extends AbstractDhRepo<DhBlockPos, BeaconBeamDTO>
int maxBlockZ = minBlockZ + LodUtil.CHUNK_WIDTH; int maxBlockZ = minBlockZ + LodUtil.CHUNK_WIDTH;
return this.getAllBeamsInBlockPosRange( return this.getAllBeamsInBlockPosRange(
minBlockX, minBlockZ, minBlockX, maxBlockX,
maxBlockX, maxBlockZ minBlockZ, maxBlockZ
); );
} }
@@ -187,8 +187,8 @@ public class BeaconBeamRepo extends AbstractDhRepo<DhBlockPos, BeaconBeamDTO>
int maxBlockZ = minBlockZ + DhSectionPos.getBlockWidth(pos); int maxBlockZ = minBlockZ + DhSectionPos.getBlockWidth(pos);
return this.getAllBeamsInBlockPosRange( return this.getAllBeamsInBlockPosRange(
minBlockX, minBlockZ, minBlockX, maxBlockX,
maxBlockX, maxBlockZ minBlockZ, maxBlockZ
); );
} }
@@ -199,8 +199,8 @@ public class BeaconBeamRepo extends AbstractDhRepo<DhBlockPos, BeaconBeamDTO>
"? <= BlockPosX AND BlockPosX <= ? AND " + "? <= BlockPosX AND BlockPosX <= ? AND " +
"? <= BlockPosZ AND BlockPosZ <= ?"; "? <= BlockPosZ AND BlockPosZ <= ?";
public List<BeaconBeamDTO> getAllBeamsInBlockPosRange( public List<BeaconBeamDTO> getAllBeamsInBlockPosRange(
int minBlockX, int minBlockZ, int minBlockX, int maxBlockX,
int maxBlockX, int maxBlockZ int minBlockZ, int maxBlockZ
) )
{ {
ArrayList<BeaconBeamDTO> beamList = new ArrayList<>(); ArrayList<BeaconBeamDTO> beamList = new ArrayList<>();
@@ -214,8 +214,8 @@ public class BeaconBeamRepo extends AbstractDhRepo<DhBlockPos, BeaconBeamDTO>
int i = 1; int i = 1;
statement.setInt(i++, minBlockX); statement.setInt(i++, minBlockX);
statement.setInt(i++, minBlockZ);
statement.setInt(i++, maxBlockX); statement.setInt(i++, maxBlockX);
statement.setInt(i++, minBlockZ);
statement.setInt(i++, maxBlockZ); statement.setInt(i++, maxBlockZ);
@@ -25,12 +25,12 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.sql.DbConnectionClosedException; import com.seibel.distanthorizons.core.sql.DbConnectionClosedException;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.BoolUtil;
import com.seibel.distanthorizons.core.util.ListUtil; import com.seibel.distanthorizons.core.util.ListUtil;
import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataInputStream;
import it.unimi.dsi.fastutil.bytes.ByteArrayList; import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.util.ArrayUtils;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.*; import java.io.*;
@@ -81,7 +81,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
@Nullable @Override @Nullable
public FullDataSourceV2DTO convertResultSetToDto(ResultSet resultSet) throws ClassCastException, IOException, SQLException public FullDataSourceV2DTO convertResultSetToDto(ResultSet resultSet) throws ClassCastException, IOException, SQLException
{ {
//======================// //======================//
@@ -101,7 +101,9 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
byte dataFormatVersion = resultSet.getByte("DataFormatVersion"); byte dataFormatVersion = resultSet.getByte("DataFormatVersion");
byte compressionModeValue = resultSet.getByte("CompressionMode"); byte compressionModeValue = resultSet.getByte("CompressionMode");
// while these values can be null in the DB, null would just equate to false
boolean applyToParent = (resultSet.getInt("ApplyToParent")) == 1; boolean applyToParent = (resultSet.getInt("ApplyToParent")) == 1;
boolean applyToChildren = (resultSet.getInt("ApplyToChildren")) == 1;
long lastModifiedUnixDateTime = resultSet.getLong("LastModifiedUnixDateTime"); long lastModifiedUnixDateTime = resultSet.getLong("LastModifiedUnixDateTime");
long createdUnixDateTime = resultSet.getLong("CreatedUnixDateTime"); long createdUnixDateTime = resultSet.getLong("CreatedUnixDateTime");
@@ -129,6 +131,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
dto.lastModifiedUnixDateTime = lastModifiedUnixDateTime; dto.lastModifiedUnixDateTime = lastModifiedUnixDateTime;
dto.createdUnixDateTime = createdUnixDateTime; dto.createdUnixDateTime = createdUnixDateTime;
dto.applyToParent = applyToParent; dto.applyToParent = applyToParent;
dto.applyToChildren = applyToChildren;
dto.levelMinY = minY; dto.levelMinY = minY;
} }
return dto; return dto;
@@ -139,13 +142,13 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
" DetailLevel, PosX, PosZ, \n" + " DetailLevel, PosX, PosZ, \n" +
" MinY, DataChecksum, \n" + " MinY, DataChecksum, \n" +
" Data, ColumnGenerationStep, ColumnWorldCompressionMode, Mapping, \n" + " Data, ColumnGenerationStep, ColumnWorldCompressionMode, Mapping, \n" +
" DataFormatVersion, CompressionMode, ApplyToParent, \n" + " DataFormatVersion, CompressionMode, ApplyToParent, ApplyToChildren, \n" +
" LastModifiedUnixDateTime, CreatedUnixDateTime) \n" + " LastModifiedUnixDateTime, CreatedUnixDateTime) \n" +
"VALUES( \n" + "VALUES( \n" +
" ?, ?, ?, \n" + " ?, ?, ?, \n" +
" ?, ?, \n" + " ?, ?, \n" +
" ?, ?, ?, ?, \n" + " ?, ?, ?, ?, \n" +
" ?, ?, ?, \n" + " ?, ?, ?, ?, \n" +
" ?, ? \n" + " ?, ? \n" +
");"; ");";
@Override @Override
@@ -159,51 +162,63 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
int i = 1; int i = 1;
statement.setObject(i++, DhSectionPos.getDetailLevel(dto.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); statement.setInt(i++, DhSectionPos.getDetailLevel(dto.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
statement.setObject(i++, DhSectionPos.getX(dto.pos)); statement.setInt(i++, DhSectionPos.getX(dto.pos));
statement.setObject(i++, DhSectionPos.getZ(dto.pos)); statement.setInt(i++, DhSectionPos.getZ(dto.pos));
statement.setObject(i++, dto.levelMinY); statement.setInt(i++, dto.levelMinY);
statement.setObject(i++, dto.dataChecksum); statement.setInt(i++, dto.dataChecksum);
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedDataByteArray.elements()), dto.compressedDataByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedDataByteArray.elements()), dto.compressedDataByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedColumnGenStepByteArray.elements()), dto.compressedColumnGenStepByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedColumnGenStepByteArray.elements()), dto.compressedColumnGenStepByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedWorldCompressionModeByteArray.elements()), dto.compressedWorldCompressionModeByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedWorldCompressionModeByteArray.elements()), dto.compressedWorldCompressionModeByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedMappingByteArray.elements()), dto.compressedMappingByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedMappingByteArray.elements()), dto.compressedMappingByteArray.size());
statement.setObject(i++, dto.dataFormatVersion); statement.setByte(i++, dto.dataFormatVersion);
statement.setObject(i++, dto.compressionModeValue); statement.setByte(i++, dto.compressionModeValue);
statement.setObject(i++, dto.applyToParent); // if nothing is present assume we don't need/want to propagate updates
statement.setBoolean(i++, BoolUtil.falseIfNull(dto.applyToParent));
statement.setBoolean(i++, BoolUtil.falseIfNull(dto.applyToChildren));
statement.setObject(i++, System.currentTimeMillis()); // last modified unix time statement.setLong(i++, System.currentTimeMillis()); // last modified unix time
statement.setObject(i++, System.currentTimeMillis()); // created unix time statement.setLong(i++, System.currentTimeMillis()); // created unix time
return statement; return statement;
} }
private final String updateSqlTemplate =
"UPDATE "+this.getTableName()+" \n" +
"SET \n" +
" MinY = ? \n" +
" ,DataChecksum = ? \n" +
" ,Data = ? \n" +
" ,ColumnGenerationStep = ? \n" +
" ,ColumnWorldCompressionMode = ? \n" +
" ,Mapping = ? \n" +
" ,DataFormatVersion = ? \n" +
" ,CompressionMode = ? \n" +
" ,ApplyToParent = ? \n" +
" ,LastModifiedUnixDateTime = ? \n" +
" ,CreatedUnixDateTime = ? \n" +
"WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?";
@Override @Override
public PreparedStatement createUpdateStatement(FullDataSourceV2DTO dto) throws SQLException public PreparedStatement createUpdateStatement(FullDataSourceV2DTO dto) throws SQLException
{ {
PreparedStatement statement = this.createPreparedStatement(this.updateSqlTemplate); // Dynamic string so we can update one, both, or neither
// of the applyTo... flags.
// This is necessary to prevent concurrent modifications when
// update propagation is run.
String updateSqlTemplate = (
"UPDATE "+this.getTableName()+" \n" +
"SET \n" +
" MinY = ? \n" +
" ,DataChecksum = ? \n" +
" ,Data = ? \n" +
" ,ColumnGenerationStep = ? \n" +
" ,ColumnWorldCompressionMode = ? \n" +
" ,Mapping = ? \n" +
" ,DataFormatVersion = ? \n" +
" ,CompressionMode = ? \n" +
// only update these values if they're present
(dto.applyToParent != null ? " ,ApplyToParent = ? \n" : "" ) +
(dto.applyToChildren != null ? " ,ApplyToChildren = ? \n" : "" ) +
" ,LastModifiedUnixDateTime = ? \n" +
" ,CreatedUnixDateTime = ? \n" +
"WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?"
// intern should help reduce memory overhead due to this string being dynamic
).intern();
PreparedStatement statement = this.createPreparedStatement(updateSqlTemplate);
if (statement == null) if (statement == null)
{ {
return null; return null;
@@ -211,24 +226,31 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
int i = 1; int i = 1;
statement.setObject(i++, dto.levelMinY); statement.setInt(i++, dto.levelMinY);
statement.setObject(i++, dto.dataChecksum); statement.setInt(i++, dto.dataChecksum);
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedDataByteArray.elements()), dto.compressedDataByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedDataByteArray.elements()), dto.compressedDataByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedColumnGenStepByteArray.elements()), dto.compressedColumnGenStepByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedColumnGenStepByteArray.elements()), dto.compressedColumnGenStepByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedWorldCompressionModeByteArray.elements()), dto.compressedWorldCompressionModeByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedWorldCompressionModeByteArray.elements()), dto.compressedWorldCompressionModeByteArray.size());
statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedMappingByteArray.elements()), dto.compressedMappingByteArray.size()); statement.setBinaryStream(i++, new ByteArrayInputStream(dto.compressedMappingByteArray.elements()), dto.compressedMappingByteArray.size());
statement.setObject(i++, dto.dataFormatVersion); statement.setByte(i++, dto.dataFormatVersion);
statement.setObject(i++, dto.compressionModeValue); statement.setByte(i++, dto.compressionModeValue);
statement.setObject(i++, dto.applyToParent); if (dto.applyToParent != null)
{
statement.setBoolean(i++, dto.applyToParent);
}
if (dto.applyToChildren != null)
{
statement.setBoolean(i++, dto.applyToChildren);
}
statement.setObject(i++, System.currentTimeMillis()); // last modified unix time statement.setLong(i++, System.currentTimeMillis()); // last modified unix time
statement.setObject(i++, dto.createdUnixDateTime); statement.setLong(i++, dto.createdUnixDateTime);
statement.setObject(i++, DhSectionPos.getDetailLevel(dto.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); statement.setInt(i++, DhSectionPos.getDetailLevel(dto.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
statement.setObject(i++, DhSectionPos.getX(dto.pos)); statement.setInt(i++, DhSectionPos.getX(dto.pos));
statement.setObject(i++, DhSectionPos.getZ(dto.pos)); statement.setInt(i++, DhSectionPos.getZ(dto.pos));
return statement; return statement;
} }
@@ -239,23 +261,35 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
// updates // // updates //
//=========// //=========//
/** should be be very similar to {@link FullDataSourceV2Repo#setApplyToChildrenSql} */
private final String setApplyToParentSql = private final String setApplyToParentSql =
"UPDATE "+this.getTableName()+" \n" + "UPDATE "+this.getTableName()+" \n" +
"SET ApplyToParent = ? \n" + "SET ApplyToParent = ? \n" +
"WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?"; "WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?";
public void setApplyToParent(long pos, boolean applyToParent) public void setApplyToParent(long pos, boolean applyToParent)
{ this.setApplyToFlag(pos, applyToParent, true); }
/** should be be very similar to {@link FullDataSourceV2Repo#setApplyToParentSql} */
private final String setApplyToChildrenSql =
"UPDATE "+this.getTableName()+" \n" +
"SET ApplyToChildren = ? \n" +
"WHERE DetailLevel = ? AND PosX = ? AND PosZ = ?";
public void setApplyToChild(long pos, boolean applyToChild)
{ this.setApplyToFlag(pos, applyToChild, false); }
private void setApplyToFlag(long pos, boolean applyFlag, boolean applyToParent)
{ {
PreparedStatement statement = this.createPreparedStatement(this.setApplyToParentSql); String sql = applyToParent ? this.setApplyToParentSql : this.setApplyToChildrenSql;
if (statement == null) try (PreparedStatement statement = this.createPreparedStatement(sql))
{ {
return; if (statement == null)
} {
return;
}
try
{
int i = 1; int i = 1;
statement.setBoolean(i++, applyToParent); statement.setBoolean(i++, applyFlag);
int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
statement.setInt(i++, detailLevel); statement.setInt(i++, detailLevel);
@@ -273,26 +307,43 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
} }
} }
private final String getPositionsToUpdateSql =
/** should be be very similar to {@link FullDataSourceV2Repo#getChildPositionsToUpdateSql} */
private final String getParentPositionsToUpdateSql =
"SELECT DetailLevel, PosX, PosZ, " + "SELECT DetailLevel, PosX, PosZ, " +
" (sqrt(pow(PosX - ?, 2) + pow(PosZ - ?, 2))) AS Distance " + " abs((PosX << (6 + DetailLevel)) - ?) + abs((PosZ << (6 + DetailLevel)) - ?) AS Distance " +
"FROM "+this.getTableName()+" " + "FROM " + this.getTableName() + " " +
"WHERE ApplyToParent = 1 " + "WHERE ApplyToParent = 1 " +
"ORDER BY Distance ASC " + "ORDER BY DetailLevel ASC, Distance ASC " +
"LIMIT ?; "; "LIMIT ?; ";
public LongArrayList getPositionsToUpdate(int targetBlockPosX, int targetBlockPosZ, int returnCount) public LongArrayList getPositionsToUpdate(int targetBlockPosX, int targetBlockPosZ, int returnCount)
{ return this.getPositionsToUpdate(targetBlockPosX, targetBlockPosZ, returnCount, true); }
/** should be be very similar to {@link FullDataSourceV2Repo#getParentPositionsToUpdateSql} */
private final String getChildPositionsToUpdateSql =
"SELECT DetailLevel, PosX, PosZ, " +
" abs((PosX << (6 + DetailLevel)) - ?) + abs((PosZ << (6 + DetailLevel)) - ?) AS Distance " +
"FROM " + this.getTableName() + " " +
"WHERE ApplyToChildren = 1 " +
"ORDER BY DetailLevel ASC, Distance ASC " +
"LIMIT ?; ";
public LongArrayList getChildPositionsToUpdate(int targetBlockPosX, int targetBlockPosZ, int returnCount)
{ return this.getPositionsToUpdate(targetBlockPosX, targetBlockPosZ, returnCount, false); }
private LongArrayList getPositionsToUpdate(int targetBlockPosX, int targetBlockPosZ, int returnCount, boolean getParentUpdates)
{ {
LongArrayList list = new LongArrayList(); LongArrayList list = new LongArrayList();
String sql = getParentUpdates ? this.getParentPositionsToUpdateSql : this.getChildPositionsToUpdateSql;
PreparedStatement statement = this.createPreparedStatement(this.getPositionsToUpdateSql); try (PreparedStatement statement = this.createPreparedStatement(sql))
if (statement == null)
{ {
return list; if (statement == null)
} {
return list;
}
try
{
int i = 1; int i = 1;
statement.setInt(i++, targetBlockPosX); statement.setInt(i++, targetBlockPosX);
statement.setInt(i++, targetBlockPosZ); statement.setInt(i++, targetBlockPosZ);
@@ -322,6 +373,7 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
} }
private final String getColumnGenerationStepSql = private final String getColumnGenerationStepSql =
"select ColumnGenerationStep, CompressionMode " + "select ColumnGenerationStep, CompressionMode " +
"from "+this.getTableName()+" " + "from "+this.getTableName()+" " +
@@ -329,15 +381,14 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
/** @return null if nothing exists for this position */ /** @return null if nothing exists for this position */
public void getColumnGenerationStepForPos(long pos, ByteArrayList outputByteArray) public void getColumnGenerationStepForPos(long pos, ByteArrayList outputByteArray)
{ {
PreparedStatement statement = this.createPreparedStatement(this.getColumnGenerationStepSql); try (PreparedStatement statement = this.createPreparedStatement(this.getColumnGenerationStepSql))
if (statement == null)
{ {
return; if (statement == null)
} {
return;
}
try
{
int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
@@ -395,9 +446,8 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
@Nullable @Nullable
public Long getTimestampForPos(long pos) public Long getTimestampForPos(long pos)
{ {
try try(PreparedStatement preparedStatement = this.createPreparedStatement(this.getTimestampForPosSql))
{ {
PreparedStatement preparedStatement = this.createPreparedStatement(this.getTimestampForPosSql);
if (preparedStatement == null) if (preparedStatement == null)
{ {
return null; return null;
@@ -437,9 +487,8 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
"AND PosZ BETWEEN ? AND ?;"; "AND PosZ BETWEEN ? AND ?;";
public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ) public Map<Long, Long> getTimestampsForRange(byte detailLevel, int startPosX, int startPosZ, int endPosX, int endPosZ)
{ {
try try(PreparedStatement preparedStatement = this.createPreparedStatement(this.getTimestampForRangeSql))
{ {
PreparedStatement preparedStatement = this.createPreparedStatement(this.getTimestampForRangeSql);
if (preparedStatement == null) if (preparedStatement == null)
{ {
return new HashMap<>(); return new HashMap<>();
@@ -488,27 +537,29 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
{ {
LongArrayList list = new LongArrayList(); LongArrayList list = new LongArrayList();
PreparedStatement statement = this.createPreparedStatement(getAllPositionsSql); try (PreparedStatement statement = this.createPreparedStatement(this.getAllPositionsSql))
if (statement == null)
{ {
return list; if (statement == null)
}
try(ResultSet result = this.query(statement))
{
while (result != null && result.next())
{ {
byte detailLevel = result.getByte("DetailLevel"); return list;
byte sectionDetailLevel = (byte) (detailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
int posX = result.getInt("PosX");
int posZ = result.getInt("PosZ");
long pos = DhSectionPos.encode(sectionDetailLevel, posX, posZ);
list.add(pos);
} }
return list;
try(ResultSet result = this.query(statement))
{
while (result != null && result.next())
{
byte detailLevel = result.getByte("DetailLevel");
byte sectionDetailLevel = (byte) (detailLevel + DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
int posX = result.getInt("PosX");
int posZ = result.getInt("PosZ");
long pos = DhSectionPos.encode(sectionDetailLevel, posX, posZ);
list.add(pos);
}
return list;
}
} }
catch (SQLException e) catch (SQLException e)
{ {
@@ -529,14 +580,13 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
{ {
int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; int detailLevel = DhSectionPos.getDetailLevel(pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
PreparedStatement statement = this.createPreparedStatement(this.getDataSizeInBytesSql); try (PreparedStatement statement = this.createPreparedStatement(this.getDataSizeInBytesSql))
if (statement == null)
{ {
return 0L; if (statement == null)
} {
return 0L;
}
try
{
int i = 1; int i = 1;
statement.setInt(i++, detailLevel); statement.setInt(i++, detailLevel);
statement.setInt(i++, DhSectionPos.getX(pos)); statement.setInt(i++, DhSectionPos.getX(pos));
@@ -565,9 +615,8 @@ public class FullDataSourceV2Repo extends AbstractDhRepo<Long, FullDataSourceV2D
/** @return the total size in bytes of the full data for this entire database */ /** @return the total size in bytes of the full data for this entire database */
public long getTotalDataSizeInBytes() public long getTotalDataSizeInBytes()
{ {
PreparedStatement statement = this.createPreparedStatement(this.getTotalDataSizeInBytesSql); try (PreparedStatement statement = this.createPreparedStatement(this.getTotalDataSizeInBytesSql);
ResultSet result = this.query(statement))
try(ResultSet result = this.query(statement))
{ {
if (result == null || !result.next()) if (result == null || !result.next())
{ {
@@ -0,0 +1,102 @@
package com.seibel.distanthorizons.core.sql.repo.phantoms;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Set;
/**
* This is used to detect leaks
* with our JDBC implementation, specifically
* to make sure all {@link AutoCloseable} objects
* are inside try-finally resources.
*/
public class AutoClosableTrackingWrapper implements InvocationHandler
{
//private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/**
* should be enabled during development to
* notify if any resources are being leaked.
*/
public static final boolean TRACK_WRAPPERS = ModInfo.IS_DEV_BUILD;
@NotNull
public final AutoCloseable wrappedClosable;
@NotNull
public final Set<AutoClosableTrackingWrapper> parentTrackingSet;
//==============//
// constructors //
//==============//
@Nullable
public static <TStatic extends AutoCloseable> TStatic wrap(
@NotNull Class<?> clazz,
@Nullable TStatic wrappedClosable,
@NotNull Set<AutoClosableTrackingWrapper> trackingSet)
{
if (!TRACK_WRAPPERS)
{
return wrappedClosable;
}
// done to prevent null pointers
if (wrappedClosable == null)
{
return null;
}
return (TStatic) Proxy.newProxyInstance(
wrappedClosable.getClass().getClassLoader(),
new Class[]{ clazz },
new AutoClosableTrackingWrapper(wrappedClosable, trackingSet)
);
}
private AutoClosableTrackingWrapper(@NotNull AutoCloseable wrappedClosable, @NotNull Set<AutoClosableTrackingWrapper> trackingSet)
{
this.wrappedClosable = wrappedClosable;
this.parentTrackingSet = trackingSet;
this.parentTrackingSet.add(this);
}
//============//
// reflection //
//============//
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
// Track the close() method
if ("close".equals(method.getName()))
{
this.wrappedClosable.close();
this.parentTrackingSet.remove(this);
return null;
}
try
{
// Delegate all other methods to the wrapped object
return method.invoke(this.wrappedClosable, args);
}
catch (InvocationTargetException e)
{
// get the target so we can filter the exception correctly up-stream
throw e.getTargetException();
}
}
}
@@ -0,0 +1,20 @@
package com.seibel.distanthorizons.core.util;
public class BoolUtil
{
/** Used to prevent null {@link Boolean} objects in if statements */
public static boolean falseIfNull(Boolean value)
{
if (value == null)
{
// default to false since null doesn't mean true in any context
// (Even in JavaScript)
return false;
}
else
{
return value;
}
}
}
@@ -0,0 +1,16 @@
package com.seibel.distanthorizons.core.util;
import java.time.Duration;
public class FormatUtil
{
public static String formatEta(Duration duration)
{
return duration.toString()
.substring(2)
.replaceAll("(\\d[HMS])(?!$)", "$1 ")
.replaceAll("\\.\\d+", "")
.toLowerCase();
}
}
@@ -137,8 +137,6 @@ public class RenderDataPointReducingList extends PhantomArrayListParent
this.sortingArray = this.pooledArraysCheckout.getShortArray(0, 0); this.sortingArray = this.pooledArraysCheckout.getShortArray(0, 0);
if (ASSERTS) this.checkLinks(); if (ASSERTS) this.checkLinks();
this.pooledArraysCheckout = null;
return; return;
} }
@@ -154,8 +152,6 @@ public class RenderDataPointReducingList extends PhantomArrayListParent
java.util.Arrays.fill(this.links.elements(), DEFAULT_LINKS); java.util.Arrays.fill(this.links.elements(), DEFAULT_LINKS);
this.data = this.pooledArraysCheckout.getLongArray(1, arrayCapacity); this.data = this.pooledArraysCheckout.getLongArray(1, arrayCapacity);
this.pooledArraysCheckout = null;
int sizeWithoutAir = 0; int sizeWithoutAir = 0;
for (int index = 0; index < size; index++) for (int index = 0; index < size; index++)
{ {
@@ -92,7 +92,9 @@ public class RenderUtil
// but far enough the fading will rarely overlap (IE only at extreme FOV) // but far enough the fading will rarely overlap (IE only at extreme FOV)
return getNearClipPlaneDistanceInBlocks(partialTicks, 0.2f); return getNearClipPlaneDistanceInBlocks(partialTicks, 0.2f);
} }
public static float getNearClipPlaneInBlocksForFading(float partialTicks) /** TODO this should be moved into the config file or something, this is confusing and obtuse to use */
@Deprecated
public static float getAutoOverdrawPrevention()
{ {
float overdraw = Config.Client.Advanced.Graphics.Culling.overdrawPrevention.get().floatValue(); float overdraw = Config.Client.Advanced.Graphics.Culling.overdrawPrevention.get().floatValue();
@@ -123,6 +125,11 @@ public class RenderUtil
} }
} }
return overdraw;
}
public static float getNearClipPlaneInBlocksForFading(float partialTicks)
{
float overdraw = getAutoOverdrawPrevention();
return getNearClipPlaneDistanceInBlocks(partialTicks, overdraw); return getNearClipPlaneDistanceInBlocks(partialTicks, overdraw);
} }
private static float getNearClipPlaneDistanceInBlocks(float partialTicks, float overdrawPreventionPercent) private static float getNearClipPlaneDistanceInBlocks(float partialTicks, float overdrawPreventionPercent)
@@ -223,6 +230,9 @@ public class RenderUtil
return "No Client World Loaded"; return "No Client World Loaded";
} }
// TODO changing to getOrLoadClientLevel() fixes Immersive Portals only rendering the level the user starts in
// however this may break how other level handling is done so James doesn't want to change it.
// Special handling may be necessary when Immersive Portals is present, although additional testing is needed.
IDhClientLevel level = clientWorld.getClientLevel(levelWrapper); IDhClientLevel level = clientWorld.getClientLevel(levelWrapper);
if (level == null) if (level == null)
{ {
@@ -24,7 +24,7 @@ public class PositionalLockProvider
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private static final ThreadPoolExecutor LOCK_CLEANUP_THREAD = ThreadUtil.makeSingleThreadPool("Positional Lock Cleanup"); private static final ThreadPoolExecutor LOCK_CLEANUP_THREAD = ThreadUtil.makeSingleDaemonThreadPool("Positional Lock Cleanup");
private static final int CLEANUP_THREAD_MAX_FREQUENCY_IN_MS = 1000; private static final int CLEANUP_THREAD_MAX_FREQUENCY_IN_MS = 1000;
/** How long a lock can be unused before it is eligible for deletion */ /** How long a lock can be unused before it is eligible for deletion */
@@ -2,18 +2,23 @@ package com.seibel.distanthorizons.core.util.threading;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.types.ConfigEntry; import com.seibel.distanthorizons.core.config.types.ConfigEntry;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
public class PriorityTaskPicker public class PriorityTaskPicker
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
private final ConfigEntry<Integer> threadCountConfig = Config.Common.MultiThreading.numberOfThreads; private final ConfigEntry<Integer> threadCountConfig = Config.Common.MultiThreading.numberOfThreads;
private final RateLimitedThreadPoolExecutor threadPoolExecutor = new RateLimitedThreadPoolExecutor( private final RateLimitedThreadPoolExecutor threadPoolExecutor = new RateLimitedThreadPoolExecutor(
@@ -22,18 +27,15 @@ public class PriorityTaskPicker
new ArrayBlockingQueue<>(this.threadCountConfig.getMax()) new ArrayBlockingQueue<>(this.threadCountConfig.getMax())
); );
// Queue of executors, used to distribute tasks across executors based on priority // List of executors
private final ArrayList<Executor> executorQueue = new ArrayList<>(); private final ArrayList<Executor> executors = new ArrayList<>();
private int nextExecutorQueuePos = 0;
// Lock to ensure task picking logic is thread-safe // Lock to ensure task picking logic is thread-safe
private final ReentrantLock taskPickerLock = new ReentrantLock(); private final ReentrantLock taskPickerLock = new ReentrantLock();
// Indicates whether a task picking attempt is needed
private final AtomicBoolean shouldPickTask = new AtomicBoolean(false);
// Tracks the number of active threads // Tracks the number of active threads
private final AtomicInteger occupiedThreads = new AtomicInteger(0); private final AtomicInteger occupiedThreads = new AtomicInteger(0);
private volatile boolean isShutDown = false; private final AtomicBoolean isShutDownRef = new AtomicBoolean(false);
@@ -42,26 +44,14 @@ public class PriorityTaskPicker
//==================// //==================//
/** /**
* Creates an executor with a specific priority. * Creates an executor.
* Higher priority executors have more exponentially entries in the distribution queue, giving them a greater chance to run tasks.
* *
* @param priority the priority level of the executor
* @return a newly created Executor * @return a newly created Executor
*/ */
public Executor createExecutor(int priority) public Executor createExecutor()
{ {
Executor executor = new Executor(); Executor executor = new Executor();
this.executors.add(executor);
int entriesToAdd = BitShiftUtil.powerOfTwo(priority);
int gapBetweenEntries = (int) (1 / (double) entriesToAdd * this.executorQueue.size());
// Distribute the executor's entries in the queue, ensuring fair distribution
for (; entriesToAdd > 0; entriesToAdd--)
{
this.executorQueue.add(executor);
Collections.rotate(this.executorQueue, -gapBetweenEntries);
}
return executor; return executor;
} }
@@ -71,33 +61,16 @@ public class PriorityTaskPicker
*/ */
private void tryStartNextTask() private void tryStartNextTask()
{ {
this.shouldPickTask.set(true); if (this.taskPickerLock.tryLock())
while (this.taskPickerLock.tryLock())
{ {
try try
{ {
// Exit if there's no longer a need to pick a task // TODO limit based on thread count so visual VM is easier to parse
if (!this.shouldPickTask.compareAndSet(true, false)) for (Executor executor : (Iterable<? extends Executor>) this.executors.stream().sorted(Comparator.comparingLong(executor -> executor.totalRuntimeNanos.get()))::iterator)
{ {
// There is a small chance for a task to end up in a 'limbo' state, TrackedRunnable task;
// when this.shouldPickTask got set to true right here and this.taskPickerLock is not unlocked yet,
// but we'll disregard that since tasks get added often enough for this to not be an issue
return; while (this.occupiedThreads.get() < this.threadCountConfig.get() && (task = executor.tasks.poll()) != null)
}
// Iterate over the executors in the queue, attempting to start tasks
for (
int taskPickAttempts = 0;
taskPickAttempts < this.executorQueue.size() && this.occupiedThreads.get() < this.threadCountConfig.get();
taskPickAttempts++, this.nextExecutorQueuePos = (this.nextExecutorQueuePos + 1) % this.executorQueue.size()
)
{
Executor executor = this.executorQueue.get(this.nextExecutorQueuePos);
TrackedRunnable task = executor.tasks.poll();
if (task != null)
{ {
try try
{ {
@@ -107,13 +80,10 @@ public class PriorityTaskPicker
// Update variables related to task status // Update variables related to task status
this.occupiedThreads.getAndIncrement(); this.occupiedThreads.getAndIncrement();
executor.runningTasks.getAndIncrement(); executor.runningTasks.getAndIncrement();
// Prevent exiting early since there might be more than this.executorQueue.size() tasks waiting in queue
taskPickAttempts = 0;
} }
catch (RejectedExecutionException e) catch (RejectedExecutionException e)
{ {
if (this.isShutDown) if (this.isShutDownRef.get())
{ {
// Clear executor's tasks since we no longer expect anything to execute // Clear executor's tasks since we no longer expect anything to execute
// Tasks from other executors will be cleared by the outer for loop // Tasks from other executors will be cleared by the outer for loop
@@ -136,12 +106,11 @@ public class PriorityTaskPicker
} }
} }
/** /** Shuts down the thread pool immediately, stopping all tasks. */
* Shuts down the thread pool immediately, stopping all tasks.
*/
public void shutdown() public void shutdown()
{ {
this.isShutDown = true; LOGGER.info("Shutting down PriorityTaskPicker thread pool...");
this.isShutDownRef.set(true);
try try
{ {
@@ -170,6 +139,7 @@ public class PriorityTaskPicker
private final AtomicInteger runningTasks = new AtomicInteger(0); private final AtomicInteger runningTasks = new AtomicInteger(0);
private final AtomicInteger completedTasks = new AtomicInteger(0); private final AtomicInteger completedTasks = new AtomicInteger(0);
private final RollingAverage runTimeInMsRollingAverage = new RollingAverage(200); private final RollingAverage runTimeInMsRollingAverage = new RollingAverage(200);
private final AtomicLong totalRuntimeNanos = new AtomicLong(0);
@Override @Override
@@ -241,6 +211,7 @@ public class PriorityTaskPicker
PriorityTaskPicker.this.occupiedThreads.getAndDecrement(); PriorityTaskPicker.this.occupiedThreads.getAndDecrement();
this.executor.runningTasks.getAndDecrement(); this.executor.runningTasks.getAndDecrement();
this.executor.completedTasks.getAndIncrement(); this.executor.completedTasks.getAndIncrement();
this.executor.totalRuntimeNanos.addAndGet(timeElapsed);
// Attempt to start another task // Attempt to start another task
PriorityTaskPicker.this.tryStartNextTask(); PriorityTaskPicker.this.tryStartNextTask();
@@ -92,18 +92,11 @@ public class ThreadPoolUtil
// thread pools // thread pools
taskPicker = new PriorityTaskPicker(); taskPicker = new PriorityTaskPicker();
// IO should never be stuck waiting for something else to complete networkCompressionThreadPool = taskPicker.createExecutor();
networkCompressionThreadPool = taskPicker.createExecutor(4); fileHandlerThreadPool = taskPicker.createExecutor();
fileHandlerThreadPool = taskPicker.createExecutor(4); chunkToLodBuilderThreadPool = taskPicker.createExecutor();
updatePropagatorThreadPool = taskPicker.createExecutor();
// Normal priority tasks worldGenThreadPool = taskPicker.createExecutor();
chunkToLodBuilderThreadPool = taskPicker.createExecutor(3);
updatePropagatorThreadPool = taskPicker.createExecutor(2);
// World gen tasks are heavy and nothing strictly depends on them, so they may wait a bit
worldGenThreadPool = taskPicker.createExecutor(0);
// single thread pools // single thread pools
beaconCullingThreadPool = ThreadUtil.makeSingleThreadPool(BEACON_CULLING_THREAD_NAME); beaconCullingThreadPool = ThreadUtil.makeSingleThreadPool(BEACON_CULLING_THREAD_NAME);
@@ -31,19 +31,6 @@ import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindab
*/ */
public interface IVersionConstants extends IBindable public interface IVersionConstants extends IBindable
{ {
/** @return the minimum height blocks can be generated */
@Deprecated // This changes per world!
int getMinimumWorldHeight();
/** @return the number of generations call per thread. */
@Deprecated // No longer used
default int getWorldGenerationCountPerThread()
{
return 8;
}
boolean isVanillaRenderedChunkSquare();
String getMinecraftVersion(); String getMinecraftVersion();
} }
@@ -79,8 +79,12 @@ public interface IMinecraftClientWrapper extends IBindable
String getUsername(); String getUsername();
// TODO returning null would be easier to understand but might make things harder to parse in some cases
/** @return (0,0,0) if no player is loaded */
DhBlockPos getPlayerBlockPos(); DhBlockPos getPlayerBlockPos();
// TODO returning null would be easier to understand but might make things harder to parse in some cases
/** @return (0,0) if no player is loaded */
DhChunkPos getPlayerChunkPos(); DhChunkPos getPlayerChunkPos();
/** /**
@@ -27,9 +27,13 @@ public interface IDimensionTypeWrapper extends IDhApiDimensionTypeWrapper, IBind
@Override @Override
boolean hasCeiling(); boolean hasCeiling();
String getName();
@Override @Override
boolean hasSkyLight(); boolean hasSkyLight();
boolean isTheEnd(); boolean isTheEnd();
double getCoordinateScale();
} }
@@ -203,13 +203,16 @@
"distanthorizons.config.client.advanced.graphics.genericRendering": "distanthorizons.config.client.advanced.graphics.genericRendering":
"Generic Object Rendering", "Generic Object Rendering",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableGenericRendering": "distanthorizons.config.client.advanced.graphics.genericRendering.enableGenericRendering":
"Enable Rendering", "Enable Rendering",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableGenericRendering.@tooltip": "distanthorizons.config.client.advanced.graphics.genericRendering.enableGenericRendering.@tooltip":
"If true non terrain objects will be rendered in DH's terrain. \nThis includes beacon beams and clouds.", "If true non terrain objects will be rendered in DH's terrain. \nThis includes beacon beams and clouds.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering": "distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering":
"Enable Beacon Rendering", "Enable Beacon Rendering",
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight":
"Beacon render height",
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight.@tooltip":
"Sets the maximum height at which beacons will render. \nThis will only affect new beacons coming into LOD render distance. \nBeacons currently visible in LOD chunks will not be affected.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering.@tooltip": "distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering.@tooltip":
"If true LOD beacon beams will be rendered.", "If true LOD beacon beams will be rendered.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableCloudRendering": "distanthorizons.config.client.advanced.graphics.genericRendering.enableCloudRendering":
@@ -587,6 +590,10 @@
"Progress Display Interval In Seconds", "Progress Display Interval In Seconds",
"distanthorizons.config.common.worldGenerator.generationProgressDisplayIntervalInSeconds.@tooltip": "distanthorizons.config.common.worldGenerator.generationProgressDisplayIntervalInSeconds.@tooltip":
"Determines how long between progress update displays.", "Determines how long between progress update displays.",
"distanthorizons.config.common.worldGenerator.generationProgressDisableMessageDisplayTimeInSeconds":
"Seconds To Show Progress Hiding Instructions",
"distanthorizons.config.common.worldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.@tooltip":
"For how many seconds should instructions for disabling the distant generator progress be displayed? \nSetting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.",
"distanthorizons.config.common.lodBuilding": "distanthorizons.config.common.lodBuilding":
@@ -620,6 +627,16 @@
"Log Migration In Chat", "Log Migration In Chat",
"distanthorizons.config.common.lodBuilding.experimental":
"Experimental",
"distanthorizons.config.common.lodBuilding.experimental.upsampleLowerDetailLodsToFillHoles":
"Upsample Lower Detail LODs To Fill Holes",
"distanthorizons.config.common.lodBuilding.experimental.upsampleLowerDetailLodsToFillHoles.@tooltip":
"When active DH will attempt to fill missing LOD data \nwith any data that is present in the tree, preventing holes when moving \nwhen a N-sized generator (or server) is active. \n\n§6EXPERIMENTAL§r Will increase harddrive use and may cause rendering issues. \nSee the config file for more details.",
"distanthorizons.config.common.multiThreading": "distanthorizons.config.common.multiThreading":
"Multi-Threading", "Multi-Threading",
@@ -727,6 +744,15 @@
"Maximum speed for uploading LODs to the clients, in KB/s.\nValue of 0 disables the limit.", "Maximum speed for uploading LODs to the clients, in KB/s.\nValue of 0 disables the limit.",
"distanthorizons.config.server.experimental":
"Experimental",
"distanthorizons.config.server.experimental.enableNSizedGeneration":
"Enable N-sized generation",
"distanthorizons.config.server.experimental.enableNSizedGeneration.@tooltip":
"When enabled on the client, this allows loading lower detail levels as needed to speed up terrain generation.\nThis must also be enabled on the server; otherwise, it will have no effect.\nFor better performance when switching LOD detail levels, enabling [upsampleLowerDetailLodsToFillHoles] is recommended.",
@@ -0,0 +1,9 @@
-- Applying to children is needed to fix a bug with N-sized generation.
-- If we don't fill the whole tree with data, it's possible to render empty/incomplete LODs, which looks bad.
alter table FullData add column ApplyToChildren BIT NULL;
--batch--
-- significantly speeds up update handling
create index FullDataApplyToChildrenIndex on FullData (ApplyToChildren) where ApplyToChildren = 1;
@@ -7,3 +7,4 @@
0050-sqlite-addApplyToParentIndex.sql 0050-sqlite-addApplyToParentIndex.sql
0060-sqlite-createChunkHashTable.sql 0060-sqlite-createChunkHashTable.sql
0070-sqlite-createBeaconBeamTable.sql 0070-sqlite-createBeaconBeamTable.sql
0080-sqlite-addApplyToChildrenColumn.sql
+127 -1
View File
@@ -19,9 +19,11 @@
package tests; package tests;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.sql.DatabaseUpdater; import com.seibel.distanthorizons.core.sql.DatabaseUpdater;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import org.apache.logging.log4j.Logger;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
@@ -34,13 +36,14 @@ import java.io.File;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Map;
/** /**
* Validates {@link AbstractDhRepo} is set up correctly. * Validates {@link AbstractDhRepo} is set up correctly.
*/ */
public class DhRepoSqliteTest public class DhRepoSqliteTest
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static String DATABASE_TYPE = "jdbc:sqlite"; public static String DATABASE_TYPE = "jdbc:sqlite";
public static String DB_FILE_NAME = "test.sqlite"; public static String DB_FILE_NAME = "test.sqlite";
@@ -208,5 +211,128 @@ public class DhRepoSqliteTest
} }
} }
/**
* leak detection is done to make sure {@link ResultSet} and {@link PreparedStatement}'s
* are properly cleaned up.
*/
@Test
public void testRepoLeakDetection()
{
TestPrimaryKeyRepo primaryKeyRepo = null;
try
{
primaryKeyRepo = new TestPrimaryKeyRepo(DATABASE_TYPE, new File(DB_FILE_NAME));
int insertCount = 10;
int readCount = 10;
Assert.assertEquals(0, primaryKeyRepo.openClosables.size());
//=============================//
// correctly closed statements //
//=============================//
{
// insert
for (int i = 0; i < insertCount; i++)
{
TestSingleKeyDto insertDto = new TestSingleKeyDto(i, "a", 0L, (byte) 0);
try (PreparedStatement statement = primaryKeyRepo.createInsertStatement(insertDto))
{
primaryKeyRepo.query(statement);
if (i % 1_000 == 0)
{
System.out.println(i + " / " + insertCount);
}
}
}
Assert.assertEquals("Insert leaks", 0, primaryKeyRepo.openClosables.size());
// read
TestSingleKeyDto expectedReadDto = new TestSingleKeyDto(1, "a", 0L, (byte) 0);
for (int i = 0; i < readCount; i++)
{
try (PreparedStatement statement = primaryKeyRepo.createSelectStatementByKey(1);
ResultSet resultSet = primaryKeyRepo.query(statement))
{
TestSingleKeyDto readDto = primaryKeyRepo.convertResultSetToDto(resultSet);
Assert.assertEquals(expectedReadDto.id, readDto.id);
if (i % 1_000 == 0)
{
System.out.println(i + " / " + readCount);
}
}
}
Assert.assertEquals("read leaks", 0, primaryKeyRepo.openClosables.size());
}
//===================//
// leaked statements //
//===================//
{
// nuke the DB so we can insert without worries
primaryKeyRepo.deleteAll();
// insert
for (int i = 0; i < insertCount; i++)
{
TestSingleKeyDto insertDto = new TestSingleKeyDto(i, "a", 0L, (byte) 0);
PreparedStatement statement = primaryKeyRepo.createInsertStatement(insertDto);
primaryKeyRepo.query(statement);
if (i % 1_000 == 0)
{
System.out.println(i + " / " + insertCount);
}
}
//Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size()); // TODO fails when built for release due to tracking being disabled
primaryKeyRepo.openClosables.clear();
// read
for (int i = 0; i < readCount; i++)
{
PreparedStatement statement = primaryKeyRepo.createSelectStatementByKey(1);
ResultSet resultSet = primaryKeyRepo.query(statement);
TestSingleKeyDto readDto = primaryKeyRepo.convertResultSetToDto(resultSet);
Assert.assertEquals(1, readDto.id);
if (i % 1_000 == 0)
{
System.out.println(i + " / " + readCount);
}
}
//Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size());
}
}
catch (SQLException e)
{
Assert.fail(e.getMessage());
}
finally
{
if (primaryKeyRepo != null)
{
primaryKeyRepo.close();
}
}
}
} }