Skip to content

Performance Optimization

This guide covers advanced performance optimization techniques for rendering in Minecraft 26.1, focusing on GPU profiling, bottleneck analysis, and optimization strategies used in the vanilla codebase.

The first step in optimization is understanding where performance bottlenecks occur. Minecraft 26.1 provides several tools and patterns for profiling rendering performance:

// Example: Basic timing setup
public class PerformanceProfiler {
private long lastFrameTime;
private float frameTime;
public void beginFrame() {
lastFrameTime = Blaze3D.getTime();
}
public void endFrame() {
frameTime = (Blaze3D.getTime() - lastFrameTime) / 1_000_000.0f;
}
public float getFrameTime() {
return frameTime;
}
}

The most common rendering bottlenecks in Minecraft are:

  1. Draw Call Overhead - Too many individual drawing operations
  2. State Changes - Frequent changes to render state (shaders, textures)
  3. Memory Bandwidth - Large amounts of data transferred to GPU
  4. Fragment Shader Complexity - Expensive pixel processing
graph TD
    A[Rendering Performance Issue] --> B{Identify Bottleneck}
    B -->|High CPU Time| C[Too Many Draw Calls]
    B -->|High GPU Time| D[Complex Shaders]
    B -->|Memory Pressure| E[Large Buffers]
    B -->|State Changes| F[Frequent Binding]
    
    C --> G[Batch Similar Objects]
    D --> H[Simplify Shaders]
    E --> I[Optimize Buffers]
    F --> J[Reduce State Changes]

Minecraft uses distance-based task prioritization to optimize rendering performance:

net.minecraft.client.renderer.chunk.CompileTaskDynamicQueue
public synchronized SectionRenderDispatcher.RenderSection.CompileTask poll(final Vec3 cameraPos) {
SectionRenderDispatcher.RenderSection.CompileTask bestInitialCompileTask = null;
SectionRenderDispatcher.RenderSection.CompileTask bestRecompileTask = null;
double bestInitialCompileDistance = Double.MAX_VALUE;
double bestRecompileDistance = Double.MAX_VALUE;
// Find closest tasks for both initial and recompile
for (int i = 0; i < this.size(); ++i) {
SectionRenderDispatcher.RenderSection.CompileTask task = this.getTask(i);
double distance = task.getDistanceFromCamera(cameraPos);
if (task.isInitialCompile()) {
if (distance < bestInitialCompileDistance) {
bestInitialCompileDistance = distance;
bestInitialCompileTask = task;
bestInitialCompileTaskIndex = i;
}
} else {
if (distance < bestRecompileDistance) {
bestRecompileDistance = distance;
bestRecompileTask = task;
bestRecompileTaskIndex = i;
}
}
}
// Prioritize based on distance and quota
if (!hasRecompileTask || hasInitialCompileTask &&
(this.recompileQuota <= 0 || !(bestRecompileDistance < bestInitialCompileDistance))) {
this.recompileQuota = 2;
return this.removeTaskByIndex(bestInitialCompileTaskIndex);
} else {
this.recompileQuota--;
return this.removeTaskByIndex(bestRecompileTaskIndex);
}
}

The most effective optimization is grouping similar objects together to minimize state changes:

// Example: Advanced batching system
public class AdvancedBatchRenderer {
private final Map<RenderState, List<RenderableObject>> renderBatches = new HashMap<>();
public void addObject(RenderableObject object) {
RenderState state = object.getRenderState();
renderBatches.computeIfAbsent(state, k -> new ArrayList<>()).add(object);
}
public void renderAll(PoseStack poseStack) {
RenderSystem.assertOnRenderThread();
for (Map.Entry<RenderState, List<RenderableObject>> entry : renderBatches.entrySet()) {
RenderState state = entry.getKey();
List<RenderableObject> batch = entry.getValue();
// Apply render state once for entire batch
state.apply();
// Render all objects in batch
for (RenderableObject object : batch) {
object.render(poseStack);
}
}
renderBatches.clear();
}
}

Optimize buffer usage with intelligent resizing strategies:

net.minecraft.client.renderer.DynamicUniformStorage
private void resizeBuffers(final int newCapacity) {
this.capacity = newCapacity;
this.oldBuffers.add(this.ringBuffer);
this.ringBuffer = new MappableRingBuffer(() -> this.label + " x" + this.blockSize,
130, this.blockSize * newCapacity);
}
private int writeUniforms(final int firstIndex, final int count, final byte[] uniforms) {
int bytesWritten = 0;
while (bytesWritten < count) {
int availableBytes = this.ringBuffer.availableBytes();
if (availableBytes == 0) {
this.increaseRingBuffer(this.blockSize);
continue;
}
int bytesToWrite = Math.min(count - bytesWritten, availableBytes);
this.ringBuffer.writeBytes(uniforms, bytesWritten, bytesToWrite);
bytesWritten += bytesToWrite;
}
return bytesWritten;
}

Implement efficient frustum culling to avoid rendering off-screen objects:

// Example: Advanced frustum culling
public class FrustumCuller {
private final Plane[] frustumPlanes = new Plane[6];
public void updateFrustum(Matrix4f projectionViewMatrix) {
// Extract frustum planes from projection-view matrix
for (int i = 0; i < 6; i++) {
frustumPlanes[i] = extractPlane(projectionViewMatrix, i);
}
}
public boolean isInFrustum(BoundingBox box) {
for (Plane plane : frustumPlanes) {
if (!box.isOnOrForwardPlane(plane)) {
return false;
}
}
return true;
}
public List<RenderableObject> cullObjects(List<RenderableObject> objects) {
return objects.stream()
.filter(obj -> isInFrustum(obj.getBoundingBox()))
.collect(Collectors.toList());
}
}

Implement LOD to reduce rendering overhead for distant objects:

// Example: LOD system for entities
public class EntityLODSystem {
private static final float[] LOD_DISTANCES = {0.0f, 16.0f, 32.0f, 64.0f};
private static final int[] LOD_LEVELS = {4, 3, 2, 1};
public int getLODLevel(Entity entity, Vec3 cameraPos) {
double distance = entity.position().distanceTo(cameraPos);
for (int i = 0; i < LOD_DISTANCES.length - 1; i++) {
if (distance >= LOD_DISTANCES[i] && distance < LOD_DISTANCES[i + 1]) {
return LOD_LEVELS[i];
}
}
return LOD_LEVELS[LOD_LEVELS.length - 1];
}
public void renderWithLOD(Entity entity, int lodLevel, PoseStack poseStack) {
switch (lodLevel) {
case 4: // Ultra quality - full detail
renderFullDetail(entity, poseStack);
break;
case 3: // High quality - reduced animation
renderHighDetail(entity, poseStack);
break;
case 2: // Medium quality - simplified model
renderMediumDetail(entity, poseStack);
break;
case 1: // Low quality - bounding box only
renderLowDetail(entity, poseStack);
break;
}
}
}

Adjust rendering quality based on performance:

// Example: Adaptive quality system
public class AdaptiveQualityManager {
private float targetFrameTime = 16.67f; // 60 FPS target
private float currentFrameTime;
private QualityLevel currentQuality = QualityLevel.HIGH;
public enum QualityLevel {
ULTRA, HIGH, MEDIUM, LOW
}
public void updateQuality(float frameTime) {
this.currentFrameTime = frameTime;
if (frameTime > targetFrameTime * 1.5f) {
// Performance is poor, reduce quality
if (currentQuality != QualityLevel.LOW) {
currentQuality = QualityLevel.values()[currentQuality.ordinal() + 1];
applyQualitySettings(currentQuality);
}
} else if (frameTime < targetFrameTime * 0.8f) {
// Performance is good, increase quality
if (currentQuality != QualityLevel.ULTRA) {
currentQuality = QualityLevel.values()[currentQuality.ordinal() - 1];
applyQualitySettings(currentQuality);
}
}
}
private void applyQualitySettings(QualityLevel level) {
switch (level) {
case ULTRA:
RenderSystem.enableCull();
setMaxRenderDistance(32);
enableShadows(true);
break;
case HIGH:
RenderSystem.enableCull();
setMaxRenderDistance(24);
enableShadows(true);
break;
case MEDIUM:
RenderSystem.enableCull();
setMaxRenderDistance(16);
enableShadows(false);
break;
case LOW:
RenderSystem.disableCull();
setMaxRenderDistance(8);
enableShadows(false);
break;
}
}
}

Use buffer arenas to minimize memory fragmentation and improve performance:

// Example: Buffer arena for efficient memory management
public class BufferArena {
private final List<ByteBufferBuilder> arenas = new ArrayList<>();
private final int arenaSize;
private int currentArena = 0;
public BufferArena(int arenaSize, int arenaCount) {
this.arenaSize = arenaSize;
for (int i = 0; i < arenaCount; i++) {
arenas.add(new ByteBufferBuilder(arenaSize));
}
}
public ByteBufferBuilder getNextArena() {
ByteBufferBuilder arena = arenas.get(currentArena);
if (arena.remaining() < arenaSize / 4) {
currentArena = (currentArena + 1) % arenas.size();
arena = arenas.get(currentArena);
arena.clear();
}
return arena;
}
public void reset() {
currentArena = 0;
for (ByteBufferBuilder arena : arenas) {
arena.clear();
}
}
}

Optimize texture usage to minimize texture binding changes:

// Example: Texture atlas management
public class TextureAtlasOptimizer {
private final Map<String, TextureAtlas> atlases = new HashMap<>();
private final Map<String, String> textureToAtlas = new HashMap<>();
public void organizeTextures(List<String> textures) {
// Group textures by type and usage frequency
Map<String, List<String>> groupedTextures = groupTexturesByType(textures);
for (Map.Entry<String, List<String>> entry : groupedTextures.entrySet()) {
String type = entry.getKey();
List<String> typeTextures = entry.getValue();
// Create optimized atlas for this texture type
TextureAtlas atlas = createOptimizedAtlas(typeTextures);
atlases.put(type, atlas);
// Map individual textures to their atlas
for (String texture : typeTextures) {
textureToAtlas.put(texture, type);
}
}
}
public void bindAtlasForTexture(String texture) {
String atlasType = textureToAtlas.get(texture);
if (atlasType != null) {
TextureAtlas atlas = atlases.get(atlasType);
RenderSystem.setShaderTexture(0, atlas.getTextureId());
}
}
}

Implement real-time performance monitoring for debugging:

// Example: Performance metrics collection
public class PerformanceMetrics {
private final Map<String, Long> renderTimes = new HashMap<>();
private final Map<String, Integer> drawCalls = new HashMap<>();
private final Map<String, Integer> stateChanges = new HashMap<>();
public void beginTimer(String operation) {
renderTimes.put(operation, Blaze3D.getTime());
}
public void endTimer(String operation) {
long startTime = renderTimes.get(operation);
long endTime = Blaze3D.getTime();
renderTimes.put(operation, endTime - startTime);
}
public void recordDrawCall(String renderType) {
drawCalls.merge(renderType, 1, Integer::sum);
}
public void recordStateChange(String stateType) {
stateChanges.merge(stateType, 1, Integer::sum);
}
public void printMetrics() {
System.out.println("=== Performance Metrics ===");
System.out.println("\nRender Times (ms):");
renderTimes.forEach((op, time) ->
System.out.printf(" %s: %.2f%n", op, time / 1_000_000.0));
System.out.println("\nDraw Calls:");
drawCalls.forEach((type, count) ->
System.out.printf(" %s: %d%n", type, count));
System.out.println("\nState Changes:");
stateChanges.forEach((type, count) ->
System.out.printf(" %s: %d%n", type, count));
}
}

Create profiling tools to identify optimization opportunities:

// Example: Advanced profiler
public class RenderProfiler {
private final Stack<TimingStack> timingStack = new Stack<>();
private final Map<String, ProfilingData> profilingData = new HashMap<>();
public void push(String operation) {
TimingStack stack = new TimingStack(operation, Blaze3D.getTime());
timingStack.push(stack);
}
public void pop() {
if (!timingStack.isEmpty()) {
TimingStack stack = timingStack.pop();
long endTime = Blaze3D.getTime();
profilingData.computeIfAbsent(stack.operation, k -> new ProfilingData())
.addSample(endTime - stack.startTime);
}
}
public List<String> getBottlenecks() {
return profilingData.entrySet().stream()
.sorted((a, b) -> Long.compare(b.getValue().getAverageTime(),
a.getValue().getAverageTime()))
.limit(5)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
private static class TimingStack {
final String operation;
final long startTime;
TimingStack(String operation, long startTime) {
this.operation = operation;
this.startTime = startTime;
}
}
private static class ProfilingData {
private final List<Long> samples = new ArrayList<>();
private long totalSamples = 0;
void addSample(long sample) {
samples.add(sample);
totalSamples++;
}
long getAverageTime() {
return samples.stream().mapToLong(Long::longValue).sum() / totalSamples;
}
}
}

Precompute static geometry to reduce per-frame calculations:

// Example: Precomputed mesh system
public class PrecomputedMesh {
private final Mesh mesh;
private final BoundingBox boundingBox;
public PrecomputedMesh(List<Vector3f> vertices, List<Integer> indices) {
this.mesh = createMesh(vertices, indices);
this.boundingBox = calculateBoundingBox(vertices);
}
public void render(PoseStack poseStack) {
// Simply bind and render precomputed geometry
mesh.bind();
mesh.render();
}
public boolean isVisible(FrustumCuller frustumCuller, PoseStack poseStack) {
BoundingBox transformedBox = boundingBox.transform(poseStack.last().pose());
return frustumCuller.isInFrustum(transformedBox);
}
}

Use lazy evaluation to defer expensive calculations until needed:

// Example: Lazy computation system
public class Lazy<T> {
private final Supplier<T> supplier;
private T value = null;
private boolean computed = false;
public Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
if (!computed) {
value = supplier.get();
computed = true;
}
return value;
}
public void invalidate() {
computed = false;
value = null;
}
}
// Usage example for expensive calculations
public class EntityRenderer {
private final Lazy<ComplexModel> cachedModel = new Lazy<>(() -> {
// Expensive model computation
return computeExpensiveModel();
});
public void render(Entity entity) {
ComplexModel model = cachedModel.get();
model.render();
}
}
  1. Profile First: Always measure before optimizing
  2. Batch Smartly: Group objects by render state, not just by type
  3. Minimize State Changes: Reduce shader, texture, and buffer bindings
  4. Use LOD: Implement distance-based detail reduction
  5. Precompute When Possible: Cache expensive calculations
  6. Monitor Memory: Watch for memory leaks and excessive allocations
// ❌ WRONG - Excessive state changes
public void renderInefficiently(List<Entity> entities) {
for (Entity entity : entities) {
RenderSystem.setShaderTexture(0, entity.getTexture());
RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
entity.render();
}
}
// ✅ CORRECT - Batch by texture
public void renderEfficiently(List<Entity> entities) {
Map<ResourceLocation, List<Entity>> grouped = entities.stream()
.collect(Collectors.groupingBy(Entity::getTexture));
for (Map.Entry<ResourceLocation, List<Entity>> entry : grouped.entrySet()) {
RenderSystem.setShaderTexture(0, entry.getKey());
RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f);
for (Entity entity : entry.getValue()) {
entity.render();
}
}
}
  1. Over-optimization: Don’t optimize code that isn’t actually a bottleneck
  2. Premature Optimization: Profile first, then optimize based on data
  3. Micro-optimizations: Focus on architectural improvements first
  4. Ignoring Frame Time: Optimize for consistent frame rates, not just maximum FPS
  5. Memory vs. Speed: Balance memory usage against rendering performance