stage VBO/IBO upload and allow global IBO

This commit is contained in:
James Seibel
2026-03-21 15:33:36 -05:00
parent 480c3b3ec5
commit 32eae23963
5 changed files with 421 additions and 76 deletions
@@ -64,7 +64,7 @@ public class ColumnRenderBufferBuilder
{
DhBlockPos minBlockPos = new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getLevelWrapper().getMinHeight(), DhSectionPos.getMinCornerBlockZ(pos));
LodBufferContainer bufferContainer = new LodBufferContainer(pos, minBlockPos);
CompletableFuture<LodBufferContainer> uploadFuture = bufferContainer.makeAndUploadBuffersAsync(quadBuilder);
CompletableFuture<LodBufferContainer> uploadFuture = bufferContainer.tryMakeAndUploadBuffersAsync(quadBuilder);
uploadFuture.whenComplete((uploadedBuffer, exception) ->
{
// clean up if not uploaded
@@ -0,0 +1,92 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class IndexBufferBuilder
{
//==========//
// building //
//==========//
//region
/** Buffer should be freed by {@link MemoryUtil#memFree} */
public static ByteBuffer createBuffer(int quadCount)
{
int indexCount = quadCount * 6; // 2 triangles per quad
ByteBuffer buffer = MemoryUtil.memAlloc(indexCount * Integer.BYTES);
buffer.order(ByteOrder.nativeOrder());
buildBufferInt(quadCount, buffer);
return buffer;
}
private static void buildBufferByte(int quadCount, ByteBuffer buffer)
{
for (int i = 0; i < quadCount; i++)
{
int vIndex = i * 4;
// First triangle
buffer.put((byte) (vIndex));
buffer.put((byte) (vIndex + 1));
buffer.put((byte) (vIndex + 2));
// Second triangle
buffer.put((byte) (vIndex + 2));
buffer.put((byte) (vIndex + 3));
buffer.put((byte) (vIndex));
}
if (buffer.hasRemaining())
{
throw new IllegalStateException("QuadElementBuffer is not full somehow after building");
}
buffer.rewind();
}
private static void buildBufferShort(int quadCount, ByteBuffer buffer)
{
for (int i = 0; i < quadCount; i++)
{
int vIndex = i * 4;
// First triangle
buffer.putShort((short) (vIndex));
buffer.putShort((short) (vIndex + 1));
buffer.putShort((short) (vIndex + 2));
// Second triangle
buffer.putShort((short) (vIndex + 2));
buffer.putShort((short) (vIndex + 3));
buffer.putShort((short) (vIndex));
}
if (buffer.hasRemaining())
{
throw new IllegalStateException("QuadElementBuffer is not full somehow after building");
}
buffer.rewind();
}
private static void buildBufferInt(int quadCount, ByteBuffer buffer)
{
for (int i = 0; i < quadCount; i++)
{
int vIndex = i * 4;
// First triangle
buffer.putInt(vIndex);
buffer.putInt(vIndex + 1);
buffer.putInt(vIndex + 2);
// Second triangle
buffer.putInt(vIndex + 2);
buffer.putInt(vIndex + 3);
buffer.putInt(vIndex);
}
if (buffer.hasRemaining())
{
throw new IllegalStateException("QuadElementBuffer is not full somehow after building");
}
buffer.rewind();
}
//endregion
}
@@ -25,11 +25,13 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.AbstractDhRenderApiDefinition;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.ILodContainerUniformBufferWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTerrainRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.IVertexBufferWrapper;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
@@ -47,6 +49,7 @@ public class LodBufferContainer implements AutoCloseable
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private static final AbstractDhRenderApiDefinition RENDER_DEF = SingletonInjector.INSTANCE.get(AbstractDhRenderApiDefinition.class);
/** the position closest to minimum X/Z infinity and the level's lowest Y */
@@ -55,8 +58,8 @@ public class LodBufferContainer implements AutoCloseable
public boolean buffersUploaded = false;
public IVertexBufferWrapper[] vbos;
public IVertexBufferWrapper[] vbosTransparent;
public IVertexBufferWrapper[] vboOpaqueWrappers;
public IVertexBufferWrapper[] vboTransparentWrappers;
public ILodContainerUniformBufferWrapper uniformContainer = WRAPPER_FACTORY.createLodContainerUniformWrapper();
@@ -73,8 +76,8 @@ public class LodBufferContainer implements AutoCloseable
{
this.pos = pos;
this.minCornerBlockPos = minCornerBlockPos;
this.vbos = new IVertexBufferWrapper[0];
this.vbosTransparent = new IVertexBufferWrapper[0];
this.vboOpaqueWrappers = new IVertexBufferWrapper[0];
this.vboTransparentWrappers = new IVertexBufferWrapper[0];
this.uniformContainer.createUniformData(this);
}
@@ -89,8 +92,13 @@ public class LodBufferContainer implements AutoCloseable
//region
/** Should be run on a DH thread. */
public synchronized CompletableFuture<LodBufferContainer> makeAndUploadBuffersAsync(LodQuadBuilder builder)
public synchronized CompletableFuture<LodBufferContainer> tryMakeAndUploadBuffersAsync(LodQuadBuilder builder)
{
//================//
// handle futures //
//================//
//region
// separate variable to prevent race condition when checking null
CompletableFuture<LodBufferContainer> oldFuture = this.uploadFutureRef.get();
if (oldFuture != null)
@@ -118,65 +126,149 @@ public class LodBufferContainer implements AutoCloseable
return oldFuture;
}
//endregion
// make the buffers
//================//
// create buffers //
//================//
//region
ArrayList<ByteBuffer> opaqueBuffers = builder.makeOpaqueVertexBuffers();
ArrayList<ByteBuffer> transparentBuffers = builder.makeTransparentVertexBuffers();
this.vbos = resizeBufferArray(this.vbos, opaqueBuffers.size());
this.vbosTransparent = resizeBufferArray(this.vbosTransparent, transparentBuffers.size());
this.vboOpaqueWrappers = resizeWrapperArray(this.vboOpaqueWrappers, opaqueBuffers.size());
this.vboTransparentWrappers = resizeWrapperArray(this.vboTransparentWrappers, transparentBuffers.size());
// mac requires separate IBO objects for each VBO when using OpenGL,
// all other OS's can share a single IBO for quicker loading times
boolean useSingleIbo = RENDER_DEF.useSingleIbo();
@Nullable ArrayList<ByteBuffer> opaqueIndexBuffers = useSingleIbo ? null : this.createIndexBuffers(opaqueBuffers);
@Nullable ArrayList<ByteBuffer> transparentIndexBuffers = useSingleIbo ? null : this.createIndexBuffers(transparentBuffers);
//endregion
// upload on MC's render thread
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Upload", () ->
//================//
// upload buffers //
//================//
//region
try
{
try
//=============//
// create VBOs //
//=============//
CompletableFuture<Void> createOpaqueFuture = createBufferWrappersAsync(future, this.vboOpaqueWrappers, opaqueBuffers);
CompletableFuture<Void> createTransparentFuture = createBufferWrappersAsync(future, this.vboTransparentWrappers, transparentBuffers);
CompletableFuture<Void> createFuture = CompletableFuture.allOf(createOpaqueFuture, createTransparentFuture);
createFuture.exceptionally((Throwable e) ->
{
// skip this event if requested
if (Thread.interrupted()
|| future.isCancelled())
// create VBOs failed //
if (!ExceptionUtil.isShutdownException(e))
{
throw new InterruptedException();
LOGGER.error("Unexpected issue creating buffer [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
// upload on the render thread
uploadBuffers(this.vbos, opaqueBuffers);
uploadBuffers(this.vbosTransparent, transparentBuffers);
this.buffersUploaded = true;
// success
future.complete(this);
}
catch (InterruptedException ignore)
{
future.complete(this);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue uploading buffer ["+this.minCornerBlockPos +"], error: ["+e.getMessage()+"].", e);
future.completeExceptionally(e);
}
finally
return null;
});
createFuture.thenRun(() ->
{
// all the buffers must be manually freed to prevent memory leaks
//=============//
// upload VBOs //
//=============//
for (ByteBuffer buffer : opaqueBuffers)
{
MemoryUtil.memFree(buffer);
}
CompletableFuture<Void> opaqueFuture = uploadBuffersAsync(future, this.vboOpaqueWrappers, opaqueBuffers, opaqueIndexBuffers);
CompletableFuture<Void> transparentFuture = uploadBuffersAsync(future, this.vboTransparentWrappers, transparentBuffers, transparentIndexBuffers);
for (ByteBuffer buffer : transparentBuffers)
CompletableFuture<Void> uploadFuture = CompletableFuture.allOf(opaqueFuture, transparentFuture);
uploadFuture.exceptionally((Throwable e) ->
{
MemoryUtil.memFree(buffer);
}
// upload failed //
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue uploading buffer [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
future.completeExceptionally(e);
return null;
});
uploadFuture.thenRun(() ->
{
// upload success /
this.buffersUploaded = true;
future.complete(this);
});
});
}
catch (Exception e)
{
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue prepping buffer uploading [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
future.completeExceptionally(e);
}
//================//
// buffer cleanup //
//================//
future.whenComplete((LodBufferContainer lodBufferContainer, Throwable throwable) ->
{
// all the buffers must be manually freed to prevent memory leaks
tryFreeByteBufferList(opaqueBuffers);
tryFreeByteBufferList(transparentBuffers);
tryFreeByteBufferList(opaqueIndexBuffers);
tryFreeByteBufferList(transparentIndexBuffers);
});
//endregion
return future;
}
private static IVertexBufferWrapper[] resizeBufferArray(IVertexBufferWrapper[] vbos, int newSize)
private static void tryFreeByteBufferList(@Nullable ArrayList<ByteBuffer> list)
{
if (list != null)
{
for (ByteBuffer buffer : list)
{
MemoryUtil.memFree(buffer);
}
}
}
private ArrayList<ByteBuffer> createIndexBuffers(ArrayList<ByteBuffer> vertexBuffers)
{
ArrayList<ByteBuffer> indexBuffers = new ArrayList<>();
for (int i = 0; i < vertexBuffers.size(); i++)
{
ByteBuffer buffer = vertexBuffers.get(i);
int size = buffer.limit() - buffer.position();
int vertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
ByteBuffer indexBuffer = IndexBufferBuilder.createBuffer(vertexCount);
indexBuffers.add(indexBuffer);
}
return indexBuffers;
}
private static IVertexBufferWrapper[] resizeWrapperArray(IVertexBufferWrapper[] vbos, int newSize)
{
if (vbos.length == newSize)
{
@@ -197,46 +289,188 @@ public class LodBufferContainer implements AutoCloseable
}
return newVbos;
}
private static void uploadBuffers(IVertexBufferWrapper[] vbos, ArrayList<ByteBuffer> byteBuffers) throws InterruptedException
private static CompletableFuture<Void> createBufferWrappersAsync(
CompletableFuture<LodBufferContainer> parentFuture,
IVertexBufferWrapper[] vboWrappers, ArrayList<ByteBuffer> vertexBuffers)
{
int vboIndex = 0;
for (int i = 0; i < byteBuffers.size(); i++)
ArrayList<CompletableFuture<Void>> createVboFutureList = new ArrayList<>();
for (int i = 0; i < vertexBuffers.size(); i++)
{
if (vboIndex >= vbos.length)
if (i >= vboWrappers.length)
{
throw new RuntimeException("Too many vertex buffers!!");
}
if (vboWrappers[i] == null)
{
final int finalVboIndex = i;
CompletableFuture<Void> future = new CompletableFuture<>();
createVboFutureList.add(future);
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Setup", () ->
{
try
{
// skip this event if requested
if (Thread.interrupted()
|| parentFuture.isCancelled())
{
throw new InterruptedException();
}
vboWrappers[finalVboIndex] = WRAPPER_FACTORY.createVboWrapper("distantHorizons:McLodRenderer");
future.complete(null);
}
catch (Exception e)
{
future.completeExceptionally(e);
}
});
}
}
if (createVboFutureList.size() == 0)
{
return CompletableFuture.completedFuture(null);
}
CompletableFuture<?>[] futureArray = new CompletableFuture[createVboFutureList.size()];
for (int i = 0; i < createVboFutureList.size(); i++)
{
futureArray[i] = createVboFutureList.get(i);
}
return CompletableFuture.allOf(futureArray);
}
/** Index buffers should be null if {@link AbstractDhRenderApiDefinition#useSingleIbo()} returns true. */
private static CompletableFuture<Void> uploadBuffersAsync(
CompletableFuture<LodBufferContainer> parentFuture,
IVertexBufferWrapper[] vboWrappers,
ArrayList<ByteBuffer> vertexBuffers, @Nullable ArrayList<ByteBuffer> indexBuffers
)
{
ArrayList<CompletableFuture<Void>> uploadFutureList = new ArrayList<>();
int vboIndex = 0;
for (int i = 0; i < vertexBuffers.size(); i++)
{
if (vboIndex >= vboWrappers.length)
{
throw new RuntimeException("Too many vertex buffers!!");
}
// get or create the VBO
if (vbos[vboIndex] == null)
{
vbos[vboIndex] = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createVboWrapper("distantHorizons:McLodRenderer");
}
IVertexBufferWrapper vbo = vbos[vboIndex];
ByteBuffer buffer = byteBuffers.get(i);
int size = buffer.limit() - buffer.position();
int vertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
// final variables for use in lambdas //
try
final int finalVboIndex = vboIndex;
final IVertexBufferWrapper finalVboWrapper = vboWrappers[vboIndex];
final ByteBuffer finalVertexBuffer = vertexBuffers.get(vboIndex);
// index buffers are optional
@Nullable final ByteBuffer finalIndexBuffer = (indexBuffers != null) ? indexBuffers.get(vboIndex) : null;
final int finalVertexCount = vertexByteBufferToVertexCount(finalVertexBuffer);
//===============//
// vertex upload //
//===============//
//region
CompletableFuture<Void> vertexUploadFuture = new CompletableFuture<>();
uploadFutureList.add(vertexUploadFuture);
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer VBO Upload", () ->
{
vbo.upload(buffer, vertexCount);
}
catch (Exception e)
try
{
// skip this event if requested
if (Thread.interrupted()
|| parentFuture.isCancelled())
{
throw new InterruptedException();
}
try
{
finalVboWrapper.uploadVertexBuffer(finalVertexBuffer, finalVertexCount);
vertexUploadFuture.complete(null);
}
catch (Exception e)
{
vboWrappers[finalVboIndex] = null;
finalVboWrapper.close();
LOGGER.error("Failed to upload buffer. Error: [" + e.getMessage() + "].", e);
}
}
catch (Exception e)
{
vertexUploadFuture.completeExceptionally(e);
}
});
//endregion
//==============//
// index upload //
//==============//
//region
if (finalIndexBuffer != null)
{
vbos[vboIndex] = null;
vbo.close();
LOGGER.error("Failed to upload buffer. Error: ["+e.getMessage()+"].", e);
CompletableFuture<Void> indexUploadFuture = new CompletableFuture<>();
uploadFutureList.add(indexUploadFuture);
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer IBO Upload", () ->
{
try
{
// skip this event if requested
if (Thread.interrupted()
|| parentFuture.isCancelled())
{
throw new InterruptedException();
}
finalVboWrapper.uploadIndexBuffer(finalIndexBuffer, finalVertexCount);
indexUploadFuture.complete(null);
}
catch (Exception e)
{
indexUploadFuture.completeExceptionally(e);
}
});
}
//endregion
vboIndex++;
}
if (vboIndex < vbos.length)
if (vboIndex < vboWrappers.length)
{
throw new RuntimeException("Too few vertex buffers!!");
}
// merge futures //
CompletableFuture<?>[] futureArray = new CompletableFuture[uploadFutureList.size()];
for (int i = 0; i < uploadFutureList.size(); i++)
{
futureArray[i] = uploadFutureList.get(i);
}
return CompletableFuture.allOf(futureArray);
}
//endregion
@@ -248,29 +482,34 @@ public class LodBufferContainer implements AutoCloseable
//================//
//region
private static int vertexByteBufferToVertexCount(ByteBuffer buffer)
{
int size = buffer.limit() - buffer.position();
int vertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
return vertexCount;
}
/** can be used when debugging */
public boolean hasNonNullVbos() { return this.vbos != null || this.vbosTransparent != null; }
public boolean hasNonNullVbos() { return this.vboOpaqueWrappers != null || this.vboTransparentWrappers != null; }
/** can be used when debugging */
public int vboBufferCount()
{
int count = 0;
if (this.vbos != null)
if (this.vboOpaqueWrappers != null)
{
count += this.vbos.length;
count += this.vboOpaqueWrappers.length;
}
if (this.vbosTransparent != null)
if (this.vboTransparentWrappers != null)
{
count += this.vbosTransparent.length;
count += this.vboTransparentWrappers.length;
}
return count;
}
public boolean uploadInProgress() { return this.uploadFutureRef.get() != null; }
//endregion
@@ -293,7 +532,7 @@ public class LodBufferContainer implements AutoCloseable
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Close", () ->
{
for (IVertexBufferWrapper buffer : this.vbos)
for (IVertexBufferWrapper buffer : this.vboOpaqueWrappers)
{
if (buffer != null)
{
@@ -301,7 +540,7 @@ public class LodBufferContainer implements AutoCloseable
}
}
for (IVertexBufferWrapper buffer : this.vbosTransparent)
for (IVertexBufferWrapper buffer : this.vboTransparentWrappers)
{
if (buffer != null)
{
@@ -1,6 +1,7 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.render;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.jar.EPlatform;
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.IDhGenericObjectVertexBufferContainer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.ILodContainerUniformBufferWrapper;
@@ -18,6 +19,16 @@ public abstract class AbstractDhRenderApiDefinition implements IBindable
/** Used for debugging */
public abstract String getApiName();
private final boolean useSingleIbo = (EPlatform.get() != EPlatform.MACOS);
/**
* Mac has a problem where binding an IBO that's longer than the VBO
* can cause OpenGL to render past the end of the VBO, throwing random junk
* on the screen. <br>
* To fix this we have to use individual IBOs for each VBO, which
* is slower due to having to construct new IBOs.
*/
public boolean useSingleIbo() { return this.useSingleIbo; }
//endregion
@@ -19,13 +19,16 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.render.objects;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.AbstractDhRenderApiDefinition;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
import java.nio.ByteBuffer;
public interface IVertexBufferWrapper extends IBindable, AutoCloseable
{
void upload(ByteBuffer buffer, int vertexCount);
void uploadVertexBuffer(ByteBuffer buffer, int vertexCount);
/** Does nothing if {@link AbstractDhRenderApiDefinition#useSingleIbo()} returns true */
void uploadIndexBuffer(ByteBuffer buffer, int vertexCount);
@Override
void close();