diff --git a/dependencies.gradle b/dependencies.gradle index 37fece0..311c8f2 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -73,7 +73,7 @@ dependencies { runtimeOnlyNonPublishable("com.github.GTNewHorizons:Avaritia:1.78:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:Avaritiaddons:1.9.3-GTNH:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:DuraDisplay:1.4.0:dev") - runtimeOnlyNonPublishable('com.github.GTNewHorizons:EnderIO:2.10.8:dev') +// runtimeOnlyNonPublishable('com.github.GTNewHorizons:EnderIO:2.10.8:dev') runtimeOnlyNonPublishable("com.github.GTNewHorizons:EnderStorage:1.8.0:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:GT5-Unofficial:5.09.52.140:dev") runtimeOnlyNonPublishable("com.github.GTNewHorizons:FloodLights:1.5.5:dev") @@ -95,8 +95,6 @@ dependencies { testImplementation(platform('org.junit:junit-bom:5.9.2')) testImplementation('org.junit.jupiter:junit-jupiter') - - runtimeOnlyNonPublishable(rfg.deobf("curse.maven:spark-361579:4271867")) } // deps may transitively add Baubles, so we replace it diff --git a/src/main/java/com/recursive_pineapple/matter_manipulator/client/rendering/BoxRenderer.java b/src/main/java/com/recursive_pineapple/matter_manipulator/client/rendering/BoxRenderer.java index cddbb6e..b5b840a 100644 --- a/src/main/java/com/recursive_pineapple/matter_manipulator/client/rendering/BoxRenderer.java +++ b/src/main/java/com/recursive_pineapple/matter_manipulator/client/rendering/BoxRenderer.java @@ -25,6 +25,8 @@ public class BoxRenderer { private final ShaderProgram program; private final int time_location; + private final VertexBuffer buffer = new VertexBuffer(DefaultVertexFormat.POSITION_COLOR_TEXTURE, GL11.GL_QUADS); + public BoxRenderer() { program = new ShaderProgram( Mods.MatterManipulator.resourceDomain, @@ -39,8 +41,6 @@ public BoxRenderer() { /** * Starts rendering fancy boxes. Should only be called once per frame, to allow quad sorting. - * - * @param partialTickTime */ public void start(double partialTickTime) { TessellatorManager.startCapturing(); @@ -163,13 +163,10 @@ public void finish() { program.use(); - // this should only be done once a frame, but there aren't any side effects from calling it more GL20.glUniform1f(time_location, (((float) (System.currentTimeMillis() % 2500)) / 1000f)); - try (VertexBuffer buffer = new VertexBuffer(DefaultVertexFormat.POSITION_COLOR_TEXTURE, GL11.GL_QUADS);) { - buffer.upload(bytes); - buffer.render(); - } + buffer.upload(bytes); + buffer.render(); ShaderProgram.clear(); diff --git a/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/FixedLengthVertexBuffer.java b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/FixedLengthVertexBuffer.java new file mode 100644 index 0000000..86407b6 --- /dev/null +++ b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/FixedLengthVertexBuffer.java @@ -0,0 +1,80 @@ +package com.recursive_pineapple.matter_manipulator.common.items.manipulator; + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; + +import org.intellij.lang.annotations.MagicConstant; +import org.lwjgl.opengl.ARBBufferStorage; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; + +/// Note: this doesn't work properly for some reason, don't use it as-is +public class FixedLengthVertexBuffer extends StreamingVertexBuffer { + + public FixedLengthVertexBuffer(VertexFormat format, int drawMode) { + super(format, drawMode); + } + + @Override + public void reallocate() { + generate(); + + // noinspection MagicConstant + setSize(this.length, this.bufferFlags); + } + + @Override + public void allocate( + int vertexCount, + @MagicConstant(intValues = { + ARBBufferStorage.GL_DYNAMIC_STORAGE_BIT, + GL30.GL_MAP_READ_BIT, + GL30.GL_MAP_WRITE_BIT, + ARBBufferStorage.GL_MAP_PERSISTENT_BIT, + ARBBufferStorage.GL_MAP_COHERENT_BIT, + ARBBufferStorage.GL_CLIENT_STORAGE_BIT, + }) int usage + ) { + // noinspection MagicConstant + if (this.id == 0 || vertexCount < this.vertexCount / 4 || vertexCount > this.vertexCount || usage != this.bufferFlags) { + generate(); + + setSize(vertexCount * (long) format.getVertexSize(), usage); + } + + this.vertexCount = vertexCount; + } + + public void setSize( + long length, + @MagicConstant(intValues = { + ARBBufferStorage.GL_DYNAMIC_STORAGE_BIT, + GL30.GL_MAP_READ_BIT, + GL30.GL_MAP_WRITE_BIT, + ARBBufferStorage.GL_MAP_PERSISTENT_BIT, + ARBBufferStorage.GL_MAP_COHERENT_BIT, + ARBBufferStorage.GL_CLIENT_STORAGE_BIT, + }) int bufferFlags + ) { + if (this.length > 0) throw new IllegalStateException("Cannot resize an immutable (fixed length) vertex buffer"); + + bind(); + + this.length = length; + this.bufferFlags = bufferFlags; + ARBBufferStorage.glBufferStorage(GL15.GL_ARRAY_BUFFER, length, bufferFlags); + + unbind(); + } + + public void flush() { + bind(); + GL30.glFlushMappedBufferRange(GL15.GL_ARRAY_BUFFER, 0, vertexCount * (long) format.getVertexSize()); + unbind(); + } + + public void flushAll() { + bind(); + GL30.glFlushMappedBufferRange(GL15.GL_ARRAY_BUFFER, 0, length); + unbind(); + } +} diff --git a/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/RenderHints.java b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/RenderHints.java index 3cd7a83..1194827 100644 --- a/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/RenderHints.java +++ b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/RenderHints.java @@ -1,7 +1,6 @@ package com.recursive_pineapple.matter_manipulator.common.items.manipulator; import java.nio.ByteBuffer; -import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.Comparator; import java.util.concurrent.ExecutionException; @@ -32,27 +31,45 @@ import org.joml.Vector3d; import org.joml.Vector3i; +import org.lwjgl.LWJGLException; +import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL15; -import org.lwjgl.util.glu.GLU; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.SharedDrawable; import lombok.Setter; @EventBusSubscriber(side = Side.CLIENT) public class RenderHints { - private static final int BYTES_PER_HINT = DefaultVertexFormat.POSITION_TEXTURE_COLOR.getVertexSize() * 4 * 6; - + /// The latest list of hints. This is not sorted in any way and can only be accessed by the main thread. private static final ArrayList HINTS = new ArrayList<>(10000); + /// The list of hints that was sent to the worker thread. This field is modified by the main thread but the list + /// itself is modified by the worker thread. This is an optimization because the list from the prior position is + /// almost certainly nearly-sorted, which should reduce the number of comparisons significantly. + private static ArrayList drawnHints = null; + private static final Vector3d LAST_PLAYER_POSITION = new Vector3d(); private static final Vector3i LAST_RENDERED_PLAYER_POSITION = new Vector3i(); private static boolean vboNeedsRebuild = false; - /** The VBO being used for rendering */ - private static VertexBuffer activeVBO; - /** The VBO that's mapped and is being written to */ - private static VertexBuffer pendingVBO; + /// The VBO that's being used for rendering + private static StreamingVertexBuffer activeVBO; + /// The VBO that's being written to by the worker thread (or is idle) + private static StreamingVertexBuffer pendingVBO; + + /// An opengl context that's active on the background thread and is used for writing to the pending VBO. + private static final SharedDrawable BACKGROUND_CONTEXT; + + static { + try { + BACKGROUND_CONTEXT = new SharedDrawable(Display.getDrawable()); + } catch (LWJGLException e) { + throw new RuntimeException("Could not initialized background SharedDrawable", e); + } + } private static final ExecutorService WORKER_THREAD = Executors.newFixedThreadPool(1); private static Future renderTask; @@ -67,12 +84,12 @@ public static void reset() { } HINTS.clear(); + drawnHints = null; vboNeedsRebuild = true; } public static void addHint(int x, int y, int z, Block block, int meta, short[] tint) { - Hint hint = new Hint(); hint.x = x; @@ -86,6 +103,9 @@ public static void addHint(int x, int y, int z, Block block, int meta, short[] t } HINTS.add(hint); + + // Invalidate the cached sort results + drawnHints = null; } @SubscribeEvent @@ -95,47 +115,62 @@ public static void onWorldLoad(WorldEvent.Load e) { } } - private static VBOResult buildVBO(ByteBuffer buffer, ArrayList hints, double xd, double yd, double zd, int xi, int yi, int zi) { + private static VBOResult buildVBO(StreamingVertexBuffer vbo, ArrayList hints, double xd, double yd, double zd, int xi, int yi, int zi) { try { Vector3d eyes = new Vector3d(xd, yd, zd); - hints.sort(Comparator.comparingDouble(info -> -eyes.distanceSquared(info.x + 0.5, info.y + 0.5, info.z + 0.5))); + try { + if (!BACKGROUND_CONTEXT.isCurrent()) { + BACKGROUND_CONTEXT.makeCurrent(); + } + } catch (LWJGLException e) { + throw new RuntimeException("Could not activate background GL context", e); + } - TessellatorManager.startCapturing(); + hints.sort(Comparator.comparingDouble(info -> -eyes.distanceSquared(info.x + 0.5, info.y + 0.5, info.z + 0.5))); - Tessellator tes = TessellatorManager.get(); + Tessellator tes = TessellatorManager.startCapturingAndGet(); tes.startDrawing(GL11.GL_QUADS); int hintCount = hints.size(); - // noinspection ForLoopReplaceableByForEach for (int i = 0; i < hintCount; i++) { hints.get(i).draw(tes, xd, yd, zd, xi, yi, zi); } final var quads = TessellatorManager.stopCapturingToPooledQuads(); - long expectedSize = (long) DefaultVertexFormat.POSITION_TEXTURE_COLOR.getVertexSize() * quads.size() * 4; + final VertexFormat format = DefaultVertexFormat.POSITION_TEXTURE_COLOR; - buffer.rewind(); + // noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (vbo) { + vbo.allocate(quads.size() * 4, GL15.GL_STREAM_DRAW); - if (expectedSize > buffer.capacity()) { - MMMod.LOG.error( - "Could not upload hint VBO: Could not insert hint quads into GL buffer (expectedSize={}, buffer.capacity={})", - expectedSize, - buffer.capacity() - ); + ByteBuffer buffer = vbo.map(GL30.GL_MAP_WRITE_BIT); - return new VBOResult(new Vector3i(xi, yi, zi), 0); - } + buffer.rewind(); - // noinspection ForLoopReplaceableByForEach - for (int i = 0, quadsSize = quads.size(); i < quadsSize; i++) { - DefaultVertexFormat.POSITION_TEXTURE_COLOR.writeQuad(quads.get(i), buffer); - } + long expectedSize = (long) format.getVertexSize() * quads.size() * 4; + + if (expectedSize > buffer.capacity()) { + MMMod.LOG.error( + "Could not upload hint VBO: Could not insert hint quads into GL buffer (expectedSize={}, buffer.capacity={})", + expectedSize, + buffer.capacity() + ); - buffer.rewind(); + return new VBOResult(new Vector3i(xi, yi, zi), 0); + } + + for (int i = 0, quadsSize = quads.size(); i < quadsSize; i++) { + format.writeQuad(quads.get(i), buffer); + } + + buffer.rewind(); + + vbo.unmap(); + } return new VBOResult(new Vector3i(xi, yi, zi), quads.size() * 4); } finally { @@ -160,29 +195,28 @@ public static void onRenderWorldLast(RenderWorldLastEvent e) { Vector3d currentPos = new Vector3d(xd, yd, zd); if (activeVBO == null) { - activeVBO = new VertexBuffer(DefaultVertexFormat.POSITION_TEXTURE_COLOR, GL11.GL_QUADS); + activeVBO = new StreamingVertexBuffer(DefaultVertexFormat.POSITION_TEXTURE_COLOR, GL11.GL_QUADS); } if (pendingVBO == null) { - pendingVBO = new VertexBuffer(DefaultVertexFormat.POSITION_TEXTURE_COLOR, GL11.GL_QUADS); + pendingVBO = new StreamingVertexBuffer(DefaultVertexFormat.POSITION_TEXTURE_COLOR, GL11.GL_QUADS); } if (renderTask != null && renderTask.isDone()) { VBOResult result = null; + try { result = renderTask.get(); } catch (InterruptedException | ExecutionException ex) { - MMMod.LOG.error("Could not cancel render hints", ex); + MMMod.LOG.error("Could not assemble render hint quads", ex); } renderTask = null; - pendingVBO.unmap(); if (result != null) { LAST_RENDERED_PLAYER_POSITION.set(result.playerPosition); - pendingVBO.vertexCount = result.vertexCount; - VertexBuffer temp = activeVBO; + StreamingVertexBuffer temp = activeVBO; activeVBO = pendingVBO; pendingVBO = temp; } @@ -192,19 +226,16 @@ public static void onRenderWorldLast(RenderWorldLastEvent e) { LAST_PLAYER_POSITION.set(currentPos); vboNeedsRebuild = false; - ArrayList hints = new ArrayList<>(HINTS); - - if (pendingVBO.mapped) pendingVBO.unmap(); - - pendingVBO.ensureSize((long) hints.size() * BYTES_PER_HINT, GL15.GL_STREAM_DRAW); - ByteBuffer buffer = pendingVBO.map(GL15.GL_WRITE_ONLY); - - if (buffer != null) { - renderTask = WORKER_THREAD.submit(() -> buildVBO(buffer, hints, xd, yd, zd, xi, yi, zi)); + // If the hint list has changed, re-copy them into the drawnHints list so that the worker thread can sort + // them. + if (drawnHints == null) { + drawnHints = new ArrayList<>(HINTS); } + + renderTask = WORKER_THREAD.submit(() -> buildVBO(pendingVBO, drawnHints, xd, yd, zd, xi, yi, zi)); } - if (activeVBO.vertexCount > 0) { + if (activeVBO.getVertexCount() > 0) { p.startSection("Draw MM Hints"); GL11.glPushMatrix(); @@ -226,8 +257,11 @@ public static void onRenderWorldLast(RenderWorldLastEvent e) { GL11.glEnable(GL11.GL_DEPTH_TEST); } - // There aren't any frames in flight, so we can re-use this buffer on the next frame without issue - activeVBO.render(); + // noinspection SynchronizeOnNonFinalField + synchronized (activeVBO) { + // There aren't any frames in flight, so we can re-use this buffer on the next frame without issue + activeVBO.render(); + } GL11.glPopAttrib(); GL11.glPopMatrix(); @@ -352,144 +386,4 @@ public VBOResult(Vector3i playerPosition, int vertexCount) { this.vertexCount = vertexCount; } } - - private static class VertexBuffer implements AutoCloseable { - - private int id; - private volatile int vertexCount; - private VertexFormat format; - private int drawMode; - - private volatile long currentSize; - private volatile int currentUsage; - private volatile ByteBuffer oldMap; - private volatile boolean mapped; - - public VertexBuffer() { - this.id = GL15.glGenBuffers(); - } - - public VertexBuffer(VertexFormat format, int drawMode) { - this(); - this.format = format; - this.drawMode = drawMode; - } - - public void bind() { - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.id); - } - - public void unbind() { - GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); - } - - public void upload(int usage, ByteBuffer buffer, int vertexCount) { - if (this.id != -1) { - this.vertexCount = vertexCount; - this.bind(); - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, usage); - this.unbind(); - } - } - - public void upload(ByteBuffer buffer) { - if (this.format == null) { - throw new IllegalStateException("No format specified for VBO upload"); - } else { - this.upload(GL15.GL_STATIC_DRAW, buffer, buffer.remaining() / this.format.getVertexSize()); - } - } - - public void close() { - if (this.id >= 0) { - GL15.glDeleteBuffers(this.id); - this.id = -1; - } - } - - public void draw(FloatBuffer floatBuffer) { - GL11.glPushMatrix(); - GL11.glLoadIdentity(); - GL11.glMultMatrix(floatBuffer); - this.draw(); - GL11.glPopMatrix(); - } - - public void draw() { - GL11.glDrawArrays(this.drawMode, 0, this.vertexCount); - } - - public void setupState() { - if (this.format == null) { - throw new IllegalStateException("No format specified for VBO setup"); - } else { - this.bind(); - this.format.setupBufferState(0L); - } - } - - public void cleanupState() { - this.format.clearBufferState(); - this.unbind(); - } - - public void render() { - this.setupState(); - this.draw(); - this.cleanupState(); - } - - public void ensureSize(long size, int usage) { - if (size > currentSize || currentSize / 4 > size || currentUsage != usage) { - bind(); - - GL15.glBufferData(GL15.GL_ARRAY_BUFFER, size, usage); - currentSize = size; - currentUsage = usage; - - unbind(); - } - } - - @SuppressWarnings("NonAtomicOperationOnVolatileField") - public ByteBuffer map(int access) { - if (mapped) throw new IllegalStateException("cannot map the same buffer twice"); - if (currentSize == 0) throw new IllegalStateException("cannot map an empty buffer"); - - bind(); - - GL11.glGetError(); - - oldMap = GL15.glMapBuffer(GL15.GL_ARRAY_BUFFER, access, currentSize, oldMap); - - if (oldMap == null) { - MMMod.LOG.error("Error mapping buffer: {}", GLU.gluErrorString(GL11.glGetError())); - } else { - mapped = true; - } - - unbind(); - - return oldMap; - } - - public void unmap() { - if (!mapped) throw new IllegalStateException("cannot unmap the same buffer twice"); - - bind(); - - GL11.glGetError(); - - GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER); - int error = GL11.glGetError(); - - if (error != 0) { - MMMod.LOG.error("Error unmapping buffer: {}", GLU.gluErrorString(error)); - } - - mapped = false; - - unbind(); - } - } } diff --git a/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/StreamingVertexBuffer.java b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/StreamingVertexBuffer.java new file mode 100644 index 0000000..60172ec --- /dev/null +++ b/src/main/java/com/recursive_pineapple/matter_manipulator/common/items/manipulator/StreamingVertexBuffer.java @@ -0,0 +1,215 @@ +package com.recursive_pineapple.matter_manipulator.common.items.manipulator; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; + +import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat; +import com.recursive_pineapple.matter_manipulator.MMMod; + +import org.intellij.lang.annotations.MagicConstant; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.util.glu.GLU; + +import lombok.Getter; + +public class StreamingVertexBuffer implements AutoCloseable { + + @Getter + protected final VertexFormat format; + @Getter + protected final int drawMode; + + @Getter + protected int id; + @Getter + protected int vertexCount; + + @Getter + protected long length; + @Getter + protected int bufferFlags; + + @Getter + protected ByteBuffer mappedBuffer; + @Getter + protected boolean mapped; + + public StreamingVertexBuffer(VertexFormat format, int drawMode) { + this.id = GL15.glGenBuffers(); + this.format = format; + this.drawMode = drawMode; + } + + /// Generates a new vertex buffer and closes the previous one, if present. + /// This should be used sparingly - it's very expensive to call it each frame. + public void generate() { + if (this.id > 0) { + close(); + } + + this.id = GL15.glGenBuffers(); + } + + @Override + public void close() { + if (this.id > 0) { + if (mapped) unmap(); + + GL15.glDeleteBuffers(this.id); + + this.id = 0; + this.vertexCount = 0; + this.length = 0; + } + } + + public void bind() { + if (this.id == 0) throw new IllegalStateException("Cannot bind unallocated VBO"); + + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, this.id); + } + + public void unbind() { + GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0); + } + + public void upload(int usage, ByteBuffer buffer, int vertexCount) { + if (this.id > 0) { + this.vertexCount = vertexCount; + this.bind(); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, buffer, usage); + this.unbind(); + } + } + + public void upload(ByteBuffer buffer) { + this.upload(GL15.GL_STATIC_DRAW, buffer, buffer.remaining() / this.format.getVertexSize()); + } + + public void draw(FloatBuffer floatBuffer) { + GL11.glPushMatrix(); + GL11.glLoadIdentity(); + GL11.glMultMatrix(floatBuffer); + this.draw(); + GL11.glPopMatrix(); + } + + public void draw() { + if (mapped) throw new IllegalStateException("Cannot draw a buffer that is mapped"); + + GL11.glDrawArrays(this.drawMode, 0, this.vertexCount); + } + + public void setupState() { + this.bind(); + this.format.setupBufferState(0L); + } + + public void cleanupState() { + this.format.clearBufferState(); + this.unbind(); + } + + public void render() { + this.setupState(); + this.draw(); + this.cleanupState(); + } + + /// Reallocates the memory stored in this VBO so that the driver can avoid synchronization flushes. Typically + /// drivers pool memory so there's a good chance it'll just pull it from the pool instead of allocating anything + /// since the length is the same. + /// Reference: [Buffer Object Streaming](https://wikis.khronos.org/opengl/Buffer_Object_Streaming) + public void reallocate() { + bind(); + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, length, bufferFlags); + unbind(); + } + + public void allocate( + int vertexCount, + @MagicConstant(intValues = { + GL15.GL_STREAM_DRAW, + GL15.GL_STREAM_READ, + GL15.GL_STREAM_COPY, + GL15.GL_STATIC_DRAW, + GL15.GL_STATIC_READ, + GL15.GL_STATIC_COPY, + GL15.GL_DYNAMIC_DRAW, + GL15.GL_DYNAMIC_READ, + GL15.GL_DYNAMIC_COPY, + }) int usage + ) { + bind(); + + this.vertexCount = vertexCount; + this.length = vertexCount * (long) format.getVertexSize(); + this.bufferFlags = usage; + + GL15.glBufferData(GL15.GL_ARRAY_BUFFER, this.length, this.bufferFlags); + + unbind(); + } + + /// Maps the buffer into the client memory space (CPU) and returns a [ByteBuffer] wrapper for it. + /// @param access See [glMapBufferRange](https://docs.gl/es3/glMapBufferRange) for more info. + public ByteBuffer map( + @MagicConstant(intValues = { + GL30.GL_MAP_READ_BIT, + GL30.GL_MAP_WRITE_BIT, + GL30.GL_MAP_INVALIDATE_RANGE_BIT, + GL30.GL_MAP_INVALIDATE_BUFFER_BIT, + GL30.GL_MAP_FLUSH_EXPLICIT_BIT, + GL30.GL_MAP_UNSYNCHRONIZED_BIT + }) int access + ) { + if (mapped) throw new IllegalStateException("cannot map the same buffer twice"); + + bind(); + + if (mappedBuffer != null) { + mappedBuffer.clear(); + } + + mappedBuffer = GL30.glMapBufferRange(GL15.GL_ARRAY_BUFFER, 0, length, access, mappedBuffer); + + if (mappedBuffer == null) { + MMMod.LOG.error("Error mapping buffer: {}", GLU.gluErrorString(GL11.glGetError())); + } else { + mapped = true; + } + + unbind(); + + return mappedBuffer; + } + + @SuppressWarnings("UnusedReturnValue") + public boolean unmap() { + if (!mapped) throw new IllegalStateException("cannot unmap the same buffer twice"); + + bind(); + + boolean valid = true; + + if (!GL15.glUnmapBuffer(GL15.GL_ARRAY_BUFFER)) { + // Something happened that corrupted the VBO, it has to be re-initialized + reallocate(); + valid = false; + } + + int error = GL11.glGetError(); + + if (error != 0) { + MMMod.LOG.error("Error unmapping buffer: {}", GLU.gluErrorString(error)); + } + + mapped = false; + + unbind(); + + return valid; + } +}