From 1422f25ad93354c20698f6e4a3386e2810e01d26 Mon Sep 17 00:00:00 2001
From: FoundationGames <43485105+FoundationGames@users.noreply.github.com>
Date: Mon, 7 Aug 2023 11:35:49 -0700
Subject: [PATCH] Make changes to VBOs and add a config option for them -
 Rendering with VBOs can be enabled and disabled - BlockEntityClientState was
 generalized into CableVBOContainer - Most reliance on an available
 BlockEntity for rendering with VBOs has been removed - Major refactoring of
 VBO rendering away from CableOutputBlockEntityRenderer - Abstracted
 BlockEntityOutputs

---
 .../entity/AbstractOutputBlockEntity.java     |  32 ++-
 .../entity/ElectronicJukeboxBlockEntity.java  |  33 ++-
 .../client/render/BlockEntityClientState.java |  41 ----
 .../phonos/client/render/CableRenderer.java   | 212 +++++++++---------
 .../client/render/CableVBOContainer.java      | 102 +++++++++
 .../block/CableOutputBlockEntityRenderer.java |  64 ++----
 .../phonos/config/PhonosClientConfig.java     |   1 +
 .../config/PhonosClientConfigScreen.java      |   1 +
 .../phonos/world/sound/CableConnection.java   |   4 +
 .../phonos/world/sound/CablePlugPoint.java    |   6 +
 .../world/sound/ConnectionCollection.java     |   7 +
 .../sound/block/BlockConnectionLayout.java    |  10 +
 .../world/sound/block/BlockEntityOutputs.java |   4 +-
 .../world/sound/block/OutputBlockEntity.java  |   6 +-
 .../resources/assets/phonos/lang/en_us.json   |   3 +-
 15 files changed, 305 insertions(+), 221 deletions(-)
 delete mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java
 create mode 100644 src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java
 create mode 100644 src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java

diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java
index bb7bfc9..142a1b8 100644
--- a/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java
+++ b/src/main/java/io/github/foundationgames/phonos/block/entity/AbstractOutputBlockEntity.java
@@ -1,6 +1,6 @@
 package io.github.foundationgames.phonos.block.entity;
 
-import io.github.foundationgames.phonos.client.render.BlockEntityClientState;
+import io.github.foundationgames.phonos.client.render.CableVBOContainer;
 import io.github.foundationgames.phonos.sound.emitter.SoundSource;
 import io.github.foundationgames.phonos.util.UniqueId;
 import io.github.foundationgames.phonos.world.sound.block.BlockConnectionLayout;
@@ -23,14 +23,15 @@ public abstract class AbstractOutputBlockEntity extends BlockEntity implements S
     public final BlockEntityOutputs outputs;
     protected @Nullable NbtCompound pendingNbt = null;
     protected final long emitterId;
-    private BlockEntityClientState clientState;
+
+    private CableVBOContainer vboContainer;
 
     public AbstractOutputBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state, BlockConnectionLayout outputLayout) {
         super(type, pos, state);
 
         this.emitterId = UniqueId.ofBlock(pos);
         this.outputs = new BlockEntityOutputs(outputLayout, this);
-        this.clientState = null;
+        this.vboContainer = null;
     }
 
     @Override
@@ -120,20 +121,29 @@ public BlockEntityOutputs getOutputs() {
     }
 
     @Override
-    public BlockEntityClientState getClientState() {
-        if (this.clientState == null) {
-            this.clientState = new BlockEntityClientState();
+    public void enforceVBOState(boolean enabled) {
+        if (this.vboContainer != null && !enabled) {
+            this.vboContainer.close();
+
+            this.vboContainer = null;
+        }
+    }
+
+    @Override
+    public CableVBOContainer getOrCreateVBOContainer() {
+        if (this.vboContainer == null) {
+            this.vboContainer = new CableVBOContainer();
         }
 
-        this.clientState.genState(this.outputs);
-        return this.clientState;
+        this.vboContainer.refresh(this.outputs);
+        return this.vboContainer;
     }
 
     @Override
     public void markRemoved() {
-        if (this.hasWorld() && this.world.isClient && this.clientState != null) {
-            this.clientState.dirty = true;
-            this.clientState.close();
+        if (this.hasWorld() && this.world.isClient() && this.vboContainer != null) {
+            this.vboContainer.rebuild = true;
+            this.vboContainer.close();
         }
         super.markRemoved();
     }
diff --git a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java
index d0f43c5..4c8698f 100644
--- a/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java
+++ b/src/main/java/io/github/foundationgames/phonos/block/entity/ElectronicJukeboxBlockEntity.java
@@ -1,7 +1,7 @@
 package io.github.foundationgames.phonos.block.entity;
 
 import io.github.foundationgames.phonos.block.PhonosBlocks;
-import io.github.foundationgames.phonos.client.render.BlockEntityClientState;
+import io.github.foundationgames.phonos.client.render.CableVBOContainer;
 import io.github.foundationgames.phonos.network.PayloadPackets;
 import io.github.foundationgames.phonos.sound.SoundStorage;
 import io.github.foundationgames.phonos.sound.emitter.SoundEmitterTree;
@@ -47,17 +47,17 @@ public class ElectronicJukeboxBlockEntity extends JukeboxBlockEntity implements
     private final BlockEntityType<?> type;
     private @Nullable NbtCompound pendingNbt = null;
     private final long emitterId;
-    private BlockEntityClientState clientState;
-
     private @Nullable SoundEmitterTree playingSound = null;
 
+    private CableVBOContainer vboContainer;
+
     public ElectronicJukeboxBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
         super(pos, state);
         this.type = type;
         this.emitterId = UniqueId.ofBlock(pos);
 
         this.outputs = new BlockEntityOutputs(OUTPUT_LAYOUT, this);
-        this.clientState = null;
+        this.vboContainer = null;
     }
 
     public ElectronicJukeboxBlockEntity(BlockPos pos, BlockState state) {
@@ -201,20 +201,29 @@ public BlockEntityOutputs getOutputs() {
     }
 
     @Override
-    public BlockEntityClientState getClientState() {
-        if (this.clientState == null) {
-            this.clientState = new BlockEntityClientState();
+    public void enforceVBOState(boolean enabled) {
+        if (this.vboContainer != null && !enabled) {
+            this.vboContainer.close();
+
+            this.vboContainer = null;
+        }
+    }
+
+    @Override
+    public CableVBOContainer getOrCreateVBOContainer() {
+        if (this.vboContainer == null) {
+            this.vboContainer = new CableVBOContainer();
         }
 
-        this.clientState.genState(this.outputs);
-        return this.clientState;
+        this.vboContainer.refresh(this.outputs);
+        return this.vboContainer;
     }
 
     @Override
     public void markRemoved() {
-        if (this.hasWorld() && this.world.isClient && this.clientState != null) {
-            this.clientState.dirty = true;
-            this.clientState.close();
+        if (this.hasWorld() && this.world.isClient() && this.vboContainer != null) {
+            this.vboContainer.rebuild = true;
+            this.vboContainer.close();
         }
         super.markRemoved();
     }
diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java b/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java
deleted file mode 100644
index 71cdaa4..0000000
--- a/src/main/java/io/github/foundationgames/phonos/client/render/BlockEntityClientState.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package io.github.foundationgames.phonos.client.render;
-
-import io.github.foundationgames.phonos.world.sound.CableConnection;
-import io.github.foundationgames.phonos.world.sound.block.BlockEntityOutputs;
-import net.minecraft.client.gl.VertexBuffer;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class BlockEntityClientState {
-    public boolean dirty;
-    public @Nullable VertexBuffer buffer = null;
-    private List<CableConnection> cachedCons = new ArrayList<>();
-
-    public void genState(BlockEntityOutputs outs) {
-        List<CableConnection> connections = new ArrayList<>();
-        outs.forEach((i, o) -> connections.add(o));
-
-        if (!cachedCons.equals(connections) || this.buffer == null) {
-            // Mark ourselves as needing re-rendering
-            dirty = true;
-
-            // Close the existing buffer
-            if (buffer != null) {
-                buffer.close();
-            }
-
-            buffer = null;
-            cachedCons = connections;
-        }
-    }
-
-    public void close() {
-        if (buffer != null) {
-            buffer.close();
-        }
-
-        buffer = null;
-    }
-}
diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java
index 0b4fc50..94f3d36 100644
--- a/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java
+++ b/src/main/java/io/github/foundationgames/phonos/client/render/CableRenderer.java
@@ -1,20 +1,21 @@
 package io.github.foundationgames.phonos.client.render;
 
-import com.mojang.blaze3d.systems.RenderSystem;
 import io.github.foundationgames.phonos.config.PhonosClientConfig;
 import io.github.foundationgames.phonos.util.PhonosUtil;
 import io.github.foundationgames.phonos.util.Pose3f;
 import io.github.foundationgames.phonos.world.sound.CableConnection;
 import io.github.foundationgames.phonos.world.sound.CablePlugPoint;
-import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gl.VertexBuffer;
 import net.minecraft.client.model.Model;
-import net.minecraft.client.render.*;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.WorldRenderer;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.MathHelper;
 import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
 import org.joml.Quaternionf;
 import org.joml.Vector3f;
 import org.joml.Vector4f;
@@ -66,127 +67,134 @@ private static void lerpCableEnd(Vector4f[] out, Vector4f[] begin, Vector4f[] en
         }
     }
 
-    public static void renderConnection(BlockEntityClientState clientState, BlockEntity e, PhonosClientConfig config, World world, CableConnection conn, MatrixStack matrices, VertexConsumer buffer, Model cableEndModel, int overlay, float tickDelta) {
+    public static void renderConnection(@Nullable CableVBOContainer vboContainer, PhonosClientConfig config, World world, CableConnection conn,
+                                        MatrixStack matrices, VertexConsumer immediate, Model cableEndModel, int overlay, float tickDelta) {
         int startLight, endLight;
 
-        // Connection points are always rendered immediate
-
+        // Connection plug points are always rendered immediate
         matrices.push();
             transformConnPoint(world, conn.start, matrices, cableStPt, tickDelta);
 
             lightPos.set(cableStPt.x, cableStPt.y, cableStPt.z);
             startLight = WorldRenderer.getLightmapCoordinates(world, lightPos);
 
-            cableEndModel.render(matrices, buffer, startLight, overlay, 1, 1, 1, 1);
+            cableEndModel.render(matrices, immediate, startLight, overlay, 1, 1, 1, 1);
         matrices.pop();
 
-
         matrices.push();
             transformConnPoint(world, conn.end, matrices, cableEnPt, tickDelta);
 
             lightPos.set(cableEnPt.x, cableEnPt.y, cableEnPt.z);
             endLight = WorldRenderer.getLightmapCoordinates(world, lightPos);
 
-            cableEndModel.render(matrices, buffer, endLight, overlay, 1, 1, 1, 1);
+            cableEndModel.render(matrices, immediate, endLight, overlay, 1, 1, 1, 1);
         matrices.pop();
+        // ---
+
+        float length = cableStPt.distance(cableEnPt);
+        double detail = config.cableLODNearDetail;
+        int segments = Math.max((int) Math.ceil(4 * length * detail), 1);
+
+        if (conn.isStatic() && vboContainer != null) { // Vbo can be used for this connection
+            if (vboContainer.rebuild) {
+                BufferBuilder buffer = Tessellator.getInstance().getBuffer();
+                matrices = new MatrixStack();
 
-        // Rendering for the first time? Generate VBOs now.
-        if (clientState.dirty) {
-            BufferBuilder builder = Tessellator.getInstance().getBuffer();
-
-            matrices = new MatrixStack();
-            matrices.push();
-
-            float vOffset = conn.color != null ? 0.125f : 0;
-            float r = conn.color != null ? conn.color.getColorComponents()[0] : 1;
-            float g = conn.color != null ? conn.color.getColorComponents()[1] : 1;
-            float b = conn.color != null ? conn.color.getColorComponents()[2] : 1;
-            float length = cableStPt.distance(cableEnPt);
-
-            double detail = config.cableLODNearDetail;
-            int segments = Math.max((int) Math.ceil(4 * length * detail), 1);
-
-            // TODO: baking does not support LODs
-//        if (config.cableLODs) {
-//            float cx = (cableStPt.x + cableEnPt.x) * 0.5f;
-//            float cy = (cableStPt.y + cableEnPt.y) * 0.5f;
-//            float cz = (cableStPt.z + cableEnPt.z) * 0.5f;
-//
-//            double sqDist = MinecraftClient.getInstance().gameRenderer.getCamera().getPos()
-//                    .squaredDistanceTo(cx, cy, cz);
-//            double delta = MathHelper.clamp(sqDist / (length * length * 4), 0, 1);
-//            detail = MathHelper.lerp(delta, config.cableLODNearDetail, config.cableLODFarDetail);
-//
-//            segments = Math.max((int) Math.ceil(4 * length * detail), Math.min(3, segments));
-//        }
-
-            final float texUWid = (float) (0.25 / detail);
-
-            cableRotAxis.set(cableEnPt.z - cableStPt.z, 0, cableEnPt.x - cableStPt.x);
-
-            float cablePitch = (float) Math.atan2(cableEnPt.y - cableStPt.y, cableRotAxis.length());
-            float cableYaw = (float) Math.atan2(cableRotAxis.z, cableRotAxis.x);
-            cableRotAxis.normalize();
-
-            loadCableEnd(cableStart);
-            loadCableEnd(cableEnd);
-            cableNormal[0].set(-PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0);
-            cableNormal[1].set(PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0);
-            cableNormal[2].set(PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0);
-            cableNormal[3].set(-PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0);
-
-            rotationCache.setAngleAxis(cablePitch, 1, 0, 0);
-            transformCableEnd(cableStart, vec -> vec.rotate(rotationCache));
-            transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache));
-            transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache));
-
-            rotationCache.setAngleAxis(Math.PI + cableYaw, 0, 1, 0);
-            transformCableEnd(cableStart, vec -> vec.rotate(rotationCache));
-            transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache));
-            transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache));
-
-            transformCableEnd(cableStart, vec -> vec.set(vec.x + cableStPt.x, vec.y + cableStPt.y, vec.z + cableStPt.z, 1));
-            transformCableEnd(cableEnd, vec -> vec.set(vec.x + cableEnPt.x, vec.y + cableEnPt.y, vec.z + cableEnPt.z, 1));
-
-            transformCableNormal(cableNormal, matrices.peek().getNormalMatrix()::transform);
-
-            for (int s = 0; s < segments; s++) {
-                float startDelta = (float) s / segments;
-                float endDelta = (float) (s + 1) / segments;
-                float startYOffset = length * 0.15f * (0.25f - (float) Math.pow(startDelta - 0.5, 2));
-                float endYOffset = length * 0.15f * (0.25f - (float) Math.pow(endDelta - 0.5, 2));
-                int segStartLight = PhonosUtil.lerpLight(startDelta, startLight, endLight);
-                int segEndLight = PhonosUtil.lerpLight(endDelta, startLight, endLight);
-
-                lerpCableEnd(currCableStart, cableStart, cableEnd, startDelta);
-                lerpCableEnd(currCableEnd, cableStart, cableEnd, endDelta);
-
-                transformCableEnd(currCableStart, vec -> vec.add(0, -startYOffset, 0, 0));
-                transformCableEnd(currCableEnd, vec -> vec.add(0, -endYOffset, 0, 0));
-
-                transformCableEnd(currCableStart, matrices.peek().getPositionMatrix()::transform);
-                transformCableEnd(currCableEnd, matrices.peek().getPositionMatrix()::transform);
-
-                for (int i = 0; i < 4; i++) {
-                    float vOffset2 = i % 2 == 0 ? vOffset + 0.0625f : vOffset;
-                    int next = (i + 1) % 4;
-                    var nml = cableNormal[i];
-
-                    builder.vertex(currCableStart[i].x, currCableStart[i].y, currCableStart[i].z).color(r, g, b, 1)
-                            .texture(texUWid, 0.3125f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next();
-                    builder.vertex(currCableEnd[i].x, currCableEnd[i].y, currCableEnd[i].z).color(r, g, b, 1)
-                            .texture(0, 0.3125f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next();
-                    builder.vertex(currCableEnd[next].x, currCableEnd[next].y, currCableEnd[next].z).color(r, g, b, 1)
-                            .texture(0, 0.375f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next();
-                    builder.vertex(currCableStart[next].x, currCableStart[next].y, currCableStart[next].z).color(r, g, b, 1)
-                            .texture(texUWid, 0.375f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next();
-                }
+                buildCableGeometry(conn, matrices, buffer, segments, length, detail, startLight, endLight, overlay);
+            }
+        } else { // Connection must be rendered immediate
+            if (config.cableLODs) {
+                float cx = (cableStPt.x + cableEnPt.x) * 0.5f;
+                float cy = (cableStPt.y + cableEnPt.y) * 0.5f;
+                float cz = (cableStPt.z + cableEnPt.z) * 0.5f;
+
+                double sqDist = MinecraftClient.getInstance().gameRenderer.getCamera().getPos()
+                        .squaredDistanceTo(cx, cy, cz);
+                double delta = MathHelper.clamp(sqDist / (length * length * 4), 0, 1);
+                detail = MathHelper.lerp(delta, config.cableLODNearDetail, config.cableLODFarDetail);
+
+                segments = Math.max((int) Math.ceil(4 * length * detail), Math.min(3, segments));
             }
 
-            matrices.pop();
+            buildCableGeometry(conn, matrices, immediate, segments, length, detail, startLight, endLight, overlay);
         }
     }
 
+    private static void buildCableGeometry(CableConnection conn, MatrixStack matrices, VertexConsumer buffer, int segments,
+                                           float length, double detail, int startLight, int endLight, int overlay) {
+        matrices.push();
+
+        float vOffset = conn.color != null ? 0.125f : 0;
+        float r = conn.color != null ? conn.color.getColorComponents()[0] : 1;
+        float g = conn.color != null ? conn.color.getColorComponents()[1] : 1;
+        float b = conn.color != null ? conn.color.getColorComponents()[2] : 1;
+
+        final float texUWid = (float) (0.25 / detail);
+
+        cableRotAxis.set(cableEnPt.z - cableStPt.z, 0, cableEnPt.x - cableStPt.x);
+
+        float cablePitch = (float) Math.atan2(cableEnPt.y - cableStPt.y, cableRotAxis.length());
+        float cableYaw = (float) Math.atan2(cableRotAxis.z, cableRotAxis.x);
+        cableRotAxis.normalize();
+
+        loadCableEnd(cableStart);
+        loadCableEnd(cableEnd);
+        cableNormal[0].set(-PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0);
+        cableNormal[1].set(PhonosUtil.SQRT2DIV2, -PhonosUtil.SQRT2DIV2, 0);
+        cableNormal[2].set(PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0);
+        cableNormal[3].set(-PhonosUtil.SQRT2DIV2, PhonosUtil.SQRT2DIV2, 0);
+
+        rotationCache.setAngleAxis(cablePitch, 1, 0, 0);
+        transformCableEnd(cableStart, vec -> vec.rotate(rotationCache));
+        transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache));
+        transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache));
+
+        rotationCache.setAngleAxis(Math.PI + cableYaw, 0, 1, 0);
+        transformCableEnd(cableStart, vec -> vec.rotate(rotationCache));
+        transformCableEnd(cableEnd, vec -> vec.rotate(rotationCache));
+        transformCableNormal(cableNormal, vec -> vec.rotate(rotationCache));
+
+        transformCableEnd(cableStart, vec -> vec.set(vec.x + cableStPt.x, vec.y + cableStPt.y, vec.z + cableStPt.z, 1));
+        transformCableEnd(cableEnd, vec -> vec.set(vec.x + cableEnPt.x, vec.y + cableEnPt.y, vec.z + cableEnPt.z, 1));
+
+        transformCableNormal(cableNormal, matrices.peek().getNormalMatrix()::transform);
+
+        for (int s = 0; s < segments; s++) {
+            float startDelta = (float) s / segments;
+            float endDelta = (float) (s + 1) / segments;
+            float startYOffset = length * 0.15f * (0.25f - (float) Math.pow(startDelta - 0.5, 2));
+            float endYOffset = length * 0.15f * (0.25f - (float) Math.pow(endDelta - 0.5, 2));
+            int segStartLight = PhonosUtil.lerpLight(startDelta, startLight, endLight);
+            int segEndLight = PhonosUtil.lerpLight(endDelta, startLight, endLight);
+
+            lerpCableEnd(currCableStart, cableStart, cableEnd, startDelta);
+            lerpCableEnd(currCableEnd, cableStart, cableEnd, endDelta);
+
+            transformCableEnd(currCableStart, vec -> vec.add(0, -startYOffset, 0, 0));
+            transformCableEnd(currCableEnd, vec -> vec.add(0, -endYOffset, 0, 0));
+
+            transformCableEnd(currCableStart, matrices.peek().getPositionMatrix()::transform);
+            transformCableEnd(currCableEnd, matrices.peek().getPositionMatrix()::transform);
+
+            for (int i = 0; i < 4; i++) {
+                float vOffset2 = i % 2 == 0 ? vOffset + 0.0625f : vOffset;
+                int next = (i + 1) % 4;
+                var nml = cableNormal[i];
+
+                buffer.vertex(currCableStart[i].x, currCableStart[i].y, currCableStart[i].z).color(r, g, b, 1)
+                        .texture(texUWid, 0.3125f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next();
+                buffer.vertex(currCableEnd[i].x, currCableEnd[i].y, currCableEnd[i].z).color(r, g, b, 1)
+                        .texture(0, 0.3125f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next();
+                buffer.vertex(currCableEnd[next].x, currCableEnd[next].y, currCableEnd[next].z).color(r, g, b, 1)
+                        .texture(0, 0.375f + vOffset2).overlay(overlay).light(segEndLight).normal(nml.x, nml.y, nml.z).next();
+                buffer.vertex(currCableStart[next].x, currCableStart[next].y, currCableStart[next].z).color(r, g, b, 1)
+                        .texture(texUWid, 0.375f + vOffset2).overlay(overlay).light(segStartLight).normal(nml.x, nml.y, nml.z).next();
+            }
+        }
+
+        matrices.pop();
+    }
+
 
     private static final Pose3f tcp_originPose = new Pose3f(new Vector3f(), new Quaternionf());
     private static final Pose3f tcp_endPose = new Pose3f(new Vector3f(), new Quaternionf());
diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java b/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java
new file mode 100644
index 0000000..bd22460
--- /dev/null
+++ b/src/main/java/io/github/foundationgames/phonos/client/render/CableVBOContainer.java
@@ -0,0 +1,102 @@
+package io.github.foundationgames.phonos.client.render;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import io.github.foundationgames.phonos.client.model.BasicModel;
+import io.github.foundationgames.phonos.config.PhonosClientConfig;
+import io.github.foundationgames.phonos.world.sound.CableConnection;
+import io.github.foundationgames.phonos.world.sound.ConnectionCollection;
+import net.minecraft.client.gl.VertexBuffer;
+import net.minecraft.client.render.*;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CableVBOContainer {
+    public boolean rebuild;
+    public @Nullable VertexBuffer buffer = null;
+    private List<CableConnection> cachedCons = new ArrayList<>();
+
+    public void refresh(ConnectionCollection conns) {
+        List<CableConnection> connections = new ArrayList<>();
+        conns.forEach((i, conn) -> {
+            if (conn.isStatic()) {
+                connections.add(conn);
+            }
+        });
+
+        if (!cachedCons.equals(connections) || this.buffer == null) {
+            // Mark ourselves as needing re-rendering
+            rebuild = true;
+
+            // Close the existing buffer
+            if (buffer != null) {
+                buffer.close();
+            }
+
+            buffer = null;
+            cachedCons = connections;
+        }
+    }
+
+    public void render(MatrixStack matrices, VertexConsumer immediate, RenderLayer layer, BasicModel cableEndModel,
+                       ConnectionCollection conns, PhonosClientConfig config, World world, int overlay, float tickDelta) {
+        boolean rebuild = this.buffer == null || this.rebuild;
+
+        if (rebuild) {
+            // If we're re-rendering into the vertexbuffer, create a new VBO,
+            // grab the tessellator and start tessellating with our vertex format
+            var vbo = new VertexBuffer(VertexBuffer.Usage.STATIC);
+            BufferBuilder builder = Tessellator.getInstance().getBuffer();
+            builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL);
+
+            this.buffer = vbo;
+        }
+
+        // Render each connection point in immediate mode, and render cables into the given vertex buffer
+        conns.forEach((i, conn) ->
+                CableRenderer.renderConnection(this, config, world, conn, matrices, immediate, cableEndModel, overlay, tickDelta));
+
+        var vbo = this.buffer;
+
+        if (rebuild) {
+            // If we rerendered, upload the buffer to the GPU and mark ourselves as not dirty
+            vbo.bind();
+            vbo.upload(Tessellator.getInstance().getBuffer().end());
+            VertexBuffer.unbind();
+            this.rebuild = false;
+        }
+
+
+
+        // Set up the render state for this render phase (and texture)
+        layer.startDrawing();
+
+        // Grab fog and set to an extravagant value
+        // TODO: this is a total hack, but it's needed to convince the shader to not apply fog everywhere
+        float realEnd = RenderSystem.getShaderFogEnd();
+        RenderSystem.setShaderFogEnd(9999999);
+
+        matrices.push();
+        // Render the buffer, which contains all the cables connected to this
+        vbo.bind();
+        vbo.draw(matrices.peek().getPositionMatrix(), RenderSystem.getProjectionMatrix(), GameRenderer.getRenderTypeEntitySolidProgram());
+        VertexBuffer.unbind();
+
+        matrices.pop();
+
+        // Reset the render state
+        RenderSystem.setShaderFogEnd(realEnd);
+        layer.endDrawing();
+    }
+
+    public void close() {
+        if (buffer != null) {
+            buffer.close();
+        }
+
+        buffer = null;
+    }
+}
diff --git a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java
index ddbc6b4..aac0c7c 100644
--- a/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java
+++ b/src/main/java/io/github/foundationgames/phonos/client/render/block/CableOutputBlockEntityRenderer.java
@@ -1,22 +1,18 @@
 package io.github.foundationgames.phonos.client.render.block;
 
-import com.mojang.blaze3d.systems.RenderSystem;
 import io.github.foundationgames.phonos.Phonos;
 import io.github.foundationgames.phonos.PhonosClient;
 import io.github.foundationgames.phonos.client.model.BasicModel;
-import io.github.foundationgames.phonos.client.render.BlockEntityClientState;
 import io.github.foundationgames.phonos.client.render.CableRenderer;
+import io.github.foundationgames.phonos.client.render.CableVBOContainer;
 import io.github.foundationgames.phonos.config.PhonosClientConfig;
 import io.github.foundationgames.phonos.world.sound.block.OutputBlockEntity;
 import net.minecraft.block.entity.BlockEntity;
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gl.VertexBuffer;
-import net.minecraft.client.render.*;
+import net.minecraft.client.render.VertexConsumerProvider;
 import net.minecraft.client.render.block.entity.BlockEntityRenderer;
 import net.minecraft.client.render.block.entity.BlockEntityRendererFactory;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.math.Vec3d;
 
 public class CableOutputBlockEntityRenderer<E extends BlockEntity & OutputBlockEntity> implements BlockEntityRenderer<E> {
     public static final Identifier TEXTURE = Phonos.id("textures/entity/audio_cable.png");
@@ -28,59 +24,25 @@ public CableOutputBlockEntityRenderer(BlockEntityRendererFactory.Context ctx) {
 
     @Override
     public void render(E entity, float tickDelta, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) {
-        RenderLayer layer = cableEndModel.getLayer(TEXTURE);
-        var buffer = vertexConsumers.getBuffer(layer);
+        var renderLayer = cableEndModel.getLayer(TEXTURE);
+        var immediate = vertexConsumers.getBuffer(renderLayer);
         var config = PhonosClientConfig.get();
 
         matrices.push();
-        matrices.translate(-entity.getPos().getX(), -entity.getPos().getY(), -entity.getPos().getZ());
-
-        BlockEntityClientState clientState = entity.getClientState();
 
-        boolean rerender = clientState.buffer == null || clientState.dirty;
-
-        if (rerender) {
-            // If we're re-rendering into the vertexbuffer, create a new VBO, grab the tessellator and start tessellating with our vertex format
-            VertexBuffer vbo = new VertexBuffer(VertexBuffer.Usage.STATIC);
-            BufferBuilder builder = Tessellator.getInstance().getBuffer();
-            builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR_TEXTURE_OVERLAY_LIGHT_NORMAL);
-            clientState.buffer = vbo;
-        }
-
-        // Render each connection point in immediate mode, and render cables into the given vertex buffer
-        entity.getOutputs().forEach((i, conn) ->
-                CableRenderer.renderConnection(clientState, entity, config,  entity.getWorld(), conn, matrices, buffer, cableEndModel, overlay, tickDelta));
+        matrices.translate(-entity.getPos().getX(), -entity.getPos().getY(), -entity.getPos().getZ());
 
-        VertexBuffer vbo = clientState.buffer;
+        boolean vbos = config.cableVBOs;
+        entity.enforceVBOState(vbos);
 
-        if (rerender) {
-            // If we rerendered, upload the buffer to the GPU and mark ourselves as not dirty
-            vbo.bind();
-            vbo.upload(Tessellator.getInstance().getBuffer().end());
-            VertexBuffer.unbind();
-            clientState.dirty = false;
+        if (vbos) {
+            CableVBOContainer vboContainer = entity.getOrCreateVBOContainer();
+            vboContainer.render(matrices, immediate, renderLayer, cableEndModel, entity.getOutputs(), config, entity.getWorld(), overlay, tickDelta);
+        } else {
+            entity.getOutputs().forEach((i, conn) ->
+                    CableRenderer.renderConnection(null, config, entity.getWorld(), conn, matrices, immediate, cableEndModel, overlay, tickDelta));
         }
 
-        // Setup the render state for this render phase (and texture)
-        layer.startDrawing();
-
-        // Grab fog and set to an extravagant value
-        // TODO: this is a total hack, but it's needed to convince the shader to not apply fog everywhere
-        float realEnd = RenderSystem.getShaderFogEnd();
-        RenderSystem.setShaderFogEnd(9999999);
-
-        matrices.push();
-        // Render the buffer, which contains all the cables connected to this
-            vbo.bind();
-            vbo.draw(matrices.peek().getPositionMatrix(), RenderSystem.getProjectionMatrix(), GameRenderer.getRenderTypeEntitySolidProgram());
-            VertexBuffer.unbind();
-
-        matrices.pop();
-
-        // Reset the render state
-        RenderSystem.setShaderFogEnd(realEnd);
-        layer.endDrawing();
-
         matrices.pop();
     }
 
diff --git a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java
index c24d812..2a0f78b 100644
--- a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java
+++ b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfig.java
@@ -16,6 +16,7 @@ public class PhonosClientConfig {
     public double streamVolume = 1;
 
     public boolean cableCulling = true;
+    public boolean cableVBOs = false;
     public boolean cableLODs = true;
     public double cableLODNearDetail = 1;
     public double cableLODFarDetail = 0.25;
diff --git a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java
index 39ce7aa..77432f8 100644
--- a/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java
+++ b/src/main/java/io/github/foundationgames/phonos/config/PhonosClientConfigScreen.java
@@ -46,6 +46,7 @@ public static PhonosClientConfigScreen create(PhonosClientConfig config, Screen
         screen.addPercentage("phonosMasterVolume", val -> copy.phonosMasterVolume = val, copy.phonosMasterVolume);
         screen.addPercentage("streamVolume", val -> copy.streamVolume = val, copy.streamVolume);
         screen.addBoolean("cableCulling", val -> copy.cableCulling = val, copy.cableCulling);
+        screen.addBoolean("cableVBOs", val -> copy.cableVBOs = val, copy.cableVBOs);
         screen.addBoolean("cableLODs", val -> copy.cableLODs = val, copy.cableLODs);
         screen.addPercentage("cableLODNearDetail", val -> copy.cableLODNearDetail = val, copy.cableLODNearDetail);
         screen.addPercentage("cableLODFarDetail", val -> copy.cableLODFarDetail = val, copy.cableLODFarDetail);
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java b/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java
index f3b69b3..5d69b7c 100644
--- a/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/CableConnection.java
@@ -29,6 +29,10 @@ public boolean shouldRemove(World world) {
         return false; //start.getPos(world).squaredDistanceTo(end.getPos(world)) > PhonosUtil.maxSquaredConnectionDistance(world);
     }
 
+    public boolean isStatic() {
+        return start.isStatic() && end.isStatic();
+    }
+
     public void writeNbt(NbtCompound nbt) {
         if (color != null) {
             nbt.putString("color", color.getName());
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java b/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java
index 0846149..c0546d7 100644
--- a/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/CablePlugPoint.java
@@ -29,4 +29,10 @@ default Vec3d calculatePos(World world, double extend) {
     void writePlugPose(World world, float delta, Pose3f out);
 
     void writeOriginPose(World world, float delta, Pose3f out);
+
+    boolean isStatic();
+
+    boolean equals(Object other);
+
+    int hashCode();
 }
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java b/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java
new file mode 100644
index 0000000..eb38602
--- /dev/null
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/ConnectionCollection.java
@@ -0,0 +1,7 @@
+package io.github.foundationgames.phonos.world.sound;
+
+import java.util.function.BiConsumer;
+
+public interface ConnectionCollection {
+    void forEach(BiConsumer<Integer, CableConnection> action);
+}
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java
index 88f894d..50c5057 100644
--- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockConnectionLayout.java
@@ -120,6 +120,11 @@ public void writeOriginPose(World world, float delta, Pose3f out) {
             out.rotation().set(RotationAxis.POSITIVE_Y.rotation(0));
         }
 
+        @Override
+        public boolean isStatic() {
+            return true;
+        }
+
         @Override
         public boolean canPlugExist(World world) {
             if (world.isPosLoaded(this.blockPos.getX(), this.blockPos.getZ())) {
@@ -239,6 +244,11 @@ public void writeOriginPose(World world, float delta, Pose3f out) {
             out.rotation().set(RotationAxis.POSITIVE_Y.rotation(0));
         }
 
+        @Override
+        public boolean isStatic() {
+            return true;
+        }
+
         @Override
         public boolean equals(Object o) {
             if (this == o) return true;
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java
index 297b9c0..712292f 100644
--- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/BlockEntityOutputs.java
@@ -1,6 +1,7 @@
 package io.github.foundationgames.phonos.world.sound.block;
 
 import io.github.foundationgames.phonos.world.sound.CableConnection;
+import io.github.foundationgames.phonos.world.sound.ConnectionCollection;
 import io.github.foundationgames.phonos.world.sound.InputPlugPoint;
 import net.minecraft.block.entity.BlockEntity;
 import net.minecraft.entity.ItemEntity;
@@ -16,7 +17,7 @@
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
-public class BlockEntityOutputs {
+public class BlockEntityOutputs implements ConnectionCollection {
     private final boolean[] skip; // Will not be purged on first tick of existing
     protected final CableConnection[] connections;
     protected final BlockConnectionLayout connectionLayout;
@@ -86,6 +87,7 @@ public int getOutputCount() {
         return count;
     }
 
+    @Override
     public void forEach(BiConsumer<Integer, CableConnection> action) {
         for (int i = 0; i < connections.length; i++) {
             if (connections[i] != null) {
diff --git a/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java b/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java
index 630b0fd..4245d92 100644
--- a/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java
+++ b/src/main/java/io/github/foundationgames/phonos/world/sound/block/OutputBlockEntity.java
@@ -1,6 +1,6 @@
 package io.github.foundationgames.phonos.world.sound.block;
 
-import io.github.foundationgames.phonos.client.render.BlockEntityClientState;
+import io.github.foundationgames.phonos.client.render.CableVBOContainer;
 import io.github.foundationgames.phonos.sound.emitter.SoundEmitter;
 import io.github.foundationgames.phonos.world.sound.InputPlugPoint;
 import net.minecraft.item.ItemStack;
@@ -19,7 +19,9 @@ public interface OutputBlockEntity extends SoundEmitter {
 
     BlockEntityOutputs getOutputs();
 
-    BlockEntityClientState getClientState();
+    void enforceVBOState(boolean enabled);
+
+    CableVBOContainer getOrCreateVBOContainer();
 
     default Direction getRotation() {
         return Direction.NORTH;
diff --git a/src/main/resources/assets/phonos/lang/en_us.json b/src/main/resources/assets/phonos/lang/en_us.json
index efecc77..e369ad7 100644
--- a/src/main/resources/assets/phonos/lang/en_us.json
+++ b/src/main/resources/assets/phonos/lang/en_us.json
@@ -66,7 +66,8 @@
     "text.config.phonos.option.phonosMasterVolume": "Loudspeaker Master Volume",
     "text.config.phonos.option.streamVolume": "Satellite Audio Volume",
     "text.config.phonos.option.cableCulling": "Cull Audio Cables not on-screen",
-    "text.config.phonos.option.cableLODs": "Use Detail Levels for Audio Cables",
+    "text.config.phonos.option.cableVBOs": "Render Audio Cables with VBOs",
+    "text.config.phonos.option.cableLODs": "Render Audio Cables with Detail Levels",
     "text.config.phonos.option.cableLODNearDetail": "Highest Audio Cable Detail Level",
     "text.config.phonos.option.cableLODFarDetail": "Lowest Audio Cable Detail Level",