/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.collect.Lists * com.google.common.collect.Queues * com.mojang.logging.LogUtils * it.unimi.dsi.fastutil.longs.Long2ObjectMap * it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap * it.unimi.dsi.fastutil.longs.LongIterator * it.unimi.dsi.fastutil.longs.LongOpenHashSet * it.unimi.dsi.fastutil.longs.LongSet * org.joml.Vector3d * org.joml.Vector3dc * org.jspecify.annotations.Nullable * org.slf4j.Logger */ package net.minecraft.client.renderer; import com.google.common.collect.Lists; import com.google.common.collect.Queues; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import net.minecraft.client.Camera; import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.Octree; import net.minecraft.client.renderer.ViewArea; import net.minecraft.client.renderer.chunk.CompiledSectionMesh; import net.minecraft.client.renderer.chunk.SectionMesh; import net.minecraft.client.renderer.chunk.SectionRenderDispatcher; import net.minecraft.client.renderer.culling.Frustum; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; import net.minecraft.server.level.ChunkTrackingView; import net.minecraft.util.Mth; import net.minecraft.util.Util; import net.minecraft.util.VisibleForDebug; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.phys.Vec3; import org.joml.Vector3d; import org.joml.Vector3dc; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class SectionOcclusionGraph { private static final Logger LOGGER = LogUtils.getLogger(); private static final Direction[] DIRECTIONS = Direction.values(); private static final int MINIMUM_ADVANCED_CULLING_DISTANCE = 60; private static final int MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE = SectionPos.blockToSectionCoord(60); private static final double CEILED_SECTION_DIAGONAL = Math.ceil(Math.sqrt(3.0) * 16.0); private boolean needsFullUpdate = true; private @Nullable Future fullUpdateTask; private @Nullable ViewArea viewArea; private final AtomicReference<@Nullable GraphState> currentGraph = new AtomicReference(); private final AtomicReference<@Nullable GraphEvents> nextGraphEvents = new AtomicReference(); private final AtomicBoolean needsFrustumUpdate = new AtomicBoolean(false); public void waitAndReset(@Nullable ViewArea viewArea) { if (this.fullUpdateTask != null) { try { this.fullUpdateTask.get(); this.fullUpdateTask = null; } catch (Exception e) { LOGGER.warn("Full update failed", (Throwable)e); } } this.viewArea = viewArea; if (viewArea != null) { this.currentGraph.set(new GraphState(viewArea)); this.invalidate(); } else { this.currentGraph.set(null); } } public void invalidate() { this.needsFullUpdate = true; } public void addSectionsInFrustum(Frustum frustum, List visibleSections, List nearbyVisibleSection) { this.currentGraph.get().storage().sectionTree.visitNodes((node, fullyVisible, depth, isClose) -> { SectionRenderDispatcher.RenderSection renderSection = node.getSection(); if (renderSection != null) { visibleSections.add(renderSection); if (isClose) { nearbyVisibleSection.add(renderSection); } } }, frustum, 32); } public boolean consumeFrustumUpdate() { return this.needsFrustumUpdate.compareAndSet(true, false); } public void onChunkReadyToRender(ChunkPos pos) { GraphEvents events; GraphEvents nextEvents = this.nextGraphEvents.get(); if (nextEvents != null) { this.addNeighbors(nextEvents, pos); } if ((events = this.currentGraph.get().events) != nextEvents) { this.addNeighbors(events, pos); } } public void schedulePropagationFrom(SectionRenderDispatcher.RenderSection section) { GraphEvents events; GraphEvents nextEvents = this.nextGraphEvents.get(); if (nextEvents != null) { nextEvents.sectionsToPropagateFrom.add(section); } if ((events = this.currentGraph.get().events) != nextEvents) { events.sectionsToPropagateFrom.add(section); } } public void update(boolean smartCull, Camera camera, Frustum frustum, List visibleSections, LongOpenHashSet loadedEmptySections) { Vec3 cameraPos = camera.position(); if (this.needsFullUpdate && (this.fullUpdateTask == null || this.fullUpdateTask.isDone())) { this.scheduleFullUpdate(smartCull, camera, cameraPos, loadedEmptySections); } this.runPartialUpdate(smartCull, frustum, visibleSections, cameraPos, loadedEmptySections); } private void scheduleFullUpdate(boolean smartCull, Camera camera, Vec3 cameraPos, LongOpenHashSet loadedEmptySections) { this.needsFullUpdate = false; LongOpenHashSet emptySections = loadedEmptySections.clone(); this.fullUpdateTask = CompletableFuture.runAsync(() -> { GraphState newState = new GraphState(this.viewArea); this.nextGraphEvents.set(newState.events); ArrayDeque queue = Queues.newArrayDeque(); this.initializeQueueForFullUpdate(camera, queue); queue.forEach(node -> newState.storage.sectionToNodeMap.put(node.section, (Node)node)); this.runUpdates(newState.storage, cameraPos, queue, smartCull, node -> {}, emptySections); this.currentGraph.set(newState); this.nextGraphEvents.set(null); this.needsFrustumUpdate.set(true); }, Util.backgroundExecutor()); } private void runPartialUpdate(boolean smartCull, Frustum frustum, List visibleSections, Vec3 cameraPos, LongOpenHashSet loadedEmptySections) { GraphState state = this.currentGraph.get(); this.queueSectionsWithNewNeighbors(state); if (!state.events.sectionsToPropagateFrom.isEmpty()) { ArrayDeque queue = Queues.newArrayDeque(); while (!state.events.sectionsToPropagateFrom.isEmpty()) { SectionRenderDispatcher.RenderSection renderSection = (SectionRenderDispatcher.RenderSection)state.events.sectionsToPropagateFrom.poll(); Node node = state.storage.sectionToNodeMap.get(renderSection); if (node == null || node.section != renderSection) continue; queue.add(node); } Frustum offsetFrustum = LevelRenderer.offsetFrustum(frustum); Consumer onSectionAdded = section -> { if (offsetFrustum.isVisible(section.getBoundingBox())) { this.needsFrustumUpdate.set(true); } }; this.runUpdates(state.storage, cameraPos, queue, smartCull, onSectionAdded, loadedEmptySections); } } private void queueSectionsWithNewNeighbors(GraphState state) { LongIterator iterator = state.events.chunksWhichReceivedNeighbors.iterator(); while (iterator.hasNext()) { long chunkWithNewNeighbor = iterator.nextLong(); List renderSections = (List)state.storage.chunksWaitingForNeighbors.get(chunkWithNewNeighbor); if (renderSections == null || !((SectionRenderDispatcher.RenderSection)renderSections.get(0)).hasAllNeighbors()) continue; state.events.sectionsToPropagateFrom.addAll(renderSections); state.storage.chunksWaitingForNeighbors.remove(chunkWithNewNeighbor); } state.events.chunksWhichReceivedNeighbors.clear(); } private void addNeighbors(GraphEvents events, ChunkPos pos) { events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x, pos.z - 1)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x, pos.z + 1)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z - 1)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x - 1, pos.z + 1)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z - 1)); events.chunksWhichReceivedNeighbors.add(ChunkPos.asLong(pos.x + 1, pos.z + 1)); } private void initializeQueueForFullUpdate(Camera camera, Queue queue) { BlockPos cameraPosition = camera.blockPosition(); long cameraSectionNode = SectionPos.asLong(cameraPosition); int cameraSectionY = SectionPos.y(cameraSectionNode); SectionRenderDispatcher.RenderSection cameraSection = this.viewArea.getRenderSection(cameraSectionNode); if (cameraSection == null) { LevelHeightAccessor heightAccessor = this.viewArea.getLevelHeightAccessor(); boolean isBelowTheWorld = cameraSectionY < heightAccessor.getMinSectionY(); int sectionY = isBelowTheWorld ? heightAccessor.getMinSectionY() : heightAccessor.getMaxSectionY(); int viewDistance = this.viewArea.getViewDistance(); ArrayList toAdd = Lists.newArrayList(); int cameraSectionX = SectionPos.x(cameraSectionNode); int cameraSectionZ = SectionPos.z(cameraSectionNode); for (int sectionX = -viewDistance; sectionX <= viewDistance; ++sectionX) { for (int sectionZ = -viewDistance; sectionZ <= viewDistance; ++sectionZ) { SectionRenderDispatcher.RenderSection renderSectionAt = this.viewArea.getRenderSection(SectionPos.asLong(sectionX + cameraSectionX, sectionY, sectionZ + cameraSectionZ)); if (renderSectionAt == null || !this.isInViewDistance(cameraSectionNode, renderSectionAt.getSectionNode())) continue; Direction sourceDirection = isBelowTheWorld ? Direction.UP : Direction.DOWN; Node node = new Node(renderSectionAt, sourceDirection, 0); node.setDirections(node.directions, sourceDirection); if (sectionX > 0) { node.setDirections(node.directions, Direction.EAST); } else if (sectionX < 0) { node.setDirections(node.directions, Direction.WEST); } if (sectionZ > 0) { node.setDirections(node.directions, Direction.SOUTH); } else if (sectionZ < 0) { node.setDirections(node.directions, Direction.NORTH); } toAdd.add(node); } } toAdd.sort(Comparator.comparingDouble(c -> cameraPosition.distSqr(SectionPos.of(c.section.getSectionNode()).center()))); queue.addAll(toAdd); } else { queue.add(new Node(cameraSection, null, 0)); } } private void runUpdates(GraphStorage storage, Vec3 cameraPos, Queue queue, boolean smartCull, Consumer onSectionAdded, LongOpenHashSet emptySections) { SectionPos cameraSectionPos = SectionPos.of(cameraPos); long cameraSectionNode = cameraSectionPos.asLong(); BlockPos cameraSectionCenter = cameraSectionPos.center(); while (!queue.isEmpty()) { long sectionNode; Node node = queue.poll(); SectionRenderDispatcher.RenderSection currentSection = node.section; if (!emptySections.contains(node.section.getSectionNode())) { if (storage.sectionTree.add(node.section)) { onSectionAdded.accept(node.section); } } else { node.section.sectionMesh.compareAndSet(CompiledSectionMesh.UNCOMPILED, CompiledSectionMesh.EMPTY); } boolean distantFromCamera = Math.abs(SectionPos.x(sectionNode = currentSection.getSectionNode()) - cameraSectionPos.x()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE || Math.abs(SectionPos.y(sectionNode) - cameraSectionPos.y()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE || Math.abs(SectionPos.z(sectionNode) - cameraSectionPos.z()) > MINIMUM_ADVANCED_CULLING_SECTION_DISTANCE; for (Direction direction : DIRECTIONS) { Node existingNode; SectionRenderDispatcher.RenderSection renderSectionAt = this.getRelativeFrom(cameraSectionNode, currentSection, direction); if (renderSectionAt == null || smartCull && node.hasDirection(direction.getOpposite())) continue; if (smartCull && node.hasSourceDirections()) { SectionMesh sectionMesh = currentSection.getSectionMesh(); boolean visible = false; for (int i = 0; i < DIRECTIONS.length; ++i) { if (!node.hasSourceDirection(i) || !sectionMesh.facesCanSeeEachother(DIRECTIONS[i].getOpposite(), direction)) continue; visible = true; break; } if (!visible) continue; } if (smartCull && distantFromCamera) { boolean maxY; boolean maxX; int renderSectionOriginX = SectionPos.sectionToBlockCoord(SectionPos.x(sectionNode)); int renderSectionOriginY = SectionPos.sectionToBlockCoord(SectionPos.y(sectionNode)); int renderSectionOriginZ = SectionPos.sectionToBlockCoord(SectionPos.z(sectionNode)); boolean bl = direction.getAxis() == Direction.Axis.X ? cameraSectionCenter.getX() > renderSectionOriginX : (maxX = cameraSectionCenter.getX() < renderSectionOriginX); boolean bl2 = direction.getAxis() == Direction.Axis.Y ? cameraSectionCenter.getY() > renderSectionOriginY : (maxY = cameraSectionCenter.getY() < renderSectionOriginY); boolean maxZ = direction.getAxis() == Direction.Axis.Z ? cameraSectionCenter.getZ() > renderSectionOriginZ : cameraSectionCenter.getZ() < renderSectionOriginZ; Vector3d checkPos = new Vector3d((double)(renderSectionOriginX + (maxX ? 16 : 0)), (double)(renderSectionOriginY + (maxY ? 16 : 0)), (double)(renderSectionOriginZ + (maxZ ? 16 : 0))); Vector3d step = new Vector3d(cameraPos.x, cameraPos.y, cameraPos.z).sub((Vector3dc)checkPos).normalize().mul(CEILED_SECTION_DIAGONAL); boolean visible = true; while (checkPos.distanceSquared(cameraPos.x, cameraPos.y, cameraPos.z) > 3600.0) { checkPos.add((Vector3dc)step); LevelHeightAccessor heightAccessor = this.viewArea.getLevelHeightAccessor(); if (checkPos.y > (double)heightAccessor.getMaxY() || checkPos.y < (double)heightAccessor.getMinY()) break; SectionRenderDispatcher.RenderSection checkSection = this.viewArea.getRenderSectionAt(BlockPos.containing(checkPos.x, checkPos.y, checkPos.z)); if (checkSection != null && storage.sectionToNodeMap.get(checkSection) != null) continue; visible = false; break; } if (!visible) continue; } if ((existingNode = storage.sectionToNodeMap.get(renderSectionAt)) != null) { existingNode.addSourceDirection(direction); continue; } Node newNode = new Node(renderSectionAt, direction, node.step + 1); newNode.setDirections(node.directions, direction); if (renderSectionAt.hasAllNeighbors()) { queue.add(newNode); storage.sectionToNodeMap.put(renderSectionAt, newNode); continue; } if (!this.isInViewDistance(cameraSectionNode, renderSectionAt.getSectionNode())) continue; storage.sectionToNodeMap.put(renderSectionAt, newNode); long chunkNode = SectionPos.sectionToChunk(renderSectionAt.getSectionNode()); ((List)storage.chunksWaitingForNeighbors.computeIfAbsent(chunkNode, l -> new ArrayList())).add(renderSectionAt); } } } private boolean isInViewDistance(long cameraSectionNode, long sectionNode) { return ChunkTrackingView.isInViewDistance(SectionPos.x(cameraSectionNode), SectionPos.z(cameraSectionNode), this.viewArea.getViewDistance(), SectionPos.x(sectionNode), SectionPos.z(sectionNode)); } private @Nullable SectionRenderDispatcher.RenderSection getRelativeFrom(long cameraSectionNode, SectionRenderDispatcher.RenderSection renderSection, Direction direction) { long relative = renderSection.getNeighborSectionNode(direction); if (!this.isInViewDistance(cameraSectionNode, relative)) { return null; } if (Mth.abs(SectionPos.y(cameraSectionNode) - SectionPos.y(relative)) > this.viewArea.getViewDistance()) { return null; } return this.viewArea.getRenderSection(relative); } @VisibleForDebug public @Nullable Node getNode(SectionRenderDispatcher.RenderSection section) { return this.currentGraph.get().storage.sectionToNodeMap.get(section); } public Octree getOctree() { return this.currentGraph.get().storage.sectionTree; } private record GraphState(GraphStorage storage, GraphEvents events) { private GraphState(ViewArea viewArea) { this(new GraphStorage(viewArea), new GraphEvents()); } } private static class GraphStorage { public final SectionToNodeMap sectionToNodeMap; public final Octree sectionTree; public final Long2ObjectMap> chunksWaitingForNeighbors; public GraphStorage(ViewArea viewArea) { this.sectionToNodeMap = new SectionToNodeMap(viewArea.sections.length); this.sectionTree = new Octree(viewArea.getCameraSectionPos(), viewArea.getViewDistance(), viewArea.sectionGridSizeY, viewArea.level.getMinY()); this.chunksWaitingForNeighbors = new Long2ObjectOpenHashMap(); } } private record GraphEvents(LongSet chunksWhichReceivedNeighbors, BlockingQueue sectionsToPropagateFrom) { private GraphEvents() { this((LongSet)new LongOpenHashSet(), new LinkedBlockingQueue()); } } private static class SectionToNodeMap { private final Node[] nodes; private SectionToNodeMap(int sectionCount) { this.nodes = new Node[sectionCount]; } public void put(SectionRenderDispatcher.RenderSection renderSection, Node node) { this.nodes[renderSection.index] = node; } public @Nullable Node get(SectionRenderDispatcher.RenderSection renderSection) { int index = renderSection.index; if (index < 0 || index >= this.nodes.length) { return null; } return this.nodes[index]; } } @VisibleForDebug public static class Node { @VisibleForDebug protected final SectionRenderDispatcher.RenderSection section; private byte sourceDirections; private byte directions; @VisibleForDebug public final int step; private Node(SectionRenderDispatcher.RenderSection section, @Nullable Direction sourceDirection, int step) { this.section = section; if (sourceDirection != null) { this.addSourceDirection(sourceDirection); } this.step = step; } private void setDirections(byte oldDirections, Direction direction) { this.directions = (byte)(this.directions | (oldDirections | 1 << direction.ordinal())); } private boolean hasDirection(Direction direction) { return (this.directions & 1 << direction.ordinal()) > 0; } private void addSourceDirection(Direction direction) { this.sourceDirections = (byte)(this.sourceDirections | (this.sourceDirections | 1 << direction.ordinal())); } @VisibleForDebug public boolean hasSourceDirection(int directionOrdinal) { return (this.sourceDirections & 1 << directionOrdinal) > 0; } private boolean hasSourceDirections() { return this.sourceDirections != 0; } public int hashCode() { return Long.hashCode(this.section.getSectionNode()); } public boolean equals(Object obj) { if (!(obj instanceof Node)) { return false; } Node other = (Node)obj; return this.section.getSectionNode() == other.section.getSectionNode(); } } }