Skip to content

Multi-threaded Rendering

This guide covers advanced multi-threaded rendering patterns in Minecraft 26.1, including asynchronous compilation, thread safety enforcement, and GPU task synchronization patterns.

Minecraft 26.1 strictly separates rendering operations from game logic through thread enforcement:

com.mojang.blaze3d.systems.RenderSystem
public final class RenderSystem {
private static final Thread MAIN_RENDER_THREAD = Thread.currentThread();
public static boolean isOnRenderThread() {
return Thread.currentThread() == MAIN_RENDER_THREAD;
}
public static void assertOnRenderThread() {
if (!isOnRenderThread()) {
throw new IllegalStateException("Not on render thread");
}
}
public static void ensureMainThread(Runnable runnable) {
if (isOnRenderThread()) {
runnable.run();
} else {
executeOnMainThread(runnable);
}
}
}
graph LR
    A[Game Thread] --> B[Logic Processing]
    A --> C[Network I/O]
    A --> D[World Updates]
    
    E[Render Thread] --> F[GPU Operations]
    E --> G[Shader Compilation]
    E --> H[Draw Calls]
    
    I[Worker Threads] --> J[Chunk Compilation]
    I --> K[Resource Loading]
    I --> L[Async Computations]
    
    A -.->|Synchronization| E
    I -.->|Task Queue| E

Use proper synchronization mechanisms for thread communication:

// Example: Thread-safe render data container
public class ThreadSafeRenderData {
private volatile RenderData data = new RenderData();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// Game thread writes data
public void updateData(RenderData newData) {
lock.writeLock().lock();
try {
this.data = newData.copy();
} finally {
lock.writeLock().unlock();
}
}
// Render thread reads data
public RenderData getData() {
lock.readLock().lock();
try {
return data;
} finally {
lock.readLock().unlock();
}
}
}

Minecraft’s chunk compilation system demonstrates advanced asynchronous patterns:

net.minecraft.client.renderer.chunk.SectionRenderDispatcher
public class SectionRenderDispatcher {
private final Executor executor;
private final ConsecutiveExecutor consecutiveExecutor;
private final CompileTaskDynamicQueue taskQueue = new CompileTaskDynamicQueue();
public CompletableFuture<CompiledChunk> compileSectionAsync(RenderSection section) {
RenderSection.CompileTask task = section.createCompileTask();
return CompletableFuture.supplyAsync(() -> {
return task.doTask(buffer);
}, this.executor.forName(task.name()))
.thenCompose(f -> f)
.whenComplete((result, throwable) -> {
if (throwable != null) {
LOGGER.error("Failed to compile chunk section", throwable);
return;
}
// Schedule upload on render thread
this.consecutiveExecutor.schedule(() -> {
uploadCompiledChunk(section, result);
});
});
}
}

Implement efficient task queuing with prioritization:

// Example: Advanced task queue system
public class PrioritizedRenderTaskQueue {
private final PriorityQueue<RenderTask> highPriorityQueue =
new PriorityQueue<>(Comparator.comparing(RenderTask::getPriority).reversed());
private final Queue<RenderTask> normalQueue = new ArrayDeque<>();
private final AtomicInteger highPriorityQuota = new AtomicInteger(5);
public void addTask(RenderTask task) {
synchronized (this) {
if (task.isHighPriority()) {
highPriorityQueue.offer(task);
} else {
normalQueue.offer(task);
}
}
}
public RenderTask pollTask() {
synchronized (this) {
// Balance high and normal priority tasks
if (!highPriorityQueue.isEmpty() && highPriorityQuota.get() > 0) {
highPriorityQuota.decrementAndGet();
return highPriorityQueue.poll();
}
if (!normalQueue.isEmpty()) {
highPriorityQuota.set(5); // Reset quota
return normalQueue.poll();
}
return highPriorityQueue.poll();
}
}
}

Ensure task ordering while maintaining parallelism:

// Excerpt: net.minecraft.client.renderer.chunk.SectionRenderDispatcher$ConsecutiveExecutor
public class ConsecutiveExecutor {
private final Queue<Runnable> taskQueue = new ArrayDeque<>();
private volatile boolean isExecuting = false;
public void schedule(Runnable task) {
synchronized (taskQueue) {
taskQueue.add(task);
if (!isExecuting) {
isExecuting = true;
executeNext();
}
}
}
private void executeNext() {
Runnable task;
synchronized (taskQueue) {
task = taskQueue.poll();
if (task == null) {
isExecuting = false;
return;
}
}
// Execute task on appropriate thread
if (RenderSystem.isOnRenderThread()) {
task.run();
executeNext(); // Continue execution
} else {
RenderSystem.executeOnMainThread(() -> {
task.run();
executeNext();
});
}
}
}

Safely create resources across thread boundaries:

// Example: Thread-safe resource manager
public class ThreadSafeResourceManager {
private final Map<ResourceKey, CompletableFuture<GpuResource>> pendingResources =
new ConcurrentHashMap<>();
private final Map<ResourceKey, GpuResource> resources = new ConcurrentHashMap<>();
public CompletableFuture<GpuResource> getResourceAsync(ResourceKey key) {
return pendingResources.computeIfAbsent(key, k -> {
return CompletableFuture.supplyAsync(() -> {
// Load resource on worker thread
ResourceData data = loadResourceData(k);
return data;
}).thenCompose(data -> {
// Create GPU resource on render thread
return CompletableFuture.supplyAsync(() -> {
RenderSystem.assertOnRenderThread();
return createGpuResource(data);
}, renderThreadExecutor);
}).whenComplete((resource, throwable) -> {
if (throwable == null) {
resources.put(k, resource);
}
pendingResources.remove(k);
});
});
}
public GpuResource getResource(ResourceKey key) {
return resources.get(key);
}
}

Use GPU fences for task synchronization:

com.mojang.blaze3d.systems.RenderSystem
public final class RenderSystem {
private static final Deque<GpuAsyncTask> PENDING_FENCES = new ArrayDeque<>();
public static void queueFencedTask(final Runnable task) {
PENDING_FENCES.addLast(new RenderSystem.GpuAsyncTask(task,
getDevice().createCommandEncoder().createFence()));
}
private static class GpuAsyncTask {
private final Runnable task;
private final GpuFence fence;
GpuAsyncTask(final Runnable task, final GpuFence fence) {
this.task = task;
this.fence = fence;
}
public boolean process() {
return fence.poll() && executeTask();
}
private boolean executeTask() {
try {
task.run();
return true;
} catch (Throwable throwable) {
LOGGER.error("Error executing GPU task", throwable);
return false;
}
}
}
public static void processPendingFences() {
assertOnRenderThread();
Iterator<GpuAsyncTask> iterator = PENDING_FENCES.iterator();
while (iterator.hasNext()) {
GpuAsyncTask task = iterator.next();
if (task.process()) {
iterator.remove();
}
}
}
}

Implement reactive rendering with data binding:

// Example: Reactive rendering system
public class ReactiveRenderSystem {
private final Map<ObservableValue, List<RenderTask>> subscriptions = new ConcurrentHashMap<>();
private final ExecutorService workerExecutor = ForkJoinPool.commonPool();
public <T> void bind(ObservableValue<T> value, Consumer<T> renderCallback) {
subscriptions.computeIfAbsent(value, k -> new ArrayList<>())
.add(new RenderTask(k, renderCallback));
value.addListener((observable, oldValue, newValue) -> {
workerExecutor.submit(() -> {
processValueChange(value, newValue);
});
});
}
private <T> void processValueChange(ObservableValue<T> value, T newValue) {
List<RenderTask> tasks = subscriptions.get(value);
if (tasks != null) {
for (RenderTask task : tasks) {
// Process on worker thread
Object result = task.processValue(newValue);
// Update render thread
RenderSystem.executeOnMainThread(() -> {
task.renderCallback.accept(result);
});
}
}
}
private static class RenderTask {
private final ObservableValue source;
private final Consumer renderCallback;
RenderTask(ObservableValue source, Consumer renderCallback) {
this.source = source;
this.renderCallback = renderCallback;
}
Object processValue(Object value) {
// Process value on worker thread
return transformValue(value);
}
}
}

Process mesh data in parallel for performance:

// Example: Parallel mesh processing
public class ParallelMeshProcessor {
private final ExecutorService executor = ForkJoinPool.commonPool();
public CompletableFuture<ProcessedMesh> processMeshAsync(Mesh mesh) {
// Split mesh into chunks for parallel processing
List<MeshChunk> chunks = mesh.splitIntoChunks(Runtime.getRuntime().availableProcessors());
// Process chunks in parallel
List<CompletableFuture<ProcessedChunk>> futures = chunks.stream()
.map(chunk -> CompletableFuture.supplyAsync(() -> processChunk(chunk), executor))
.collect(Collectors.toList());
// Combine results
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> combineProcessedChunks(futures, chunks));
}
private ProcessedChunk processChunk(MeshChunk chunk) {
// Complex processing on worker thread
ProcessedChunk processed = new ProcessedChunk();
// Generate vertices
for (Triangle triangle : chunk.getTriangles()) {
Vertex[] vertices = generateVertices(triangle);
processed.addVertices(vertices);
}
// Calculate normals
calculateNormals(processed);
// Generate texture coordinates
generateTextureCoordinates(processed);
return processed;
}
private ProcessedMesh combineProcessedChunks(
List<CompletableFuture<ProcessedChunk>> futures,
List<MeshChunk> originalChunks) {
ProcessedMesh result = new ProcessedMesh();
for (int i = 0; i < futures.size(); i++) {
try {
ProcessedChunk chunk = futures.get(i).get();
result.addChunk(chunk);
} catch (InterruptedException | ExecutionException e) {
LOGGER.error("Failed to process mesh chunk", e);
}
}
return result;
}
}

Use lock-free structures for high-performance scenarios:

// Example: Lock-free render data queue
public class LockFreeRenderQueue<T> {
private final AtomicReference<Node<T>> head = new AtomicReference<>();
private final AtomicReference<Node<T>> tail = new AtomicReference<>();
public void offer(T item) {
Node<T> newNode = new Node<>(item);
Node<T> currentTail = tail.getAndSet(newNode);
if (currentTail != null) {
currentTail.next = newNode;
} else {
// Queue was empty
head.compareAndSet(null, newNode);
}
}
public T poll() {
Node<T> currentHead = head.get();
if (currentHead == null) {
return null;
}
Node<T> next = currentHead.next;
if (!head.compareAndSet(currentHead, next)) {
return null; // Lost race
}
if (next == null) {
tail.compareAndSet(currentHead, null);
}
return currentHead.item;
}
private static class Node<T> {
volatile T item;
volatile Node<T> next;
Node(T item) {
this.item = item;
}
}
}

Optimize memory visibility and synchronization:

// Example: Optimized render state synchronization
public class OptimizedRenderState {
private volatile long stateVersion = 0;
private final AtomicReference<RenderState> currentState = new AtomicReference<>();
private final ThreadLocal<RenderState> cachedState = new ThreadLocal<>();
public void updateState(RenderState newState) {
currentState.set(newState);
// Memory barrier ensures visibility
UnsafeAccess.UNSAFE.storeFence();
stateVersion++;
}
public RenderState getState() {
long lastSeenVersion = ThreadLocalVersions.get();
if (lastSeenVersion != stateVersion) {
RenderState state = currentState.get();
cachedState.set(state);
ThreadLocalVersions.set(stateVersion);
}
return cachedState.get();
}
private static final ThreadLocal<Long> ThreadLocalVersions = ThreadLocal.withInitial(() -> -1L);
}

Create tools for debugging thread-related issues:

// Example: Thread debugging utilities
public class ThreadDebugUtils {
private static final Map<Thread, StackTraceElement[]> threadStates = new ConcurrentHashMap<>();
public static void captureThreadStates() {
for (Thread thread : Thread.getAllStackTraces().keySet()) {
threadStates.put(thread, thread.getStackTrace());
}
}
public static void logRenderThreadState() {
Thread renderThread = getRenderThread();
StackTraceElement[] stack = threadStates.get(renderThread);
if (stack != null) {
LOGGER.info("Render Thread Stack:");
for (StackTraceElement element : stack) {
LOGGER.info(" " + element.toString());
}
}
}
public static void detectPotentialDeadlocks() {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadBean.findDeadlockedThreads();
if (deadlockedThreads != null) {
ThreadInfo[] threadInfos = threadBean.getThreadInfo(deadlockedThreads);
for (ThreadInfo info : threadInfos) {
LOGGER.error("Deadlocked thread: " + info.getThreadName());
for (StackTraceElement element : info.getStackTrace()) {
LOGGER.error(" " + element.toString());
}
}
}
}
}

Profile multi-threaded performance:

// Example: Multi-threaded profiler
public class MultiThreadedProfiler {
private final Map<String, ThreadMetrics> metrics = new ConcurrentHashMap<>();
public void startProfiling(String operation) {
Thread currentThread = Thread.currentThread();
String key = currentThread.getName() + ":" + operation;
metrics.computeIfAbsent(key, k -> new ThreadMetrics())
.startOperation();
}
public void endProfiling(String operation) {
Thread currentThread = Thread.currentThread();
String key = currentThread.getName() + ":" + operation;
ThreadMetrics threadMetrics = metrics.get(key);
if (threadMetrics != null) {
threadMetrics.endOperation();
}
}
public void printProfilingResults() {
metrics.forEach((key, metrics) -> {
System.out.printf("Thread: %s%n", key);
System.out.printf(" Operations: %d%n", metrics.getOperationCount());
System.out.printf(" Average Time: %.2f ms%n", metrics.getAverageTime());
System.out.printf(" Total Time: %.2f ms%n", metrics.getTotalTime());
});
}
private static class ThreadMetrics {
private long operationStartTime;
private int operationCount = 0;
private long totalTime = 0;
void startOperation() {
operationStartTime = Blaze3D.getTime();
}
void endOperation() {
long endTime = Blaze3D.getTime();
totalTime += (endTime - operationStartTime);
operationCount++;
}
long getAverageTime() {
return operationCount > 0 ? totalTime / operationCount : 0;
}
long getTotalTime() {
return totalTime;
}
int getOperationCount() {
return operationCount;
}
}
}
  1. Always verify render thread before calling rendering methods
  2. Use proper synchronization for shared data structures
  3. Minimize lock contention with fine-grained locking
  4. Prefer immutable data for cross-thread communication
  5. Use lock-free structures for high-frequency operations
// ❌ WRONG - Blocking render thread
public void renderBlocking() {
// This blocks the render thread!
CompletableFuture.supplyAsync(this::doHeavyComputation).join();
renderResult();
}
// ✅ CORRECT - Async pattern
public void renderAsync() {
CompletableFuture.supplyAsync(this::doHeavyComputation)
.thenAccept(result -> {
RenderSystem.executeOnMainThread(() -> {
renderResult(result);
});
});
}
  1. Race Conditions: Concurrent access to shared state
  2. Deadlocks: Improper lock ordering
  3. Thread Contention: Excessive synchronization
  4. Memory Visibility: Missing memory barriers
  5. Resource Leaks: Forgetting to clean up thread resources
  1. Thread Safety Violations: Always check RenderSystem.isOnRenderThread()
  2. Blocking Operations: Never block the render thread with heavy computations
  3. Improper Synchronization: Use proper locking mechanisms for shared data
  4. Resource Creation: Create GPU resources only on render thread
  5. Task Ordering: Use consecutive executors for ordered task execution