Performance Optimization
Performance Optimization
Section titled “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.
GPU Profiling and Bottleneck Analysis
Section titled “GPU Profiling and Bottleneck Analysis”Understanding Rendering Performance
Section titled “Understanding Rendering Performance”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 setuppublic 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; }}Identifying Common Bottlenecks
Section titled “Identifying Common Bottlenecks”The most common rendering bottlenecks in Minecraft are:
- Draw Call Overhead - Too many individual drawing operations
- State Changes - Frequent changes to render state (shaders, textures)
- Memory Bandwidth - Large amounts of data transferred to GPU
- 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]
Distance-Based Prioritization
Section titled “Distance-Based Prioritization”Minecraft uses distance-based task prioritization to optimize rendering performance:
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); }}Advanced Batching Techniques
Section titled “Advanced Batching Techniques”State-Based Batching
Section titled “State-Based Batching”The most effective optimization is grouping similar objects together to minimize state changes:
// Example: Advanced batching systempublic 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(); }}Dynamic Buffer Resizing
Section titled “Dynamic Buffer Resizing”Optimize buffer usage with intelligent resizing strategies:
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;}Frustum Culling Optimization
Section titled “Frustum Culling Optimization”Implement efficient frustum culling to avoid rendering off-screen objects:
// Example: Advanced frustum cullingpublic 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()); }}Frame Rate Optimization
Section titled “Frame Rate Optimization”Level of Detail (LOD) Systems
Section titled “Level of Detail (LOD) Systems”Implement LOD to reduce rendering overhead for distant objects:
// Example: LOD system for entitiespublic 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; } }}Adaptive Quality Settings
Section titled “Adaptive Quality Settings”Adjust rendering quality based on performance:
// Example: Adaptive quality systempublic 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; } }}Memory Bandwidth Optimization
Section titled “Memory Bandwidth Optimization”Buffer Arena Management
Section titled “Buffer Arena Management”Use buffer arenas to minimize memory fragmentation and improve performance:
// Example: Buffer arena for efficient memory managementpublic 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(); } }}Texture Atlas Optimization
Section titled “Texture Atlas Optimization”Optimize texture usage to minimize texture binding changes:
// Example: Texture atlas managementpublic 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()); } }}Performance Monitoring
Section titled “Performance Monitoring”Real-time Performance Metrics
Section titled “Real-time Performance Metrics”Implement real-time performance monitoring for debugging:
// Example: Performance metrics collectionpublic 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)); }}Performance Profiling Tools
Section titled “Performance Profiling Tools”Create profiling tools to identify optimization opportunities:
// Example: Advanced profilerpublic 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; } }}Common Optimization Patterns
Section titled “Common Optimization Patterns”Precomputed Geometry
Section titled “Precomputed Geometry”Precompute static geometry to reduce per-frame calculations:
// Example: Precomputed mesh systempublic 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); }}Lazy Evaluation
Section titled “Lazy Evaluation”Use lazy evaluation to defer expensive calculations until needed:
// Example: Lazy computation systempublic 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 calculationspublic 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(); }}Best Practices
Section titled “Best Practices”Performance Guidelines
Section titled “Performance Guidelines”- Profile First: Always measure before optimizing
- Batch Smartly: Group objects by render state, not just by type
- Minimize State Changes: Reduce shader, texture, and buffer bindings
- Use LOD: Implement distance-based detail reduction
- Precompute When Possible: Cache expensive calculations
- Monitor Memory: Watch for memory leaks and excessive allocations
Common Pitfalls to Avoid
Section titled “Common Pitfalls to Avoid”// ❌ WRONG - Excessive state changespublic 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 texturepublic 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(); } }}Next Steps
Section titled “Next Steps”- Multi-threaded Rendering - Advanced thread safety and async patterns
- Memory Management - Advanced buffer and resource management
- Advanced Shader Techniques - Optimized shader development
Common Issues
Section titled “Common Issues”- Over-optimization: Don’t optimize code that isn’t actually a bottleneck
- Premature Optimization: Profile first, then optimize based on data
- Micro-optimizations: Focus on architectural improvements first
- Ignoring Frame Time: Optimize for consistent frame rates, not just maximum FPS
- Memory vs. Speed: Balance memory usage against rendering performance