Drastically improve frame stability

This commit is contained in:
James Seibel
2026-03-21 15:20:28 -05:00
parent 6c0736a2a0
commit 480c3b3ec5
6 changed files with 85 additions and 37 deletions
@@ -32,6 +32,7 @@ import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer; import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo; import com.seibel.distanthorizons.core.sql.repo.AbstractDhRepo;
import com.seibel.distanthorizons.core.util.objects.Pair; import com.seibel.distanthorizons.core.util.objects.Pair;
@@ -123,6 +124,8 @@ public class SharedApi
// needs to be closed on world shutdown to clear out un-processed chunks // needs to be closed on world shutdown to clear out un-processed chunks
WORLD_CHUNK_UPDATE_MANAGER.clear(); WORLD_CHUNK_UPDATE_MANAGER.clear();
RenderThreadTaskHandler.INSTANCE.clearDebugStats();
// recommend that the garbage collector cleans up any objects from the old world and thread pools // recommend that the garbage collector cleans up any objects from the old world and thread pools
System.gc(); System.gc();
@@ -500,8 +500,11 @@ public class LodQuadBuilder
return maxBufferByteSize; return maxBufferByteSize;
} }
// how big a single VBO can be in bytes // 2 MB
int maxVboByteSize = 10 * 1024 * 1024; // 10 MB // note: this is relatively small (10 MB was the previous max) to reduce stuttering
// during the upload process by having smaller upload steps
int maxVboByteSize = 2 * 1024 * 1024;
int maxQuadsPerBuffer = maxVboByteSize / BYTES_PER_QUAD; int maxQuadsPerBuffer = maxVboByteSize / BYTES_PER_QUAD;
// integer truncation to remove decimal component // integer truncation to remove decimal component
int fullSizedBuffer = maxQuadsPerBuffer * BYTES_PER_QUAD; int fullSizedBuffer = maxQuadsPerBuffer * BYTES_PER_QUAD;
@@ -5,12 +5,11 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat; import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.logging.DhLogger; import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen; import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.TimerUtil; import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.ModInfo;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -19,27 +18,37 @@ import java.util.List;
import java.util.Timer; import java.util.Timer;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.atomic.LongAdder;
public class RenderThreadTaskHandler public class RenderThreadTaskHandler
{ {
public static final DhLogger LOGGER = new DhLoggerBuilder() private static final DhLogger LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logRendererEventToFile) .fileLevelConfig(Config.Common.Logging.logRendererEventToFile)
.build(); .build();
private static final DhLogger RATE_LIMITED_LOGGER = new DhLoggerBuilder()
.fileLevelConfig(Config.Common.Logging.logRendererEventToFile)
.maxCountPerSecond(4)
.build();
private static final ConcurrentLinkedQueue<QueuedRunnable> RENDER_THREAD_RUNNABLE_QUEUE = new ConcurrentLinkedQueue<>(); private static final ConcurrentLinkedQueue<QueuedRunnable> RENDER_THREAD_RUNNABLE_QUEUE = new ConcurrentLinkedQueue<>();
private static final ConcurrentHashMap<String, RollingAverage> AVERAGE_MS_RUN_TIME_BY_TASK_NAME = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, RollingAverage> AVERAGE_NANO_RUN_TIME_BY_TASK_NAME = new ConcurrentHashMap<>();
private static final LongAdder COMPLETED_TASK_COUNTER = new LongAdder(); private static final LongAdder COMPLETED_TASK_COUNTER = new LongAdder();
private static final NumberFormat DECIMAL_NUMBER_FORMAT = NumberFormat.getNumberInstance();
private static final NumberFormat INT_NUMBER_FORMAT = NumberFormat.getIntegerInstance();
private static final boolean LOG_SLOW_TASKS = false;
private static final Timer TIMER = TimerUtil.CreateTimer("Cleanup timer"); private static final Timer TIMER = TimerUtil.CreateTimer("Cleanup timer");
private static final long MS_BETWEEN_CLEANUP_TICKS = 1_000L; private static final long MS_BETWEEN_CLEANUP_TICKS = 1_000L;
private static final long MS_BEFORE_RUN_CLEANUP_TIMER = 1_000L; private static final long NANOS_BEFORE_RUN_CLEANUP_TIMER = TimeUnit.NANOSECONDS.convert(1_000L, TimeUnit.MILLISECONDS);
public static final RenderThreadTaskHandler INSTANCE = new RenderThreadTaskHandler(); public static final RenderThreadTaskHandler INSTANCE = new RenderThreadTaskHandler();
private long msSinceTasksRun = System.currentTimeMillis(); private long nanoSinceTasksRun = System.nanoTime();
@@ -89,45 +98,63 @@ public class RenderThreadTaskHandler
{ {
IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class); IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
// https://fpstoms.com/
int frameLimit = MC_RENDER.getFrameLimit(); int frameLimit = MC_RENDER.getFrameLimit();
if (frameLimit <= 1) if (frameLimit <= 1)
{ {
frameLimit = 4; // 240 FPS frameLimit = 240;
} }
// https://fpstoms.com/
int msPerFrame = 1000 / frameLimit; int msPerFrame = 1000 / frameLimit;
msPerFrame /= 2; // divide the time in half so we can only impact half of the framerate at worst long nanoPerFrame = msPerFrame * 1_000_000L;
this.runRenderThreadTasks(msPerFrame); nanoPerFrame /= 2; // divide the time in half so we can only impact half of the framerate at worst
this.runRenderThreadTasks(nanoPerFrame);
} }
private void runRenderThreadTasks(long msMaxRunTime) private void runRenderThreadTasks(long nanoMaxRunTime)
{ {
long startTimeMs = System.currentTimeMillis(); long loopStartTimeNano = System.nanoTime();
this.msSinceTasksRun = startTimeMs; this.nanoSinceTasksRun = loopStartTimeNano;
QueuedRunnable runnable = RENDER_THREAD_RUNNABLE_QUEUE.poll(); QueuedRunnable runnable = RENDER_THREAD_RUNNABLE_QUEUE.poll();
while(runnable != null) while(runnable != null)
{ {
long taskStartNano = System.nanoTime();
runnable.run(); runnable.run();
// only try running for a limited amount of time to prevent lag spikes // only try running for a limited amount of time to prevent lag spikes
long currentTimeMs = System.currentTimeMillis(); long taskNano = System.nanoTime() - taskStartNano;
long runDuration = currentTimeMs - startTimeMs; long totalLoopNano = System.nanoTime() - loopStartTimeNano;
// stat tracking // stat tracking
if (ModInfo.IS_DEV_BUILD) if (ModInfo.IS_DEV_BUILD)
{ {
if (!AVERAGE_MS_RUN_TIME_BY_TASK_NAME.containsKey(runnable.name)) if (!AVERAGE_NANO_RUN_TIME_BY_TASK_NAME.containsKey(runnable.name))
{ {
AVERAGE_MS_RUN_TIME_BY_TASK_NAME.put(runnable.name, new RollingAverage(1_000)); AVERAGE_NANO_RUN_TIME_BY_TASK_NAME.put(runnable.name, new RollingAverage(1_000));
} }
AVERAGE_MS_RUN_TIME_BY_TASK_NAME.get(runnable.name).add(runDuration); AVERAGE_NANO_RUN_TIME_BY_TASK_NAME.get(runnable.name).add(totalLoopNano);
COMPLETED_TASK_COUNTER.increment(); COMPLETED_TASK_COUNTER.increment();
} }
if (runDuration > msMaxRunTime)
// estimate when our ending nano-time would be once the next task is run
long expectedNextTaskNano = totalLoopNano
// doubling this task's time gives a rough over-estimate of how long the next task should take
+ (taskNano * 2);
// If the next task would push us over the max run time, stop now.
// This prevents stuttering at the cost of lower throughput.
if (expectedNextTaskNano >= nanoMaxRunTime)
{ {
if (LOG_SLOW_TASKS
&& totalLoopNano > nanoMaxRunTime)
{
// this task took longer than what we wanted
RATE_LIMITED_LOGGER.warn("["+runnable.name+"] slow, actual ["+totalLoopNano+"], allowed ["+nanoMaxRunTime+"].");
}
break; break;
} }
@@ -141,9 +168,9 @@ public class RenderThreadTaskHandler
*/ */
private void manualCleanupTick() private void manualCleanupTick()
{ {
long nowMs = System.currentTimeMillis(); long nowNano = System.nanoTime();
long msSinceLast = nowMs - this.msSinceTasksRun; long nanoSinceLast = nowNano - this.nanoSinceTasksRun;
if (msSinceLast < MS_BEFORE_RUN_CLEANUP_TIMER) if (nanoSinceLast < NANOS_BEFORE_RUN_CLEANUP_TIMER)
{ {
return; return;
} }
@@ -153,7 +180,7 @@ public class RenderThreadTaskHandler
// Run the queued tasks on MC's executor (hopefully this should always run, // Run the queued tasks on MC's executor (hopefully this should always run,
// even if DH's render code isn't being hit). // even if DH's render code isn't being hit).
IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
MC.executeOnRenderThread(() -> this.runRenderThreadTasks(250)); MC.executeOnRenderThread(() -> this.runRenderThreadTasks(500 * 1_000_000L));
} }
//endregion //endregion
@@ -165,6 +192,16 @@ public class RenderThreadTaskHandler
//===========// //===========//
///region ///region
/**
* if tasks are currently queued the debug
* stats may not be zero after this method has been called.
*/
public void clearDebugStats()
{
AVERAGE_NANO_RUN_TIME_BY_TASK_NAME.clear();
COMPLETED_TASK_COUNTER.reset();
}
public void addDebugMenuStringsToList(List<String> messageList) public void addDebugMenuStringsToList(List<String> messageList)
{ {
if (!ModInfo.IS_DEV_BUILD) if (!ModInfo.IS_DEV_BUILD)
@@ -181,29 +218,30 @@ public class RenderThreadTaskHandler
NumberFormat numberFormat = F3Screen.NUMBER_FORMAT; String queueSize = DECIMAL_NUMBER_FORMAT.format(RENDER_THREAD_RUNNABLE_QUEUE.size());
String completedCount = DECIMAL_NUMBER_FORMAT.format(COMPLETED_TASK_COUNTER.sum());
String queueSize = numberFormat.format(RENDER_THREAD_RUNNABLE_QUEUE.size());
String completedCount = numberFormat.format(COMPLETED_TASK_COUNTER.sum());
String messageHeader = "Render Tasks, Queue: "+o+queueSize+cf+", Done: "+g+completedCount+cf; String messageHeader = "Render Tasks, Queue: "+o+queueSize+cf+", Done: "+g+completedCount+cf;
messageList.add(messageHeader); messageList.add(messageHeader);
AVERAGE_MS_RUN_TIME_BY_TASK_NAME.forEach((name, rollingAverage) -> AVERAGE_NANO_RUN_TIME_BY_TASK_NAME.forEach((name, rollingAverage) ->
{ {
// thread runtime // thread runtime
String runTimeAvgStr; String runTimeAvgStr;
double runTimeAvgInMs = rollingAverage.getAverage(); double runTimeAvgInNano = rollingAverage.getAverage();
if (!Double.isNaN(runTimeAvgInMs)) if (!Double.isNaN(runTimeAvgInNano))
{ {
runTimeAvgStr = numberFormat.format(runTimeAvgInMs); double runTimeAvgInMs = runTimeAvgInNano / 1_000_000.0;
runTimeAvgStr = DECIMAL_NUMBER_FORMAT.format(runTimeAvgInMs);
} }
else else
{ {
runTimeAvgStr = "<0"; runTimeAvgStr = "<0";
} }
String message = name+" Avg: "+b+runTimeAvgStr+"ms"+cf+" #: "+y+rollingAverage.getLifetimeCount()+cf; String lifetimeCount = INT_NUMBER_FORMAT.format(rollingAverage.getLifetimeCount());
String message = name+" Avg: "+b+runTimeAvgStr+"ms"+cf+" #: "+y+lifetimeCount+cf;
messageList.add(message); messageList.add(message);
}); });
} }
@@ -258,6 +296,11 @@ public class RenderThreadTaskHandler
} }
catch (Exception e) catch (Exception e)
{ {
if (ExceptionUtil.isShutdownException(e))
{
return;
}
RuntimeException error = new RuntimeException("Uncaught Exception during GL call execution. StackTrace: ["+(this.stackTrace != null ? "Present" : "Missing")+"] Error: ["+e.getMessage()+"]", e); RuntimeException error = new RuntimeException("Uncaught Exception during GL call execution. StackTrace: ["+(this.stackTrace != null ? "Present" : "Missing")+"] Error: ["+e.getMessage()+"]", e);
if (this.stackTrace != null) if (this.stackTrace != null)
{ {
@@ -113,6 +113,7 @@ public class RollingAverage
/** rounded to two decimals*/ /** rounded to two decimals*/
public String getAverageRoundedString() { return String.format("%.2f", this.getAverage()); } public String getAverageRoundedString() { return String.format("%.2f", this.getAverage()); }
/** how many items have been added to the rolling average since it's last {@link RollingAverage#clear()} */
public long getLifetimeCount() { return this.lifetimeCount; } public long getLifetimeCount() { return this.lifetimeCount; }
//endregion //endregion
@@ -319,7 +319,7 @@ public class PhantomArrayListPool
pool.returnCheckout(checkout); pool.returnCheckout(checkout);
if (pool.logGarbageCollectedStacks if (pool.logGarbageCollectedStacks
&& checkout.allocationStackTrace != null) // stack trace shouldn't be null, but just in case && checkout.allocationStackTrace != null) // stack trace shouldn't be null, but just in case
{ {
putAndIncrementTrackingString(checkout.allocationStackTrace, allocationStackTraceCountPairList); putAndIncrementTrackingString(checkout.allocationStackTrace, allocationStackTraceCountPairList);
} }
@@ -110,9 +110,7 @@ public interface IWrapperFactory extends IDhApiWrapperFactory, IBindable
IVertexBufferWrapper createVboWrapper(String name); IVertexBufferWrapper createVboWrapper(String name);
ILodContainerUniformBufferWrapper createLodContainerUniformWrapper(); ILodContainerUniformBufferWrapper createLodContainerUniformWrapper();
IDhGenericObjectVertexBufferContainer createGenericObjectVboContainer(); IDhGenericObjectVertexBufferContainer createGenericObjectVboContainer();
IDhGenericRenderer createGenericRenderer(); IDhGenericRenderer createGenericRenderer();
} }