Compare commits

..

86 Commits

Author SHA1 Message Date
s809 034ec7d656 Bump protocol version 2025-08-16 21:01:45 +05:00
s809 fb5e15a2f1 Add a server keys feature 2025-08-16 20:59:28 +05:00
s809 674fc30e77 Replace pooled buffers with unpooled 2025-08-07 17:55:22 +05:00
s809 a05bd307f9 Reduce network logging by default 2025-07-27 23:21:13 +05:00
James Seibel d78a50ce49 up version number 2.3.4 -> 2.3.5 2025-07-19 14:59:14 -05:00
James Seibel 013eab9268 add space to self updater warning log 2025-07-19 14:57:22 -05:00
James Seibel 435cbde238 remove dev from version number 2025-07-19 14:56:53 -05:00
s809 d7040bad13 Load level on player add if missing 2025-07-19 19:05:52 +05:00
James Seibel a588070ce1 up version number 2.3.3 -> 2.3.4 2025-07-12 09:35:05 -05:00
James Seibel d156772438 remove dev from the version number 2025-07-12 09:34:18 -05:00
James Seibel de7ae41769 Fix API config renderingEnabled() changing the user value
Fixes #1083
2025-07-12 08:16:35 -05:00
James Seibel 618ad1938b full data DTO close data source if corrupted 2025-07-10 22:24:33 -05:00
James Seibel 5b10263f82 minor format cleanup 2025-07-10 07:27:23 -05:00
James Seibel 34f914c52f Mark a unit test as deprecated
Done to suppress warnings in compiler log
2025-07-10 07:26:41 -05:00
James Seibel 67b766c674 Fix monoliths when connected to a server 2025-07-10 07:26:14 -05:00
James Seibel a3e7469203 Fixes !1078 (lag due to beacon updating on server) 2025-07-09 07:28:25 -05:00
James Seibel 4ecaa6a9a1 Potentially fix an issue with AMD GPU shader compiling
Fix from Cortex and the Canvas mod
2025-07-08 07:22:36 -05:00
James Seibel 24f9dadc58 Disable vanilla fading when shaders are active 2025-07-07 07:49:36 -05:00
s809 b3ebaffa85 Disable enableAdaptiveTransferSpeed bby default 2025-07-01 22:03:12 +05:00
James Seibel b7ac1909d6 Fix config UI changes not always saving 2025-07-01 07:45:26 -05:00
James Seibel 32c3118afa comment out Z_STD compression 2025-06-30 06:54:27 -05:00
James Seibel 3a525f53f0 Rename world gen mode "Internal Server" -> "Full - Save Chunks" 2025-06-28 13:57:58 -05:00
James Seibel f3947312c1 Re-Add Z_STD compression for testing 2025-06-28 11:37:06 -05:00
James Seibel 839ea1e778 increase ram amount for unit tests
needed for compression tests
2025-06-28 11:36:17 -05:00
James Seibel d2becd2c03 Fix rare fade error when restarting the LodRenderer 2025-06-28 10:45:36 -05:00
James Seibel 7d87347199 Fix multiplayer null pointer 2025-06-28 09:22:23 -05:00
James Seibel f4117751c9 Fix world-gen progress not showing in release builds 2025-06-27 07:29:31 -05:00
James Seibel a8a085f296 Move RenderState to core 2025-06-26 07:50:53 -05:00
James Seibel 317319593e rename renderDeferredLods -> renderDeferredLodsForShaders 2025-06-25 07:47:08 -05:00
James Seibel 4633f90a03 Add null handling to ServerPlayerStateManager.handlePluginMessage() 2025-06-25 07:45:43 -05:00
James Seibel 5802bbb3f3 keep cave culling for medium quality preset
This may be changed back at some point in the future, but depending on the usecase (IE vanilla survival) cave culling is generally better than not having it, and if people see weirdness they can probably guess that increasing the quality preset may fix it.
2025-06-23 07:23:26 -05:00
James Seibel e93d5b90f1 Disable cave culling for medium quality and higher 2025-06-17 07:15:28 -05:00
James Seibel 9be56607a5 Reduce stuttering with fast world gen 2025-06-14 16:17:28 -05:00
James Seibel 91743bf742 Add Api Before/After Text Create events
Deprecate DhApiColorDepthTextureCreatedEvent since it is less obvious when it fires
2025-06-09 07:50:21 -05:00
James Seibel d40d293f54 Fix hash collisions in FullDataPointIdMap 2025-06-06 07:43:38 -05:00
James Seibel a075e60e3e Fix GLMC.glDeleteTextures() calls 2025-06-04 07:07:39 -05:00
s809 d72c7c3695 Check LOD timestamps in file handler threads 2025-06-03 23:41:47 +05:00
Ran 309fa07664 Merge branch 'fix_max_y' into 'main'
Fix max Y validation

See merge request distant-horizons-team/distant-horizons-core!85
2025-05-18 00:32:51 +00:00
Stewart Borle 0a017567c4 Fix max Y validation 2025-05-18 00:32:51 +00:00
James Seibel e01261da5c Remove line ending from editorconfig
Done to fix some issues with some devs on linux
2025-05-17 11:47:00 -05:00
James Seibel a0879d07c5 json indent 2 -> 4
for consistency
2025-05-17 11:25:18 -05:00
Ran bbb15263f2 Fix gradle versioning 2025-05-03 11:21:05 +10:00
s809 5ca3563c66 Bump protocol version 2025-05-03 00:08:03 +05:00
s809 30256a2779 Send scaled generation bounds coordinates 2025-05-03 00:08:03 +05:00
Ran 4b4f10f5e6 Fix gradle versioning for core application 2025-05-02 12:44:25 +10:00
s809 ad995544f7 Use bytesReceived instead of decreasing multiplicatively 2025-04-20 23:59:34 +05:00
s809 d521e931f4 Change data send tick rate 4 -> 20 2025-04-20 18:26:07 +05:00
s809 dd30a8274a Add a config entry and refactor 2025-04-20 18:25:27 +05:00
s809 3ca5efadc9 Adaptive data transfer speed 2025-04-20 03:02:18 +05:00
Ran 09174c2d2a Improve LodDataBuilder.java
- Use bitwise modulo
- Don't compute certain things 256 times when they can be computed once.
- Removed expressions that are always false
- Improved comments
2025-04-11 11:24:16 +10:00
James Seibel e079b28e77 maybe break n-sized rendering but fix LOD loading getting stuck 2025-04-07 06:56:53 -05:00
James Seibel 136124a703 up version number 2.3.2 -> 2.3.3 2025-04-05 09:11:19 -05:00
James Seibel 3ed50e5134 remove dev from version number 2025-04-05 09:10:01 -05:00
James Seibel b5e3e6867c Improve DH world gen progress message 2025-04-02 07:25:14 -05:00
James Seibel 3e04342148 Add FIXME comments to Lod and Fade renderers 2025-04-02 07:24:38 -05:00
James Seibel 6699b568df Fix memory leaks due to un-closed thread pools and worlds
How did it take this long to realize the DhWorld objects were never being closed?
2025-03-30 17:30:57 -05:00
James Seibel 53bee4ad42 Remove unused code in LodRenderer 2025-03-30 16:55:01 -05:00
James Seibel 5d5e462221 Fix the sun/moon and stars not rendering 2025-03-30 16:49:58 -05:00
James Seibel d9b924cfed Fix beacon beams now going through some blocks 2025-03-30 15:23:19 -05:00
James Seibel 8bd70d593c Fix flashing on MC 1.21.5 in non-overworld dimensions 2025-03-30 14:36:51 -05:00
James Seibel 5597044604 don't log InterruptedException during threadPool shutdown 2025-03-29 20:11:31 -05:00
James Seibel 5d7c043d06 Fix fog for MC 1.16.5 2025-03-29 19:22:51 -05:00
James Seibel 4aac61b37f hide repo double close warnings in release 2025-03-29 15:39:45 -05:00
James Seibel 22460fa1f5 Fix duplicate world gen due to short memoization time
Reverts 276f2adf00
2025-03-29 15:30:28 -05:00
James Seibel 2d127c7d98 Fix an infinite loop in the lighting engine
Not sure how I didn't catch this until MC 1.21.5
2025-03-29 15:29:34 -05:00
James Seibel 91e17c420a Fix SSAO applying to sky 2025-03-29 10:31:48 -05:00
James Seibel 93f5a85cb5 Fix MC 1.21.5 rendering and bright glass on sky 2025-03-29 10:31:34 -05:00
James Seibel b275971486 re-add stencil to GL state
shouldn't be needed, but just in case
2025-03-29 09:52:41 -05:00
James Seibel 1234ff4d28 up version number 2.3.1 -> 2.3.2 2025-03-25 07:17:27 -05:00
James Seibel f9bd7e2daf remove dev from version number 2025-03-25 07:16:43 -05:00
James Seibel 8ec4e235eb Add config to only log GL errors once 2025-03-19 22:02:53 -05:00
James Seibel b8a59d0ef6 Attempt to fix Linux complaining about glIsFramebuffer() 2025-03-19 18:34:02 -05:00
James Seibel e500143781 Potentially fix GL errors when accessing the default FBO on Linux 2025-03-19 17:00:30 -05:00
James Seibel 406468b54c Fix restoring textures to the default FBO 2025-03-18 20:18:13 -05:00
James Seibel 6857300ae2 Add stack tracing to GL error logging 2025-03-18 18:10:00 -05:00
James Seibel 6775ee23c3 fix passing in the wrong flags to glBufferStorage()
Might Resolve #964 and #950
2025-03-18 07:43:20 -05:00
James Seibel 44645943e2 Fix terrain API repo failing if no cache was provided 2025-03-18 07:42:31 -05:00
James Seibel f385c4a56b Fix changing graphics settings on world load via API 2025-03-16 14:29:56 -05:00
James Seibel 0cf5e6d594 Fix GL error logging 2025-03-14 10:17:52 -05:00
James Seibel 7b5b8da0d2 decrease gen message timeout 5 -> 2 seconds
done to make it consistently appear
2025-03-13 21:17:01 -05:00
James Seibel 851f2ccd06 Add additional error checking/handling to Shader compiling 2025-03-13 21:12:29 -05:00
James Seibel 6c40389c07 up version number 2.3.0-b -> 2.3.1-b-dev 2025-03-08 08:11:01 -06:00
James Seibel fada9e4cf6 Fix repo leak unit test failing in release 2025-03-08 08:10:43 -06:00
James Seibel 06198fdbb8 Revert "temporarily disable sqlite tests for release"
This reverts commit ebc1114a51.
2025-03-06 07:43:14 -06:00
James Seibel 3158eed5a3 Merge branch 'main' of gitlab.com:distant-horizons-team/distant-horizons-core 2025-03-06 07:42:32 -06:00
s809 d2ff4a5806 Add some debugging info for DTOs 2025-03-02 20:08:45 +05:00
95 changed files with 2230 additions and 1140 deletions
+1 -2
View File
@@ -4,7 +4,6 @@
[*] [*]
charset = utf-8 charset = utf-8
end_of_line = crlf
indent_size = 4 indent_size = 4
indent_style = space indent_style = space
insert_final_newline = false insert_final_newline = false
@@ -537,7 +536,7 @@ ij_groovy_wrap_chain_calls_after_dot = false
ij_groovy_wrap_long_lines = false ij_groovy_wrap_long_lines = false
[{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}] [{*.har,*.json,*.png.mcmeta,mcmod.info,pack.mcmeta}]
indent_size = 2 indent_size = 4
ij_json_array_wrapping = split_into_lines ij_json_array_wrapping = split_into_lines
ij_json_keep_blank_lines_in_code = 0 ij_json_keep_blank_lines_in_code = 0
ij_json_keep_indents_on_empty_lines = false ij_json_keep_indents_on_empty_lines = false
@@ -39,8 +39,8 @@ public enum EDhApiDataCompressionMode
/** /**
* Should only be used internally and for unit testing. <br><br> * Should only be used internally and for unit testing. <br><br>
* *
* Read Speed: 1.64 MS / DTO <br> * Read Speed: 6.09 MS / DTO <br>
* Write Speed: 12.44 MS / DTO <br> * Write Speed: 6.01 MS / DTO <br>
* Compression ratio: 1.0 <br> * Compression ratio: 1.0 <br>
*/ */
@DisallowSelectingViaConfigGui @DisallowSelectingViaConfigGui
@@ -49,28 +49,29 @@ public enum EDhApiDataCompressionMode
/** /**
* Extremely fast (often faster than uncompressed), but generally poor compression. <br><br> * Extremely fast (often faster than uncompressed), but generally poor compression. <br><br>
* *
* Read Speed: 1.85 MS / DTO <br> * Read Speed: 3.25 MS / DTO <br>
* Write Speed: 9.46 MS / DTO <br> * Write Speed: 5.99 MS / DTO <br>
* Compression ratio: 0.3638 <br> * Compression ratio: 0.4513 <br>
*/ */
LZ4(1), LZ4(1),
/* ///**
* Decent speed and good compression. <br><br> // * Decent speed and good compression. <br><br>
* // *
* Read Speed: 11.78 MS / DTO <br> // * Read Speed: 9.31 MS / DTO <br>
* Write Speed: 16.76 MS / DTO <br> // * Write Speed: 15.13 MS / DTO <br>
* Compression ratio: 0.2199 <br> // * Compression ratio: 0.2606 <br>
*/ // */
//@Deprecated ////@DisallowSelectingViaConfigGui
//Z_STD(2), //Z_STD(2),
/** /**
* Extremely slow, but very good compression. <br><br> * Extremely slow, but very good compression. <br><br>
* *
* Read Speed: 12.25 MS / DTO <br> * Read Speed: 13.29 MS / DTO <br>
* Write Speed: 490.07 MS / DTO <br> * Write Speed: 70.95 MS / DTO <br>
* Compression ratio: 0.1242 <br> * Compression ratio: 0.2068 <br>
*/ */
LZMA2(3); LZMA2(3);
@@ -40,6 +40,7 @@ public enum EDhApiLoggerMode
LOG_DEBUG_TO_CHAT_AND_FILE(Level.DEBUG, Level.DEBUG), LOG_DEBUG_TO_CHAT_AND_FILE(Level.DEBUG, Level.DEBUG),
LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.WARN), LOG_WARNING_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.WARN),
LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.ERROR), LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE(Level.INFO, Level.ERROR),
LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE(Level.ERROR, Level.WARN),
; ;
public final Level levelForFile; public final Level levelForFile;
@@ -9,12 +9,15 @@ package com.seibel.distanthorizons.api.interfaces.data;
* @version 2024-7-14 * @version 2024-7-14
* @since API 3.0.0 * @since API 3.0.0
*/ */
public interface IDhApiTerrainDataCache public interface IDhApiTerrainDataCache // TODO should this be AutoClosable?
{ {
/** /**
* Removes any data that's currently stored in this cache. * Removes any data that's currently stored in this cache.
* This cane be done to free up memory or invalidate * This cane be done to free up memory or invalidate
* the cache so fresh data can be pulled in. * the cache so fresh data can be pulled in.
* <br><br>
* This should be called before de-referencing this object
* so DH can handle any necessary cleanup for internal objects.
*/ */
void clear(); void clear();
@@ -0,0 +1,48 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/**
* Called after Distant Horizons (re)creates
* the color and depth textures it renders to. <br>
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public abstract class DhApiAfterColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiTextureCreatedParam>
{
/** Fired before Distant Horizons creates. */
public abstract void onResize(DhApiEventParam<DhApiTextureCreatedParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<DhApiTextureCreatedParam> event) { this.onResize(event); }
}
@@ -0,0 +1,49 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/**
* Called before Distant Horizons (re)creates
* the color and depth textures it renders to. <br>
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public abstract class DhApiBeforeColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiTextureCreatedParam>
{
/** Fired before Distant Horizons creates. */
public abstract void onResize(DhApiEventParam<DhApiTextureCreatedParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<DhApiTextureCreatedParam> event) { this.onResize(event); }
}
@@ -22,15 +22,18 @@ package com.seibel.distanthorizons.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam; import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
/** /**
* Called whenever Distant Horizons (re)creates * Called before Distant Horizons (re)creates
* the color and depth textures it renders to. <br> * the color and depth textures it renders to. <br>
* *
* @author James Seibel * @author James Seibel
* @version 2024-3-2 * @version 2024-3-2
* @since API 2.0.0 * @since API 2.0.0
* @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
*/ */
@Deprecated
public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam> public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam>
{ {
/** Fired before Distant Horizons creates. */ /** Fired before Distant Horizons creates. */
@@ -73,6 +76,15 @@ public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<
this.newHeight = newHeight; this.newHeight = newHeight;
} }
public EventParam(DhApiTextureCreatedParam textureCreatedParam)
{
this.previousWidth = textureCreatedParam.previousWidth;
this.previousHeight = textureCreatedParam.previousHeight;
this.newWidth = textureCreatedParam.newWidth;
this.newHeight = textureCreatedParam.newHeight;
}
@Override @Override
@@ -0,0 +1,68 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.api.methods.events.sharedParameterObjects;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.objects.math.DhApiMat4f;
/**
* Contains information relevant to when Distant Horizons (re)creates
* depth/color textures for rendering.
*
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
*/
public class DhApiTextureCreatedParam implements IDhApiEventParam
{
/** Measured in pixels */
public final int previousWidth;
/** Measured in pixels */
public final int previousHeight;
/** Measured in pixels */
public final int newWidth;
/** Measured in pixels */
public final int newHeight;
public DhApiTextureCreatedParam(
int previousWidth, int previousHeight,
int newWidth, int newHeight)
{
this.previousWidth = previousWidth;
this.previousHeight = previousHeight;
this.newWidth = newWidth;
this.newHeight = newHeight;
}
@Override
public DhApiTextureCreatedParam copy()
{
return new DhApiTextureCreatedParam(
this.previousWidth, this.previousHeight,
this.newWidth, this.newHeight
);
}
}
@@ -31,21 +31,21 @@ 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 = 10; public static final int PROTOCOL_VERSION = 12;
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"; public static final String VERSION = "2.3.5-b-dev";
/** 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");
/** This version should only be updated when breaking changes are introduced to the DH API */ /** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 4; public static final int API_MAJOR_VERSION = 4;
/** This version should be updated whenever new methods are added to the DH API */ /** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0; public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */ /** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0; public static final int API_PATCH_VERSION = 0;
@@ -31,14 +31,12 @@ import com.seibel.distanthorizons.coreapi.interfaces.config.IConverter;
public class RenderModeEnabledConverter implements IConverter<EDhApiRendererMode, Boolean> public class RenderModeEnabledConverter implements IConverter<EDhApiRendererMode, Boolean>
{ {
@Override public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled) @Override
{ public EDhApiRendererMode convertToCoreType(Boolean renderingEnabled)
return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; { return renderingEnabled ? EDhApiRendererMode.DEFAULT : EDhApiRendererMode.DISABLED; }
}
@Override public Boolean convertToApiType(EDhApiRendererMode renderingMode) @Override
{ public Boolean convertToApiType(EDhApiRendererMode renderingMode)
return renderingMode == EDhApiRendererMode.DEFAULT; { return renderingMode == EDhApiRendererMode.DEFAULT; }
}
} }
+6
View File
@@ -61,3 +61,9 @@ shadowJar {
// relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil" // relocate "it.unimi.dsi.fastutil", "${librariesLocation}.unimi.dsi.fastutil"
mergeServiceFiles() mergeServiceFiles()
} }
test {
// this is necessary specifically for the Compression tests since those
// need more than the default 512 MB of RAM
jvmArgs '-Xmx4096m'
}
@@ -26,6 +26,7 @@ import com.seibel.distanthorizons.api.interfaces.config.client.*;
import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue; import com.seibel.distanthorizons.api.objects.config.DhApiConfigValue;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode; import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.coreapi.util.converters.RenderModeEnabledConverter;
public class DhApiGraphicsConfig implements IDhApiGraphicsConfig public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
{ {
@@ -60,7 +61,7 @@ public class DhApiGraphicsConfig implements IDhApiGraphicsConfig
@Override @Override
public IDhApiConfigValue<Boolean> renderingEnabled() public IDhApiConfigValue<Boolean> renderingEnabled()
{ return new DhApiConfigValue<Boolean, Boolean>(Config.Client.quickEnableRendering); } { return new DhApiConfigValue<EDhApiRendererMode, Boolean>(Config.Client.Advanced.Debugging.rendererMode, new RenderModeEnabledConverter()); }
@Override @Override
public IDhApiConfigValue<EDhApiRendererMode> renderingMode() public IDhApiConfigValue<EDhApiRendererMode> renderingMode()
@@ -197,10 +197,10 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
ILevelWrapper coreLevelWrapper = (ILevelWrapper) levelWrapper; ILevelWrapper coreLevelWrapper = (ILevelWrapper) levelWrapper;
if (!(apiDataCache instanceof DhApiTerrainDataCache)) // the data cache can be null, but must be our own implementation
if (apiDataCache != null
&& !(apiDataCache instanceof DhApiTerrainDataCache))
{ {
// custom level wrappers aren't supported,
// the API user must get a level wrapper from our code somewhere
return DhApiResult.createFail("Unsupported [" + IDhApiTerrainDataCache.class.getSimpleName() + "] implementation, only the core class [" + DhApiTerrainDataCache.class.getSimpleName() + "] is a valid parameter."); return DhApiResult.createFail("Unsupported [" + IDhApiTerrainDataCache.class.getSimpleName() + "] implementation, only the core class [" + DhApiTerrainDataCache.class.getSimpleName() + "] is a valid parameter.");
} }
DhApiTerrainDataCache dataCache = (DhApiTerrainDataCache) apiDataCache; DhApiTerrainDataCache dataCache = (DhApiTerrainDataCache) apiDataCache;
@@ -226,10 +226,9 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
// get the data source // // get the data source //
//=====================// //=====================//
FullDataSourceV2 dataSource = null;
try try
{ {
FullDataSourceV2 dataSource = null;
// try using the cached data if possible // try using the cached data if possible
if (dataCache != null) if (dataCache != null)
{ {
@@ -244,7 +243,12 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
{ {
return DhApiResult.createFail("Unable to find/generate any data at the " + DhSectionPos.class.getSimpleName() + " [" + DhSectionPos.toString(sectionPos) + "]."); return DhApiResult.createFail("Unable to find/generate any data at the " + DhSectionPos.class.getSimpleName() + " [" + DhSectionPos.toString(sectionPos) + "].");
} }
dataCache.add(sectionPos, dataSource);
// save to the cache if present
if (dataCache != null)
{
dataCache.add(sectionPos, dataSource);
}
} }
@@ -316,6 +320,14 @@ public class DhApiTerrainDataRepo implements IDhApiTerrainDataRepo
LOGGER.error("Unexpected exception in getTerrainDataColumnArray. Error: [" + e.getMessage() + "]", e); LOGGER.error("Unexpected exception in getTerrainDataColumnArray. Error: [" + e.getMessage() + "]", e);
return DhApiResult.createFail("Unexpected exception: [" + e.getMessage() + "]."); return DhApiResult.createFail("Unexpected exception: [" + e.getMessage() + "].");
} }
finally
{
if (dataCache == null
&& dataSource != null)
{
dataSource.close();
}
}
} }
@@ -89,6 +89,15 @@ public class ClientApi
/** this includes the is dev build message and low allocated memory warning */ /** this includes the is dev build message and low allocated memory warning */
private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000; private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000;
/**
* This isn't the cleanest way of storing variables before passing them to the LOD renderer,
* but due to how mixins work and the inconsistency between MC versions,
* having a static object that stores a single frame's data
* is often the easiest solution. <br><br>
*
* Only downside is making sure each variable is populated before rendering.
*/
public static final RenderState RENDER_STATE = new RenderState();
private boolean isDevBuildMessagePrinted = false; private boolean isDevBuildMessagePrinted = false;
@@ -389,7 +398,7 @@ public class ClientApi
// rendering // // rendering //
//===========// //===========//
/** Should be called before {@link ClientApi#renderDeferredLods} */ /** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) public void renderLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks)
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, false); } { this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, false); }
@@ -397,9 +406,10 @@ public class ClientApi
* Only necessary when Shaders are in use. * Only necessary when Shaders are in use.
* Should be called after {@link ClientApi#renderLods} * Should be called after {@link ClientApi#renderLods}
*/ */
public void renderDeferredLods(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) public void renderDeferredLodsForShaders(IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks)
{ this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, true); } { this.renderLodLayer(levelWrapper, mcModelViewMatrix, mcProjectionMatrix, partialTicks, true); }
private void renderLodLayer( private void renderLodLayer(
IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper levelWrapper, Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks,
boolean renderingDeferredLayer) boolean renderingDeferredLayer)
@@ -445,6 +455,25 @@ public class ClientApi
//Mat4f mcCombined = mcModelViewMatrix.copy();
//mcCombined.multiply(mcProjectionMatrix);
//
//com.seibel.distanthorizons.api.objects.math.DhApiMat4f dhCombined = renderEventParam.dhModelViewMatrix.copy();
//dhCombined.multiply(renderEventParam.dhProjectionMatrix);
//
//LOGGER.info("\n\n" +
// "API\n" +
// "Mc MVM: \n" + mcModelViewMatrix.toString() + "\n" +
// "Mc Proj: \n" + mcProjectionMatrix + "\n" +
// "Mc Combined:\n" + mcCombined.toString() + "\n" +
// "\n" +
// "DH MVM: \n" + renderEventParam.dhModelViewMatrix.toString() + "\n" +
// "DH Proj: \n" + renderEventParam.dhProjectionMatrix + "\n" +
// "DH Combined:\n" + mcCombined.toString()
//);
// render validation // // render validation //
try try
@@ -555,25 +584,26 @@ public class ClientApi
public void renderFadeOpaque(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level) public void renderFadeOpaque(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level)
{ {
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
// only fade when requested
&& Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS
// don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{ {
if (Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() == EDhApiMcRenderingFadeMode.DOUBLE_PASS) FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
{
FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
}
} }
} }
/** should be called after DH and MC finish rendering so we can smooth the transition between the two */ /** should be called after DH and MC finish rendering so we can smooth the transition between the two */
public void renderFade(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level) public void renderFade(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks, IClientLevelWrapper level)
{ {
// only fade when DH is rendering // only fade when DH is rendering
if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT) if (Config.Client.Advanced.Debugging.rendererMode.get() == EDhApiRendererMode.DEFAULT
// only fade when requested
&& Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() != EDhApiMcRenderingFadeMode.NONE
// don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{ {
// fade if any level fading is active FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
if (Config.Client.Advanced.Graphics.Quality.vanillaFadeMode.get() != EDhApiMcRenderingFadeMode.NONE)
{
FadeRenderer.INSTANCE.render(mcModelViewMatrix, mcProjectionMatrix, partialTicks, level);
}
} }
} }
@@ -652,7 +682,8 @@ public class ClientApi
private void detectAndSendBootTimeWarnings() private void detectAndSendBootTimeWarnings()
{ {
// dev build // dev build
if (ModInfo.IS_DEV_BUILD && !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists()) if (ModInfo.IS_DEV_BUILD
&& !this.isDevBuildMessagePrinted && MC_CLIENT.playerExists())
{ {
this.isDevBuildMessagePrinted = true; this.isDevBuildMessagePrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -669,7 +700,8 @@ public class ClientApi
// memory // memory
if (this.staticStartupMessageSentRecently()) return; if (this.staticStartupMessageSentRecently()) return;
if (!this.lowMemoryWarningPrinted && Config.Common.Logging.Warning.showLowMemoryWarningOnStartup.get()) if (!this.lowMemoryWarningPrinted
&& Config.Common.Logging.Warning.showLowMemoryWarningOnStartup.get())
{ {
this.lowMemoryWarningPrinted = true; this.lowMemoryWarningPrinted = true;
this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis(); this.lastStaticWarningMessageSentMsTime = System.currentTimeMillis();
@@ -694,7 +726,8 @@ public class ClientApi
// high vanilla render distance // high vanilla render distance
if (this.staticStartupMessageSentRecently()) return; if (this.staticStartupMessageSentRecently()) return;
if (!this.highVanillaRenderDistanceWarningPrinted && Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get()) if (!this.highVanillaRenderDistanceWarningPrinted
&& Config.Common.Logging.Warning.showHighVanillaRenderDistanceWarning.get())
{ {
// DH generally doesn't need a vanilla render distance above 12 // DH generally doesn't need a vanilla render distance above 12
if (MC_RENDER.getRenderDistance() > 12) if (MC_RENDER.getRenderDistance() > 12)
@@ -721,7 +754,8 @@ public class ClientApi
{ {
if (this.lastStaticWarningMessageSentMsTime == 0) if (this.lastStaticWarningMessageSentMsTime == 0)
{ {
return true; // no static message has ever been sent
return false;
} }
long timeSinceLastMessage = System.currentTimeMillis() - this.lastStaticWarningMessageSentMsTime; long timeSinceLastMessage = System.currentTimeMillis() - this.lastStaticWarningMessageSentMsTime;
@@ -740,4 +774,47 @@ public class ClientApi
*/ */
public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); } public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); }
//================//
// helper classes //
//================//
public static class RenderState
{
public Mat4f mcModelViewMatrix = null;
public Mat4f mcProjectionMatrix = null;
public float frameTime = -1;
public void canRenderOrThrow() throws IllegalStateException
{
String errorReasons = "";
if (this.mcModelViewMatrix == null)
{
errorReasons += "no MVM Matrix, ";
}
if (this.mcProjectionMatrix == null)
{
errorReasons += "no Projection Matrix, ";
}
if (this.frameTime == -1)
{
errorReasons += "no Frame Time, ";
}
if (!errorReasons.isEmpty())
{
throw new IllegalStateException(errorReasons);
}
}
}
} }
@@ -75,7 +75,12 @@ public class ClientPluginChannelApi
private void onLevelInitMessage(LevelInitMessage msg) private void onLevelInitMessage(LevelInitMessage msg)
{ {
if (!msg.levelKey.matches(LevelInitMessage.VALIDATION_REGEX)) if (!msg.serverKey.matches(LevelInitMessage.SERVER_KEY_REGEX))
{
throw new IllegalArgumentException("Server sent invalid server key.");
}
if (!msg.levelKey.matches(LevelInitMessage.LEVEL_KEY_REGEX))
{ {
throw new IllegalArgumentException("Server sent invalid level key."); throw new IllegalArgumentException("Server sent invalid level key.");
} }
@@ -105,10 +110,12 @@ public class ClientPluginChannelApi
this.levelUnloadHandler.accept(clientLevel); this.levelUnloadHandler.accept(clientLevel);
} }
if (existingKeyedClientLevel == null || !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey)) if (existingKeyedClientLevel == null
|| !existingKeyedClientLevel.getServerKey().equals(msg.serverKey)
|| !existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{ {
LOGGER.info("Loading level with key: [" + msg.levelKey + "]."); LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.levelKey); IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.serverKey, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel); this.levelLoadHandler.accept(keyedLevel);
} }
}); });
@@ -99,6 +99,11 @@ public class SharedApi
public static void setDhWorld(AbstractDhWorld newWorld) public static void setDhWorld(AbstractDhWorld newWorld)
{ {
AbstractDhWorld oldWorld = currentWorld;
if (oldWorld != null)
{
oldWorld.close();
}
currentWorld = newWorld; currentWorld = newWorld;
// starting and stopping the DataRenderTransformer is necessary to prevent attempting to // starting and stopping the DataRenderTransformer is necessary to prevent attempting to
@@ -1015,9 +1015,23 @@ public class Config
public static class OpenGl public static class OpenGl
{ {
public static ConfigEntry<Boolean> overrideVanillaGLLogger = new ConfigEntry.Builder<Boolean>() public static ConfigEntry<Boolean> overrideVanillaGLLogger = new ConfigEntry.Builder<Boolean>()
.set(ModInfo.IS_DEV_BUILD) .set(true)
.comment("" .comment(""
+ "Requires a reboot to change. \n" + "Defines how OpenGL errors are handled. \n "
+ "Requires rebooting Minecraft to change. \n"
+ "Will catch OpenGL errors thrown by other mods. \n"
+ "")
.build();
public static ConfigEntry<Boolean> onlyLogGlErrorsOnce = new ConfigEntry.Builder<Boolean>()
.set(true)
.comment(""
+ "If true each Open GL error will only be logged once. \n"
+ "Enabling this may cause some error logs to be missed. \n"
+ "Does nothing if overrideVanillaGLLogger is set to false. \n"
+ " \n"
+ "Generally this can be kept as 'true' to prevent log spam. \n"
+ "However, Please set this to 'false' if a developer needs your log to debug a GL issue. \n"
+ "") + "")
.build(); .build();
@@ -1281,7 +1295,7 @@ public class Config
public static ConfigEntry<Integer> generationProgressDisplayIntervalInSeconds = new ConfigEntry.Builder<Integer>() public static ConfigEntry<Integer> generationProgressDisplayIntervalInSeconds = new ConfigEntry.Builder<Integer>()
.setChatCommandName("generation.logInterval") .setChatCommandName("generation.logInterval")
.setMinDefaultMax(1, 5, 60 * 60 * 4) // max = 4 hours .setMinDefaultMax(1, 2, 60 * 60 * 4) // max = 4 hours
.comment("" .comment(""
+ "How often should the distant generator progress be displayed? \n" + "How often should the distant generator progress be displayed? \n"
+ "") + "")
@@ -1328,20 +1342,20 @@ public class Config
+ EDhApiDataCompressionMode.UNCOMPRESSED + " \n" + EDhApiDataCompressionMode.UNCOMPRESSED + " \n"
+ "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n" + "Should only be used for testing, is worse in every way vs ["+EDhApiDataCompressionMode.LZ4+"].\n"
+ "Expected Compression Ratio: 1.0\n" + "Expected Compression Ratio: 1.0\n"
+ "Estimated average DTO read speed: 1.64 milliseconds\n" + "Estimated average DTO read speed: 3.25 milliseconds\n"
+ "Estimated average DTO write speed: 12.44 milliseconds\n" + "Estimated average DTO write speed: 5.99 milliseconds\n"
+ "\n" + "\n"
+ EDhApiDataCompressionMode.LZ4 + " \n" + EDhApiDataCompressionMode.LZ4 + " \n"
+ "A good option if you're CPU limited and have plenty of hard drive space.\n" + "A good option if you're CPU limited and have plenty of hard drive space.\n"
+ "Expected Compression Ratio: 0.36\n" + "Expected Compression Ratio: 0.26\n"
+ "Estimated average DTO read speed: 1.85 ms\n" + "Estimated average DTO read speed: 1.85 ms\n"
+ "Estimated average DTO write speed: 9.46 ms\n" + "Estimated average DTO write speed: 9.46 ms\n"
+ "\n" + "\n"
+ EDhApiDataCompressionMode.LZMA2 + " \n" + EDhApiDataCompressionMode.LZMA2 + " \n"
+ "Slow but very good compression.\n" + "Slow but very good compression.\n"
+ "Expected Compression Ratio: 0.14\n" + "Expected Compression Ratio: 0.2\n"
+ "Estimated average DTO read speed: 11.89 ms\n" + "Estimated average DTO read speed: 13.29 ms\n"
+ "Estimated average DTO write speed: 192.01 ms\n" + "Estimated average DTO write speed: 70.95 ms\n"
+ "") + "")
.build(); .build();
@@ -1506,7 +1520,7 @@ public class Config
public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>() public static ConfigEntry<EDhApiLoggerMode> logNetworkEvent = new ConfigEntry.Builder<EDhApiLoggerMode>()
.setChatCommandName("logging.logNetworkEvent") .setChatCommandName("logging.logNetworkEvent")
.set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE) .set(EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE)
.comment("" .comment(""
+ "If enabled, the mod will log information about network operations. \n" + "If enabled, the mod will log information about network operations. \n"
+ "This can be useful for debugging.") + "This can be useful for debugging.")
@@ -1585,6 +1599,28 @@ public class Config
+ "") + "")
.build(); .build();
public static ConfigEntry<Integer> serverId = new ConfigEntry.Builder<Integer>()
.set(new Random().nextInt())
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.comment(""
+ "DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.\n"
+ "Autogenerated ID used to prevent multiple independent servers from accidentally\n"
+ "writing over each other's LODs when the same serverKey is set on both.\n"
+ "")
.build();
public static ConfigEntry<String> serverKey = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.serverKey")
.setAppearance(EConfigEntryAppearance.ONLY_IN_FILE)
.set("")
.comment(""
+ "Custom server key used which can be used to always reuse the same LOD data folder,\n"
+ "for cases when the server doesn't have a static IP for some reason.\n"
+ "If this value is empty, the client itself decides which folder name to use.\n"
+ "Requires rejoining the server to apply after changing.\n"
+ "")
.build();
public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>() public static ConfigEntry<String> levelKeyPrefix = new ConfigEntry.Builder<String>()
.setChatCommandName("levelKeys.prefix") .setChatCommandName("levelKeys.prefix")
.set("") .set("")
@@ -1699,6 +1735,15 @@ public class Config
+ "Value of 0 disables the limit." + "Value of 0 disables the limit."
+ "") + "")
.build(); .build();
public static ConfigEntry<Boolean> enableAdaptiveTransferSpeed = new ConfigEntry.Builder<Boolean>()
.set(false)
.comment(""
+ "Enables adaptive transfer speed based on client performance.\n"
+ "If true, DH will automatically adjust transfer rate to minimize connection lag.\n"
+ "If false, transfer speed will remain fixed.\n"
+ "")
.build();
public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build(); public static ConfigCategory experimental = new ConfigCategory.Builder().set(Experimental.class).build();
@@ -105,6 +105,15 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.put(EDhApiQualityPreset.HIGH, true); this.put(EDhApiQualityPreset.HIGH, true);
this.put(EDhApiQualityPreset.EXTREME, true); this.put(EDhApiQualityPreset.EXTREME, true);
}}); }});
private final ConfigEntryWithPresetOptions<EDhApiQualityPreset, Boolean> caveCulling = new ConfigEntryWithPresetOptions<>(Config.Client.Advanced.Graphics.Culling.enableCaveCulling,
new HashMap<EDhApiQualityPreset, Boolean>()
{{
this.put(EDhApiQualityPreset.MINIMUM, true);
this.put(EDhApiQualityPreset.LOW, true);
this.put(EDhApiQualityPreset.MEDIUM, true);
this.put(EDhApiQualityPreset.HIGH, false);
this.put(EDhApiQualityPreset.EXTREME, false);
}});
@@ -123,6 +132,7 @@ public class RenderQualityPresetConfigEventHandler extends AbstractPresetConfigE
this.configList.add(this.ssaoEnabled); this.configList.add(this.ssaoEnabled);
this.configList.add(this.vanillaFade); this.configList.add(this.vanillaFade);
this.configList.add(this.dhDither); this.configList.add(this.dhDither);
this.configList.add(this.caveCulling);
for (ConfigEntryWithPresetOptions<EDhApiQualityPreset, ?> config : this.configList) for (ConfigEntryWithPresetOptions<EDhApiQualityPreset, ?> config : this.configList)
@@ -70,7 +70,12 @@ public class ConfigFileHandling
this.configBase = configBase; this.configBase = configBase;
this.configPath = configPath; this.configPath = configPath;
this.nightConfig = CommentedFileConfig.builder(this.configPath.toFile()).build(); this.nightConfig = CommentedFileConfig
.builder(this.configPath.toFile())
// sync is needed so file reading/writing only happens during locked sections,
// otherwise some GUI changes may be lost when changing screens
.sync()
.build();
} }
@@ -321,10 +326,7 @@ public class ConfigFileHandling
* *
* @apiNote This overwrites any value currently stored in the config * @apiNote This overwrites any value currently stored in the config
*/ */
public void loadNightConfig() public void loadNightConfig() { this.loadNightConfig(this.nightConfig); }
{
loadNightConfig(this.nightConfig);
}
/** /**
* Does {@link CommentedFileConfig#load()} but with error checking * Does {@link CommentedFileConfig#load()} but with error checking
* *
@@ -353,7 +355,7 @@ public class ConfigFileHandling
{ {
System.out.println("Creating file failed"); System.out.println("Creating file failed");
this.logger.error(ex); this.logger.error(ex);
SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + configPath + "]. Please check the file is ok and you have the permissions", ex); SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class).crashMinecraft("Loading file and resetting config file failed at path [" + this.configPath + "]. Please check the file is ok and you have the permissions", ex);
} }
} }
@@ -29,14 +29,12 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory; import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* WARNING: This is not THREAD-SAFE! <br><br> * WARNING: This is not THREAD-SAFE! <br><br>
@@ -352,14 +350,14 @@ 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 ConcurrentHashMap<Integer, Entry> ENTRY_BY_HASH = new ConcurrentHashMap<>(); /** two levels are present so we don't need to use a key object */
/** lock is necessary since {@link Int2ReferenceOpenHashMap} isn't concurrent and concurrent threads can cause infinite loops */ private static final ConcurrentHashMap<IBiomeWrapper, ConcurrentHashMap<IBlockStateWrapper, Entry>> ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER = new ConcurrentHashMap<>();
private static final ReentrantReadWriteLock ENTRY_POOL_LOCK = new ReentrantReadWriteLock();
public final IBiomeWrapper biome; public final IBiomeWrapper biome;
public final IBlockStateWrapper blockState; public final IBlockStateWrapper blockState;
private Integer hashCode = null; private int hashCode = 0;
private boolean hashGenerated = false;
private String serialString = null; private String serialString = null;
@@ -370,25 +368,21 @@ public class FullDataPointIdMap
public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState) public static Entry getEntry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{ {
int entryHash = generateHashCode(biome, blockState); // check for existing entry
ConcurrentHashMap<IBlockStateWrapper, Entry> entryByBlockState = ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER.get(biome);
// try getting the existing Entry if (entryByBlockState != null)
Entry entry = ENTRY_BY_HASH.get(entryHash);
if (entry != null)
{ {
return entry; Entry entry = entryByBlockState.get(blockState);
if (entry != null)
{
return entry;
}
} }
// create the missing entry // Lazily create the inner map and new Entry
return ENTRY_BY_HASH.compute(entryHash, (Integer newHash, Entry currentEntry) -> return ENTRY_BY_BLOCKSTATE_BY_BIOMEWRAPPER
{ .computeIfAbsent(biome, newBiome -> new ConcurrentHashMap<>())
if (currentEntry != null) .computeIfAbsent(blockState, newBlockState -> new Entry(biome, blockState));
{
return currentEntry;
}
return new Entry(biome, blockState);
});
} }
private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState) private Entry(IBiomeWrapper biome, IBlockStateWrapper blockState)
{ {
@@ -402,13 +396,18 @@ public class FullDataPointIdMap
// overrides // // overrides //
//===========// //===========//
/**
* Reminder: this hash code won't always be unique, collisions can occur;
* because of that this hash shouldn't be the only unique identifier for this object.
*/
@Override @Override
public int hashCode() public int hashCode()
{ {
// cache the hash code to improve speed // cache the hash code to improve speed
if (this.hashCode == null) if (!this.hashGenerated)
{ {
this.hashCode = generateHashCode(this); this.hashCode = generateHashCode(this);
this.hashGenerated = true;
} }
return this.hashCode; return this.hashCode;
@@ -430,10 +429,14 @@ public class FullDataPointIdMap
public boolean equals(Object otherObj) public boolean equals(Object otherObj)
{ {
if (otherObj == this) if (otherObj == this)
{
return true; return true;
}
if (!(otherObj instanceof Entry)) if (!(otherObj instanceof Entry))
{
return false; return false;
}
Entry other = (Entry) otherObj; Entry other = (Entry) otherObj;
return other.biome.getSerialString().equals(this.biome.getSerialString()) return other.biome.getSerialString().equals(this.biome.getSerialString())
@@ -65,8 +65,6 @@ public class LodDataBuilder
// only block lighting is needed here, sky lighting is populated at the data source stage // only block lighting is needed here, sky lighting is populated at the data source stage
LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect()); LodUtil.assertTrue(chunkWrapper.isDhBlockLightingCorrect());
int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX()); int sectionPosX = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getX());
int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ()); int sectionPosZ = getXOrZSectionPosFromChunkPos(chunkWrapper.getChunkPos().getZ());
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
@@ -80,47 +78,31 @@ public class LodDataBuilder
// compute the chunk dataSource offset // compute the chunk dataSource offset
// this offset is used to determine where in the dataSource this chunk's data should go // this offset is used to determine where in the dataSource this chunk's data should go
int chunkOffsetX = chunkWrapper.getChunkPos().getX();
if (chunkWrapper.getChunkPos().getX() < 0)
{
// expected offset positions:
// chunkPos -> offset
// 5 -> 1
// 4 -> 0 ---
// 3 -> 3
// 2 -> 2
// 1 -> 1
// 0 -> 0 ===
// -1 -> 3
// -2 -> 2
// -3 -> 1
// -4 -> 0 ---
// -5 -> 3
chunkOffsetX = ((chunkOffsetX) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE);
if (chunkOffsetX != 0)
{
chunkOffsetX += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
}
else
{
chunkOffsetX %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE;
}
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ(); // expected offset positions:
if (chunkWrapper.getChunkPos().getZ() < 0) // chunkPos -> offset
{ // 5 -> 1
chunkOffsetZ = ((chunkOffsetZ) % FullDataSourceV2.NUMB_OF_CHUNKS_WIDE); // 4 -> 0 ---
if (chunkOffsetZ != 0) // 3 -> 3
{ // 2 -> 2
chunkOffsetZ += FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; // 1 -> 1
} // 0 -> 0 ===
} // -1 -> 3
else // -2 -> 2
{ // -3 -> 1
chunkOffsetZ %= FullDataSourceV2.NUMB_OF_CHUNKS_WIDE; // -4 -> 0 ---
} // -5 -> 3
// Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
// For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
// Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int chunkOffsetX = chunkWrapper.getChunkPos().getX() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
int chunkOffsetZ = chunkWrapper.getChunkPos().getZ() & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1);
// Convert from chunk coordinates to block coordinates
chunkOffsetX *= LodUtil.CHUNK_WIDTH;
chunkOffsetZ *= LodUtil.CHUNK_WIDTH; chunkOffsetZ *= LodUtil.CHUNK_WIDTH;
@@ -138,54 +120,49 @@ public class LodDataBuilder
IBlockStateWrapper previousBlockState = null; IBlockStateWrapper previousBlockState = null;
int minBuildHeight = chunkWrapper.getMinNonEmptyHeight(); int minBuildHeight = chunkWrapper.getMinNonEmptyHeight();
int exclusiveMaxBuildHeight = chunkWrapper.getExclusiveMaxBuildHeight();
int inclusiveMinBuildHeight = chunkWrapper.getInclusiveMinBuildHeight();
int dataCapacity = chunkWrapper.getHeight() / 4;
for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++)
{ {
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
{ {
LongArrayList longs = dataSource.get( // Calculate column position
relBlockX + chunkOffsetX, int columnX = relBlockX + chunkOffsetX;
relBlockZ + chunkOffsetZ); int columnZ = relBlockZ + chunkOffsetZ;
// Get column data
LongArrayList longs = dataSource.get(columnX, columnZ);
if (longs == null) if (longs == null)
{ {
longs = new LongArrayList(chunkWrapper.getHeight() / 4); longs = new LongArrayList(dataCapacity);
} }
else else
{ {
longs.clear(); longs.clear();
} }
int lastY = chunkWrapper.getExclusiveMaxBuildHeight(); int lastY = exclusiveMaxBuildHeight;
IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ); IBiomeWrapper biome = chunkWrapper.getBiome(relBlockX, lastY, relBlockZ);
IBlockStateWrapper blockState = AIR; IBlockStateWrapper blockState = AIR;
int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); int mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState);
// Determine lighting (we are at the height limit. There are no torches here, and sky is not obscured.) // TODO: Per face lighting someday?
byte blockLight = LodUtil.MIN_MC_LIGHT;
byte skyLight = LodUtil.MAX_MC_LIGHT;
byte blockLight; // Get the maximum height from both heightmaps
byte skyLight;
if (lastY < chunkWrapper.getExclusiveMaxBuildHeight())
{
// FIXME: The lastY +1 offset is to reproduce the old behavior. Remove this when we get per-face lighting
blockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, lastY + 1, relBlockZ);
skyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, lastY + 1, relBlockZ);
}
else
{
//we are at the height limit. There are no torches here, and sky is not obscured.
blockLight = LodUtil.MIN_MC_LIGHT;
skyLight = LodUtil.MAX_MC_LIGHT;
}
// determine the starting Y Pos
int y = Math.max( int y = Math.max(
// max between both heightmaps to account for solid invisible blocks (glass) // 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) // and non-solid opaque blocks (at one point this was stairs, not sure what would fit this now)
chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ), chunkWrapper.getLightBlockingHeightMapValue(relBlockX, relBlockZ),
chunkWrapper.getSolidHeightMapValue(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 < exclusiveMaxBuildHeight)
{ {
try try
{ {
@@ -198,7 +175,7 @@ public class LodDataBuilder
{ {
if (!getTopErrorLogged) if (!getTopErrorLogged)
{ {
LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + chunkWrapper.getExclusiveMaxBuildHeight() + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e); LOGGER.warn("Unexpected issue in LodDataBuilder, future errors won't be logged. Chunk [" + chunkWrapper.getChunkPos() + "] with max height: [" + exclusiveMaxBuildHeight + "] had issue getting block at pos [" + relBlockX + "," + y + "," + relBlockZ + "] error: " + e.getMessage(), e);
getTopErrorLogged = true; getTopErrorLogged = true;
} }
@@ -207,7 +184,7 @@ public class LodDataBuilder
} }
} }
// Process blocks from top to bottom
for (; y >= minBuildHeight; y--) for (; y >= minBuildHeight; y--)
{ {
IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ); IBiomeWrapper newBiome = chunkWrapper.getBiome(relBlockX, y, relBlockZ);
@@ -215,10 +192,10 @@ public class LodDataBuilder
byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ); byte newBlockLight = (byte) chunkWrapper.getDhBlockLight(relBlockX, y + 1, relBlockZ);
byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ); byte newSkyLight = (byte) chunkWrapper.getDhSkyLight(relBlockX, y + 1, relBlockZ);
// save the biome/block change // Save the biome/block change if different from previous
if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) if (!newBiome.equals(biome) || !newBlockState.equals(blockState))
{ {
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
biome = newBiome; biome = newBiome;
blockState = newBlockState; blockState = newBlockState;
@@ -228,13 +205,12 @@ public class LodDataBuilder
lastY = y; lastY = y;
} }
} }
longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight));
dataSource.setSingleColumn(longs, // Add the final data point
relBlockX + chunkOffsetX, longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - inclusiveMinBuildHeight, blockLight, skyLight));
relBlockZ + chunkOffsetZ,
EDhApiWorldGenerationStep.LIGHT, // Set the column in the data source
worldCompressionMode); dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
} }
} }
@@ -275,7 +251,7 @@ public class LodDataBuilder
{ {
long currentPoint = centerColumn.getLong(centerIndex); long currentPoint = centerColumn.getLong(centerIndex);
// translucent data points are not eligible to be culled. // Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint)) if (isTranslucent(dataSource, currentPoint))
{ {
continue; continue;
@@ -283,8 +259,8 @@ public class LodDataBuilder
// the top segment should never be culled. // the top segment should never be culled.
if (centerIndex == 0 if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1)) || isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
) )
{ {
continue; continue;
} }
@@ -293,8 +269,8 @@ public class LodDataBuilder
// assume it will not be seen from below, // assume it will not be seen from below,
// because this would imply the player is in the void. // because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size() if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1)) && isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
) )
{ {
continue; continue;
} }
@@ -327,9 +303,11 @@ public class LodDataBuilder
continue; continue;
} }
// current point is fully surrounded. remove it. // Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex); centerColumn.removeLong(centerIndex);
// make the above data point cover the area that the current point used to occupy.
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1); long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint)); above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above)); above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
@@ -340,29 +318,29 @@ public class LodDataBuilder
} }
/** /**
checks if centerPoint is "covered" by opaque data points in adjacentColumn. checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range, centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level. there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs. @param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered. @param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint. @param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at. @param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range. not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it. @return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint. as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative. segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop. the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned. centerPoint is covered; both are packed into the same int, and returned.
*/ */
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex) private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{ {
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint); int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
@@ -402,9 +380,13 @@ public class LodDataBuilder
int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ); int sectionPosZ = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosZ);
long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ); long pos = DhSectionPos.encode(DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL, sectionPosX, sectionPosZ);
// chunk relative block position in the data source // Fast modulo calculation using bitwise AND since NUMB_OF_CHUNKS_WIDE is a power of 2 (4)
int relSourceBlockX = Math.floorMod(apiChunk.chunkPosX, 4) * LodUtil.CHUNK_WIDTH; // For any number n: n & (2^k - 1) is equivalent to Math.floorMod(n, 2^k)
int relSourceBlockZ = Math.floorMod(apiChunk.chunkPosZ, 4) * LodUtil.CHUNK_WIDTH; // Original: Math.floorMod(x, 4) - Handles negative numbers, gives non-negative result in range [0,3]
// Bitwise: x & (4-1) - Also gives non-negative result in range [0,3]
// Example: -5 & 3 = 3, which equals Math.floorMod(-5, 4) = 3
int relSourceBlockX = (apiChunk.chunkPosX & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
int relSourceBlockZ = (apiChunk.chunkPosZ & (FullDataSourceV2.NUMB_OF_CHUNKS_WIDE - 1)) * LodUtil.CHUNK_WIDTH;
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(pos);
for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++) for (int relBlockZ = 0; relBlockZ < LodUtil.CHUNK_WIDTH; relBlockZ++)
@@ -533,7 +515,7 @@ public class LodDataBuilder
} }
// is there a gap between the last datapoint? // is there a gap between the last datapoint?
if (topYPos != lastBottomYPos if (topYPos != lastBottomYPos
&& lastBottomYPos != Integer.MIN_VALUE) && lastBottomYPos != Integer.MIN_VALUE)
{ {
throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly."); throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly.");
} }
@@ -81,13 +81,20 @@ public class ClientOnlySaveStructure implements ISaveStructure
{ {
IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper; IServerKeyedClientLevel keyedClientLevel = (IServerKeyedClientLevel) newLevelWrapper;
LOGGER.info("Loading level [" + newLevelWrapper.getDhIdentifier() + "] with key: [" + keyedClientLevel.getServerLevelKey() + "]."); LOGGER.info("Loading level [" + newLevelWrapper.getDhIdentifier() + "] with key: [" + keyedClientLevel.getServerLevelKey() + "].");
String serverKey = keyedClientLevel.getServerKey();
if (serverKey.isEmpty())
{
serverKey = getServerFolderName();
}
// This world was identified by the server directly, so we can know for sure which folder to use. // This world was identified by the server directly, so we can know for sure which folder to use.
saveFolder = getSaveFolderByLevelId(keyedClientLevel.getServerLevelKey()); saveFolder = getSaveFolderByLevelId(serverKey, keyedClientLevel.getServerLevelKey());
} }
else else
{ {
// get the default folder // get the default folder
saveFolder = getSaveFolderByLevelId(levelWrapper.getDhIdentifier()); saveFolder = getSaveFolderByLevelId(getServerFolderName(), levelWrapper.getDhIdentifier());
} }
// Allow API users to override the save folder // Allow API users to override the save folder
@@ -116,7 +123,7 @@ public class ClientOnlySaveStructure implements ISaveStructure
return this.getSaveFolder(levelWrapper); return this.getSaveFolder(levelWrapper);
} }
return getSaveFolderByLevelId(levelWrapper.getDimensionType().getName()); return getSaveFolderByLevelId(getServerFolderName(), levelWrapper.getDimensionType().getName());
} }
@@ -173,11 +180,11 @@ public class ClientOnlySaveStructure implements ISaveStructure
} }
private static File getSaveFolderByLevelId(String dimensionName) private static File getSaveFolderByLevelId(String folderName, String dimensionName)
{ {
String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar String path = MC_SHARED.getInstallationDirectory().getPath() + File.separatorChar
+ SERVER_DATA_FOLDER_NAME + File.separatorChar + SERVER_DATA_FOLDER_NAME + File.separatorChar
+ getServerFolderName() + File.separatorChar + folderName + File.separatorChar
+ dimensionName.replaceAll(":", "@@"); + dimensionName.replaceAll(":", "@@");
return new File(path); return new File(path);
@@ -328,7 +328,8 @@ public class DhLightingEngine
continue; continue;
} }
if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight() || relNeighbourBlockPos.getY() > neighbourChunk.getExclusiveMaxBuildHeight()) if (relNeighbourBlockPos.getY() < neighbourChunk.getMinNonEmptyHeight()
|| relNeighbourBlockPos.getY() >= neighbourChunk.getExclusiveMaxBuildHeight())
{ {
// the light pos is outside the chunk's min/max height, // the light pos is outside the chunk's min/max height,
// this can happen if given a chunk that hasn't finished generating // this can happen if given a chunk that hasn't finished generating
@@ -113,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(
(int) (this.networkState.sessionConfig.getGenerationBoundsX() / this.level.levelWrapper.getDimensionType().getCoordinateScale()), this.networkState.sessionConfig.getGenerationBoundsX(),
(int) (this.networkState.sessionConfig.getGenerationBoundsZ() / this.level.levelWrapper.getDimensionType().getCoordinateScale()) this.networkState.sessionConfig.getGenerationBoundsZ()
)) > this.networkState.sessionConfig.getGenerationBoundsRadius()) )) > this.networkState.sessionConfig.getGenerationBoundsRadius())
{ {
return false; return false;
@@ -111,7 +111,7 @@ public class SelfUpdater
} }
if (!ModrinthGetter.mcVersions.contains(mcVersion)) if (!ModrinthGetter.mcVersions.contains(mcVersion))
{ {
LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Modrinth, only findable versions are ["+ StringUtil.join(",", ModrinthGetter.mcVersions) +"]"); LOGGER.warn("Minecraft version ["+ mcVersion +"] is not findable on Modrinth, only findable versions are ["+ StringUtil.join(", ", ModrinthGetter.mcVersions) +"]");
return false; return false;
} }
@@ -277,7 +277,7 @@ public abstract class AbstractDhLevel implements IDhLevel
// locked to prevent two threads from updating the same section at the same time // locked to prevent two threads from updating the same section at the same time
ReentrantLock lock = this.beaconUpdateLockContainer.getLockForPos(sectionPosForLock); ReentrantLock lock = this.beaconUpdateLockContainer.getLockForPos(sectionPosForLock); // TODO this can cause a lot of slow-downs
try try
{ {
lock.lock(); lock.lock();
@@ -151,10 +151,9 @@ 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(
(int) (Config.Server.generationBoundsX.get() / coordinateScale), serverPlayerState.sessionConfig.getGenerationBoundsX(),
(int) (Config.Server.generationBoundsZ.get() / coordinateScale) serverPlayerState.sessionConfig.getGenerationBoundsZ()
)) > 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"));
@@ -262,29 +261,27 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
} }
LodUtil.assertTrue(this.beaconBeamRepo != null, "beaconBeamRepo should not be null"); LodUtil.assertTrue(this.beaconBeamRepo != null, "beaconBeamRepo should not be null");
try (FullDataPayload payload = new FullDataPayload(data, this.beaconBeamRepo.getAllBeamsForPos(data.getPos()))) FullDataPayload payload = new FullDataPayload(data, this.beaconBeamRepo.getAllBeamsForPos(data.getPos()));
for (ServerPlayerState serverPlayerState : this.serverPlayerStateManager.getReadyPlayers())
{ {
for (ServerPlayerState serverPlayerState : this.serverPlayerStateManager.getReadyPlayers()) if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper)
{ {
if (serverPlayerState.getServerPlayer().getLevel() != this.serverLevelWrapper) continue;
{ }
continue;
}
if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled()) if (!serverPlayerState.sessionConfig.isRealTimeUpdatesEnabled())
{ {
continue; continue;
} }
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.sessionConfig.getMaxUpdateDistanceRadius()) if (distanceFromPlayer <= serverPlayerState.sessionConfig.getMaxUpdateDistanceRadius())
{
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () ->
{ {
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () -> serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
{ });
serverPlayerState.networkSession.sendMessage(new FullDataPartialUpdateMessage(this.serverLevelWrapper, payload));
});
}
} }
} }
}); });
@@ -258,7 +258,11 @@ public class ClientLevelModule implements Closeable, AbstractDataSourceHandler.I
public void clearRenderCache() public void clearRenderCache()
{ {
this.clientLevel.getClientLevelWrapper().clearBlockColorCache(); IClientLevelWrapper clientLevelWrapper = this.clientLevel.getClientLevelWrapper();
if (clientLevelWrapper != null)
{
clientLevelWrapper.clearBlockColorCache();
}
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get(); ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
if (ClientRenderState != null && ClientRenderState.quadtree != null) if (ClientRenderState != null && ClientRenderState.quadtree != null)
@@ -29,6 +29,7 @@ import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV
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;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue; import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState; import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue; import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
@@ -41,12 +42,14 @@ import com.seibel.distanthorizons.core.render.renderer.generic.GenericObjectRend
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.DebugRenderer;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -56,13 +59,16 @@ import java.io.File;
import java.util.*; 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.Executor;
import java.util.concurrent.TimeUnit; 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
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
protected static final ConfigBasedLogger NETWORK_LOGGER = new ConfigBasedLogger(LogManager.getLogger(),
() -> Config.Common.Logging.logNetworkEvent.get());
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
public final ClientLevelModule clientside; public final ClientLevelModule clientside;
@@ -157,17 +163,38 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
return; return;
} }
try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(message.payload))
try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(message.payload))
{ {
if (!message.isSameLevelAs(this.levelWrapper)) boolean isSameLevel = message.isSameLevelAs(this.levelWrapper);
NETWORK_LOGGER.debug("Buffer {} isSameLevel: {}", message.payload.dtoBufferId, isSameLevel);
if (!isSameLevel)
{ {
return; return;
} }
this.updateBeaconBeamsForSectionPos(dataSourceDto.pos, message.payload.beaconBeams);
Executor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor != null)
{
executor.execute(() ->
{
try
{
// TODO this has a lock which can cause stuttering/lag issues
this.updateBeaconBeamsForSectionPos(dataSourceDto.pos, message.payload.beaconBeams);
}
catch (Exception e)
{
LOGGER.error("Unexpected erorr while updating full data source, error: ["+e.getMessage()+"].", e);
}
});
}
FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper); FullDataSourceV2 fullDataSource = dataSourceDto.createDataSource(this.levelWrapper);
this.updateDataSourcesAsync(fullDataSource).whenComplete((result, e) -> fullDataSource.close()); this.updateDataSourcesAsync(fullDataSource)
.whenComplete((result, e) -> fullDataSource.close());
} }
catch (Exception e) catch (Exception e)
{ {
@@ -35,6 +35,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import org.jetbrains.annotations.Nullable;
import java.awt.*; import java.awt.*;
import java.util.List; import java.util.List;
@@ -107,6 +108,7 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
} }
} }
@Nullable
@Override @Override
public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); } public IClientLevelWrapper getClientLevelWrapper() { return MC_CLIENT.getWrappedClientLevel(); }
@@ -25,6 +25,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.Nullable;
public interface IDhClientLevel extends IDhLevel public interface IDhClientLevel extends IDhLevel
{ {
@@ -35,6 +36,7 @@ public interface IDhClientLevel extends IDhLevel
int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block); int computeBaseColor(DhBlockPos pos, IBiomeWrapper biome, IBlockStateWrapper block);
@Nullable
IClientLevelWrapper getClientLevelWrapper(); IClientLevelWrapper getClientLevelWrapper();
/** /**
@@ -30,7 +30,7 @@ public interface IKeyedClientLevelManager extends IBindable
{ {
IServerKeyedClientLevel getServerKeyedLevel(); IServerKeyedClientLevel getServerKeyedLevel();
/** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */ /** Called when a client level is wrapped by a ServerEnhancedClientLevel, for integration into mod internals. */
IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String levelKey); IServerKeyedClientLevel setServerKeyedLevel(IClientLevelWrapper clientLevel, String serverKey, String levelKey);
void clearKeyedLevel(); void clearKeyedLevel();
boolean isEnabled(); boolean isEnabled();
@@ -24,6 +24,9 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapp
/** Enhances a {@link IClientLevelWrapper} with server provided level information. */ /** Enhances a {@link IClientLevelWrapper} with server provided level information. */
public interface IServerKeyedClientLevel extends IClientLevelWrapper public interface IServerKeyedClientLevel extends IClientLevelWrapper
{ {
/** Returns the folder name the server wants the player to use. */
String getServerKey();
/** Returns the level key, which is used to select the correct folder on the client. */ /** Returns the level key, which is used to select the correct folder on the client. */
String getServerLevelKey(); String getServerLevelKey();
@@ -290,7 +290,7 @@ public class WorldGenModule implements Closeable
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 left."; String message = "DH is loading chunks. " + remainingChunkCountStr + " left.";
// show a message about how to disable progress logging if requested // show a message about how to disable progress logging if requested
int msToShowDisableInstructions = Config.Common.WorldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.get() * 1_000; int msToShowDisableInstructions = Config.Common.WorldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.get() * 1_000;
@@ -256,7 +256,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
if (response.payload != null) if (response.payload != null)
{ {
FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSourceAndReleaseBuffer(response.payload); FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(response.payload);
// set application flags based on the received detail level, // set application flags based on the received detail level,
// this is needed so the data sources propagate correctly // this is needed so the data sources propagate correctly
@@ -0,0 +1,71 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier;
import java.util.function.IntConsumer;
import java.util.function.IntSupplier;
public class ClientCongestionControl
{
private static final double ADDITIVE_INCREASE = 50000;
private static final long INTERVAL_MS = 1000;
private final Runnable rateUpdateHandler;
private final AtomicLong bytesReceived = new AtomicLong(0);
private double desiredRate;
private long lastAdjustTime;
public ClientCongestionControl(
Runnable rateUpdateHandler
)
{
this.rateUpdateHandler = rateUpdateHandler;
this.reset();
}
public void reset()
{
this.desiredRate = ADDITIVE_INCREASE;
this.lastAdjustTime = System.currentTimeMillis();
this.bytesReceived.set(0);
}
public void onPayloadReceived(FullDataSplitMessage message)
{
long now = System.currentTimeMillis();
if (now - this.lastAdjustTime >= INTERVAL_MS)
{
this.adjustRate(now);
}
this.bytesReceived.addAndGet(message.buffer.readableBytes());
}
private void adjustRate(long now)
{
double throughput = this.bytesReceived.getAndSet(0);
if (throughput >= this.desiredRate)
{
this.desiredRate += ADDITIVE_INCREASE;
}
else
{
this.desiredRate = Math.max(throughput - ADDITIVE_INCREASE / 2, 1000);
}
this.lastAdjustTime = now;
this.rateUpdateHandler.run();
}
public int getDesiredRate()
{
return (int) (this.desiredRate / 1000);
}
}
@@ -1,6 +1,7 @@
package com.seibel.distanthorizons.core.multiplayer.client; package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig; import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
@@ -56,6 +57,23 @@ public class ClientNetworkState implements Closeable
private long serverTimeOffset = 0; private long serverTimeOffset = 0;
public long getServerTimeOffset() { return this.serverTimeOffset; } public long getServerTimeOffset() { return this.serverTimeOffset; }
private final ClientCongestionControl congestionControl = new ClientCongestionControl(
() -> {
if (Config.Server.enableAdaptiveTransferSpeed.get())
{
this.sendConfigMessage(false);
}
}
);
private final ConfigChangeListener<Boolean> adaptiveTransferSpeedListener = new ConfigChangeListener<>(Config.Server.enableAdaptiveTransferSpeed, isEnabled -> {
if (isEnabled)
{
this.congestionControl.reset();
}
this.sendConfigMessage();
});
//=============// //=============//
@@ -116,6 +134,7 @@ public class ClientNetworkState implements Closeable
}); });
this.networkSession.registerHandler(FullDataSplitMessage.class, this.fullDataPayloadReceiver::receiveChunk); this.networkSession.registerHandler(FullDataSplitMessage.class, this.fullDataPayloadReceiver::receiveChunk);
this.networkSession.registerHandler(FullDataSplitMessage.class, this.congestionControl::onPayloadReceived);
} }
} }
@@ -127,10 +146,22 @@ public class ClientNetworkState implements Closeable
public void sendConfigMessage() public void sendConfigMessage() { this.sendConfigMessage(true); }
public void sendConfigMessage(boolean blocking)
{ {
this.configReceived = false; SessionConfig sessionConfig = new SessionConfig();
this.getSession().sendMessage(new SessionConfigMessage(new SessionConfig()));
if (Config.Server.enableAdaptiveTransferSpeed.get())
{
sessionConfig.constrainValue(Config.Server.maxDataTransferSpeed, this.congestionControl.getDesiredRate());
}
if (blocking)
{
this.configReceived = false;
}
this.getSession().sendMessage(new SessionConfigMessage(sessionConfig));
} }
@@ -166,6 +197,7 @@ public class ClientNetworkState implements Closeable
public void close() public void close()
{ {
this.fullDataPayloadReceiver.close(); this.fullDataPayloadReceiver.close();
this.adaptiveTransferSpeedListener.close();
this.configAnyChangeListener.close(); this.configAnyChangeListener.close();
this.networkSession.close(); this.networkSession.close();
} }
@@ -18,7 +18,7 @@ public class SessionConfig implements INetworkObject
private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>(); private static final LinkedHashMap<String, Entry> CONFIG_ENTRIES = new LinkedHashMap<>();
private final LinkedHashMap<String, Object> values = new LinkedHashMap<>(); private final HashMap<String, Object> values = new HashMap<>();
public SessionConfig constrainingConfig; public SessionConfig constrainingConfig;
@@ -33,9 +33,9 @@ public class SessionConfig implements INetworkObject
registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, Boolean::logicalAnd); registerConfigEntry(Config.Common.WorldGenerator.enableDistantGeneration, Boolean::logicalAnd);
registerConfigEntry(Config.Server.maxGenerationRequestDistance, Math::min); registerConfigEntry(Config.Server.maxGenerationRequestDistance, Math::min);
registerConfigEntry(Config.Server.generationBoundsX, (x, y) -> x); registerConfigEntry(Config.Server.generationBoundsX, (x, y) -> y);
registerConfigEntry(Config.Server.generationBoundsZ, (x, y) -> x); registerConfigEntry(Config.Server.generationBoundsZ, (x, y) -> y);
registerConfigEntry(Config.Server.generationBoundsRadius, (x, y) -> x); registerConfigEntry(Config.Server.generationBoundsRadius, (x, y) -> y);
registerConfigEntry(Config.Server.generationRequestRateLimit, Math::min); registerConfigEntry(Config.Server.generationRequestRateLimit, Math::min);
registerConfigEntry(Config.Server.enableRealTimeUpdates, Boolean::logicalAnd); registerConfigEntry(Config.Server.enableRealTimeUpdates, Boolean::logicalAnd);
@@ -119,10 +119,17 @@ public class SessionConfig implements INetworkObject
} }
return (this.constrainingConfig != null return (this.constrainingConfig != null
? (T) entry.valueConstrainer.apply(value, this.constrainingConfig.getValue(name)) ? (T) entry.valueConstrainer.apply(this.constrainingConfig.getValue(name), value)
: value); : value);
} }
public <T> void constrainValue(ConfigEntry<T> configEntry, T value) { this.constrainValue(configEntry.getChatCommandName(), value); }
private void constrainValue(String name, Object value)
{
Entry entry = CONFIG_ENTRIES.get(name);
this.values.put(name, entry.valueConstrainer.apply(this.getValue(name), value));
}
private Map<String, ?> getValues() private Map<String, ?> getValues()
{ {
return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap( return CONFIG_ENTRIES.keySet().stream().collect(Collectors.toMap(
@@ -9,18 +9,17 @@ import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMe
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO; import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
/** /**
* @see FullDataSplitMessage * @see FullDataSplitMessage
*/ */
public class FullDataPayload implements INetworkObject, AutoCloseable public class FullDataPayload implements INetworkObject
{ {
private static final AtomicInteger lastBufferId = new AtomicInteger(); private static final AtomicInteger lastBufferId = new AtomicInteger();
@@ -47,7 +46,7 @@ public class FullDataPayload implements INetworkObject, AutoCloseable
EDhApiDataCompressionMode compressionMode = Config.Common.LodBuilding.dataCompression.get(); EDhApiDataCompressionMode compressionMode = Config.Common.LodBuilding.dataCompression.get();
try (FullDataSourceV2DTO dataSourceDto = FullDataSourceV2DTO.CreateFromDataSource(fullDataSource, compressionMode)) try (FullDataSourceV2DTO dataSourceDto = FullDataSourceV2DTO.CreateFromDataSource(fullDataSource, compressionMode))
{ {
this.dtoBuffer = ByteBufAllocator.DEFAULT.buffer(); this.dtoBuffer = Unpooled.buffer();
dataSourceDto.encode(this.dtoBuffer); dataSourceDto.encode(this.dtoBuffer);
} }
} }
@@ -85,12 +84,6 @@ public class FullDataPayload implements INetworkObject, AutoCloseable
// base overrides // // base overrides //
//================// //================//
@Override
public void close()
{
this.dtoBuffer.release();
}
@Override @Override
public String toString() public String toString()
{ {
@@ -9,8 +9,8 @@ import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage; import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO; import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import java.util.Objects; import java.util.Objects;
@@ -24,17 +24,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
private final ConcurrentMap<Integer, CompositeByteBuf> buffersById = CacheBuilder.newBuilder() private final ConcurrentMap<Integer, CompositeByteBuf> buffersById = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS) .expireAfterAccess(10, TimeUnit.SECONDS)
.removalListener((RemovalNotification<Integer, CompositeByteBuf> notification) -> .<Integer, CompositeByteBuf>build().asMap();
{
// If an entry was replaced without removing, the buffer has to be released manually
if (notification.getCause() != RemovalCause.REPLACED)
{
Objects.requireNonNull(notification.getValue()).release();
}
})
.build().asMap();
@Override @Override
public void close() public void close()
@@ -48,13 +38,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
{ {
if (message.isFirst) if (message.isFirst)
{ {
if (composite != null) composite = Unpooled.compositeBuffer();
{
composite.release();
LOGGER.debug("Released existing full data buffer [" + message.bufferId + "]");
}
composite = ByteBufAllocator.DEFAULT.compositeBuffer();
LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]"); LOGGER.debug("Created new full data buffer [" + message.bufferId + "]: [" + composite + "]");
} }
else if (composite == null) else if (composite == null)
@@ -69,19 +53,21 @@ public class FullDataPayloadReceiver implements AutoCloseable
}); });
} }
public FullDataSourceV2DTO decodeDataSourceAndReleaseBuffer(FullDataPayload msg) public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload)
{ {
CompositeByteBuf compositeByteBuffer = this.buffersById.get(msg.dtoBufferId); CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null); LodUtil.assertTrue(compositeByteBuffer != null);
try try
{ {
return INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer); FullDataSourceV2DTO dataSourceDto = INetworkObject.decodeToInstance(FullDataSourceV2DTO.CreateEmptyDataSourceForDecoding(), compositeByteBuffer);
LOGGER.debug("Buffer {} DTO: {}", payload.dtoBufferId, dataSourceDto);
return dataSourceDto;
} }
finally finally
{ {
// Releasing the buffer is handled by cache // Releasing the buffer is handled by cache
this.buffersById.remove(msg.dtoBufferId); this.buffersById.remove(payload.dtoBufferId);
} }
} }
@@ -13,7 +13,7 @@ import java.util.function.*;
public class FullDataPayloadSender implements AutoCloseable public class FullDataPayloadSender implements AutoCloseable
{ {
private static final int TICK_RATE = 4; private static final int TICK_RATE = 20;
/** 1 Mebibyte minus 576 bytes for other info */ /** 1 Mebibyte minus 576 bytes for other info */
public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000; public static final int FULL_DATA_SPLIT_SIZE_IN_BYTES = 1_048_000;
@@ -38,12 +38,6 @@ public class FullDataPayloadSender implements AutoCloseable
public void close() public void close()
{ {
this.tickTimerTask.cancel(); this.tickTimerTask.cancel();
PendingTransfer pendingTransfer;
while ((pendingTransfer = this.transferQueue.poll()) != null)
{
pendingTransfer.close();
}
} }
@@ -78,36 +72,25 @@ public class FullDataPayloadSender implements AutoCloseable
if (pendingTransfer.buffer.readableBytes() == 0) if (pendingTransfer.buffer.readableBytes() == 0)
{ {
pendingTransfer.sendFinalMessage.run(); pendingTransfer.sendFinalMessage.run();
pendingTransfer.close();
this.transferQueue.poll(); this.transferQueue.poll();
} }
} }
} }
private static class PendingTransfer implements AutoCloseable private static class PendingTransfer
{ {
public final int bufferId; public final int bufferId;
public final ByteBuf buffer; public final ByteBuf buffer;
public final Runnable sendFinalMessage; public final Runnable sendFinalMessage;
private final AtomicBoolean isClosed = new AtomicBoolean();
private PendingTransfer(FullDataPayload payload, Runnable sendFinalMessage) private PendingTransfer(FullDataPayload payload, Runnable sendFinalMessage)
{ {
this.bufferId = payload.dtoBufferId; this.bufferId = payload.dtoBufferId;
this.buffer = payload.dtoBuffer.retainedDuplicate().readerIndex(0); this.buffer = payload.dtoBuffer.duplicate().readerIndex(0);
this.sendFinalMessage = sendFinalMessage; this.sendFinalMessage = sendFinalMessage;
} }
@Override
public void close()
{
if (this.isClosed.compareAndSet(false, true))
{
this.buffer.release();
}
}
} }
} }
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode; import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
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.file.fullDatafile.GeneratedFullDataSourceProvider; import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.level.AbstractDhServerLevel; import com.seibel.distanthorizons.core.level.AbstractDhServerLevel;
import com.seibel.distanthorizons.core.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
@@ -59,45 +60,80 @@ public class FullDataSourceRequestHandler
} }
// the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated
long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1; AbstractExecutorService fileHandlerExecutor = ThreadPoolUtil.getFileHandlerExecutor();
// the server timestamp will be null if no LOD data exists for this position if (fileHandlerExecutor == null)
Long serverTimestamp = this.fullDataSourceProvider().getTimestampForPos(message.sectionPos);
if (serverTimestamp == null
|| serverTimestamp <= clientTimestamp)
{ {
// either no data exists to sync, or the client is already up to date // shouldn't normally happen, but just in case
rateLimiterSet.syncOnLoginRateLimiter.release(); LOGGER.warn("Unable to send FullDataSourceResponseMessage - getFileHandlerExecutor() is null");
message.sendResponse(new FullDataSourceResponseMessage(null));
return; return;
} }
AbstractExecutorService networkCompressionExecutor = ThreadPoolUtil.getNetworkCompressionExecutor();
AbstractExecutorService executor = ThreadPoolUtil.getNetworkCompressionExecutor(); if (networkCompressionExecutor == null)
if (executor == null)
{ {
// shouldn't normally happen, but just in case // shouldn't normally happen, but just in case
LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null"); LOGGER.warn("Unable to send FullDataSourceResponseMessage - getNetworkCompressionExecutor() is null");
return; return;
} }
this.fullDataSourceProvider().getAsync(message.sectionPos).thenAcceptAsync(fullDataSource ->
{
try (FullDataPayload payload = new FullDataPayload(fullDataSource, this.getAllBeamsForPos(message.sectionPos)))
{
fullDataSource.close();
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () -> // get the data requested by the client
{ CompletableFuture<FullDataSourceV2> getServerDatasourceFuture = CompletableFuture.supplyAsync(() ->
message.sendResponse(new FullDataSourceResponseMessage(payload));
rateLimiterSet.syncOnLoginRateLimiter.release();
});
}
catch (Exception e)
{ {
LOGGER.error("Unexpected issue getting request for pos ["+DhSectionPos.toString(message.sectionPos)+"], error: ["+e.getMessage()+"].", e); try
} {
}, executor); // the client timestamp will be null if we want to retrieve the LOD regardless of when it was last updated
long clientTimestamp = (message.clientTimestamp != null) ? message.clientTimestamp : -1;
// the server timestamp will be null if no LOD data exists for this position
Long serverTimestamp = this.fullDataSourceProvider().getTimestampForPos(message.sectionPos);
if (serverTimestamp == null
|| serverTimestamp <= clientTimestamp)
{
// either no data exists to sync, or the client is already up to date
rateLimiterSet.syncOnLoginRateLimiter.release();
message.sendResponse(new FullDataSourceResponseMessage(null));
return null;
}
// get the server's datasource
return this.fullDataSourceProvider().get(message.sectionPos);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue getting server-side LOD for request at pos [" + DhSectionPos.toString(message.sectionPos) + "], error: [" + e.getMessage() + "].", e);
return null;
}
}, fileHandlerExecutor);
// send the found data
getServerDatasourceFuture.thenAcceptAsync(fullDataSource ->
{
try
{
// no server data source found
if (fullDataSource == null)
{
return;
}
// send the found data source to client
FullDataPayload payload = new FullDataPayload(fullDataSource, this.getAllBeamsForPos(message.sectionPos));
fullDataSource.close();
serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () ->
{
message.sendResponse(new FullDataSourceResponseMessage(payload));
rateLimiterSet.syncOnLoginRateLimiter.release();
});
}
catch (Exception e)
{
LOGGER.error("Unexpected issue sending request for pos [" + DhSectionPos.toString(message.sectionPos) + "], error: [" + e.getMessage() + "].", e);
}
}, networkCompressionExecutor);
} }
public void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet) public void queueWorldGenForRequestMessage(ServerPlayerState serverPlayerState, FullDataSourceRequestMessage message, ServerPlayerState.RateLimiterSet rateLimiterSet)
@@ -207,19 +243,17 @@ public class FullDataSourceRequestHandler
} }
CompletableFuture.runAsync(() -> CompletableFuture.runAsync(() ->
{ {
try (FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource, this.getAllBeamsForPos(entry.getKey()))) FullDataPayload payload = new FullDataPayload(requestGroup.fullDataSource, this.getAllBeamsForPos(entry.getKey()));
requestGroup.fullDataSource.close();
for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values())
{ {
requestGroup.fullDataSource.close(); this.requestGroupsByFutureId.remove(requestData.futureId());
for (DataSourceRequestGroup.RequestData requestData : requestGroup.requestMessages.values()) requestData.serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () -> {
{ requestData.message.sendResponse(new FullDataSourceResponseMessage(payload));
this.requestGroupsByFutureId.remove(requestData.futureId()); requestData.rateLimiterSet.generationRequestRateLimiter.release();
});
requestData.serverPlayerState.fullDataPayloadSender.sendInChunks(payload, () -> {
requestData.message.sendResponse(new FullDataSourceResponseMessage(payload));
requestData.rateLimiterSet.generationRequestRateLimiter.release();
});
}
} }
}, executor); }, executor);
} }
@@ -24,9 +24,13 @@ public class ServerPlayerState implements Closeable
{ {
private final ConfigChangeListener<String> levelKeyPrefixChangeListener private final ConfigChangeListener<String> levelKeyPrefixChangeListener
= new ConfigChangeListener<>(Config.Server.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged); = new ConfigChangeListener<>(Config.Server.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged);
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::onSessionConfigChanged); private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::sendConfigMessage);
private final String serverKeyWithoutId = Config.Server.serverKey.get();
private final String serverKey = (this.serverKeyWithoutId.isEmpty() ? "" : Config.Server.serverId.get() + "_" + this.serverKeyWithoutId.trim())
.replaceAll("[^" + LevelInitMessage.PART_ALLOWED_CHARS_REGEX + " ]", "")
.replaceAll(" ", "_");
private String lastLevelKey = ""; private String lastLevelKey = "";
@@ -56,8 +60,9 @@ public class ServerPlayerState implements Closeable
this.networkSession.registerHandler(SessionConfigMessage.class, (sessionConfigMessage) -> this.networkSession.registerHandler(SessionConfigMessage.class, (sessionConfigMessage) ->
{ {
this.sessionConfig.constrainingConfig = sessionConfigMessage.config; this.sessionConfig.constrainingConfig = sessionConfigMessage.config;
this.sendLevelKey(); this.sendLevelKey();
this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); this.sendConfigMessage();
}); });
this.networkSession.registerHandler(CloseInternalEvent.class, event -> { this.networkSession.registerHandler(CloseInternalEvent.class, event -> {
@@ -88,12 +93,19 @@ public class ServerPlayerState implements Closeable
if (!levelKey.equals(this.lastLevelKey)) if (!levelKey.equals(this.lastLevelKey))
{ {
this.lastLevelKey = levelKey; this.lastLevelKey = levelKey;
this.networkSession.sendMessage(new LevelInitMessage(levelKey)); this.networkSession.sendMessage(new LevelInitMessage(this.serverKey, levelKey));
} }
} }
} }
private void onSessionConfigChanged() { this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig)); } private void sendConfigMessage()
{
double coordinateScale = this.getServerPlayer().getLevel().getDimensionType().getCoordinateScale();
this.sessionConfig.constrainValue(Config.Server.generationBoundsX, (int) (Config.Server.generationBoundsX.get() / coordinateScale));
this.sessionConfig.constrainValue(Config.Server.generationBoundsZ, (int) (Config.Server.generationBoundsZ.get() / coordinateScale));
this.networkSession.sendMessage(new SessionConfigMessage(this.sessionConfig));
}
@@ -46,6 +46,13 @@ public class ServerPlayerStateManager
public void handlePluginMessage(IServerPlayerWrapper player, AbstractNetworkMessage message) public void handlePluginMessage(IServerPlayerWrapper player, AbstractNetworkMessage message)
{ {
// done to prevent a rare null-pointer on Neo/Forge
if (player == null || message == null)
{
return;
}
MessageQueueState messageQueue = this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new MessageQueueState()); MessageQueueState messageQueue = this.messageQueueByPlayerWrapper.computeIfAbsent(player, k -> new MessageQueueState());
messageQueue.messageQueue.add(message); messageQueue.messageQueue.add(message);
@@ -10,12 +10,18 @@ public class LevelInitMessage extends AbstractNetworkMessage
public static final String PART_ALLOWED_CHARS_REGEX = "a-zA-Z0-9-_"; public static final String PART_ALLOWED_CHARS_REGEX = "a-zA-Z0-9-_";
// A plain string of characters
// 1-150 characters in total
public static final String SERVER_KEY_REGEX = String.format("^(?=.{1,%s}$)[%s]+$",
MAX_LENGTH, PART_ALLOWED_CHARS_REGEX);
// prefix@namespace:path // prefix@namespace:path
// 1-150 characters in total, all parts except namespace can be omitted // 1-150 characters in total, all parts except namespace can be omitted
public static final String VALIDATION_REGEX = String.format("^(?=.{1,%s}$)([%s]+@)?[%s]+(:[%s]+)?$", public static final String LEVEL_KEY_REGEX = String.format("^(?=.{1,%s}$)([%s]+@)?[%s]+(:[%s]+)?$",
MAX_LENGTH, PART_ALLOWED_CHARS_REGEX, PART_ALLOWED_CHARS_REGEX, PART_ALLOWED_CHARS_REGEX); MAX_LENGTH, PART_ALLOWED_CHARS_REGEX, PART_ALLOWED_CHARS_REGEX, PART_ALLOWED_CHARS_REGEX);
public String serverKey;
public String levelKey; public String levelKey;
public long serverTime; public long serverTime;
@@ -26,8 +32,9 @@ public class LevelInitMessage extends AbstractNetworkMessage
//==============// //==============//
public LevelInitMessage() { } public LevelInitMessage() { }
public LevelInitMessage(String levelKey) public LevelInitMessage(String serverKey, String levelKey)
{ {
this.serverKey = serverKey;
this.levelKey = levelKey; this.levelKey = levelKey;
this.serverTime = System.currentTimeMillis(); this.serverTime = System.currentTimeMillis();
} }
@@ -41,6 +48,7 @@ public class LevelInitMessage extends AbstractNetworkMessage
@Override @Override
public void encode(ByteBuf out) public void encode(ByteBuf out)
{ {
this.writeString(this.serverKey, out);
this.writeString(this.levelKey, out); this.writeString(this.levelKey, out);
out.writeLong(this.serverTime); out.writeLong(this.serverTime);
} }
@@ -48,6 +56,7 @@ public class LevelInitMessage extends AbstractNetworkMessage
@Override @Override
public void decode(ByteBuf in) public void decode(ByteBuf in)
{ {
this.serverKey = this.readString(in);
this.levelKey = this.readString(in); this.levelKey = this.readString(in);
this.serverTime = in.readLong(); this.serverTime = in.readLong();
} }
@@ -62,6 +71,7 @@ public class LevelInitMessage extends AbstractNetworkMessage
public MoreObjects.ToStringHelper toStringHelper() public MoreObjects.ToStringHelper toStringHelper()
{ {
return super.toStringHelper() return super.toStringHelper()
.add("serverKey", this.serverKey)
.add("levelKey", this.levelKey) .add("levelKey", this.levelKey)
.add("serverTime", this.serverTime); .add("serverTime", this.serverTime);
} }
@@ -22,8 +22,8 @@ package com.seibel.distanthorizons.core.network.messages.fullData;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.multiplayer.fullData.FullDataPayload; import com.seibel.distanthorizons.core.multiplayer.fullData.FullDataPayload;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage; import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.util.TimerUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.Timer; import java.util.Timer;
@@ -34,16 +34,10 @@ import java.util.Timer;
*/ */
public class FullDataSplitMessage extends AbstractNetworkMessage public class FullDataSplitMessage extends AbstractNetworkMessage
{ {
private static final long BUFFER_RELEASE_DELAY_MS = 5000L;
public int bufferId; public int bufferId;
public ByteBuf buffer; public ByteBuf buffer;
public boolean isFirst; public boolean isFirst;
// Reference counting is unreliable here for some reason so this is a "fix"
private static final Timer bufferReleaseTimer = TimerUtil.CreateTimer("FullDataBufferCleanupTimer");
private boolean releaseScheduled = false;
//==============// //==============//
// constructors // // constructors //
@@ -72,12 +66,6 @@ public class FullDataSplitMessage extends AbstractNetworkMessage
out.writeBytes(this.buffer.readerIndex(0)); out.writeBytes(this.buffer.readerIndex(0));
out.writeBoolean(this.isFirst); out.writeBoolean(this.isFirst);
if (!this.releaseScheduled)
{
bufferReleaseTimer.schedule(TimerUtil.createTimerTask(this.buffer::release), BUFFER_RELEASE_DELAY_MS);
this.releaseScheduled = true;
}
} }
@Override @Override
@@ -86,7 +74,7 @@ public class FullDataSplitMessage extends AbstractNetworkMessage
this.bufferId = in.readInt(); this.bufferId = in.readInt();
int bufferSize = in.readInt(); int bufferSize = in.readInt();
this.buffer = in.readBytes(bufferSize); this.buffer = Unpooled.copiedBuffer(in.readSlice(bufferSize));
this.isFirst = in.readBoolean(); this.isFirst = in.readBoolean();
} }
@@ -306,9 +306,9 @@ public class PhantomArrayListPool
{ {
// we only want to log when something has been returned // we only want to log when something has been returned
if (checkoutCount != 0 if (checkoutCount != 0
|| returnedByteArrayCount != 0 || returnedByteArrayCount != 0
|| returnedShortArrayCount != 0 || returnedShortArrayCount != 0
|| returnedLongArrayCount != 0) || returnedLongArrayCount != 0)
{ {
LOGGER.warn("Pool: ["+ pool.name+"] phantom recovery. Returned checkouts:["+F3Screen.NUMBER_FORMAT.format(checkoutCount)+"], byte:["+F3Screen.NUMBER_FORMAT.format(returnedByteArrayCount)+"], short:["+F3Screen.NUMBER_FORMAT.format(returnedShortArrayCount)+"], long:["+F3Screen.NUMBER_FORMAT.format(returnedLongArrayCount)+"]."); LOGGER.warn("Pool: ["+ pool.name+"] phantom recovery. Returned checkouts:["+F3Screen.NUMBER_FORMAT.format(checkoutCount)+"], byte:["+F3Screen.NUMBER_FORMAT.format(returnedByteArrayCount)+"], short:["+F3Screen.NUMBER_FORMAT.format(returnedShortArrayCount)+"], long:["+F3Screen.NUMBER_FORMAT.format(returnedLongArrayCount)+"].");
@@ -399,9 +399,11 @@ 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
// TODO this is commented out since some users reported LODs refusing to
// load at their expected higher-detail levels
// this check is specifically for N-sized world generators where the higher quality // 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 // data source may not exist yet, this is done to prevent holes while waiting for said generator
&& renderSection.getFullDataSourceExists() //&& renderSection.getFullDataSourceExists()
) )
{ {
nodesNeedingLoading.add(renderSection); nodesNeedingLoading.add(renderSection);
@@ -510,10 +510,13 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{ {
// TODO memoization is needed for multiplayer, otherwise // TODO memoization is needed for multiplayer, otherwise
// new retrieval requests won't be submitted. // new retrieval requests won't be submitted.
// TODO why is that the case? Shouldn't the missing positions be un-changing? // TODO why is that the case? Shouldn't the missing positions be un-changing?
// TODO setting this value to low can cause world gen to slow down significantly
// due to a race condition where the world gen thinks it is finished, but the results
// haven't been saved to file yet, causing the gen to fire again
this.missingGenerationPosFunc = Suppliers.memoizeWithExpiration( this.missingGenerationPosFunc = Suppliers.memoizeWithExpiration(
() -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos), () -> this.fullDataSourceProvider.getPositionsToRetrieve(this.pos),
15, TimeUnit.SECONDS); 10, TimeUnit.MINUTES);
} }
LongArrayList missingGenerationPos = this.getMissingGenerationPos(); LongArrayList missingGenerationPos = this.getMissingGenerationPos();
@@ -359,6 +359,7 @@ public class RenderBufferHandler implements AutoCloseable
// debug wireframe setup // // debug wireframe setup //
//=======================// //=======================//
// TODO move this logic into LodRenderer so all the GL states can be handled there
boolean renderWireframe = Config.Client.Advanced.Debugging.renderWireframe.get(); boolean renderWireframe = Config.Client.Advanced.Debugging.renderWireframe.get();
if (renderWireframe) if (renderWireframe)
{ {
@@ -25,10 +25,10 @@ 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.logging.ConfigBasedLogger; import com.seibel.distanthorizons.core.logging.ConfigBasedLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.objects.GLMessage; import com.seibel.distanthorizons.core.util.objects.GLMessages.*;
import com.seibel.distanthorizons.core.util.objects.GLMessageOutputStream;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.glfw.GLFW; import org.lwjgl.glfw.GLFW;
@@ -38,31 +38,28 @@ import org.lwjgl.opengl.GLCapabilities;
import org.lwjgl.opengl.GLUtil; import org.lwjgl.opengl.GLUtil;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.invoke.MethodHandles; import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
/** /**
* A singleton that holds references to different openGL contexts * A singleton that holds references to different openGL contexts
* and GPU capabilities. * and GPU capabilities.
*
* <p>
* Helpful OpenGL resources:
* <p>
* https://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-AsynchronousBufferTransfers.pdf <br>
* https://learnopengl.com/Advanced-OpenGL/Advanced-Data <br>
* https://www.slideshare.net/CassEveritt/approaching-zero-driver-overhead <br><br>
*
* https://gamedev.stackexchange.com/questions/91995/edit-vbo-data-or-create-a-new-one <br>
* https://stackoverflow.com/questions/63509735/massive-performance-loss-with-glmapbuffer <br><br>
*/ */
public class GLProxy public class GLProxy
{ {
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final Logger LOGGER = DhLoggerBuilder.getLogger(MethodHandles.lookup().lookupClass().getSimpleName()); private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public static final ConfigBasedLogger GL_LOGGER = new ConfigBasedLogger(LogManager.getLogger(GLProxy.class), public static final ConfigBasedLogger GL_LOGGER = new ConfigBasedLogger(LogManager.getLogger(GLProxy.class),
() -> Config.Common.Logging.logRendererGLEvent.get()); () -> Config.Common.Logging.logRendererGLEvent.get());
public static final Set<String> LOGGED_GL_MESSAGES = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
private static GLProxy instance = null; private static GLProxy instance = null;
@@ -79,7 +76,29 @@ public class GLProxy
private final EDhApiGpuUploadMethod preferredUploadMethod; private final EDhApiGpuUploadMethod preferredUploadMethod;
public final GLMessage.Builder vanillaDebugMessageBuilder = GLMessage.Builder.DEFAULT_MESSAGE_BUILDER; public final GLMessageBuilder vanillaDebugMessageBuilder =
new GLMessageBuilder(
(type) ->
{
if (type == EGLMessageType.POP_GROUP)
return false;
else if (type == EGLMessageType.PUSH_GROUP)
return false;
else if (type == EGLMessageType.MARKER)
return false;
else
return true;
},
(severity) ->
{
// notifications can generally be ignored (if they are logged at all)
if (severity == EGLMessageSeverity.NOTIFICATION)
return false;
else
return true;
},
null
);
@@ -258,6 +277,7 @@ public class GLProxy
// logging // // logging //
//=========// //=========//
/** this method is called on the render thread at the point of the GL Error */
private static void logMessage(GLMessage msg) private static void logMessage(GLMessage msg)
{ {
EDhApiGLErrorHandlingMode errorHandlingMode = Config.Client.Advanced.Debugging.OpenGl.glErrorHandlingMode.get(); EDhApiGLErrorHandlingMode errorHandlingMode = Config.Client.Advanced.Debugging.OpenGl.glErrorHandlingMode.get();
@@ -268,44 +288,56 @@ public class GLProxy
if (msg.type == GLMessage.EType.ERROR || msg.type == GLMessage.EType.UNDEFINED_BEHAVIOR) boolean onlyLogOnce = Config.Client.Advanced.Debugging.OpenGl.onlyLogGlErrorsOnce.get();
String errorMessage = "GL ERROR [" + msg.id + "] from [" + msg.source + "]: [" + msg.message + "]"+(onlyLogOnce ? " this message will only be logged once" : "")+".";
if (onlyLogOnce
&& !LOGGED_GL_MESSAGES.add(errorMessage))
{
// this message has already been logged
return;
}
// create an exception so we get a stacktrace of where the message was triggered from
RuntimeException exception = new RuntimeException(errorMessage);
if (msg.type == EGLMessageType.ERROR || msg.type == EGLMessageType.UNDEFINED_BEHAVIOR)
{ {
// critical error // critical error
GL_LOGGER.error("GL ERROR " + msg.id + " from " + msg.source + ": " + msg.message); GL_LOGGER.error(exception.getMessage(), exception);
if (errorHandlingMode == EDhApiGLErrorHandlingMode.LOG_THROW) if (errorHandlingMode == EDhApiGLErrorHandlingMode.LOG_THROW)
{ {
throw new RuntimeException("GL ERROR: " + msg); // will probably crash the game,
// good for quickly checking if there's a problem while preventing log spam
throw exception;
} }
} }
else else
{ {
// non-critical log // non-critical log
GLMessage.ESeverity severity = msg.severity; EGLMessageSeverity severity = msg.severity;
RuntimeException ex = new RuntimeException("GL MESSAGE: " + msg);
if (severity == null) if (severity == null)
{ {
// just in case the message was malformed // just in case the message was malformed
severity = GLMessage.ESeverity.LOW; severity = EGLMessageSeverity.LOW;
} }
switch (severity) switch (severity)
{ {
case HIGH: case HIGH:
GL_LOGGER.error("{}", ex); GL_LOGGER.error(exception.getMessage(), exception);
break; break;
case MEDIUM: case MEDIUM:
GL_LOGGER.warn("{}", ex); GL_LOGGER.warn(exception.getMessage(), exception);
break; break;
case LOW: case LOW:
GL_LOGGER.info("{}", ex); GL_LOGGER.info(exception.getMessage(), exception);
break; break;
case NOTIFICATION: case NOTIFICATION:
GL_LOGGER.debug("{}", ex); GL_LOGGER.debug(exception.getMessage(), exception);
break; break;
} }
} }
@@ -35,7 +35,7 @@ public class GLState
public int vao; public int vao;
public int vbo; public int vbo;
public int ebo; public int ebo;
public int[] fbo; public int fbo;
public int texture2D; public int texture2D;
/** IE: GL_TEXTURE0, GL_TEXTURE1, etc. */ /** IE: GL_TEXTURE0, GL_TEXTURE1, etc. */
public int activeTextureNumber; public int activeTextureNumber;
@@ -57,10 +57,10 @@ public class GLState
public boolean depth; public boolean depth;
public boolean writeToDepthBuffer; public boolean writeToDepthBuffer;
public int depthFunc; public int depthFunc;
//public boolean stencil; public boolean stencil;
//public int stencilFunc; public int stencilFunc;
//public int stencilRef; public int stencilRef;
//public int stencilMask; public int stencilMask;
public int[] view; public int[] view;
public boolean cull; public boolean cull;
public int cullMode; public int cullMode;
@@ -68,12 +68,7 @@ public class GLState
public GLState() public GLState() { this.saveState(); }
{
this.fbo = new int[FBO_MAX];
this.saveState();
}
public void saveState() public void saveState()
{ {
@@ -82,7 +77,7 @@ public class GLState
this.vbo = GL32.glGetInteger(GL32.GL_ARRAY_BUFFER_BINDING); this.vbo = GL32.glGetInteger(GL32.GL_ARRAY_BUFFER_BINDING);
this.ebo = GL32.glGetInteger(GL32.GL_ELEMENT_ARRAY_BUFFER_BINDING); this.ebo = GL32.glGetInteger(GL32.GL_ELEMENT_ARRAY_BUFFER_BINDING);
GL32.glGetIntegerv(GL32.GL_FRAMEBUFFER_BINDING, this.fbo); this.fbo = GL32.glGetInteger(GL32.GL_FRAMEBUFFER_BINDING);
this.texture2D = GL32.glGetInteger(GL32.GL_TEXTURE_BINDING_2D); this.texture2D = GL32.glGetInteger(GL32.GL_TEXTURE_BINDING_2D);
this.activeTextureNumber = GL32.glGetInteger(GL32.GL_ACTIVE_TEXTURE); this.activeTextureNumber = GL32.glGetInteger(GL32.GL_ACTIVE_TEXTURE);
@@ -101,9 +96,19 @@ public class GLState
GLMC.glActiveTexture(this.activeTextureNumber); GLMC.glActiveTexture(this.activeTextureNumber);
this.frameBufferTexture0 = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME); if (this.fbo != 0)
this.frameBufferTexture1 = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT1, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME); {
this.frameBufferDepthTexture = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_DEPTH_ATTACHMENT, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME); this.frameBufferTexture0 = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
this.frameBufferTexture1 = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT1, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
this.frameBufferDepthTexture = GL32.glGetFramebufferAttachmentParameteri(GL32.GL_FRAMEBUFFER, GL32.GL_DEPTH_ATTACHMENT, GL32.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME);
}
else
{
// attempting to get values from the default framebuffer can throw errors on Linux
this.frameBufferTexture0 = 0;
this.frameBufferTexture1 = 0;
this.frameBufferDepthTexture = 0;
}
this.blend = GL32.glIsEnabled(GL32.GL_BLEND); this.blend = GL32.glIsEnabled(GL32.GL_BLEND);
this.scissor = GL32.glIsEnabled(GL32.GL_SCISSOR_TEST); this.scissor = GL32.glIsEnabled(GL32.GL_SCISSOR_TEST);
@@ -116,10 +121,10 @@ public class GLState
this.depth = GL32.glIsEnabled(GL32.GL_DEPTH_TEST); this.depth = GL32.glIsEnabled(GL32.GL_DEPTH_TEST);
this.writeToDepthBuffer = GL32.glGetInteger(GL32.GL_DEPTH_WRITEMASK) == GL32.GL_TRUE; this.writeToDepthBuffer = GL32.glGetInteger(GL32.GL_DEPTH_WRITEMASK) == GL32.GL_TRUE;
this.depthFunc = GL32.glGetInteger(GL32.GL_DEPTH_FUNC); this.depthFunc = GL32.glGetInteger(GL32.GL_DEPTH_FUNC);
//this.stencil = GL32.glIsEnabled(GL32.GL_STENCIL_TEST); this.stencil = GL32.glIsEnabled(GL32.GL_STENCIL_TEST);
//this.stencilFunc = GL32.glGetInteger(GL32.GL_STENCIL_FUNC); this.stencilFunc = GL32.glGetInteger(GL32.GL_STENCIL_FUNC);
//this.stencilRef = GL32.glGetInteger(GL32.GL_STENCIL_REF); this.stencilRef = GL32.glGetInteger(GL32.GL_STENCIL_REF);
//this.stencilMask = GL32.glGetInteger(GL32.GL_STENCIL_VALUE_MASK); this.stencilMask = GL32.glGetInteger(GL32.GL_STENCIL_VALUE_MASK);
this.view = new int[4]; this.view = new int[4];
GL32.glGetIntegerv(GL32.GL_VIEWPORT, this.view); GL32.glGetIntegerv(GL32.GL_VIEWPORT, this.view);
this.cull = GL32.glIsEnabled(GL32.GL_CULL_FACE); this.cull = GL32.glIsEnabled(GL32.GL_CULL_FACE);
@@ -131,38 +136,33 @@ public class GLState
public String toString() public String toString()
{ {
return "GLState{" + return "GLState{" +
"program=" + this.program + ", vao=" + this.vao + ", vbo=" + this.vbo + ", ebo=" + this.ebo + ", fbo=" + this.fbo[0] + "program=" + this.program + ", vao=" + this.vao + ", vbo=" + this.vbo + ", ebo=" + this.ebo + ", fbo=" + this.fbo +
", text=" + GLEnums.getString(this.texture2D) + "@" + this.activeTextureNumber + ", text0=" + GLEnums.getString(this.texture0) + ", text=" + GLEnums.getString(this.texture2D) + "@" + this.activeTextureNumber + ", text0=" + GLEnums.getString(this.texture0) +
", FB text0=" + this.frameBufferTexture0 + ", FB text0=" + this.frameBufferTexture0 +
", FB text1=" + this.frameBufferTexture1 + ", FB text1=" + this.frameBufferTexture1 +
", FB depth=" + this.frameBufferDepthTexture + ", FB depth=" + this.frameBufferDepthTexture +
", blend=" + this.blend + ", scissor=" + this.scissor + ", blendMode=" + GLEnums.getString(this.blendSrcColor) + "," + GLEnums.getString(this.blendDstColor) + ", blend=" + this.blend + ", scissor=" + this.scissor + ", blendMode=" + GLEnums.getString(this.blendSrcColor) + "," + GLEnums.getString(this.blendDstColor) +
", depth=" + this.depth + ", depth=" + this.depth +
//", depthFunc=" + GLEnums.getString(this.depthFunc) + ", stencil=" + this.stencil + ", stencilFunc=" + ", depthFunc=" + GLEnums.getString(this.depthFunc) + ", stencil=" + this.stencil +
//GLEnums.getString(this.stencilFunc) + ", stencilRef=" + this.stencilRef + ", stencilMask=" + this.stencilMask + ", stencilFunc=" + GLEnums.getString(this.stencilFunc) + ", stencilRef=" + this.stencilRef + ", stencilMask=" + this.stencilMask +
", view={x:" + this.view[0] + ", y:" + this.view[1] + ", view={x:" + this.view[0] + ", y:" + this.view[1] +
", w:" + this.view[2] + ", h:" + this.view[3] + "}" + ", cull=" + this.cull + ", cullMode=" ", w:" + this.view[2] + ", h:" + this.view[3] + "}" + ", cull=" + this.cull +
+ GLEnums.getString(this.cullMode) + ", polyMode=" + GLEnums.getString(this.polyMode) + ", cullMode=" + GLEnums.getString(this.cullMode) + ", polyMode=" + GLEnums.getString(this.polyMode) +
'}'; '}';
} }
public void RestoreFrameBuffer()
{
// explicitly unbinding the frame buffer is necessary to prevent GL_CLEAR calls from hitting the wrong buffer
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, 0);
for (int i = 0; i < FBO_MAX; i++)
{
int buffer = this.fbo[i];
if (i > 0 && buffer == 0) break;
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, GL32.glIsFramebuffer(buffer) ? buffer : 0);
}
}
public void restore() public void restore()
{ {
this.RestoreFrameBuffer(); // explicitly unbinding the frame buffer is necessary to prevent GL_CLEAR calls from hitting the wrong buffer
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, 0);
boolean frameBufferSet = false;
if (this.fbo != 0 && GL32.glIsFramebuffer(this.fbo))
{
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.fbo);
frameBufferSet = true;
}
if (this.blend) if (this.blend)
{ {
@@ -197,9 +197,13 @@ public class GLState
GLMC.glActiveTexture(this.activeTextureNumber); GLMC.glActiveTexture(this.activeTextureNumber);
GLMC.glBindTexture(GL32.glIsTexture(this.texture2D) ? this.texture2D : 0); GLMC.glBindTexture(GL32.glIsTexture(this.texture2D) ? this.texture2D : 0);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, this.frameBufferTexture0, 0); // attempting to set textures on the default frame buffer (ID 0) will throw errors
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT1, GL32.GL_TEXTURE_2D, this.frameBufferTexture1, 0); if (frameBufferSet)
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_DEPTH_ATTACHMENT, GL32.GL_TEXTURE_2D, this.frameBufferDepthTexture, 0); {
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, this.frameBufferTexture0, 0);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT1, GL32.GL_TEXTURE_2D, this.frameBufferTexture1, 0);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_DEPTH_ATTACHMENT, GL32.GL_TEXTURE_2D, this.frameBufferDepthTexture, 0);
}
GL32.glBindVertexArray(GL32.glIsVertexArray(this.vao) ? this.vao : 0); GL32.glBindVertexArray(GL32.glIsVertexArray(this.vao) ? this.vao : 0);
GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, GL32.glIsBuffer(this.vbo) ? this.vbo : 0); GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, GL32.glIsBuffer(this.vbo) ? this.vbo : 0);
@@ -229,15 +233,15 @@ public class GLState
} }
GLMC.glDepthFunc(this.depthFunc); GLMC.glDepthFunc(this.depthFunc);
//if (this.stencil) if (this.stencil)
//{ {
// GL32.glEnable(GL32.GL_STENCIL_TEST); GL32.glEnable(GL32.GL_STENCIL_TEST);
//} }
//else else
//{ {
// GL32.glDisable(GL32.GL_STENCIL_TEST); GL32.glDisable(GL32.GL_STENCIL_TEST);
//} }
//GL32.glStencilFunc(this.stencilFunc, this.stencilRef, this.stencilMask); GL32.glStencilFunc(this.stencilFunc, this.stencilRef, this.stencilMask);
GL32.glViewport(this.view[0], this.view[1], this.view[2], this.view[3]); GL32.glViewport(this.view[0], this.view[1], this.view[2], this.view[3]);
if (this.cull) if (this.cull)
@@ -223,7 +223,7 @@ public class GLBuffer implements AutoCloseable
this.destroyAsync(); this.destroyAsync();
this.create(true); this.create(true);
this.bind(); this.bind();
GL44.glBufferStorage(this.getBufferBindingTarget(), bb, bufferStorageHint); GL44.glBufferStorage(this.getBufferBindingTarget(), bb, 0);
this.size = bbSize; this.size = bbSize;
} }
/** Requires the buffer to be bound */ /** Requires the buffer to be bound */
@@ -25,22 +25,31 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import com.seibel.distanthorizons.core.render.glObject.GLProxy; import com.seibel.distanthorizons.core.render.glObject.GLProxy;
import org.lwjgl.PointerBuffer;
import org.lwjgl.opengl.GL32; import org.lwjgl.opengl.GL32;
import org.lwjgl.opengl.GL32C;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.system.NativeType;
/** /**
* This object holds a OpenGL reference to a shader * This object holds a OpenGL reference to a shader
* and allows for reading in and compiling a shader file. * and allows for reading in and compiling a shader file.
*
* @author James Seibel
* @version 11-8-2021
*/ */
public class Shader public class Shader
{ {
/** OpenGL shader ID */ /** OpenGL shader ID */
public final int id; public final int id;
//==============//
// constructors //
//==============//
/** /**
* Creates a shader with specified type. * Creates a shader with specified type.
* *
@@ -51,52 +60,99 @@ public class Shader
*/ */
public Shader(int type, String path, boolean absoluteFilePath) public Shader(int type, String path, boolean absoluteFilePath)
{ {
GLProxy.GL_LOGGER.info("Loading shader at " + path); GLProxy.GL_LOGGER.info("Loading shader at [" + path + "]");
// Create an empty shader object // Create an empty shader object
id = GL32.glCreateShader(type); this.id = GL32.glCreateShader(type);
StringBuilder source = loadFile(path, absoluteFilePath, new StringBuilder()); if (this.id == 0)
GL32.glShaderSource(id, source); {
throw new IllegalArgumentException("Failed to create shader with type ["+type+"].");
}
GL32.glCompileShader(id); StringBuilder source = loadFile(path, absoluteFilePath, new StringBuilder());
safeShaderSource(this.id, source);
GL32.glCompileShader(this.id);
// check if the shader compiled // check if the shader compiled
int status = GL32.glGetShaderi(id, GL32.GL_COMPILE_STATUS); int status = GL32.glGetShaderi(this.id, GL32.GL_COMPILE_STATUS);
if (status != GL32.GL_TRUE) if (status != GL32.GL_TRUE)
{ {
String message = "Shader compiler error. Details: " + GL32.glGetShaderInfoLog(id); String message = "Shader compiler error. Details: ["+GL32.glGetShaderInfoLog(this.id)+"].";
free(); // important! this.free(); // important!
throw new RuntimeException(message); throw new RuntimeException(message);
} }
GLProxy.GL_LOGGER.info("Shader at " + path + " loaded sucessfully."); GLProxy.GL_LOGGER.info("Shader at " + path + " loaded successfully.");
} }
public Shader(int type, String sourceString) public Shader(int type, String sourceString)
{ {
GLProxy.GL_LOGGER.info("Loading shader with type: {}", type); GLProxy.GL_LOGGER.info("Loading shader with type: ["+type+"]");
GLProxy.GL_LOGGER.debug("Source:\n{}", sourceString); GLProxy.GL_LOGGER.debug("Source: \n["+sourceString+"]");
// Create an empty shader object if (sourceString == null || sourceString.isEmpty())
id = GL32.glCreateShader(type); {
GL32.glShaderSource(id, sourceString); throw new IllegalArgumentException("No shader source given.");
}
GL32.glCompileShader(id); // Create an empty shader object
this.id = GL32.glCreateShader(type);
if (this.id == 0)
{
throw new IllegalArgumentException("Failed to create shader with type ["+type+"] and Source: \n["+sourceString+"].");
}
safeShaderSource(this.id, sourceString);
GL32.glCompileShader(this.id);
// check if the shader compiled // check if the shader compiled
int status = GL32.glGetShaderi(id, GL32.GL_COMPILE_STATUS); int status = GL32.glGetShaderi(this.id, GL32.GL_COMPILE_STATUS);
if (status != GL32.GL_TRUE) if (status != GL32.GL_TRUE)
{ {
String message = "Shader compiler error. Details: " + GL32.glGetShaderInfoLog(id); String message = "Shader compiler error. Details: [" + GL32.glGetShaderInfoLog(this.id) + "]\n";
message += "\nSource:\n" + sourceString; message += "Source: \n[" + sourceString + "]";
free(); // important! this.free(); // important!
throw new RuntimeException(message); throw new RuntimeException(message);
} }
GLProxy.GL_LOGGER.info("Shader loaded sucessfully."); GLProxy.GL_LOGGER.info("Shader loaded sucessfully.");
} }
// REMEMBER to always free the resource!
public void free()
//=========//
// helpers //
//=========//
/**
* Identical in function to {@link GL32C#glShaderSource(int, CharSequence)} but
* passes a null pointer for string length to force the driver to rely on the null
* terminator for string length. This is a workaround for an apparent flaw with some
* AMD drivers that don't receive or interpret the length correctly, resulting in
* an access violation when the driver tries to read past the string memory.
*
* <p>Hat tip to fewizz for the find and the fix.
*
* <p>Source: https://github.com/vram-guild/canvas/commit/820bf754092ccaf8d0c169620c2ff575722d7d96
*/
private static void safeShaderSource(@NativeType("GLuint") int glId, @NativeType("GLchar const **") CharSequence source)
{ {
GL32.glDeleteShader(id); final MemoryStack stack = MemoryStack.stackGet();
final int stackPointer = stack.getPointer();
try
{
final ByteBuffer sourceBuffer = MemoryUtil.memUTF8(source, true);
final PointerBuffer pointers = stack.mallocPointer(1);
pointers.put(sourceBuffer);
GL32.nglShaderSource(glId, 1, pointers.address0(), 0);
org.lwjgl.system.APIUtil.apiArrayFree(pointers.address0(), 1);
}
finally
{
stack.setPointer(stackPointer);
}
} }
public void free() { GL32.glDeleteShader(this.id); }
public static StringBuilder loadFile(String path, boolean absoluteFilePath, StringBuilder stringBuilder) public static StringBuilder loadFile(String path, boolean absoluteFilePath, StringBuilder stringBuilder)
{ {
try try
@@ -132,4 +188,6 @@ public class Shader
return stringBuilder; return stringBuilder;
} }
} }
@@ -1,5 +1,7 @@
package com.seibel.distanthorizons.core.render.glObject.texture; package com.seibel.distanthorizons.core.render.glObject.texture;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper;
import org.lwjgl.opengl.GL11C; import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL13C; import org.lwjgl.opengl.GL13C;
import org.lwjgl.opengl.GL43C; import org.lwjgl.opengl.GL43C;
@@ -8,12 +10,15 @@ import java.nio.ByteBuffer;
public class DHDepthTexture public class DHDepthTexture
{ {
private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class);
private int id; private int id;
public DHDepthTexture(int width, int height, EDhDepthBufferFormat format) public DHDepthTexture(int width, int height, EDhDepthBufferFormat format)
{ {
this.id = GL43C.glGenTextures(); this.id = GL43C.glGenTextures();
resize(width, height, format); this.resize(width, height, format);
GL43C.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST); GL43C.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST);
GL43C.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST); GL43C.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST);
@@ -24,27 +29,31 @@ public class DHDepthTexture
} }
// For internal use by Iris for copying data. Do not use this in DH. // For internal use by Iris for copying data. Do not use this in DH.
public DHDepthTexture(int id) { public DHDepthTexture(int id) { this.id = id; }
this.id = id;
}
public void resize(int width, int height, EDhDepthBufferFormat format) public void resize(int width, int height, EDhDepthBufferFormat format)
{ {
GL43C.glBindTexture(GL43C.GL_TEXTURE_2D, getTextureId()); GL43C.glBindTexture(GL43C.GL_TEXTURE_2D, this.getTextureId());
GL43C.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, format.getGlInternalFormat(), width, height, 0, GL43C.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, format.getGlInternalFormat(), width, height, 0,
format.getGlType(), format.getGlFormat(), (ByteBuffer) null); format.getGlType(), format.getGlFormat(), (ByteBuffer) null);
} }
public int getTextureId() public int getTextureId()
{ {
if (id == -1) throw new IllegalStateException("Depth texture does not exist!"); if (this.id == -1)
return id; {
throw new IllegalStateException("Depth texture does not exist!");
}
return this.id;
} }
public void destroy() public void destroy()
{ {
GL43C.glDeleteTextures(getTextureId()); GLMC.glDeleteTextures(this.getTextureId());
this.id = -1; this.id = -1;
} }
} }
@@ -1,5 +1,7 @@
package com.seibel.distanthorizons.core.render.glObject.texture; package com.seibel.distanthorizons.core.render.glObject.texture;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper;
import org.joml.Vector2i; import org.joml.Vector2i;
import org.lwjgl.opengl.GL11C; import org.lwjgl.opengl.GL11C;
import org.lwjgl.opengl.GL13C; import org.lwjgl.opengl.GL13C;
@@ -9,6 +11,9 @@ import java.nio.ByteBuffer;
public class DhColorTexture public class DhColorTexture
{ {
private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class);
private final EDhInternalTextureFormat internalFormat; private final EDhInternalTextureFormat internalFormat;
private final EDhPixelFormat format; private final EDhPixelFormat format;
private final EDhPixelType type; private final EDhPixelType type;
@@ -100,7 +105,7 @@ public class DhColorTexture
this.throwIfInvalid(); this.throwIfInvalid();
this.isValid = false; this.isValid = false;
GL43C.glDeleteTextures(this.id); GLMC.glDeleteTextures(this.id);
} }
/** @throws IllegalStateException if the texture isn't valid */ /** @throws IllegalStateException if the texture isn't valid */
@@ -86,22 +86,31 @@ public class FadeRenderer
this.fadeFramebuffer = -1; this.fadeFramebuffer = -1;
} }
if (this.fadeTexture != -1)
{
GLMC.glDeleteTextures(this.fadeTexture);
this.fadeTexture = -1;
}
this.fadeFramebuffer = GL32.glGenFramebuffers(); this.fadeFramebuffer = GL32.glGenFramebuffers();
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.fadeFramebuffer); GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.fadeFramebuffer);
this.fadeTexture = GL32.glGenTextures();
GLMC.glBindTexture(this.fadeTexture);
GL32.glTexImage2D(GL32.GL_TEXTURE_2D, 0, GL32.GL_RGBA16, width, height, 0, GL32.GL_RGBA, GL32.GL_UNSIGNED_SHORT_4_4_4_4, (ByteBuffer) null);
GL32.glTexParameteri(GL32.GL_TEXTURE_2D, GL32.GL_TEXTURE_MIN_FILTER, GL32.GL_LINEAR);
GL32.glTexParameteri(GL32.GL_TEXTURE_2D, GL32.GL_TEXTURE_MAG_FILTER, GL32.GL_LINEAR);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, this.fadeTexture, 0);
// Applying the fade texture is only needed if MC is drawing to their own frame buffer,
// otherwise we can directly render to their texture
if (MC_RENDER.mcRendersToFrameBuffer())
{
if (this.fadeTexture != -1)
{
GLMC.glDeleteTextures(this.fadeTexture);
this.fadeTexture = -1;
}
this.fadeTexture = GL32.glGenTextures();
GLMC.glBindTexture(this.fadeTexture);
GL32.glTexImage2D(GL32.GL_TEXTURE_2D, 0, GL32.GL_RGBA16, width, height, 0, GL32.GL_RGBA, GL32.GL_UNSIGNED_SHORT_4_4_4_4, (ByteBuffer) null);
GL32.glTexParameteri(GL32.GL_TEXTURE_2D, GL32.GL_TEXTURE_MIN_FILTER, GL32.GL_LINEAR);
GL32.glTexParameteri(GL32.GL_TEXTURE_2D, GL32.GL_TEXTURE_MAG_FILTER, GL32.GL_LINEAR);
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, this.fadeTexture, 0);
}
else
{
GL32.glFramebufferTexture2D(GL32.GL_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, GL32.GL_TEXTURE_2D, MC_RENDER.getColorTextureId(), 0);
}
} }
@@ -146,8 +155,13 @@ public class FadeRenderer
profiler.popPush("Fade Apply"); profiler.popPush("Fade Apply");
FadeApplyShader.INSTANCE.fadeTexture = this.fadeTexture; // Applying the fade texture is only needed if MC is drawing to their own frame buffer,
FadeApplyShader.INSTANCE.render(partialTicks); // otherwise we can directly render to their texture
if (MC_RENDER.mcRendersToFrameBuffer())
{
FadeApplyShader.INSTANCE.fadeTexture = this.fadeTexture;
FadeApplyShader.INSTANCE.render(partialTicks);
}
profiler.pop(); profiler.pop();
} }
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiFrameb
import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShaderProgram; import com.seibel.distanthorizons.api.interfaces.override.rendering.IDhApiShaderProgram;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*; import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam; import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiTextureCreatedParam;
import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer; import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector; import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
@@ -557,34 +558,24 @@ public class LodRenderer
activeFrameBuffer.bind(); activeFrameBuffer.bind();
boolean clearTextures = !ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeTextureClearEvent.class, renderEventParam);
if (clearTextures)
{
if (this.usingMcFrameBuffer && framebufferOverride == null)
{
// Due to using MC/Optifine's framebuffer we need to re-bind the depth texture,
// otherwise we'll be writing to MC/Optifine's depth texture which causes rendering issues
activeFrameBuffer.addDepthAttachment(this.depthTexture.getTextureId(), EDhDepthBufferFormat.DEPTH32F.isCombinedStencil());
// don't clear the color texture, that removes the sky
GL32.glClear(GL32.GL_DEPTH_BUFFER_BIT);
}
else if (firstPass)
{
GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT);
}
}
// by default draw everything as triangles // by default draw everything as triangles
GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_FILL); GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_FILL);
GLMC.enableFaceCulling(); GLMC.enableFaceCulling();
GLMC.glBlendFunc(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA);
GLMC.glBlendFuncSeparate(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA, GL32.GL_ONE, GL32.GL_ZERO);
GL32.glDisable(GL32.GL_SCISSOR_TEST);
// Enable depth test and depth mask // Enable depth test and depth mask
GLMC.enableDepthTest(); GLMC.enableDepthTest();
GLMC.glDepthFunc(GL32.GL_LESS); GLMC.glDepthFunc(GL32.GL_LESS);
GLMC.enableDepthMask(); GLMC.enableDepthMask();
// This is required for MC versions 1.21.5+
// due to MC updating the lightmap by changing the viewport size
GL32.glViewport(0, 0, this.cachedWidth, this.cachedHeight);
/*---------Bind required objects--------*/ /*---------Bind required objects--------*/
// Setup LodRenderProgram and the LightmapTexture if it has not yet been done // Setup LodRenderProgram and the LightmapTexture if it has not yet been done
// also binds LightmapTexture, VAO, and ShaderProgram // also binds LightmapTexture, VAO, and ShaderProgram
@@ -605,6 +596,33 @@ public class LodRenderer
} }
this.lodRenderProgram.fillUniformData(renderEventParam); this.lodRenderProgram.fillUniformData(renderEventParam);
// needs to be fired after all the textures have been created/bound
boolean clearTextures = !ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeTextureClearEvent.class, renderEventParam);
if (clearTextures)
{
GL32.glClearDepth(1.0);
float[] clearColorValues = new float[4];
GL32.glGetFloatv(GL32.GL_COLOR_CLEAR_VALUE, clearColorValues);
GL32.glClearColor(clearColorValues[0], clearColorValues[1], clearColorValues[2], 1.0f);
if (this.usingMcFrameBuffer && framebufferOverride == null)
{
// Due to using MC/Optifine's framebuffer we need to re-bind the depth texture,
// otherwise we'll be writing to MC/Optifine's depth texture which causes rendering issues
activeFrameBuffer.addDepthAttachment(this.depthTexture.getTextureId(), EDhDepthBufferFormat.DEPTH32F.isCombinedStencil());
// don't clear the color texture, that removes the sky
GL32.glClear(GL32.GL_DEPTH_BUFFER_BIT);
}
else if (firstPass)
{
GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT);
}
}
} }
/** Setup all render objects - MUST be called on the render thread */ /** Setup all render objects - MUST be called on the render thread */
@@ -656,7 +674,7 @@ public class LodRenderer
if(this.framebuffer.getStatus() != GL32.GL_FRAMEBUFFER_COMPLETE) if(this.framebuffer.getStatus() != GL32.GL_FRAMEBUFFER_COMPLETE)
{ {
// This generally means something wasn't bound, IE missing either the color or depth texture // This generally means something wasn't bound, IE missing either the color or depth texture
SPAM_LOGGER.warn("FrameBuffer ["+this.framebuffer.getId()+"] isn't complete."); EVENT_LOGGER.warn("FrameBuffer ["+this.framebuffer.getId()+"] isn't complete.");
} }
@@ -675,12 +693,15 @@ public class LodRenderer
this.cachedWidth = MC_RENDER.getTargetFrameBufferViewportWidth(); this.cachedWidth = MC_RENDER.getTargetFrameBufferViewportWidth();
this.cachedHeight = MC_RENDER.getTargetFrameBufferViewportHeight(); this.cachedHeight = MC_RENDER.getTargetFrameBufferViewportHeight();
DhApiTextureCreatedParam textureCreatedParam = new DhApiTextureCreatedParam(
oldWidth, oldHeight,
this.cachedWidth, this.cachedHeight
);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiColorDepthTextureCreatedEvent.class,
new DhApiColorDepthTextureCreatedEvent.EventParam(
oldWidth, oldHeight, ApiEventInjector.INSTANCE.fireAllEvents(DhApiColorDepthTextureCreatedEvent.class, new DhApiColorDepthTextureCreatedEvent.EventParam(textureCreatedParam));
this.cachedWidth, this.cachedHeight ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeColorDepthTextureCreatedEvent.class, textureCreatedParam);
));
// also update the override if present // also update the override if present
@@ -712,29 +733,13 @@ public class LodRenderer
{ {
this.nullableColorTexture = null; this.nullableColorTexture = null;
} }
ApiEventInjector.INSTANCE.fireAllEvents(DhApiAfterColorDepthTextureCreatedEvent.class, textureCreatedParam);
} }
private Color getFogColor(float partialTicks)
{
Color fogColor;
if (Config.Client.Advanced.Graphics.Fog.colorMode.get() == EDhApiFogColorMode.USE_SKY_COLOR)
{
fogColor = MC_RENDER.getSkyColor();
}
else
{
fogColor = MC_RENDER.getFogColor(partialTicks);
}
return fogColor;
}
private Color getSpecialFogColor(float partialTicks) { return MC_RENDER.getSpecialFogColor(partialTicks); }
//===============// //===============//
// API functions // // API functions //
//===============// //===============//
@@ -748,7 +753,10 @@ public class LodRenderer
public static int getActiveColorTextureId() { return activeColorTextureId; } public static int getActiveColorTextureId() { return activeColorTextureId; }
private void setActiveDepthTextureId(int depthTextureId) { activeDepthTextureId = depthTextureId; } private void setActiveDepthTextureId(int depthTextureId) { activeDepthTextureId = depthTextureId; }
/** Returns -1 if no texture has been bound yet */ /**
* FIXME it's possible for this to return an invalid texture ID if the renderer is being re-built at the same time
* Returns -1 if no texture has been bound yet
*/
public static int getActiveDepthTextureId() { return activeDepthTextureId; } public static int getActiveDepthTextureId() { return activeDepthTextureId; }
@@ -796,6 +804,9 @@ public class LodRenderer
if (this.depthTexture != null) if (this.depthTexture != null)
this.depthTexture.destroy(); this.depthTexture.destroy();
this.setActiveDepthTextureId(-1);
this.setActiveColorTextureId(-1);
EVENT_LOGGER.info("Renderer Cleanup Complete"); EVENT_LOGGER.info("Renderer Cleanup Complete");
}); });
} }
@@ -75,6 +75,18 @@ public class DhApplyShader extends AbstractShaderRenderer
@Override @Override
protected void onRender() protected void onRender()
{
if (MC_RENDER.mcRendersToFrameBuffer())
{
this.renderToFrameBuffer();
}
else
{
this.renderToMcTexture();
}
}
// TODO merge duplicate code between these to render methods
private void renderToFrameBuffer()
{ {
int targetFrameBuffer = MC_RENDER.getTargetFrameBuffer(); int targetFrameBuffer = MC_RENDER.getTargetFrameBuffer();
if (targetFrameBuffer == -1) if (targetFrameBuffer == -1)
@@ -87,9 +99,15 @@ public class DhApplyShader extends AbstractShaderRenderer
GLMC.disableDepthTest(); GLMC.disableDepthTest();
GLMC.enableBlend(); // blending isn't needed, we're manually merging the MC and DH textures
GL32.glBlendEquation(GL32.GL_FUNC_ADD); // Note: this prevents the sun/moon and stars from rendering through transparent LODs,
GLMC.glBlendFunc(GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA); // however this also fixes transparent LODs from glowing when rendered against the sky during the day
GLMC.disableBlend();
// old blending logic in case it's ever needed:
//GLMC.enableBlend();
//GL32.glBlendEquation(GL32.GL_FUNC_ADD);
//GLMC.glBlendFunc(GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA);
GLMC.glActiveTexture(GL32.GL_TEXTURE0); GLMC.glActiveTexture(GL32.GL_TEXTURE0);
GLMC.glBindTexture(LodRenderer.getActiveColorTextureId()); GLMC.glBindTexture(LodRenderer.getActiveColorTextureId());
@@ -110,5 +128,66 @@ public class DhApplyShader extends AbstractShaderRenderer
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, targetFrameBuffer); GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, targetFrameBuffer);
} }
private void renderToMcTexture()
{
int targetColorTextureId = MC_RENDER.getColorTextureId();
if (targetColorTextureId == -1)
{
return;
}
int dhFrameBufferId = LodRenderer.getActiveFramebufferId();
if (dhFrameBufferId == -1)
{
return;
}
int mcFrameBufferId = MC_RENDER.getTargetFrameBuffer();
if (mcFrameBufferId == -1)
{
return;
}
GLState state = new GLState();
GLMC.disableDepthTest();
// blending isn't needed, we're just directly merging the MC and DH textures
// Note: this prevents the sun/moon and stars from rendering through transparent LODs,
// however this also fixes
GLMC.disableBlend();
// old blending logic in case it's ever needed:
//GLMC.enableBlend();
//GL32.glBlendEquation(GL32.GL_FUNC_ADD);
//GLMC.glBlendFunc(GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA);
GLMC.glActiveTexture(GL32.GL_TEXTURE0);
GLMC.glBindTexture(LodRenderer.getActiveColorTextureId());
GL32.glUniform1i(this.gDhColorTextureUniform, 0);
GLMC.glActiveTexture(GL32.GL_TEXTURE1);
GLMC.glBindTexture(LodRenderer.getActiveDepthTextureId());
GL32.glUniform1i(this.gDepthMapUniform, 1);
GL32.glFramebufferTexture(GL32.GL_DRAW_FRAMEBUFFER, GL32.GL_COLOR_ATTACHMENT0, targetColorTextureId, 0);
// Copy to MC's texture via MC's framebuffer
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, dhFrameBufferId);
ScreenQuad.INSTANCE.render();
// restore everything, except at this point the MC framebuffer should now be used instead
state.restore();
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, mcFrameBufferId);
}
} }
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.render.renderer.shaders; package com.seibel.distanthorizons.core.render.renderer.shaders;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.render.glObject.GLState;
import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram; import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram;
import com.seibel.distanthorizons.core.render.renderer.FadeRenderer; import com.seibel.distanthorizons.core.render.renderer.FadeRenderer;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
@@ -104,6 +105,12 @@ public class FadeApplyShader extends AbstractShaderRenderer
@Override @Override
protected void onRender() protected void onRender()
{ {
if (!MC_RENDER.mcRendersToFrameBuffer())
{
throw new IllegalStateException("If Minecraft is directly rendering to a texture the apply shader isn't needed, just draw the fade directly to the MC color texture.");
}
GLMC.disableBlend(); GLMC.disableBlend();
// Depth testing must be disabled otherwise this application shader won't apply anything. // Depth testing must be disabled otherwise this application shader won't apply anything.
@@ -119,6 +126,9 @@ public class FadeApplyShader extends AbstractShaderRenderer
ScreenQuad.INSTANCE.render(); ScreenQuad.INSTANCE.render();
GLMC.enableDepthTest(); GLMC.enableDepthTest();
} }
} }
@@ -155,6 +155,19 @@ public class FadeShader extends AbstractShaderRenderer
@Override @Override
protected void onRender() protected void onRender()
{ {
int depthTextureId = LodRenderer.getActiveDepthTextureId();
int colorTextureId = LodRenderer.getActiveColorTextureId();
if (depthTextureId == -1
|| colorTextureId == -1)
{
// the renderer is currently being re-built and/or inactive,
// we don't need to/can't render fading
return;
}
GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.frameBuffer); GLMC.glBindFramebuffer(GL32.GL_FRAMEBUFFER, this.frameBuffer);
GLMC.disableScissorTest(); GLMC.disableScissorTest();
GLMC.disableDepthTest(); GLMC.disableDepthTest();
@@ -165,7 +178,7 @@ public class FadeShader extends AbstractShaderRenderer
GL32.glUniform1i(this.uMcDepthTexture, 0); GL32.glUniform1i(this.uMcDepthTexture, 0);
GLMC.glActiveTexture(GL32.GL_TEXTURE1); GLMC.glActiveTexture(GL32.GL_TEXTURE1);
GLMC.glBindTexture(LodRenderer.getActiveDepthTextureId()); GLMC.glBindTexture(depthTextureId);
GL32.glUniform1i(this.uDhDepthTexture, 1); GL32.glUniform1i(this.uDhDepthTexture, 1);
GLMC.glActiveTexture(GL32.GL_TEXTURE2); GLMC.glActiveTexture(GL32.GL_TEXTURE2);
@@ -173,7 +186,7 @@ public class FadeShader extends AbstractShaderRenderer
GL32.glUniform1i(this.uCombinedMcDhColorTexture, 2); GL32.glUniform1i(this.uCombinedMcDhColorTexture, 2);
GLMC.glActiveTexture(GL32.GL_TEXTURE3); GLMC.glActiveTexture(GL32.GL_TEXTURE3);
GLMC.glBindTexture(LodRenderer.getActiveColorTextureId()); GLMC.glBindTexture(colorTextureId);
GL32.glUniform1i(this.uDhColorTexture, 3); GL32.glUniform1i(this.uDhColorTexture, 3);
@@ -84,6 +84,7 @@ public class FogApplyShader extends AbstractShaderRenderer
GLMC.glActiveTexture(GL32.GL_TEXTURE1); GLMC.glActiveTexture(GL32.GL_TEXTURE1);
GLMC.glBindTexture(LodRenderer.getActiveDepthTextureId()); GLMC.glBindTexture(LodRenderer.getActiveDepthTextureId());
GL32.glUniform1i(this.depthTextureUniform, 1); GL32.glUniform1i(this.depthTextureUniform, 1);
} }
@@ -28,9 +28,11 @@ import com.seibel.distanthorizons.core.render.glObject.shader.ShaderProgram;
import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.LodRenderer;
import com.seibel.distanthorizons.core.render.renderer.ScreenQuad; import com.seibel.distanthorizons.core.render.renderer.ScreenQuad;
import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.util.math.Mat4f; import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import org.lwjgl.opengl.GL32; import org.lwjgl.opengl.GL32;
import java.awt.*; import java.awt.*;
@@ -41,6 +43,8 @@ public class FogShader extends AbstractShaderRenderer
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class); private static final IMinecraftGLWrapper GLMC = SingletonInjector.INSTANCE.get(IMinecraftGLWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
public int frameBuffer; public int frameBuffer;
@@ -48,6 +52,7 @@ public class FogShader extends AbstractShaderRenderer
private Mat4f inverseMvmProjMatrix; private Mat4f inverseMvmProjMatrix;
//==========// //==========//
// Uniforms // // Uniforms //
//==========// //==========//
@@ -253,7 +258,15 @@ public class FogShader extends AbstractShaderRenderer
// this is necessary for MC 1.16 (IE Legacy OpenGL) // this is necessary for MC 1.16 (IE Legacy OpenGL)
// otherwise the framebuffer isn't cleared correctly and the fog smears across the screen // otherwise the framebuffer isn't cleared correctly and the fog smears across the screen
GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT); if (MC_RENDER.runningLegacyOpenGL())
{
// in another part of the DH code we set the fog color to opaque, here it needs to be transparent
float[] clearColorValues = new float[4];
GL32.glGetFloatv(GL32.GL_COLOR_CLEAR_VALUE, clearColorValues);
GL32.glClearColor(clearColorValues[0], clearColorValues[1], clearColorValues[2], 0.0f);
GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT);
}
ScreenQuad.INSTANCE.render(); ScreenQuad.INSTANCE.render();
@@ -134,9 +134,6 @@ public class SSAOApplyShader extends AbstractShaderRenderer
// it should be automatically restored after rendering is complete. // it should be automatically restored after rendering is complete.
GLMC.disableDepthTest(); GLMC.disableDepthTest();
GLMC.glActiveTexture(GL32.GL_TEXTURE0);
GLMC.glBindTexture(0);
// apply the rendered SSAO to the LODs // apply the rendered SSAO to the LODs
GLMC.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, SSAOShader.INSTANCE.frameBuffer); GLMC.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, SSAOShader.INSTANCE.frameBuffer);
GLMC.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId()); GLMC.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId());
@@ -142,7 +142,16 @@ public class FullDataSourceV2DTO
public FullDataSourceV2 createDataSource(@NotNull ILevelWrapper levelWrapper) throws IOException, InterruptedException, DataCorruptedException public FullDataSourceV2 createDataSource(@NotNull ILevelWrapper levelWrapper) throws IOException, InterruptedException, DataCorruptedException
{ {
FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(this.pos); FullDataSourceV2 dataSource = FullDataSourceV2.createEmpty(this.pos);
this.internalPopulateDataSource(dataSource, levelWrapper, false); try
{
this.internalPopulateDataSource(dataSource, levelWrapper, false);
}
catch (Exception e)
{
dataSource.close();
throw e;
}
return dataSource; return dataSource;
} }
@@ -454,7 +463,7 @@ public class FullDataSourceV2DTO
{ {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
.add("levelMinY", this.levelMinY) .add("levelMinY", this.levelMinY)
.add("pos", this.pos) .add("pos", DhSectionPos.toString(this.pos))
.add("dataChecksum", this.dataChecksum) .add("dataChecksum", this.dataChecksum)
.add("compressedDataByteArray length", this.compressedDataByteArray.size()) .add("compressedDataByteArray length", this.compressedDataByteArray.size())
.add("compressedColumnGenStepByteArray length", this.compressedColumnGenStepByteArray.size()) .add("compressedColumnGenStepByteArray length", this.compressedColumnGenStepByteArray.size())
@@ -25,6 +25,7 @@ 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.sql.repo.phantoms.AutoClosableTrackingWrapper;
import com.seibel.distanthorizons.core.util.KeyedLockContainer; import com.seibel.distanthorizons.core.util.KeyedLockContainer;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -501,7 +502,12 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
else else
{ {
LOGGER.warn("Attempting to close already closed database connection: [" + connectionString + "]"); // these warnings can be ignored in release builds, as long as the connection is closed it doesn't really matter
// TODO fix duplicate closes
if (ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Attempting to close already closed database connection: [" + connectionString + "]");
}
} }
} }
} }
@@ -562,7 +568,12 @@ public abstract class AbstractDhRepo<TKey, TDTO extends IBaseDTO<TKey>> implemen
} }
else else
{ {
LOGGER.warn("Attempting to close already closed database connection: [" + this.connectionString + "]"); // these warnings can be ignored in release builds, as long as the connection is closed it doesn't really matter
// TODO fix duplicate closes
if (ModInfo.IS_DEV_BUILD)
{
LOGGER.warn("Attempting to close already closed database connection: [" + this.connectionString + "]");
}
} }
} }
ACTIVE_CONNECTION_STRINGS_BY_REPO.remove(this); ACTIVE_CONNECTION_STRINGS_BY_REPO.remove(this);
@@ -73,7 +73,10 @@ public class RenderDataPointUtil
public final static int EMPTY_DATA = 0; public final static int EMPTY_DATA = 0;
public final static int MAX_WORLD_Y_SIZE = 4096;
// the maximum valid Y value is the maximum min y + world height.
// min y is [-2032, 2031], height is < 4064.
public final static int MAX_WORLD_Y_SIZE = 2031 + 4064;
public final static int ALPHA_DOWNSIZE_SHIFT = 4; public final static int ALPHA_DOWNSIZE_SHIFT = 4;
@@ -1,430 +0,0 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.ModInfo;
import org.apache.logging.log4j.Logger;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.function.Function;
/**
* Handles parsing and creating string messages from OpenGL messages.
*
* @author Leetom
* @version 2022-10-1
*/
public final class GLMessage
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger(MethodHandles.lookup().lookupClass().getSimpleName());
static final String HEADER = "[LWJGL] OpenGL debug message";
public final EType type;
public final ESeverity severity;
public final ESource source;
public final String id;
public final String message;
/** This is needed since gl callback will not have the correct class loader set, which causes issues. */
static void initLoadClass()
{
Builder dummy = new Builder();
dummy.add(GLMessage.HEADER);
dummy.add("ID");
dummy.add(":");
dummy.add("dummyId");
dummy.add("Source");
dummy.add(":");
dummy.add(ESource.API.name);
dummy.add("Type");
dummy.add(":");
dummy.add(EType.OTHER.name);
dummy.add("Severity");
dummy.add(":");
dummy.add(ESeverity.LOW.name);
dummy.add("Message");
dummy.add(":");
dummy.add("dummyMessage");
}
static
{
initLoadClass();
}
GLMessage(EType type, ESeverity severity, ESource source, String id, String message)
{
this.type = type;
this.source = source;
this.severity = severity;
this.id = id;
this.message = message;
}
@Override
public String toString() { return "[level:" + severity + ", type:" + type + ", source:" + source + ", id:" + id + ", msg:{" + message + "}]"; }
//==============//
// helper enums //
//==============//
public enum EType
{
ERROR,
DEPRECATED_BEHAVIOR,
UNDEFINED_BEHAVIOR,
PORTABILITY,
PERFORMANCE,
MARKER,
PUSH_GROUP,
POP_GROUP,
OTHER;
private static final HashMap<String, EType> ENUM_BY_NAME = new HashMap<>();
private final String name;
static
{
for (EType type : EType.values())
{
ENUM_BY_NAME.put(type.name, type);
}
}
EType() { name = super.toString().toUpperCase(); }
@Override
public final String toString() { return name; }
public static EType get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
public enum ESource
{
API,
WINDOW_SYSTEM,
SHADER_COMPILER,
THIRD_PARTY,
APPLICATION,
OTHER;
private static final HashMap<String, ESource> ENUM_BY_NAME = new HashMap<>();
public final String name;
static
{
for (ESource source : ESource.values())
{
ENUM_BY_NAME.put(source.name, source);
}
}
ESource() { name = super.toString().toUpperCase(); }
@Override
public final String toString() { return name; }
public static ESource get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
public enum ESeverity
{
HIGH,
MEDIUM,
LOW,
NOTIFICATION;
public final String name;
static final HashMap<String, ESeverity> ENUM_BY_NAME = new HashMap<>();
static
{
for (ESeverity severity : ESeverity.values())
{
ENUM_BY_NAME.put(severity.name, severity);
}
}
ESeverity() { name = super.toString().toUpperCase(); }
@Override
public final String toString() { return name; }
public static ESeverity get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
//================//
// helper classes //
//================//
/**
* Expected message format: <br>
* <code>
* [LWJGL] OpenGL debug message <br>
* ID: 0x20071 <br>
* Source: API <br>
* Type: OTHER <br>
* Severity: NOTIFICATION <br>
* Message: Buffer detailed info: Buffer object 1014084 (bound to ...
* </code>
*/
public static class Builder
{
/** how many stages are present in the message parser */
private static final int FINAL_PARSER_STAGE_INDEX = 15;
public static final Builder DEFAULT_MESSAGE_BUILDER =
new Builder(
(type) ->
{ // type filter
if (type == GLMessage.EType.POP_GROUP)
return false;
if (type == GLMessage.EType.PUSH_GROUP)
return false;
if (type == GLMessage.EType.MARKER)
return false;
// if (type == GLMessage.Type.PERFORMANCE) return false;
return true;
},
(severity) ->
{ // severity filter
if (severity == GLMessage.ESeverity.NOTIFICATION)
return false;
return true;
},
null
);
private final StringBuilder inProgressMessageBuilder = new StringBuilder();
private EType type;
private ESeverity severity;
private ESource source;
/** if the function returns false the message will be allowed */
private final Function<EType, Boolean> typeFilter;
/** if the function returns false the message will be allowed */
private final Function<ESeverity, Boolean> severityFilter;
/** if the function returns false the message will be allowed */
private final Function<ESource, Boolean> sourceFilter;
private String id;
private String message;
/** how far into the message parser this builder is */
private int parserStage = 0;
static
{
initLoadClass();
}
public Builder() { this(null, null, null); }
public Builder(
Function<EType, Boolean> typeFilter,
Function<ESeverity, Boolean> severityFilter,
Function<ESource, Boolean> sourceFilter)
{
this.typeFilter = typeFilter;
this.severityFilter = severityFilter;
this.sourceFilter = sourceFilter;
}
/**
* Adds the given string to the message builder. <br> <br>
*
* Will log a warning if the string given wasn't expected
* for the next stage of the OpenGL message format.<br> <br>
*
* @return null if the message isn't complete
*/
public GLMessage add(String str)
{
// TODO fix implementation for MC 1.20.2 and newer
// please see the incomplete GLMessageTest for an example as to how the message formats differ
if (true)
return null;
str = str.trim();
if (str.isEmpty())
return null;
boolean parseSuccess = runNextParserStage(str);
if (parseSuccess && parserStage > FINAL_PARSER_STAGE_INDEX)
{
this.parserStage = 0;
GLMessage msg = new GLMessage(this.type, this.severity, this.source, this.id, this.message);
if (doesMessagePassFilters(msg))
{
return msg;
}
}
else if (!parseSuccess)
{
LOGGER.warn("Failed to parse GLMessage line '{}' at stage {}", str, parserStage);
}
// the message isn't finished yet
return null;
// TODO implement a method that works for both MC 1.20.2+ and 1.20.1-
//if (str.equals(HEADER) && inProgressMessageBuilder.length() != 0)
//{
// boolean parseSuccess = runNextParserStage(str);
// if (parseSuccess && parserStage > FINAL_PARSER_STAGE_INDEX)
// {
// this.parserStage = 0;
// GLMessage msg = new GLMessage(this.type, this.severity, this.source, this.id, this.message);
// if (doesMessagePassFilters(msg))
// {
// return msg;
// }
// else
// {
// inProgressMessageBuilder.setLength(0);
// return null;
// }
// }
// else
// {
// if (!parseSuccess)
// {
// LOGGER.warn("Failed to parse GLMessage line '{}' at stage {}", str, parserStage);
// inProgressMessageBuilder.setLength(0);
// }
//
// return null;
// }
//}
//else
//{
// inProgressMessageBuilder.append(str);
// return null;
//}
}
private boolean doesMessagePassFilters(GLMessage msg)
{
if (this.sourceFilter != null && !this.sourceFilter.apply(msg.source))
return false;
else if (this.typeFilter != null && !this.typeFilter.apply(msg.type))
return false;
else if (this.severityFilter != null && !this.severityFilter.apply(msg.severity))
return false;
else
return true;
}
/** @return true if the given string was expected next for the OpenGL message format */
private boolean runNextParserStage(String str)
{
switch (this.parserStage)
{
case 0:
return checkAndIncStage(str, GLMessage.HEADER);
case 1:
return checkAndIncStage(str, "ID");
case 2:
return checkAndIncStage(str, ":");
case 3:
this.id = str;
this.parserStage++;
return true;
case 4:
return checkAndIncStage(str, "Source");
case 5:
return checkAndIncStage(str, ":");
case 6:
this.source = ESource.get(str);
this.parserStage++;
return true;
case 7:
return checkAndIncStage(str, "Type");
case 8:
return checkAndIncStage(str, ":");
case 9:
this.type = EType.get(str);
this.parserStage++;
return true;
case 10:
return checkAndIncStage(str, "Severity");
case 11:
return checkAndIncStage(str, ":");
case 12:
this.severity = ESeverity.get(str);
this.parserStage++;
return true;
case 13:
return checkAndIncStage(str, "Message");
case 14:
return checkAndIncStage(str, ":");
case 15:
this.message = str;
this.parserStage++;
return true;
default:
return false;
}
}
/**
* Returns true and increments the parserStage
* if the given and expected strings are the same.
*/
private boolean checkAndIncStage(String givenString, String expectedString)
{
boolean equal = givenString.equals(expectedString);
//boolean equal = givenString.contains(expectedString);
if (equal)
this.parserStage++;
return equal;
}
} // builder class
}
@@ -0,0 +1,54 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects.GLMessages;
import java.util.HashMap;
public enum EGLMessageSeverity
{
HIGH,
MEDIUM,
LOW,
NOTIFICATION;
public final String name;
static final HashMap<String, EGLMessageSeverity> ENUM_BY_NAME = new HashMap<>();
static
{
for (EGLMessageSeverity severity : EGLMessageSeverity.values())
{
ENUM_BY_NAME.put(severity.name, severity);
}
}
EGLMessageSeverity() { this.name = super.toString().toUpperCase(); }
@Override
public final String toString() { return this.name; }
public static EGLMessageSeverity get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
@@ -0,0 +1,55 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects.GLMessages;
import java.util.HashMap;
public enum EGLMessageSource
{
API,
WINDOW_SYSTEM,
SHADER_COMPILER,
THIRD_PARTY,
APPLICATION,
OTHER;
private static final HashMap<String, EGLMessageSource> ENUM_BY_NAME = new HashMap<>();
public final String name;
static
{
for (EGLMessageSource source : EGLMessageSource.values())
{
ENUM_BY_NAME.put(source.name, source);
}
}
EGLMessageSource() { this.name = super.toString().toUpperCase(); }
@Override
public final String toString() { return this.name; }
public static EGLMessageSource get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
@@ -0,0 +1,58 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects.GLMessages;
import java.util.HashMap;
public enum EGLMessageType
{
ERROR,
DEPRECATED_BEHAVIOR,
UNDEFINED_BEHAVIOR,
PORTABILITY,
PERFORMANCE,
MARKER,
PUSH_GROUP,
POP_GROUP,
OTHER;
private static final HashMap<String, EGLMessageType> ENUM_BY_NAME = new HashMap<>();
public final String name;
static
{
for (EGLMessageType type : EGLMessageType.values())
{
ENUM_BY_NAME.put(type.name, type);
}
}
EGLMessageType() { this.name = super.toString().toUpperCase(); }
@Override
public final String toString() { return this.name; }
public static EGLMessageType get(String name) { return ENUM_BY_NAME.get(name.toUpperCase()); }
}
@@ -0,0 +1,56 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects.GLMessages;
public final class GLMessage
{
static final String HEADER = "[LWJGL] OpenGL debug message";
public final EGLMessageType type;
public final EGLMessageSeverity severity;
public final EGLMessageSource source;
public final String id;
public final String message;
GLMessage(EGLMessageType type, EGLMessageSeverity severity, EGLMessageSource source, String id, String message)
{
this.type = type;
this.source = source;
this.severity = severity;
this.id = id;
this.message = message;
}
@Override
public String toString()
{
return "level: [" + this.severity + "], " +
"type: [" + this.type + "], " +
"source: [" + this.source + "], " +
"id: [" + this.id + "], " +
"msg: [" + this.message + "]";
}
}
@@ -0,0 +1,319 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util.objects.GLMessages;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.apache.logging.log4j.Logger;
import java.util.function.Consumer;
import java.util.function.Function;
/** Expected message formats can be found in GLMessageTest. */
public class GLMessageBuilder
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** how many stages are present in the message parser */
private static final int FINAL_LEGACY_PARSER_STAGE_INDEX = 15;
private static final int FINAL_NEW_PARSER_STAGE_INDEX = 5;
private EGLMessageType type;
private EGLMessageSeverity severity;
private EGLMessageSource source;
/** if the function returns false the message will be allowed */
private final Function<EGLMessageType, Boolean> typeFilter;
/** if the function returns false the message will be allowed */
private final Function<EGLMessageSeverity, Boolean> severityFilter;
/** if the function returns false the message will be allowed */
private final Function<EGLMessageSource, Boolean> sourceFilter;
private String id;
private String message;
/** how far into the message parser this builder is */
private int parserStage = 0;
private boolean legacyMessage = true;
//==============//
// constructors //
//==============//
public GLMessageBuilder() { this(null, null, null); }
public GLMessageBuilder(
Function<EGLMessageType, Boolean> typeFilter,
Function<EGLMessageSeverity, Boolean> severityFilter,
Function<EGLMessageSource, Boolean> sourceFilter)
{
this.typeFilter = typeFilter;
this.severityFilter = severityFilter;
this.sourceFilter = sourceFilter;
}
//=================//
// message parsing //
//=================//
/**
* Adds the given string to the message builder. <br> <br>
*
* Will log a warning if the string given wasn't expected
* for the next stage of the OpenGL message format.<br> <br>
*
* @return null if the message isn't complete
*/
public GLMessage add(String str)
{
str = str.trim();
if (str.isEmpty())
{
return null;
}
boolean messageFinished = false;
boolean parseSuccess = this.runNextParserStage(str);
if (this.legacyMessage
&& this.parserStage > FINAL_LEGACY_PARSER_STAGE_INDEX)
{
messageFinished = true;
}
else if (!this.legacyMessage
&& this.parserStage > FINAL_NEW_PARSER_STAGE_INDEX)
{
messageFinished = true;
}
if (parseSuccess && messageFinished)
{
this.parserStage = 0;
GLMessage msg = new GLMessage(this.type, this.severity, this.source, this.id, this.message);
if (this.doesMessagePassFilters(msg))
{
return msg;
}
}
else if (!parseSuccess && messageFinished)
{
LOGGER.warn("Failed to parse GLMessage line [" + str + "] at stage [" + this.parserStage + "]");
}
// the message isn't finished yet
return null;
}
private boolean doesMessagePassFilters(GLMessage msg)
{
if (this.sourceFilter != null && !this.sourceFilter.apply(msg.source))
return false;
else if (this.typeFilter != null && !this.typeFilter.apply(msg.type))
return false;
else if (this.severityFilter != null && !this.severityFilter.apply(msg.severity))
return false;
else
return true;
}
/** @return true if the given string was expected next for the OpenGL message format */
private boolean runNextParserStage(String str)
{
if (this.parserStage == 0)
{
return this.checkExactAndIncStage(str, GLMessage.HEADER);
}
else if (this.parserStage == 1)
{
// legacy message only contains "ID" (not the colon)
this.legacyMessage = !str.contains("ID: ");
}
if (this.legacyMessage)
{
return this.runNextLegacyParserStage(str);
}
else
{
return this.runNextNewParserStage(str);
}
}
/** MC 1.20.2 and older */
private boolean runNextLegacyParserStage(String str)
{
switch (this.parserStage)
{
case 0:
throw new IllegalStateException("Parser should be past stage ["+this.parserStage+"], next stage is [1].");
case 1:
return this.checkExactAndIncStage(str, "ID");
case 2:
return this.checkExactAndIncStage(str, ":");
case 3:
this.id = str;
this.parserStage++;
return true;
case 4:
return this.checkExactAndIncStage(str, "Source");
case 5:
return this.checkExactAndIncStage(str, ":");
case 6:
this.source = EGLMessageSource.get(str);
this.parserStage++;
return true;
case 7:
return this.checkExactAndIncStage(str, "Type");
case 8:
return this.checkExactAndIncStage(str, ":");
case 9:
this.type = EGLMessageType.get(str);
this.parserStage++;
return true;
case 10:
return this.checkExactAndIncStage(str, "Severity");
case 11:
return this.checkExactAndIncStage(str, ":");
case 12:
this.severity = EGLMessageSeverity.get(str);
this.parserStage++;
return true;
case 13:
return this.checkExactAndIncStage(str, "Message");
case 14:
return this.checkExactAndIncStage(str, ":");
case 15:
this.message = str;
this.parserStage++;
return true;
default:
return false;
}
}
/** after MC 1.20.2 */
private boolean runNextNewParserStage(String str)
{
switch (this.parserStage)
{
case 0:
throw new IllegalStateException("Parser should be past stage [" + this.parserStage + "], next stage is [1].");
case 1:
String idPrefix = "ID: ";
return this.checkPrefixAndRun(str, idPrefix,
(line) ->
{
this.id = trySubstring(str, idPrefix.length());
this.parserStage++;
});
case 2:
String sourcePrefix = "Source: ";
return this.checkPrefixAndRun(str, sourcePrefix,
(line) ->
{
String sourceString = trySubstring(str, sourcePrefix.length());
this.source = EGLMessageSource.get(sourceString);
this.parserStage++;
});
case 3:
String typePrefix = "Type: ";
return this.checkPrefixAndRun(str, typePrefix,
(line) ->
{
String sourceString = trySubstring(str, typePrefix.length());
this.type = EGLMessageType.get(sourceString);
this.parserStage++;
});
case 4:
String severityPrefix = "Severity: ";
return this.checkPrefixAndRun(str, severityPrefix,
(line) ->
{
String sourceString = trySubstring(str, severityPrefix.length());
this.severity = EGLMessageSeverity.get(sourceString);
this.parserStage++;
});
case 5:
String messagePrefix = "Message: ";
return this.checkPrefixAndRun(str, messagePrefix,
(line) ->
{
this.message = trySubstring(str, messagePrefix.length());
this.parserStage++;
});
default:
return false;
}
}
//================//
// helper methods //
//================//
/**
* Returns true and increments the parserStage
* if the message and expected strings are the same.
*/
private boolean checkExactAndIncStage(String message, String expectedString)
{
boolean equal = message.equals(expectedString);
if (equal)
{
this.parserStage++;
}
return equal;
}
/**
* Returns true and increments the parserStage
* if the message starts with the given prefix.
*/
private boolean checkPrefixAndRun(String message, String expectedPrefix, Consumer<String> successConsumer)
{
boolean equal = message.startsWith(expectedPrefix);
if (equal)
{
successConsumer.accept(message);
}
return equal;
}
/** returns "" if the string isn't long enough */
private static String trySubstring(String string, int beginIndex)
{
if (beginIndex > string.length())
{
// prevent index-out-of-bounds errors
// if the message isn't what we expected
return "";
}
return string.substring(beginIndex);
}
}
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.seibel.distanthorizons.core.util.objects; package com.seibel.distanthorizons.core.util.objects.GLMessages;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -27,12 +27,12 @@ import java.util.function.Consumer;
public final class GLMessageOutputStream extends OutputStream public final class GLMessageOutputStream extends OutputStream
{ {
final Consumer<GLMessage> func; final Consumer<GLMessage> func;
final GLMessage.Builder builder; final GLMessageBuilder builder;
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
public GLMessageOutputStream(Consumer<GLMessage> func, GLMessage.Builder builder) public GLMessageOutputStream(Consumer<GLMessage> func, GLMessageBuilder builder)
{ {
this.func = func; this.func = func;
this.builder = builder; this.builder = builder;
@@ -41,24 +41,30 @@ public final class GLMessageOutputStream extends OutputStream
@Override @Override
public void write(int b) public void write(int b)
{ {
buffer.write(b); this.buffer.write(b);
if (b == '\n') flush(); if (b == '\n')
{
this.flush();
}
} }
@Override @Override
public void flush() public void flush()
{ {
String str = buffer.toString(); String str = this.buffer.toString();
GLMessage msg = builder.add(str); GLMessage msg = this.builder.add(str);
if (msg != null) func.accept(msg); if (msg != null)
buffer.reset(); {
this.func.accept(msg);
}
this.buffer.reset();
} }
@Override @Override
public void close() throws IOException public void close() throws IOException
{ {
flush(); this.flush();
buffer.close(); this.buffer.close();
} }
} }
@@ -20,10 +20,8 @@
package com.seibel.distanthorizons.core.util.objects.dataStreams; package com.seibel.distanthorizons.core.util.objects.dataStreams;
import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode; import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
import com.seibel.distanthorizons.core.Initializer;
import com.seibel.distanthorizons.core.util.objects.DataCorruptedException;
import com.seibel.distanthorizons.coreapi.ModInfo;
import net.jpountz.lz4.LZ4FrameInputStream; import net.jpountz.lz4.LZ4FrameInputStream;
//import org.apache.commons.compress.compressors.zstandard.ZstdCompressorInputStream;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.tukaani.xz.ResettableArrayCache; import org.tukaani.xz.ResettableArrayCache;
@@ -44,6 +42,7 @@ import java.io.*;
public class DhDataInputStream extends DataInputStream public class DhDataInputStream extends DataInputStream
{ {
private static final ThreadLocal<ResettableArrayCache> LZMA_RESETTABLE_ARRAY_CACHE_GETTER = ThreadLocal.withInitial(() -> new ResettableArrayCache(new LzmaArrayCache())); private static final ThreadLocal<ResettableArrayCache> LZMA_RESETTABLE_ARRAY_CACHE_GETTER = ThreadLocal.withInitial(() -> new ResettableArrayCache(new LzmaArrayCache()));
//private static final ThreadLocal<ZstdArrayCache> ZSTD_RESETTABLE_ARRAY_CACHE_GETTER = ThreadLocal.withInitial(() -> new ZstdArrayCache());
private static final Logger LOGGER = LogManager.getLogger(); private static final Logger LOGGER = LogManager.getLogger();
@@ -62,6 +61,8 @@ public class DhDataInputStream extends DataInputStream
return stream; return stream;
case LZ4: case LZ4:
return new LZ4FrameInputStream(stream); return new LZ4FrameInputStream(stream);
//case Z_STD:
// return new ZstdCompressorInputStream(stream, ZSTD_RESETTABLE_ARRAY_CACHE_GETTER.get());
case LZMA2: case LZMA2:
// using an array cache significantly reduces GC pressure // using an array cache significantly reduces GC pressure
ResettableArrayCache arrayCache = LZMA_RESETTABLE_ARRAY_CACHE_GETTER.get(); ResettableArrayCache arrayCache = LZMA_RESETTABLE_ARRAY_CACHE_GETTER.get();
@@ -23,6 +23,7 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiDataCompressionMode;
import net.jpountz.lz4.LZ4Factory; import net.jpountz.lz4.LZ4Factory;
import net.jpountz.lz4.LZ4FrameOutputStream; import net.jpountz.lz4.LZ4FrameOutputStream;
import net.jpountz.xxhash.XXHashFactory; import net.jpountz.xxhash.XXHashFactory;
//import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.tukaani.xz.*; import org.tukaani.xz.*;
@@ -53,6 +54,9 @@ public class DhDataOutputStream extends DataOutputStream
{ {
case UNCOMPRESSED: case UNCOMPRESSED:
return stream; return stream;
//case Z_STD:
// return new ZstdCompressorOutputStream(stream, 3, true, true);
case LZ4: case LZ4:
return new LZ4FrameOutputStream(stream, return new LZ4FrameOutputStream(stream,
LZ4FrameOutputStream.BLOCKSIZE.SIZE_64KB, -1L, LZ4FrameOutputStream.BLOCKSIZE.SIZE_64KB, -1L,
@@ -63,8 +63,16 @@ public class LzmaArrayCache extends ArrayCache
{ {
return new byte[size]; return new byte[size];
} }
// the array needs to be cleared to prevent accidentally sending dirty data
Arrays.fill(array, (byte)0); // the array only sometimes needs to be cleared,
// clearing all the time results in unnecessary slowdowns
if (fillWithZeros)
{
// TODO it appears that this can prevent the CPU from working on
// other tasks, thus causing render thread lag even when run on a separate thread
Arrays.fill(array, (byte) 0);
}
return array; return array;
} }
@@ -107,8 +115,16 @@ public class LzmaArrayCache extends ArrayCache
{ {
return new int[size]; return new int[size];
} }
// the array needs to be cleared to prevent accidentally sending dirty data
Arrays.fill(array, (byte)0); // the array only sometimes needs to be cleared,
// clearing all the time results in unnecessary slowdowns
if (fillWithZeros)
{
// TODO it appears that this can prevent the CPU from working on
// other tasks, thus causing render thread lag even when run on a separate thread
Arrays.fill(array, (byte) 0);
}
return array; return array;
} }
@@ -0,0 +1,85 @@
package com.seibel.distanthorizons.core.util.objects.dataStreams;
//import com.github.luben.zstd.BufferPool;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
import org.apache.logging.log4j.Logger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntUnaryOperator;
/**
* LZMA requires a custom object to cache it's backend arrays.
*/
public class ZstdArrayCache //implements BufferPool
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/**
* In James' testing the byte and int caches only ever had to store 2 and 4 arrays respectively.
* With the in mind we could take a few shortcuts, but if that changes then we need to be
* notified as it might cause issues with the current logic.
*/
public static final int WARN_CACHE_LENGTH_EXCEEDED = 10;
public static final AtomicInteger MAX_BYTE_CACHE_LENGTH_REF = new AtomicInteger(WARN_CACHE_LENGTH_EXCEEDED);
public final IntUnaryOperator maxByteCacheSizeUnaryOperator = (x) -> Math.max(this.bufferCache.size(), x);
/**
* generally only 2 items long <br>
* {@link Int2ReferenceArrayMap} can be used since the cache should only be a few items long.
* If the array ends up being longer then this design will need to be changed.
*/
public final Int2ReferenceArrayMap<ArrayList<ByteBuffer>> bufferCache = new Int2ReferenceArrayMap<>();
//=============//
// byte arrays //
//=============//
//@Override
public ByteBuffer get(int size)
{
ArrayList<ByteBuffer> cacheList = this.bufferCache.computeIfAbsent(size, (newSize) -> new ArrayList<>(4));
if (cacheList.isEmpty())
{
return ByteBuffer.allocate(size);
}
ByteBuffer array = cacheList.remove(cacheList.size()-1);
if (array == null)
{
return ByteBuffer.allocate(size);
}
return array;
}
//@Override
public void release(ByteBuffer buffer)
{
int size = buffer.array().length;
this.bufferCache.computeIfAbsent(size, (newSize) -> new ArrayList<>());
this.bufferCache.get(size).add(buffer);
if (this.bufferCache.size() > WARN_CACHE_LENGTH_EXCEEDED)
{
int previousMax = MAX_BYTE_CACHE_LENGTH_REF.getAndUpdate(this.maxByteCacheSizeUnaryOperator);
int newMax = MAX_BYTE_CACHE_LENGTH_REF.get();
if (newMax > previousMax)
{
LOGGER.warn("LZMA byte array cache expected size exceeded. Expected max length ["+WARN_CACHE_LENGTH_EXCEEDED+"], actual length ["+this.bufferCache.size()+"].");
}
}
}
}
@@ -78,9 +78,11 @@ public class RateLimitedThreadPoolExecutor extends ThreadPoolExecutor
long runTime = System.nanoTime() - this.runStartTime.get(); long runTime = System.nanoTime() - this.runStartTime.get();
Thread.sleep(TimeUnit.NANOSECONDS.toMillis((long) (runTime / this.runTimeRatioConfig.get() - runTime))); Thread.sleep(TimeUnit.NANOSECONDS.toMillis((long) (runTime / this.runTimeRatioConfig.get() - runTime)));
} }
catch (InterruptedException e) catch (InterruptedException ignore)
{ {
throw new RuntimeException(e); // if this thread is interrupted that means the
// thread pool is being shut down,
// we don't need to log that.
} }
} }
@@ -89,7 +89,14 @@ public class ThreadPoolUtil
public static void setupThreadPools() public static void setupThreadPools()
{ {
// thread pools //==================//
// main thread pool //
//==================//
if (taskPicker != null)
{
taskPicker.shutdown();
}
taskPicker = new PriorityTaskPicker(); taskPicker = new PriorityTaskPicker();
networkCompressionThreadPool = taskPicker.createExecutor(); networkCompressionThreadPool = taskPicker.createExecutor();
@@ -98,8 +105,22 @@ public class ThreadPoolUtil
updatePropagatorThreadPool = taskPicker.createExecutor(); updatePropagatorThreadPool = taskPicker.createExecutor();
worldGenThreadPool = taskPicker.createExecutor(); worldGenThreadPool = taskPicker.createExecutor();
// single thread pools
//=========================//
// standalone thread pools //
//=========================//
if (beaconCullingThreadPool != null)
{
beaconCullingThreadPool.shutdown();
}
beaconCullingThreadPool = ThreadUtil.makeSingleThreadPool(BEACON_CULLING_THREAD_NAME); beaconCullingThreadPool = ThreadUtil.makeSingleThreadPool(BEACON_CULLING_THREAD_NAME);
if (fullDataMigrationThreadPool != null)
{
fullDataMigrationThreadPool.shutdown();
}
fullDataMigrationThreadPool = ThreadUtil.makeSingleThreadPool(FULL_DATA_MIGRATION_THREAD_NAME); fullDataMigrationThreadPool = ThreadUtil.makeSingleThreadPool(FULL_DATA_MIGRATION_THREAD_NAME);
} }
@@ -52,7 +52,7 @@ public abstract class AbstractDhServerWorld<TDhServerLevel extends AbstractDhSer
public void addPlayer(IServerPlayerWrapper serverPlayer) public void addPlayer(IServerPlayerWrapper serverPlayer)
{ {
ServerPlayerState playerState = this.serverPlayerStateManager.registerJoinedPlayer(serverPlayer); ServerPlayerState playerState = this.serverPlayerStateManager.registerJoinedPlayer(serverPlayer);
this.getLevel(serverPlayer.getLevel()).addPlayer(serverPlayer); ((TDhServerLevel) this.getOrLoadServerLevel(serverPlayer.getLevel())).addPlayer(serverPlayer);
Iterator<TDhServerLevel> it = this.dhLevelByLevelWrapper.values().stream().distinct().iterator(); Iterator<TDhServerLevel> it = this.dhLevelByLevelWrapper.values().stream().distinct().iterator();
while (it.hasNext()) while (it.hasNext())
@@ -39,8 +39,8 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
public final ClientOnlySaveStructure saveStructure; public final ClientOnlySaveStructure saveStructure;
public final ClientNetworkState networkState = new ClientNetworkState(); public final ClientNetworkState networkState = new ClientNetworkState();
public ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("Client World Ticker Thread"); public final ExecutorService dhTickerThread = ThreadUtil.makeSingleThreadPool("Client World Ticker Thread");
public EventLoop eventLoop = new EventLoop(this.dhTickerThread, this::_clientTick); public final EventLoop eventLoop = new EventLoop(this.dhTickerThread, this::_clientTick);
@@ -128,6 +128,8 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{ {
this.networkState.close(); this.networkState.close();
this.dhTickerThread.shutdownNow();
for (DhClientLevel dhClientLevel : this.levels.values()) for (DhClientLevel dhClientLevel : this.levels.values())
{ {
@@ -37,6 +37,7 @@ public class DhServerWorld extends AbstractDhServerWorld<DhServerLevel>
} }
//================// //================//
// level handling // // level handling //
//================// //================//
@@ -23,9 +23,11 @@ import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public interface IDhWorld // TODO why is this exist alongside AbstractDhWorld?
public interface IDhWorld extends Closeable
{ {
IDhLevel getOrLoadLevel(@NotNull ILevelWrapper levelWrapper); IDhLevel getOrLoadLevel(@NotNull ILevelWrapper levelWrapper);
@@ -49,6 +49,11 @@ public interface IBlockStateWrapper extends IDhApiBlockStateWrapper
boolean isBeaconBlock(); boolean isBeaconBlock();
/** IE a glass block that can affect the beacon beam color */ /** IE a glass block that can affect the beacon beam color */
boolean isBeaconTintBlock(); boolean isBeaconTintBlock();
/**
* Returns true for any blocks that allow beacon beams to go through.
* IE: glass, stairs, bedrock, chests, end portal frames, carpet, cake
*/
boolean allowsBeaconBeamPassage();
/** /**
* The blocks used by a beacon's base * The blocks used by a beacon's base
* IE Iron, diamond, gold, etc. * IE Iron, diamond, gold, etc.
@@ -381,8 +381,9 @@ public interface IChunkWrapper extends IBindable
for (int y = beaconRelPos.getY() +1; y <= maxY; y++) for (int y = beaconRelPos.getY() +1; y <= maxY; y++)
{ {
IBlockStateWrapper block = centerChunk.getBlockState(beaconRelPos.getX(), y, beaconRelPos.getZ()); IBlockStateWrapper block = centerChunk.getBlockState(beaconRelPos.getX(), y, beaconRelPos.getZ());
if (!block.isAir() && block.getOpacity() == LodUtil.BLOCK_FULLY_OPAQUE) if (!block.allowsBeaconBeamPassage())
{ {
// beam is blocked by this block
return null; return null;
} }
@@ -60,6 +60,9 @@ public interface IMinecraftRenderWrapper extends IBindable
int getScreenWidth(); int getScreenWidth();
int getScreenHeight(); int getScreenHeight();
boolean mcRendersToFrameBuffer();
boolean runningLegacyOpenGL();
/** @return -1 if no valid framebuffer is available yet */ /** @return -1 if no valid framebuffer is available yet */
int getTargetFrameBuffer(); // Note: Iris is now hooking onto this for DH + Iris compat, try not to change (unless we wanna deal with some annoyances) int getTargetFrameBuffer(); // Note: Iris is now hooking onto this for DH + Iris compat, try not to change (unless we wanna deal with some annoyances)
// Iris commit: https://github.com/IrisShaders/Iris/commit/a76a240527e93780bbcba57c09bef377419d47a7#diff-7b9ded0c79bbcdb130010373387756a28ee8d3640d522c0a5b7acd0abbfc20aeR16 // Iris commit: https://github.com/IrisShaders/Iris/commit/a76a240527e93780bbcba57c09bef377419d47a7#diff-7b9ded0c79bbcdb130010373387756a28ee8d3640d522c0a5b7acd0abbfc20aeR16
@@ -477,7 +477,11 @@
"distanthorizons.config.client.advanced.debugging.openGl.overrideVanillaGLLogger": "distanthorizons.config.client.advanced.debugging.openGl.overrideVanillaGLLogger":
"Override Vanilla GL Logger", "Override Vanilla GL Logger",
"distanthorizons.config.client.advanced.debugging.openGl.overrideVanillaGLLogger.@tooltip": "distanthorizons.config.client.advanced.debugging.openGl.overrideVanillaGLLogger.@tooltip":
"Defines how OpenGL errors are handled. \n Requires rebooting Minecraft to apply. \nMay incorrectly catch OpenGL errors thrown by other mods.", "Defines how OpenGL errors are handled. \nRequires rebooting Minecraft to change. \nWill catch OpenGL errors thrown by other mods.",
"distanthorizons.config.client.advanced.debugging.openGl.onlyLogGlErrorsOnce":
"Only Log GL Errors Once",
"distanthorizons.config.client.advanced.debugging.openGl.onlyLogGlErrorsOnce.@tooltip":
"If true each Open GL error will only be logged once. \nTEnabling this may cause some error logs to be missed. \nDoes nothing if overrideVanillaGLLogger is set to false. \n\nGenerally this can be kept as 'true' to prevent log spam. \nHowever, Please set this to 'false' if a developer needs your log to debug a GL issue. \n",
"distanthorizons.config.client.advanced.debugging.openGl.glErrorHandlingMode": "distanthorizons.config.client.advanced.debugging.openGl.glErrorHandlingMode":
"OpenGL Error Handling Mode", "OpenGL Error Handling Mode",
"distanthorizons.config.client.advanced.debugging.openGl.glErrorHandlingMode.@tooltip": "distanthorizons.config.client.advanced.debugging.openGl.glErrorHandlingMode.@tooltip":
@@ -742,6 +746,10 @@
"Maximum Data Transfer Speed, KB/s", "Maximum Data Transfer Speed, KB/s",
"distanthorizons.config.server.maxDataTransferSpeed.@tooltip": "distanthorizons.config.server.maxDataTransferSpeed.@tooltip":
"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.enableAdaptiveTransferSpeed":
"Enable Adaptive Transfer Speed",
"distanthorizons.config.server.enableAdaptiveTransferSpeed.@tooltip":
"Enables adaptive transfer speed based on client performance.\nIf true, DH will automatically adjust transfer rate to minimize connection lag.\nIf false, transfer speed will remain fixed.",
"distanthorizons.config.server.experimental": "distanthorizons.config.server.experimental":
@@ -913,7 +921,7 @@
"distanthorizons.config.enum.EDhApiDistantGeneratorMode.FEATURES": "distanthorizons.config.enum.EDhApiDistantGeneratorMode.FEATURES":
"Features", "Features",
"distanthorizons.config.enum.EDhApiDistantGeneratorMode.INTERNAL_SERVER": "distanthorizons.config.enum.EDhApiDistantGeneratorMode.INTERNAL_SERVER":
"Internal Server", "Full - Save Chunks",
"distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY": "distanthorizons.config.enum.EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY":
"Overlay", "Overlay",
@@ -927,9 +935,11 @@
"distanthorizons.config.enum.EDhApiDataCompressionMode.UNCOMPRESSED": "distanthorizons.config.enum.EDhApiDataCompressionMode.UNCOMPRESSED":
"Uncompressed", "Uncompressed",
"distanthorizons.config.enum.EDhApiDataCompressionMode.LZ4": "distanthorizons.config.enum.EDhApiDataCompressionMode.LZ4":
"Fast/Big - LZ4", "Fastest/Big - LZ4",
"distanthorizons.config.enum.EDhApiDataCompressionMode.Z_STD":
"Fast/Small - Z_STD",
"distanthorizons.config.enum.EDhApiDataCompressionMode.LZMA2": "distanthorizons.config.enum.EDhApiDataCompressionMode.LZMA2":
"Slow/Small - LZMA2", "Slow/Smallest - LZMA2",
"distanthorizons.config.enum.EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS": "distanthorizons.config.enum.EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS":
"1. Merge Same Blocks", "1. Merge Same Blocks",
@@ -1012,6 +1022,8 @@
"File: Info, Chat: Warning", "File: Info, Chat: Warning",
"distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE": "distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_INFO_TO_FILE":
"File: Info, Chat: Error", "File: Info, Chat: Error",
"distanthorizons.config.enum.EDhApiLoggerMode.LOG_ERROR_TO_CHAT_AND_WARNING_TO_FILE":
"File: Warning, Chat: Error",
"distanthorizons.config.enum.EDhApiGpuUploadMethod.AUTO": "distanthorizons.config.enum.EDhApiGpuUploadMethod.AUTO":
"Auto", "Auto",
+12 -3
View File
@@ -8,17 +8,26 @@ uniform sampler2D gDhColorTexture;
uniform sampler2D gDhDepthTexture; uniform sampler2D gDhDepthTexture;
/**
* LOD application shader
*
* This merges the rendered LODs into Minecraft's texture/FBO
*/
void main() void main()
{ {
fragColor = vec4(0.0); fragColor = vec4(0.0);
float fragmentDepth = texture(gDhDepthTexture, TexCoord).r;
// a fragment depth of "1" means the fragment wasn't drawn to, // a fragment depth of "1" means the fragment wasn't drawn to,
// only update fragments that were drawn to // only update fragments that were drawn to
float fragmentDepth = texture(gDhDepthTexture, TexCoord).r;
if (fragmentDepth != 1) if (fragmentDepth != 1)
{ {
fragColor = texture(gDhColorTexture, TexCoord); fragColor = texture(gDhColorTexture, TexCoord);
} }
else
{
// use the original MC texture if no LODs were drawn to this fragment
discard;
}
} }
@@ -8,15 +8,18 @@ uniform sampler2D uColorTexture;
uniform sampler2D uDepthTexture; uniform sampler2D uDepthTexture;
/**
* Fog application shader
*
* This merges the rendered fog onto DH's rendered LODs
*/
void main() void main()
{ {
fragColor = vec4(1.0); fragColor = vec4(0.0);
float fragmentDepth = textureLod(uDepthTexture, TexCoord, 0).r;
// a fragment depth of "1" means the fragment wasn't drawn to, // a fragment depth of "1" means the fragment wasn't drawn to,
// only update fragments that were drawn to // only update fragments that were drawn to
float fragmentDepth = textureLod(uDepthTexture, TexCoord, 0).r;
if (fragmentDepth != 1) if (fragmentDepth != 1)
{ {
fragColor = texture(uColorTexture, TexCoord); fragColor = texture(uColorTexture, TexCoord);
+58 -56
View File
@@ -25,11 +25,11 @@ import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo; import com.seibel.distanthorizons.core.sql.repo.FullDataSourceV2Repo;
import com.seibel.distanthorizons.coreapi.util.StringUtil; import com.seibel.distanthorizons.coreapi.util.StringUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongArrayList;
//import org.apache.commons.compress.compressors.zstandard.ZstdCompressorOutputStream;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test;
import java.io.*; import java.io.*;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
/** /**
* <strong>Note:</strong> * <strong>Note:</strong>
@@ -47,22 +47,14 @@ import java.text.StringCharacterIterator;
*/ */
public class CompressionTest public class CompressionTest
{ {
public static String TEST_DIR = "C:\\DistantHorizonsWorkspace\\distant-horizons\\run\\saves\\Arcapelago\\data"; public static String TEST_DIR = "C:\\DistantHorizonsWorkspace\\distant-horizons\\run\\client\\saves\\Archipelego\\data";
public static String DB_FILE_NAME_PREFIX = "DistantHorizons"; public static String DB_FILE_NAME_PREFIX = "DistantHorizons";
public static String UNCOMPRESSED_DB_FILE_NAME = "DistantHorizons.sqlite"; public static String UNCOMPRESSED_DB_FILE_NAME = "DistantHorizons_uncompressed.sqlite";
/** -1 will test all of them */ /** -1 will test all of them */
public static int MAX_DTO_TEST_COUNT = -1; public static int MAX_DTO_TEST_COUNT = -1;
//@Test
public void NoCompression()
{
String compressorName = "Uncompressed";
this.testCompressor(compressorName, EDhApiDataCompressionMode.UNCOMPRESSED);
}
// collapse the following commented out code when looking at tests // collapse the following commented out code when looking at tests
//@Test //@Test
@@ -132,17 +124,6 @@ public class CompressionTest
//} //}
//@Test
//public void Zstd()
//{
// String compressorName = "Zstd";
//
// DhDataInputStream.CreateInputStreamFunc createInputStreamFunc = (inputStream) -> new ZstdInputStream(inputStream);
// DhDataOutputStream.CreateOutputStreamFunc createOutputStreamFunc = (outputStream) -> new ZstdOutputStream(outputStream);
//
// this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc);
//}
////@Test ////@Test
//public void ZstdDictionary() throws SQLException // isn't any better than normal Zstd //public void ZstdDictionary() throws SQLException // isn't any better than normal Zstd
//{ //{
@@ -205,6 +186,13 @@ public class CompressionTest
// this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc); // this.testCompressor(compressorName, createInputStreamFunc, createOutputStreamFunc);
//} //}
//@Test
public void NoCompression()
{
String compressorName = "Uncompressed";
this.testCompressor(compressorName, EDhApiDataCompressionMode.UNCOMPRESSED);
}
//@Test //@Test
public void Lz4() // fast, poor compression public void Lz4() // fast, poor compression
{ {
@@ -213,11 +201,11 @@ public class CompressionTest
} }
//@Test //@Test
//public void Zstd() // middle of the road public void Zstd() // middle of the road
//{ {
// String compressorName = "Zstd"; //String compressorName = "Zstd";
// this.testCompressor(compressorName, EDhApiDataCompressionMode.Z_STD); //this.testCompressor(compressorName, EDhApiDataCompressionMode.Z_STD);
//} }
//@Test //@Test
public void LZMA2() // very slow, very good compression though public void LZMA2() // very slow, very good compression though
@@ -254,13 +242,15 @@ public class CompressionTest
long totalCompressedFileSizeInBytes; long totalCompressedFileSizeInBytes;
FullDataSourceV2Repo uncompressedRepo = null;
FullDataSourceV2Repo compressedRepo = null;
try try
{ {
String uncompressedDatabaseFilePath = TEST_DIR + "/" + UNCOMPRESSED_DB_FILE_NAME; String uncompressedDatabaseFilePath = TEST_DIR + "/" + UNCOMPRESSED_DB_FILE_NAME;
File uncompressedDatabaseFile = new File(uncompressedDatabaseFilePath); File uncompressedDatabaseFile = new File(uncompressedDatabaseFilePath);
Assert.assertTrue(uncompressedDatabaseFile.exists()); Assert.assertTrue(uncompressedDatabaseFile.exists());
FullDataSourceV2Repo uncompressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", uncompressedDatabaseFile); uncompressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", uncompressedDatabaseFile);
String compressedDatabaseFilePath = TEST_DIR + "/output/" + DB_FILE_NAME_PREFIX + "_" + compressorName + ".sqlite"; String compressedDatabaseFilePath = TEST_DIR + "/output/" + DB_FILE_NAME_PREFIX + "_" + compressorName + ".sqlite";
@@ -268,7 +258,7 @@ public class CompressionTest
compressedDatabaseFile.mkdirs(); compressedDatabaseFile.mkdirs();
compressedDatabaseFile.delete(); compressedDatabaseFile.delete();
Assert.assertTrue(!compressedDatabaseFile.exists()); Assert.assertTrue(!compressedDatabaseFile.exists());
FullDataSourceV2Repo compressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", uncompressedDatabaseFile); compressedRepo = new FullDataSourceV2Repo("jdbc:sqlite", compressedDatabaseFile);
@@ -292,47 +282,47 @@ public class CompressionTest
// uncompressed input // // uncompressed input //
FullDataSourceV2DTO uncompressedDto = uncompressedRepo.getByKey(pos); try (FullDataSourceV2DTO uncompressedDto = uncompressedRepo.getByKey(pos))
Assert.assertEquals(uncompressedDto.compressionModeValue, EDhApiDataCompressionMode.UNCOMPRESSED.value); {
FullDataSourceV2 uncompressedDataSource = uncompressedDto.createUnitTestDataSource(); Assert.assertEquals(uncompressedDto.compressionModeValue, EDhApiDataCompressionMode.UNCOMPRESSED.value);
FullDataSourceV2 uncompressedDataSource = uncompressedDto.createUnitTestDataSource();
long uncompressedDtoSize = uncompressedRepo.getDataSizeInBytes(pos); long uncompressedDtoSize = uncompressedRepo.getDataSizeInBytes(pos);
minUncompressedDtoSizeInBytes = Math.min(uncompressedDtoSize, minUncompressedDtoSizeInBytes); minUncompressedDtoSizeInBytes = Math.min(uncompressedDtoSize, minUncompressedDtoSizeInBytes);
maxUncompressedDtoSizeInBytes = Math.max(uncompressedDtoSize, maxUncompressedDtoSizeInBytes); maxUncompressedDtoSizeInBytes = Math.max(uncompressedDtoSize, maxUncompressedDtoSizeInBytes);
avgUncompressedDtoSizeInBytes += uncompressedDtoSize; avgUncompressedDtoSizeInBytes += uncompressedDtoSize;
// compress file //
// compress file // long startWriteNanoTime = System.nanoTime();
long startWriteNanoTime = System.nanoTime(); FullDataSourceV2DTO compressedDto = FullDataSourceV2DTO.CreateFromDataSource(uncompressedDataSource, compressionMode);
compressedRepo.save(compressedDto);
FullDataSourceV2DTO compressedDto = FullDataSourceV2DTO.CreateFromDataSource(uncompressedDataSource, compressionMode); long endWriteNanoTime = System.nanoTime();
compressedRepo.save(compressedDto); totalWriteTimeInNano += (endWriteNanoTime - startWriteNanoTime);
long endWriteNanoTime = System.nanoTime();
totalWriteTimeInNano += (endWriteNanoTime - startWriteNanoTime);
long compressedDtoSize = compressedRepo.getDataSizeInBytes(pos); long compressedDtoSize = compressedRepo.getDataSizeInBytes(pos);
minCompressedDtoSizeInBytes = Math.min(compressedDtoSize, minCompressedDtoSizeInBytes); minCompressedDtoSizeInBytes = Math.min(compressedDtoSize, minCompressedDtoSizeInBytes);
maxCompressedDtoSizeInBytes = Math.max(compressedDtoSize, maxCompressedDtoSizeInBytes); maxCompressedDtoSizeInBytes = Math.max(compressedDtoSize, maxCompressedDtoSizeInBytes);
avgCompressedDtoSizeInBytes += compressedDtoSize; avgCompressedDtoSizeInBytes += compressedDtoSize;
// read compressed file //
// read compressed file // long startReadNanoTime = System.nanoTime();
long startReadNanoTime = System.nanoTime(); compressedDto = compressedRepo.getByKey(pos);
FullDataSourceV2 compressedDataSource = compressedDto.createUnitTestDataSource();
compressedDto = compressedRepo.getByKey(pos); long endReadMsTime = System.nanoTime();
FullDataSourceV2 compressedDataSource = compressedDto.createUnitTestDataSource(); totalReadTimeInNano += (endReadMsTime - startReadNanoTime);
long endReadMsTime = System.nanoTime();
totalReadTimeInNano += (endReadMsTime - startReadNanoTime);
processedDtoCount++; processedDtoCount++;
}
} }
catch (Exception | Error e) catch (Exception | Error e)
{ {
@@ -371,6 +361,18 @@ public class CompressionTest
e.printStackTrace(); e.printStackTrace();
Assert.fail(e.getMessage()); Assert.fail(e.getMessage());
} }
finally
{
if(uncompressedRepo != null)
{
uncompressedRepo.close();
}
if(compressedRepo != null)
{
compressedRepo.close();
}
}
} }
+10 -2
View File
@@ -23,6 +23,7 @@ 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 com.seibel.distanthorizons.core.sql.repo.phantoms.AutoClosableTrackingWrapper;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@@ -218,6 +219,12 @@ public class DhRepoSqliteTest
@Test @Test
public void testRepoLeakDetection() public void testRepoLeakDetection()
{ {
if (!AutoClosableTrackingWrapper.TRACK_WRAPPERS)
{
System.out.println("Skipping repo leak detection unit test. Leak tracking is disabled.");
return;
}
TestPrimaryKeyRepo primaryKeyRepo = null; TestPrimaryKeyRepo primaryKeyRepo = null;
try try
{ {
@@ -298,7 +305,8 @@ public class DhRepoSqliteTest
} }
} }
//Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size()); // TODO fails when built for release due to tracking being disabled // TODO fails when built for release due to tracking being disabled
Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size());
primaryKeyRepo.openClosables.clear(); primaryKeyRepo.openClosables.clear();
@@ -318,7 +326,7 @@ public class DhRepoSqliteTest
} }
} }
//Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size()); Assert.assertNotEquals(0, primaryKeyRepo.openClosables.size());
} }
} }
catch (SQLException e) catch (SQLException e)
+45 -21
View File
@@ -19,7 +19,7 @@
package tests; package tests;
import com.seibel.distanthorizons.core.util.objects.GLMessage; import com.seibel.distanthorizons.core.util.objects.GLMessages.*;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@@ -28,66 +28,88 @@ import java.util.ArrayList;
public class GLMessageTest public class GLMessageTest
{ {
public static final String MESSAGE_ID = "0x20071"; public static final String MESSAGE_ID = "0x20071";
public static final GLMessage.ESource MESSAGE_SOURCE = GLMessage.ESource.API; public static final EGLMessageSource MESSAGE_SOURCE = EGLMessageSource.API;
public static final GLMessage.EType MESSAGE_TYPE = GLMessage.EType.OTHER; public static final EGLMessageType MESSAGE_TYPE = EGLMessageType.OTHER;
public static final GLMessage.ESeverity MESSAGE_SEVERITY = GLMessage.ESeverity.NOTIFICATION; public static final EGLMessageSeverity MESSAGE_SEVERITY = EGLMessageSeverity.NOTIFICATION;
public static final String MESSAGE = "Buffer detailed info: Buffer object 1014084 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as\" \"the source for buffer object operations."; public static final String MESSAGE = "Buffer detailed info: Buffer object 1014084 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as\" \"the source for buffer object operations.";
/** This is how debug messages were sent prior to Minecraft 1.20.2 */ /** This is how debug messages were sent prior to Minecraft 1.20.2 */
private static final String[] PRE_1_20_2_MESSAGE_ARRAY = private static final String[] OLD_MESSAGE_ARRAY =
{ {
"[LWJGL] OpenGL debug message" "[LWJGL] OpenGL debug message"
,"ID", ":", "0x20071" ,"ID", ":", MESSAGE_ID
,"Source", ":", "API" ,"Source", ":", MESSAGE_SOURCE.name
,"Type", ":", "OTHER" ,"Type", ":", MESSAGE_TYPE.name
,"Severity", ":", "NOTIFICATION" ,"Severity", ":", MESSAGE_SEVERITY.name
,"Message", ":", "Buffer detailed info: Buffer object 1014084 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as\" \"the source for buffer object operations." ,"Message", ":", MESSAGE
// optional addition to force the builder into noticing the message ended, shouldn't be necessary // optional addition to force the builder into noticing the message ended, shouldn't be necessary
//,"[LWJGL] OpenGL debug message" //,"[LWJGL] OpenGL debug message"
}; };
/** This is how debug messages were sent after (and including) Minecraft 1.20.2 */ /** This is how debug messages were sent after (and including) Minecraft 1.20.2 */
private static final String[] POST_1_20_2_MESSAGE_ARRAY = private static final String[] NEW_MESSAGE_ARRAY =
{ {
"[LWJGL] OpenGL debug message" "[LWJGL] OpenGL debug message"
,"ID: 0x20071" ,"ID: " + MESSAGE_ID
,"Source: API" ,"Source: " + MESSAGE_SOURCE.name
,"Type: OTHER" ,"Type: " + MESSAGE_TYPE.name
,"Severity: NOTIFICATION" ,"Severity: " + MESSAGE_SEVERITY.name
,"Message: Buffer detailed info: Buffer object 1014084 (bound to GL_ARRAY_BUFFER_ARB, usage hint is GL_STATIC_DRAW) will use VIDEO memory as\" \"the source for buffer object operations." ,"Message: " + MESSAGE
// optional addition to force the builder into noticing the message ended, shouldn't be necessary // optional addition to force the builder into noticing the message ended, shouldn't be necessary
//,"[LWJGL] OpenGL debug message" //,"[LWJGL] OpenGL debug message"
}; };
public final GLMessageBuilder messageBuilder = new GLMessageBuilder(null, null, null);
//=======//
// tests //
//=======//
@Test @Test
public void preMc1_20_2() public void preMc1_20_2()
{ {
ArrayList<GLMessage> messageList = new ArrayList<>(); ArrayList<GLMessage> messageList = new ArrayList<>();
for (String str : PRE_1_20_2_MESSAGE_ARRAY) for (String str : OLD_MESSAGE_ARRAY)
{ {
GLMessage message = GLMessage.Builder.DEFAULT_MESSAGE_BUILDER.add(str); GLMessage message = this.messageBuilder.add(str);
if (message != null) if (message != null)
{ {
messageList.add(message); messageList.add(message);
} }
} }
//Assert.assertEquals("Incorrect message parse count.", 1, messageList.size()); Assert.assertEquals("Incorrect message parse count.", 1, messageList.size());
//testMessage(messageList.get(0)); messageMatchesExpected(messageList.get(0));
} }
@Test @Test
public void mc1_20_2() public void mc1_20_2()
{ {
// TODO ArrayList<GLMessage> messageList = new ArrayList<>();
for (String str : NEW_MESSAGE_ARRAY)
{
GLMessage message = this.messageBuilder.add(str);
if (message != null)
{
messageList.add(message);
}
}
Assert.assertEquals("Incorrect message parse count.", 1, messageList.size());
messageMatchesExpected(messageList.get(0));
} }
//================//
// helper methods //
//================//
private static void messageMatchesExpected(GLMessage testMessage) private static void messageMatchesExpected(GLMessage testMessage)
{ {
Assert.assertEquals(MESSAGE_ID, testMessage.id); Assert.assertEquals(MESSAGE_ID, testMessage.id);
@@ -97,4 +119,6 @@ public class GLMessageTest
Assert.assertEquals(MESSAGE, testMessage.message); Assert.assertEquals(MESSAGE, testMessage.message);
} }
} }
@@ -27,6 +27,7 @@ import org.apache.logging.log4j.core.config.Configurator;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@Deprecated
public class SquareIntersectTest public class SquareIntersectTest
{ {
private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final Logger LOGGER = DhLoggerBuilder.getLogger();