diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java index cfe9aae5a8..0230e6f82b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java @@ -8,6 +8,8 @@ import java.nio.ByteBuffer; public class FallbackStagingBuffer implements StagingBuffer { + private static final float BYTES_PER_NANO_LIMIT = 8_000_000.0f / (1_000_000_000.0f / 60.0f); // MB per frame at 60fps + private final GlMutableBuffer fallbackBufferObject; public FallbackStagingBuffer(CommandList commandList) { @@ -39,4 +41,9 @@ public void flip() { public String toString() { return "Fallback"; } + + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (frameDuration * BYTES_PER_NANO_LIMIT); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java index 5252861cad..a7b6223204 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java @@ -15,6 +15,8 @@ import java.util.List; public class MappedStagingBuffer implements StagingBuffer { + private static final float UPLOAD_LIMIT_MARGIN = 0.8f; + private static final EnumBitField STORAGE_FLAGS = EnumBitField.of(GlBufferStorageFlags.PERSISTENT, GlBufferStorageFlags.CLIENT_STORAGE, GlBufferStorageFlags.MAP_WRITE); @@ -156,6 +158,11 @@ public void flip() { } } + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (this.capacity * UPLOAD_LIMIT_MARGIN); + } + private static final class CopyCommand { private final GlBuffer buffer; private final long readOffset; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java index d3087e8df5..0fe71170d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java @@ -13,4 +13,6 @@ public interface StagingBuffer { void delete(CommandList commandList); void flip(); + + long getUploadSizeLimit(long frameDuration); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java index 6a43897da0..991ad81d35 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java @@ -14,6 +14,7 @@ import net.caffeinemc.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; import net.caffeinemc.mods.sodium.client.gui.options.storage.SodiumOptionsStorage; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.minecraft.client.AttackIndicatorStatus; import net.minecraft.client.InactivityFpsLimit; @@ -279,12 +280,12 @@ public static OptionPage performance() { .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) .build() ) - .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) - .setName(Component.translatable("sodium.options.always_defer_chunk_updates.name")) - .setTooltip(Component.translatable("sodium.options.always_defer_chunk_updates.tooltip")) - .setControl(TickBoxControl::new) + .add(OptionImpl.createBuilder(DeferMode.class, sodiumOpts) + .setName(Component.translatable("sodium.options.defer_chunk_updates.name")) + .setTooltip(Component.translatable("sodium.options.defer_chunk_updates.tooltip")) + .setControl(option -> new CyclingControl<>(option, DeferMode.class)) .setImpact(OptionImpact.HIGH) - .setBinding((opts, value) -> opts.performance.alwaysDeferChunkUpdates = value, opts -> opts.performance.alwaysDeferChunkUpdates) + .setBinding((opts, value) -> opts.performance.chunkBuildDeferMode = value, opts -> opts.performance.chunkBuildDeferMode) .setFlags(OptionFlag.REQUIRES_RENDERER_UPDATE) .build()) .build() diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java index ec681430b8..68c9e8842d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java @@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.FileUtil; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; @@ -37,8 +38,7 @@ public static SodiumGameOptions defaults() { public static class PerformanceSettings { public int chunkBuilderThreads = 0; - @SerializedName("always_defer_chunk_updates_v2") // this will reset the option in older configs - public boolean alwaysDeferChunkUpdates = true; + public DeferMode chunkBuildDeferMode = DeferMode.ALWAYS; public boolean animateOnlyVisibleTextures = true; public boolean useEntityCulling = true; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index c2e72fb2e4..cce2cedb13 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -36,7 +36,6 @@ import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.core.BlockPos; -import net.minecraft.core.SectionPos; import net.minecraft.server.level.BlockDestructionProgress; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; @@ -193,14 +192,14 @@ public void setupTerrain(Camera camera, float fogDistance = RenderSystem.getShaderFog().end(); if (this.lastCameraPos == null) { - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } if (this.lastProjectionMatrix == null) { this.lastProjectionMatrix = new Matrix4f(projectionMatrix); } boolean cameraLocationChanged = !pos.equals(this.lastCameraPos); boolean cameraAngleChanged = pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance; - boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix); + boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix, 0.0001f); this.lastProjectionMatrix = projectionMatrix; @@ -208,33 +207,30 @@ public void setupTerrain(Camera camera, this.lastCameraYaw = yaw; if (cameraLocationChanged || cameraAngleChanged || cameraProjectionChanged) { - this.renderSectionManager.markGraphDirty(); + this.renderSectionManager.notifyChangedCamera(); } this.lastFogDistance = fogDistance; - this.renderSectionManager.updateCameraState(pos, camera); + this.renderSectionManager.prepareFrame(pos); if (cameraLocationChanged) { profiler.popPush("translucent_triggering"); this.renderSectionManager.processGFNIMovement(new CameraMovement(this.lastCameraPos, pos)); - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } int maxChunkUpdates = updateChunksImmediately ? this.renderDistance : 1; - for (int i = 0; i < maxChunkUpdates; i++) { - if (this.renderSectionManager.needsUpdate()) { - profiler.popPush("chunk_render_lists"); + profiler.popPush("chunk_render_lists"); - this.renderSectionManager.update(camera, viewport, spectator); - } + this.renderSectionManager.updateRenderLists(camera, viewport, spectator, updateChunksImmediately); profiler.popPush("chunk_update"); this.renderSectionManager.cleanupAndFlip(); - this.renderSectionManager.updateChunks(updateChunksImmediately); + this.renderSectionManager.updateChunks(viewport, updateChunksImmediately); profiler.popPush("chunk_upload"); @@ -255,6 +251,7 @@ public void setupTerrain(Camera camera, } private void processChunkEvents() { + this.renderSectionManager.beforeSectionUpdates(); var tracker = ChunkTrackerHolder.get(this.level); tracker.forEachEvent(this.renderSectionManager::onChunkAdded, this.renderSectionManager::onChunkRemoved); } @@ -345,9 +342,8 @@ private void renderBlockEntities(PoseStack matrices, while (renderSectionIterator.hasNext()) { var renderSectionId = renderSectionIterator.nextByteAsInt(); - var renderSection = renderRegion.getSection(renderSectionId); - var blockEntities = renderSection.getCulledBlockEntities(); + var blockEntities = renderRegion.getCulledBlockEntities(renderSectionId); if (blockEntities == null) { continue; @@ -372,7 +368,7 @@ private void renderGlobalBlockEntities(PoseStack matrices, LocalPlayer player, LocalBooleanRef isGlowing) { for (var renderSection : this.renderSectionManager.getSectionsWithGlobalEntities()) { - var blockEntities = renderSection.getGlobalBlockEntities(); + var blockEntities = renderSection.getRegion().getGlobalBlockEntities(renderSection.getSectionIndex()); if (blockEntities == null) { continue; @@ -446,9 +442,7 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume while (renderSectionIterator.hasNext()) { var renderSectionId = renderSectionIterator.nextByteAsInt(); - var renderSection = renderRegion.getSection(renderSectionId); - - var blockEntities = renderSection.getCulledBlockEntities(); + var blockEntities = renderRegion.getCulledBlockEntities(renderSectionId); if (blockEntities == null) { continue; @@ -461,7 +455,7 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume } for (var renderSection : this.renderSectionManager.getSectionsWithGlobalEntities()) { - var blockEntities = renderSection.getGlobalBlockEntities(); + var blockEntities = renderSection.getRegion().getGlobalBlockEntities(renderSection.getSectionIndex()); if (blockEntities == null) { continue; @@ -474,10 +468,13 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume } // the volume of a section multiplied by the number of sections to be checked at most - private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 15; + private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 50; /** - * Returns whether or not the entity intersects with any visible chunks in the graph. + * Returns whether the entity intersects with any visible chunks in the graph. + * + * Note that this method assumes the entity is within the frustum. It does not perform a frustum check. + * * @return True if the entity is visible, otherwise false */ public boolean isEntityVisible(EntityRenderer renderer, T entity) { @@ -495,7 +492,7 @@ public boolean isEntityVisible(E // bail on very large entities to avoid checking many sections double entityVolume = (bb.maxX - bb.minX) * (bb.maxY - bb.minY) * (bb.maxZ - bb.minZ); if (entityVolume > MAX_ENTITY_CHECK_VOLUME) { - // TODO: do a frustum check instead, even large entities aren't visible if they're outside the frustum + // large entities are only frustum tested, their sections are not checked for visibility return true; } @@ -509,48 +506,28 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y return true; } - int minX = SectionPos.posToSectionCoord(x1 - 0.5D); - int minY = SectionPos.posToSectionCoord(y1 - 0.5D); - int minZ = SectionPos.posToSectionCoord(z1 - 0.5D); - - int maxX = SectionPos.posToSectionCoord(x2 + 0.5D); - int maxY = SectionPos.posToSectionCoord(y2 + 0.5D); - int maxZ = SectionPos.posToSectionCoord(z2 + 0.5D); - - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - for (int y = minY; y <= maxY; y++) { - if (this.renderSectionManager.isSectionVisible(x, y, z)) { - return true; - } - } - } - } - - return false; + return this.renderSectionManager.isBoxVisible(x1, y1, z1, x2, y2, z2); } public String getChunksDebugString() { - // C: visible/total D: distance - // TODO: add dirty and queued counts - return String.format("C: %d/%d D: %d", this.renderSectionManager.getVisibleChunkCount(), this.renderSectionManager.getTotalSections(), this.renderDistance); + return this.renderSectionManager.getChunksDebugString(); } /** * Schedules chunk rebuilds for all chunks in the specified block region. */ - public void scheduleRebuildForBlockArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { - this.scheduleRebuildForChunks(minX >> 4, minY >> 4, minZ >> 4, maxX >> 4, maxY >> 4, maxZ >> 4, important); + public void scheduleRebuildForBlockArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean playerChanged) { + this.scheduleRebuildForChunks(minX >> 4, minY >> 4, minZ >> 4, maxX >> 4, maxY >> 4, maxZ >> 4, playerChanged); } /** * Schedules chunk rebuilds for all chunks in the specified chunk region. */ - public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { + public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean playerChanged) { for (int chunkX = minX; chunkX <= maxX; chunkX++) { for (int chunkY = minY; chunkY <= maxY; chunkY++) { for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) { - this.scheduleRebuildForChunk(chunkX, chunkY, chunkZ, important); + this.scheduleRebuildForChunk(chunkX, chunkY, chunkZ, playerChanged); } } } @@ -559,8 +536,8 @@ public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int /** * Schedules a chunk rebuild for the render belonging to the given chunk section position. */ - public void scheduleRebuildForChunk(int x, int y, int z, boolean important) { - this.renderSectionManager.scheduleRebuild(x, y, z, important); + public void scheduleRebuildForChunk(int x, int y, int z, boolean playerChanged) { + this.renderSectionManager.scheduleRebuild(x, y, z, playerChanged); } public Collection getDebugStrings() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index 2cd7859716..46fc02a7aa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -1,24 +1,46 @@ package net.caffeinemc.mods.sodium.client.render.chunk; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; +import org.jetbrains.annotations.NotNull; +/** + * Important: Whether the task is scheduled immediately after its creation. Otherwise, they're scheduled through asynchronous culling that collects non-important tasks. + * Defer mode: For important tasks, how fast they are going to be executed. One or zero frame deferral only allows one or zero frames to pass before the frame blocks on the task. Always deferral allows the task to be deferred indefinitely, but if it's important it will still be put to the front of the queue. + */ public enum ChunkUpdateType { - SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT), - INITIAL_BUILD(128, ChunkBuilder.HIGH_EFFORT), - REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT); - - private final int maximumQueueSize; - private final int taskEffort; - - ChunkUpdateType(int maximumQueueSize, int taskEffort) { - this.maximumQueueSize = maximumQueueSize; - this.taskEffort = taskEffort; + SORT(2), + INITIAL_BUILD(0), + REBUILD(1), + IMPORTANT_REBUILD(DeferMode.ZERO_FRAMES, 1), + IMPORTANT_SORT(DeferMode.ZERO_FRAMES, 2); + + private final DeferMode deferMode; + private final boolean important; + private final float priorityValue; + + ChunkUpdateType(float priorityValue) { + this.deferMode = DeferMode.ALWAYS; + this.important = false; + this.priorityValue = priorityValue; + } + + ChunkUpdateType(@NotNull DeferMode deferMode, float priorityValue) { + this.deferMode = deferMode; + this.important = true; + this.priorityValue = priorityValue; } - public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, ChunkUpdateType next) { - if (prev == null || prev == SORT || prev == next) { + /** + * Returns a promoted update type if the new update type is more important than the previous one. Nothing is returned if the update type is the same or less important. + * + * @param prev Previous update type + * @param next New update type + * @return Promoted update type or {@code null} if the update type is the same or less important + */ + public static ChunkUpdateType getPromotedTypeChange(ChunkUpdateType prev, ChunkUpdateType next) { + if (prev == next) { + return null; + } + if (prev == null || prev == SORT || prev == INITIAL_BUILD) { return next; } if (next == IMPORTANT_REBUILD @@ -29,15 +51,15 @@ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, Chunk return null; } - public int getMaximumQueueSize() { - return this.maximumQueueSize; + public boolean isImportant() { + return this.important; } - public boolean isImportant() { - return this == IMPORTANT_REBUILD || this == IMPORTANT_SORT; + public DeferMode getDeferMode(DeferMode importantRebuildDeferMode) { + return this == IMPORTANT_REBUILD ? importantRebuildDeferMode : this.deferMode; } - public int getTaskEffort() { - return this.taskEffort; + public float getPriorityValue() { + return this.priorityValue; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java new file mode 100644 index 0000000000..978e745fe2 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.client.render.chunk; + +import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.minecraft.network.chat.Component; + +public enum DeferMode implements TextProvider { + ALWAYS("sodium.options.defer_chunk_updates.always"), + ONE_FRAME("sodium.options.defer_chunk_updates.one_frame"), + ZERO_FRAMES("sodium.options.defer_chunk_updates.zero_frames"); + + private final Component name; + + DeferMode(String name) { + this.name = Component.translatable(name); + } + + @Override + public Component getLocalizedName() { + return this.name; + } + + public boolean allowsUnlimitedUploadSize() { + return this == ZERO_FRAMES; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 9aa933474c..faac0e1e5b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirectionSet; @@ -7,10 +8,8 @@ import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; -import net.minecraft.world.level.block.entity.BlockEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,7 +29,7 @@ public class RenderSection { private long visibilityData = VisibilityEncoding.NULL; private int incomingDirections; - private int lastVisibleFrame = -1; + private int lastVisibleSearchToken = -1; private int adjacentMask; public RenderSection @@ -41,22 +40,18 @@ public class RenderSection { adjacentWest, adjacentEast; - // Rendering State - private boolean built = false; // merge with the flags? - private int flags = RenderSectionFlags.NONE; - private BlockEntity @Nullable[] globalBlockEntities; - private BlockEntity @Nullable[] culledBlockEntities; - private TextureAtlasSprite @Nullable[] animatedSprites; @Nullable private TranslucentData translucentData; // Pending Update State @Nullable private CancellationToken taskCancellationToken = null; + private long lastMeshResultSize = MeshResultSize.NO_DATA; @Nullable private ChunkUpdateType pendingUpdateType; + private long pendingUpdateSince; private int lastUploadFrame = -1; private int lastSubmittedFrame = -1; @@ -72,7 +67,6 @@ public RenderSection(RenderRegion region, int chunkX, int chunkY, int chunkZ) { int rX = this.getChunkX() & RenderRegion.REGION_WIDTH_M; int rY = this.getChunkY() & RenderRegion.REGION_HEIGHT_M; int rZ = this.getChunkZ() & RenderRegion.REGION_LENGTH_M; - this.sectionIndex = LocalSectionIndex.pack(rX, rY, rZ); this.region = region; @@ -148,37 +142,35 @@ public boolean setInfo(@Nullable BuiltSectionInfo info) { } private boolean setRenderState(@NotNull BuiltSectionInfo info) { - var prevBuilt = this.built; - var prevFlags = this.flags; + var prevFlags = this.region.getSectionFlags(this.sectionIndex); var prevVisibilityData = this.visibilityData; - this.built = true; - this.flags = info.flags; + this.region.setSectionRenderState(this.sectionIndex, info); this.visibilityData = info.visibilityData; - this.globalBlockEntities = info.globalBlockEntities; - this.culledBlockEntities = info.culledBlockEntities; - this.animatedSprites = info.animatedSprites; - // the section is marked as having received graph-relevant changes if it's build state, flags, or connectedness has changed. // the entities and sprites don't need to be checked since whether they exist is encoded in the flags. - return !prevBuilt || prevFlags != this.flags || prevVisibilityData != this.visibilityData; + return prevFlags != this.region.getSectionFlags(this.sectionIndex) || prevVisibilityData != this.visibilityData; } private boolean clearRenderState() { - var wasBuilt = this.built; + var wasBuilt = this.isBuilt(); - this.built = false; - this.flags = RenderSectionFlags.NONE; + this.region.clearSectionRenderState(this.sectionIndex); this.visibilityData = VisibilityEncoding.NULL; - this.globalBlockEntities = null; - this.culledBlockEntities = null; - this.animatedSprites = null; // changes to data if it moves from built to not built don't matter, so only build state changes matter return wasBuilt; } + public void setLastMeshResultSize(long size) { + this.lastMeshResultSize = size; + } + + public long getLastMeshResultSize() { + return this.lastMeshResultSize; + } + /** * Returns the chunk section position which this render refers to in the level. */ @@ -207,14 +199,6 @@ public int getOriginZ() { return this.chunkZ << 4; } - /** - * @return The squared distance from the center of this chunk in the level to the center of the block position - * given by {@param pos} - */ - public float getSquaredDistance(BlockPos pos) { - return this.getSquaredDistance(pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f); - } - /** * @return The squared distance from the center of this chunk to the given block position */ @@ -272,7 +256,7 @@ public String toString() { } public boolean isBuilt() { - return this.built; + return (this.region.getSectionFlags(this.sectionIndex) & RenderSectionFlags.MASK_IS_BUILT) != 0; } public int getSectionIndex() { @@ -283,12 +267,16 @@ public RenderRegion getRegion() { return this.region; } - public void setLastVisibleFrame(int frame) { - this.lastVisibleFrame = frame; + public boolean needsRender() { + return this.region.sectionNeedsRender(this.sectionIndex); + } + + public void setLastVisibleSearchToken(int frame) { + this.lastVisibleSearchToken = frame; } - public int getLastVisibleFrame() { - return this.lastVisibleFrame; + public int getLastVisibleSearchToken() { + return this.lastVisibleSearchToken; } public int getIncomingDirections() { @@ -303,13 +291,6 @@ public void setIncomingDirections(int directions) { this.incomingDirections = directions; } - /** - * Returns a bitfield containing the {@link RenderSectionFlags} for this built section. - */ - public int getFlags() { - return this.flags; - } - /** * Returns the occlusion culling data which determines this chunk's connectedness on the visibility graph. */ @@ -317,28 +298,6 @@ public long getVisibilityData() { return this.visibilityData; } - /** - * Returns the collection of animated sprites contained by this rendered chunk section. - */ - public TextureAtlasSprite @Nullable[] getAnimatedSprites() { - return this.animatedSprites; - } - - /** - * Returns the collection of block entities contained by this rendered chunk. - */ - public BlockEntity @Nullable[] getCulledBlockEntities() { - return this.culledBlockEntities; - } - - /** - * Returns the collection of block entities contained by this rendered chunk, which are not part of its culling - * volume. These entities should always be rendered regardless of the render being visible in the frustum. - */ - public BlockEntity @Nullable[] getGlobalBlockEntities() { - return this.globalBlockEntities; - } - public @Nullable CancellationToken getTaskCancellationToken() { return this.taskCancellationToken; } @@ -351,8 +310,17 @@ public void setTaskCancellationToken(@Nullable CancellationToken token) { return this.pendingUpdateType; } - public void setPendingUpdate(@Nullable ChunkUpdateType type) { + public long getPendingUpdateSince() { + return this.pendingUpdateSince; + } + + public void setPendingUpdate(ChunkUpdateType type, long now) { this.pendingUpdateType = type; + this.pendingUpdateSince = now; + } + + public void clearPendingUpdate() { + this.pendingUpdateType = null; } public void prepareTrigger(boolean isDirectTrigger) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index cb19504069..c52f14035a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -4,6 +4,17 @@ public class RenderSectionFlags { public static final int HAS_BLOCK_GEOMETRY = 0; public static final int HAS_BLOCK_ENTITIES = 1; public static final int HAS_ANIMATED_SPRITES = 2; + public static final int IS_BUILT = 3; + + public static final int MASK_HAS_BLOCK_GEOMETRY = 1 << HAS_BLOCK_GEOMETRY; + public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; + public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; + public static final int MASK_IS_BUILT = 1 << IS_BUILT; + public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; public static final int NONE = 0; + + public static boolean needsRender(int flags) { + return (flags & MASK_NEEDS_RENDER) != 0; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index d91c5496ab..ab08a493be 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1,47 +1,46 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import com.mojang.blaze3d.systems.RenderSystem; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; -import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -import it.unimi.dsi.fastutil.objects.ReferenceSet; -import it.unimi.dsi.fastutil.objects.ReferenceSets; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.longs.*; +import it.unimi.dsi.fastutil.objects.*; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; +import net.caffeinemc.mods.sodium.client.render.chunk.async.*; import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.*; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.*; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.DeferMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.PriorityMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.NoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.RemovableMultiForest; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats; import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil; import net.caffeinemc.mods.sodium.client.render.util.RenderAsserts; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.caffeinemc.mods.sodium.client.world.LevelSlice; import net.caffeinemc.mods.sodium.client.world.cloned.ChunkRenderContext; import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSectionCache; @@ -61,8 +60,14 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; public class RenderSectionManager { + private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); + private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); + private final ChunkBuilder builder; private final RenderRegionManager regions; @@ -71,6 +76,12 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); + private final JobDurationEstimator jobDurationEstimator = new JobDurationEstimator(); + private final MeshTaskSizeEstimator meshTaskSizeEstimator = new MeshTaskSizeEstimator(); + private ChunkJobCollector lastBlockingCollector; + private int thisFrameBlockingTasks; + private int nextFrameBlockingTasks; + private int deferredTasks; private final ChunkRenderer chunkRenderer; @@ -84,28 +95,45 @@ public class RenderSectionManager { private final SortTriggering sortTriggering; - private ChunkJobCollector lastBlockingCollector; - @NotNull private SortedRenderLists renderLists; - @NotNull - private Map> taskLists; + private DeferredTaskList frustumTaskLists; + private DeferredTaskList globalTaskLists; + private final EnumMap> importantTasks; - private int lastUpdatedFrame; + private int frame; + private int lastGraphDirtyFrame; + private long lastFrameDuration = -1; + private long averageFrameDuration = -1; + private long lastFrameAtTime = System.nanoTime(); + private static final float FRAME_DURATION_UPDATE_RATIO = 0.05f; - private boolean needsGraphUpdate; + private boolean needsGraphUpdate = true; + private boolean needsRenderListUpdate = true; + private boolean cameraChanged = false; + private boolean needsFrustumTaskListUpdate = true; - private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; + private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(RenderSectionManager::makeAsyncCullThread); + private final ObjectArrayList> pendingTasks = new ObjectArrayList<>(); + private GlobalCullTask pendingGlobalCullTask = null; + private final IntArrayList concurrentlySubmittedTasks = new IntArrayList(); + + private SectionTree renderTree = null; + private TaskSectionTree globalTaskTree = null; + private final Map cullResults = new EnumMap<>(CullType.class); + private final RemovableMultiForest renderableSectionTree; + + private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl(); + public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) { this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT); this.level = level; this.builder = new ChunkBuilder(level, ChunkMeshFormats.COMPACT); - this.needsGraphUpdate = true; this.renderDistance = renderDistance; this.sortTriggering = new SortTriggering(); @@ -116,38 +144,377 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.renderLists = SortedRenderLists.empty(); this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level); - this.taskLists = new EnumMap<>(ChunkUpdateType.class); + this.renderableSectionTree = new RemovableMultiForest(renderDistance); - for (var type : ChunkUpdateType.values()) { - this.taskLists.put(type, new ArrayDeque<>()); + this.importantTasks = new EnumMap<>(DeferMode.class); + for (var deferMode : DeferMode.values()) { + this.importantTasks.put(deferMode, new ReferenceLinkedOpenHashSet<>()); } } - public void updateCameraState(Vector3dc cameraPosition, Camera camera) { - this.cameraBlockPos = camera.getBlockPosition(); + public void prepareFrame(Vector3dc cameraPosition) { + var now = System.nanoTime(); + this.lastFrameDuration = now - this.lastFrameAtTime; + this.lastFrameAtTime = now; + if (this.averageFrameDuration == -1) { + this.averageFrameDuration = this.lastFrameDuration; + } else { + this.averageFrameDuration = MathUtil.exponentialMovingAverage(this.averageFrameDuration, this.lastFrameDuration, FRAME_DURATION_UPDATE_RATIO); + } + this.averageFrameDuration = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000); + + this.frame += 1; + this.needsRenderListUpdate |= this.cameraChanged; + this.needsFrustumTaskListUpdate |= this.needsRenderListUpdate; + this.cameraPosition = cameraPosition; } - public void update(Camera camera, Viewport viewport, boolean spectator) { - this.lastUpdatedFrame += 1; + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { + // do sync bfs based on update immediately (flawless frames) or if the camera moved too much + var shouldRenderSync = this.cameraTimingControl.getShouldRenderSync(camera); + if ((updateImmediately || shouldRenderSync) && (this.needsGraphUpdate || this.needsRenderListUpdate)) { + renderSync(camera, viewport, spectator); + return; + } + + if (this.needsGraphUpdate) { + this.lastGraphDirtyFrame = this.frame; + } + + // discard unusable present and pending frustum-tested trees + if (this.cameraChanged) { + this.cullResults.remove(CullType.FRUSTUM); - this.createTerrainRenderList(camera, viewport, this.lastUpdatedFrame, spectator); + this.pendingTasks.removeIf(task -> { + if (task instanceof FrustumCullTask cullTask) { + cullTask.setCancelled(); + return true; + } + return false; + }); + } + // remove all tasks that aren't in progress yet + this.pendingTasks.removeIf(AsyncRenderTask::cancelIfNotStarted); + + this.unpackTaskResults(null); + + this.scheduleAsyncWork(camera, viewport, spectator); + + if (this.needsFrustumTaskListUpdate) { + this.updateFrustumTaskList(viewport); + } + if (this.needsRenderListUpdate) { + processRenderListUpdate(viewport); + } + + this.needsRenderListUpdate = false; + this.needsFrustumTaskListUpdate = false; this.needsGraphUpdate = false; + this.cameraChanged = false; } - private void createTerrainRenderList(Camera camera, Viewport viewport, int frame, boolean spectator) { - this.resetRenderLists(); - + private void renderSync(Camera camera, Viewport viewport, boolean spectator) { final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - var visitor = new VisibleChunkCollector(frame); + // cancel running tasks to prevent two bfs running at the same time, which will cause race conditions + for (var task : this.pendingTasks) { + task.setCancelled(); + task.getResult(); + } + this.pendingTasks.clear(); + this.pendingGlobalCullTask = null; + this.concurrentlySubmittedTasks.clear(); + + var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM, this.level); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, CancellationToken.NEVER_CANCELLED); + tree.prepareForTraversal(); + + this.frustumTaskLists = tree.getPendingTaskLists(); + this.globalTaskLists = null; + this.cullResults.put(CullType.FRUSTUM, tree); + this.renderTree = tree; + + this.renderLists = tree.createRenderLists(viewport); + + // remove the other trees, they're very wrong by now + this.cullResults.remove(CullType.WIDE); + this.cullResults.remove(CullType.REGULAR); + + this.needsRenderListUpdate = false; + this.needsGraphUpdate = false; + this.cameraChanged = false; + } + + private SectionTree unpackTaskResults(Viewport waitingViewport) { + SectionTree latestTree = null; + CullType latestTreeCullType = null; + + var it = this.pendingTasks.iterator(); + while (it.hasNext()) { + var task = it.next(); + if (waitingViewport == null && !task.isDone()) { + continue; + } + it.remove(); + + // unpack the task and its result based on its type + switch (task) { + case FrustumCullTask frustumCullTask -> { + var result = frustumCullTask.getResult(); + this.frustumTaskLists = result.getFrustumTaskLists(); + + // ensure no useless frustum tree is accepted + if (!this.cameraChanged) { + var tree = result.getTree(); + this.cullResults.put(CullType.FRUSTUM, tree); + latestTree = tree; + latestTreeCullType = CullType.FRUSTUM; + + this.needsRenderListUpdate = true; + } + } + case GlobalCullTask globalCullTask -> { + var result = globalCullTask.getResult(); + var tree = result.getTaskTree(); + this.globalTaskLists = result.getGlobalTaskLists(); + this.frustumTaskLists = result.getFrustumTaskLists(); + this.globalTaskTree = tree; + var cullType = globalCullTask.getCullType(); + this.cullResults.put(cullType, tree); + latestTree = tree; + latestTreeCullType = cullType; + + this.needsRenderListUpdate = true; + this.pendingGlobalCullTask = null; + + // mark changes on the global task tree if they were scheduled while the task was already running + for (int i = 0, length = this.concurrentlySubmittedTasks.size(); i < length; i += 3) { + var x = this.concurrentlySubmittedTasks.getInt(i); + var y = this.concurrentlySubmittedTasks.getInt(i + 1); + var z = this.concurrentlySubmittedTasks.getInt(i + 2); + tree.markSectionTask(x, y, z); + } + this.concurrentlySubmittedTasks.clear(); + } + case FrustumTaskCollectionTask collectionTask -> + this.frustumTaskLists = collectionTask.getResult().getFrustumTaskLists(); + default -> { + throw new IllegalStateException("Unexpected task type: " + task); + } + } + } + + if (waitingViewport != null && latestTree != null) { + var searchDistance = this.getSearchDistanceForCullType(latestTreeCullType); + if (latestTree.isValidFor(waitingViewport, searchDistance)) { + return latestTree; + } + } + return null; + } + + private static Thread makeAsyncCullThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("Sodium Async Cull Thread"); + return thread; + } + + private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectator) { + // if the origin section doesn't exist, cull tasks won't produce any useful results + if (!this.occlusionCuller.graphOriginPresent(viewport)) { + return; + } + + // submit tasks of types that are applicable and not yet running + AsyncRenderTask currentRunningTask = null; + if (!this.pendingTasks.isEmpty()) { + currentRunningTask = this.pendingTasks.getFirst(); + } + + // pick a scheduling order based on if there's been a graph update and if the render list is dirty + var scheduleOrder = getScheduleOrder(); - this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame); + for (var type : scheduleOrder) { + var tree = this.cullResults.get(type); + + // don't schedule frustum tasks if the camera just changed to prevent throwing them away constantly + // since they're going to be invalid by the time they're completed in the next frame + if (type == CullType.FRUSTUM && this.cameraChanged) { + continue; + } + + // schedule a task of this type if there's no valid and current result for it yet + var searchDistance = this.getSearchDistanceForCullType(type); + if ((tree == null || tree.getFrame() < this.lastGraphDirtyFrame || !tree.isValidFor(viewport, searchDistance)) && + // and if there's no currently running task that will produce a valid and current result + (currentRunningTask == null || + currentRunningTask instanceof CullTask cullTask && cullTask.getCullType() != type || + currentRunningTask.getFrame() < this.lastGraphDirtyFrame)) { + var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + + // use the last dirty frame as the frame timestamp to avoid wrongly marking task results as more recent if they're simply scheduled later but did work on the same state of the graph if there's been no graph invalidation since + var task = switch (type) { + case WIDE, REGULAR -> + new GlobalCullTask(viewport, searchDistance, this.lastGraphDirtyFrame, this.occlusionCuller, useOcclusionCulling, this.sectionByPosition, type, this.level); + case FRUSTUM -> + // note that there is some danger with only giving the frustum tasks the last graph dirty frame and not the real current frame, but these are mitigated by deleting the frustum result when the camera changes. + new FrustumCullTask(viewport, searchDistance, this.lastGraphDirtyFrame, this.occlusionCuller, useOcclusionCulling, this.level); + }; + task.submitTo(this.asyncCullExecutor); + this.pendingTasks.add(task); + + if (task instanceof GlobalCullTask globalCullTask) { + this.pendingGlobalCullTask = globalCullTask; + } + } + } + } + + private static final CullType[] WIDE_TO_NARROW = { CullType.WIDE, CullType.REGULAR, CullType.FRUSTUM }; + private static final CullType[] NARROW_TO_WIDE = { CullType.FRUSTUM, CullType.REGULAR, CullType.WIDE }; + private static final CullType[] COMPROMISE = { CullType.REGULAR, CullType.FRUSTUM, CullType.WIDE }; + + private CullType[] getScheduleOrder() { + // if the camera is stationary, do the FRUSTUM update first to prevent the rendered section count from oscillating + if (!this.cameraChanged) { + return NARROW_TO_WIDE; + } + + // if only the render list is dirty but there's no graph update, do REGULAR first and potentially do FRUSTUM opportunistically + if (!this.needsGraphUpdate) { + return COMPROMISE; + } + + // if both are dirty, the camera is moving and loading new sections, do WIDE first to ensure there's any correct result + return WIDE_TO_NARROW; + } + + private static final LongArrayList timings = new LongArrayList(); + + private void updateFrustumTaskList(Viewport viewport) { + // schedule generating a frustum task list if there's no frustum tree task running + if (this.globalTaskTree != null) { + var frustumTaskListPending = false; + for (var task : this.pendingTasks) { + if (task instanceof CullTask cullTask && cullTask.getCullType() == CullType.FRUSTUM || + task instanceof FrustumTaskCollectionTask) { + frustumTaskListPending = true; + break; + } + } + if (!frustumTaskListPending) { + var searchDistance = this.getSearchDistance(); + var task = new FrustumTaskCollectionTask(viewport, searchDistance, this.frame, this.sectionByPosition, this.globalTaskTree); + task.submitTo(this.asyncCullExecutor); + this.pendingTasks.add(task); + } + } + } + + private void processRenderListUpdate(Viewport viewport) { + // pick the narrowest valid tree. This tree is either up-to-date or the origin is out of the graph as otherwise sync bfs would have been triggered (in graph but moving rapidly) + SectionTree bestValidTree = null; + SectionTree bestAnyTree = null; + for (var type : NARROW_TO_WIDE) { + var tree = this.cullResults.get(type); + if (tree == null) { + continue; + } + + // pick the most recent and most valid tree + float searchDistance = this.getSearchDistanceForCullType(type); + int treeFrame = tree.getFrame(); + if (bestAnyTree == null || treeFrame > bestAnyTree.getFrame()) { + bestAnyTree = tree; + } + if (!tree.isValidFor(viewport, searchDistance)) { + continue; + } + if (bestValidTree == null || treeFrame > bestValidTree.getFrame()) { + bestValidTree = tree; + } + } + + // use out-of-graph fallback if the origin section is not loaded and there's no valid tree (missing origin section, empty world) + if (bestValidTree == null && this.isOutOfGraph(viewport.getChunkCoord())) { + this.renderOutOfGraph(viewport); + return; + } + + // wait for pending tasks to maybe supply a valid tree if there's no current tree (first frames after initial load/reload) + if (bestAnyTree == null) { + var result = this.unpackTaskResults(viewport); + if (result != null) { + bestValidTree = result; + } + } + + // use the best valid tree, or even invalid tree if necessary + if (bestValidTree != null) { + bestAnyTree = bestValidTree; + } + if (bestAnyTree == null) { + this.renderOutOfGraph(viewport); + return; + } + + var start = System.nanoTime(); + + var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); + bestAnyTree.traverse(visibleCollector, viewport, this.getSearchDistance()); + this.renderLists = visibleCollector.createRenderLists(viewport); + + var end = System.nanoTime(); + var time = end - start; + timings.add(time); + if (timings.size() >= 500) { + var average = timings.longStream().average().orElse(0); + System.out.println("Render list generation took " + (average) / 1000 + "µs over " + timings.size() + " samples"); + timings.clear(); + } + + this.renderTree = bestAnyTree; + } + + private void renderOutOfGraph(Viewport viewport) { + var searchDistance = this.getSearchDistance(); + var visitor = new FallbackVisibleChunkCollector(viewport, searchDistance, this.sectionByPosition, this.regions, this.frame); + + this.renderableSectionTree.prepareForTraversal(); + this.renderableSectionTree.traverse(visitor, viewport, searchDistance); this.renderLists = visitor.createRenderLists(viewport); - this.taskLists = visitor.getRebuildLists(); + this.frustumTaskLists = visitor.getPendingTaskLists(); + this.globalTaskLists = null; + this.renderTree = null; + } + + private boolean isOutOfGraph(SectionPos pos) { + var sectionY = pos.getY(); + return this.level.getMaxSectionY() <= sectionY && sectionY <= this.level.getMaxSectionY() && !this.sectionByPosition.containsKey(pos.asLong()); + } + + public void markGraphDirty() { + this.needsGraphUpdate = true; + } + + public void notifyChangedCamera() { + this.cameraChanged = true; + } + + public boolean needsUpdate() { + return this.needsGraphUpdate; + } + + private float getSearchDistanceForCullType(CullType cullType) { + if (cullType.isFogCulled) { + return this.getSearchDistance(); + } else { + return this.getRenderDistance(); + } } private float getSearchDistance() { @@ -167,8 +534,7 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { BlockPos origin = camera.getBlockPosition(); if (spectator && this.level.getBlockState(origin) - .isSolidRender()) - { + .isSolidRender()) { useOcclusionCulling = false; } else { useOcclusionCulling = Minecraft.getInstance().smartCull; @@ -176,12 +542,8 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { return useOcclusionCulling; } - private void resetRenderLists() { - this.renderLists = SortedRenderLists.empty(); - - for (var list : this.taskLists.values()) { - list.clear(); - } + public void beforeSectionUpdates() { + this.renderableSectionTree.ensureCapacity(this.getRenderDistance()); } public void onSectionAdded(int x, int y, int z) { @@ -204,13 +566,14 @@ public void onSectionAdded(int x, int y, int z) { if (section.hasOnlyAir()) { this.updateSectionInfo(renderSection, BuiltSectionInfo.EMPTY); } else { - renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD); + this.renderableSectionTree.add(renderSection); + renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD, this.lastFrameAtTime); } this.connectNeighborNodes(renderSection); // force update to schedule build task - this.needsGraphUpdate = true; + this.markGraphDirty(); } public void onSectionRemoved(int x, int y, int z) { @@ -221,6 +584,8 @@ public void onSectionRemoved(int x, int y, int z) { return; } + this.renderableSectionTree.remove(x, y, z); + if (section.getTranslucentData() != null) { this.sortTriggering.removeSection(section.getTranslucentData(), sectionPos); } @@ -237,7 +602,7 @@ public void onSectionRemoved(int x, int y, int z) { section.delete(); // force update to remove section from render lists - this.needsGraphUpdate = true; + this.markGraphDirty(); } public void renderLayer(ChunkRenderMatrices matrices, TerrainRenderPass pass, double x, double y, double z) { @@ -263,13 +628,7 @@ public void tickVisibleRenders() { } while (iterator.hasNext()) { - var section = region.getSection(iterator.nextByteAsInt()); - - if (section == null) { - continue; - } - - var sprites = section.getAnimatedSprites(); + var sprites = region.getAnimatedSprites(iterator.nextByteAsInt()); if (sprites == null) { continue; @@ -282,14 +641,20 @@ public void tickVisibleRenders() { } } - public boolean isSectionVisible(int x, int y, int z) { - RenderSection render = this.getRenderSection(x, y, z); + private boolean isSectionEmpty(int x, int y, int z) { + long key = SectionPos.asLong(x, y, z); + RenderSection section = this.sectionByPosition.get(key); - if (render == null) { - return false; + if (section == null) { + return true; } - return render.getLastVisibleFrame() == this.lastUpdatedFrame; + return !section.needsRender(); + } + + // renderTree is not necessarily frustum-filtered but that is ok since the caller makes sure to eventually also perform a frustum test on the box being tested (see EntityRendererMixin) + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + return this.renderTree == null || this.renderTree.isBoxVisible(x1, y1, z1, x2, y2, z2, this::isSectionEmpty); } public void uploadChunks() { @@ -303,7 +668,9 @@ public void uploadChunks() { // (sort results never change the graph) // generally there's no sort results without a camera movement, which would also trigger // a graph update, but it can sometimes happen because of async task execution - this.needsGraphUpdate |= this.processChunkBuildResults(results); + if (this.processChunkBuildResults(results)) { + this.markGraphDirty(); + } for (var result : results) { result.destroy(); @@ -321,6 +688,10 @@ private boolean processChunkBuildResults(ArrayList results) { if (result instanceof ChunkBuildOutput chunkBuildOutput) { touchedSectionInfo |= this.updateSectionInfo(result.render, chunkBuildOutput.info); + var resultSize = chunkBuildOutput.getResultSize(); + result.render.setLastMeshResultSize(resultSize); + this.meshTaskSizeEstimator.addData(MeshResultSize.forSection(result.render, resultSize)); + if (chunkBuildOutput.translucentData != null) { this.sortTriggering.integrateTranslucentData(oldData, chunkBuildOutput.translucentData, this.cameraPosition, this::scheduleSort); @@ -344,10 +715,18 @@ private boolean processChunkBuildResults(ArrayList results) { result.render.setLastUploadFrame(result.submitTime); } + this.meshTaskSizeEstimator.updateModels(); + return touchedSectionInfo; } private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) { + if (info == null || !RenderSectionFlags.needsRender(info.flags)) { + this.renderableSectionTree.remove(render); + } else { + this.renderableSectionTree.add(render); + } + var infoChanged = render.setInfo(info); if (info == null || ArrayUtils.isEmpty(info.globalBlockEntities)) { @@ -357,7 +736,7 @@ private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) { } } - private static List filterChunkBuildResults(ArrayList outputs) { + private List filterChunkBuildResults(ArrayList outputs) { var map = new Reference2ReferenceLinkedOpenHashMap(); for (var output : outputs) { @@ -379,12 +758,19 @@ private static List filterChunkBuildResults(ArrayList collectChunkBuildResults() { ArrayList results = new ArrayList<>(); + ChunkJobResult result; while ((result = this.buildResults.poll()) != null) { results.add(result.unwrap()); + var jobEffort = result.getJobEffort(); + if (jobEffort != null) { + this.jobDurationEstimator.addData(jobEffort); + } } + this.jobDurationEstimator.updateModels(); + return results; } @@ -393,7 +779,11 @@ public void cleanupAndFlip() { this.regions.update(); } - public void updateChunks(boolean updateImmediately) { + public void updateChunks(Viewport viewport, boolean updateImmediately) { + this.thisFrameBlockingTasks = 0; + this.nextFrameBlockingTasks = 0; + this.deferredTasks = 0; + var thisFrameBlockingCollector = this.lastBlockingCollector; this.lastBlockingCollector = null; if (thisFrameBlockingCollector == null) { @@ -403,24 +793,29 @@ public void updateChunks(boolean updateImmediately) { if (updateImmediately) { // for a perfect frame where everything is finished use the last frame's blocking collector // and add all tasks to it so that they're waited on - this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector, Long.MAX_VALUE, viewport); + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { + var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); + var remainingUploadSize = this.regions.getStagingBuffer().getUploadSizeLimit(this.averageFrameDuration); + var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); - var deferredCollector = new ChunkJobCollector( - this.builder.getHighEffortSchedulingBudget(), - this.builder.getLowEffortSchedulingBudget(), - this.buildResults::add); + var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. if (SodiumClientMod.options().performance.getSortBehavior().getDeferMode() == DeferMode.ZERO_FRAMES) { - this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector, remainingUploadSize, viewport); } else { - this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector, remainingUploadSize, viewport); } + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); + this.nextFrameBlockingTasks = nextFrameBlockingCollector.getSubmittedTaskCount(); + this.deferredTasks = deferredCollector.getSubmittedTaskCount(); + // wait on this frame's blocking collector which contains the important tasks from this frame // and semi-important tasks from the last frame thisFrameBlockingCollector.awaitCompletion(this.builder); @@ -431,79 +826,188 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( - ChunkJobCollector importantCollector, - ChunkJobCollector semiImportantCollector, - ChunkJobCollector deferredCollector) { - this.submitSectionTasks(importantCollector, ChunkUpdateType.IMPORTANT_SORT, true); - this.submitSectionTasks(semiImportantCollector, ChunkUpdateType.IMPORTANT_REBUILD, true); + ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector, long remainingUploadSize, Viewport viewport) { + remainingUploadSize = submitImportantSectionTasks(importantCollector, remainingUploadSize, DeferMode.ZERO_FRAMES, viewport); + remainingUploadSize = submitImportantSectionTasks(semiImportantCollector, remainingUploadSize, DeferMode.ONE_FRAME, viewport); + remainingUploadSize = submitImportantSectionTasks(deferredCollector, remainingUploadSize, DeferMode.ALWAYS, viewport); - // since the sort tasks are run last, the effort category can be ignored and - // simply fills up the remaining budget. Splitting effort categories is still - // important to prevent high effort tasks from using up the entire budget if it - // happens to divide evenly. - this.submitSectionTasks(deferredCollector, ChunkUpdateType.REBUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.INITIAL_BUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.SORT, true); + submitDeferredSectionTasks(deferredCollector, remainingUploadSize); } - private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType type, boolean ignoreEffortCategory) { - var queue = this.taskLists.get(type); + private static final LongPriorityQueue EMPTY_TASK_QUEUE = new LongPriorityQueue() { + @Override + public void enqueue(long x) { + throw new UnsupportedOperationException(); + } - while (!queue.isEmpty() && collector.hasBudgetFor(type.getTaskEffort(), ignoreEffortCategory)) { - RenderSection section = queue.remove(); + @Override + public long dequeueLong() { + throw new UnsupportedOperationException(); + } - if (section.isDisposed()) { - continue; + @Override + public long firstLong() { + throw new UnsupportedOperationException(); + } + + @Override + public LongComparator comparator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + }; + + private void submitDeferredSectionTasks(ChunkJobCollector collector, long remainingUploadSize) { + LongPriorityQueue frustumQueue = this.frustumTaskLists; + LongPriorityQueue globalQueue = this.globalTaskLists; + float frustumPriorityBias = 0; + float globalPriorityBias = 0; + + if (frustumQueue != null) { + frustumPriorityBias = this.frustumTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); + } else { + frustumQueue = EMPTY_TASK_QUEUE; + } + + if (globalQueue != null) { + globalPriorityBias = this.globalTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); + } else { + globalQueue = EMPTY_TASK_QUEUE; + } + + float frustumPriority = Float.POSITIVE_INFINITY; + float globalPriority = Float.POSITIVE_INFINITY; + long frustumItem = 0; + long globalItem = 0; + + while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && collector.hasBudgetRemaining() && remainingUploadSize > 0) { + // get the first item from the non-empty queues and see which one has higher priority. + // if the priority is not infinity, then the item priority was fetched the last iteration and doesn't need updating. + if (!frustumQueue.isEmpty() && Float.isInfinite(frustumPriority)) { + frustumItem = frustumQueue.firstLong(); + frustumPriority = PendingTaskCollector.decodePriority(frustumItem) + frustumPriorityBias; + } + if (!globalQueue.isEmpty() && Float.isInfinite(globalPriority)) { + globalItem = globalQueue.firstLong(); + globalPriority = PendingTaskCollector.decodePriority(globalItem) + globalPriorityBias; } - // stop if the section is in this list but doesn't have this update type - var pendingUpdate = section.getPendingUpdate(); - if (pendingUpdate != null && pendingUpdate != type) { - continue; + // pick the task with the higher priority, decode the section, and schedule its task if it exists + RenderSection section; + if (frustumPriority < globalPriority) { + frustumQueue.dequeueLong(); + frustumPriority = Float.POSITIVE_INFINITY; + + section = this.frustumTaskLists.decodeAndFetchSection(this.sectionByPosition, frustumItem); + } else { + globalQueue.dequeueLong(); + globalPriority = Float.POSITIVE_INFINITY; + + section = this.globalTaskLists.decodeAndFetchSection(this.sectionByPosition, globalItem); } - int frame = this.lastUpdatedFrame; - ChunkBuilderTask task; - if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { - task = this.createSortTask(section, frame); + if (section != null) { + remainingUploadSize -= submitSectionTask(collector, section); + } + } + } - if (task == null) { - // when a sort task is null it means the render section has no dynamic data and - // doesn't need to be sorted. Nothing needs to be done. + private long submitImportantSectionTasks(ChunkJobCollector collector, long remainingUploadSize, DeferMode deferMode, Viewport viewport) { + var it = this.importantTasks.get(deferMode).iterator(); + var importantRebuildDeferMode = SodiumClientMod.options().performance.chunkBuildDeferMode; + + while (it.hasNext() && collector.hasBudgetRemaining() && (deferMode.allowsUnlimitedUploadSize() || remainingUploadSize > 0)) { + var section = it.next(); + var pendingUpdate = section.getPendingUpdate(); + if (pendingUpdate != null && pendingUpdate.getDeferMode(importantRebuildDeferMode) == deferMode && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { + // isSectionVisible includes a special case for not testing empty sections against the tree as they won't be in it + if (this.renderTree == null || this.renderTree.isSectionVisible(viewport, section)) { + remainingUploadSize -= submitSectionTask(collector, section, pendingUpdate); + } else { + // don't remove if simply not visible currently but still relevant continue; } - } else { - task = this.createRebuildTask(section, frame); - - if (task == null) { - // if the section is empty or doesn't exist submit this null-task to set the - // built flag on the render section. - // It's important to use a NoData instead of null translucency data here in - // order for it to clear the old data from the translucency sorting system. - // This doesn't apply to sorting tasks as that would result in the section being - // marked as empty just because it was scheduled to be sorted and its dynamic - // data has since been removed. In that case simply nothing is done as the - // rebuild that must have happened in the meantime includes new non-dynamic - // index data. - var result = ChunkJobResult.successfully(new ChunkBuildOutput( - section, frame, NoData.forEmptySection(section.getPosition()), - BuiltSectionInfo.EMPTY, Collections.emptyMap())); - this.buildResults.add(result); - - section.setTaskCancellationToken(null); - } } + it.remove(); + } + + return remainingUploadSize; + } - if (task != null) { - var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); - collector.addSubmittedJob(job); + private long submitSectionTask(ChunkJobCollector collector, @NotNull RenderSection section) { + // don't schedule tasks for sections that don't need it anymore, + // since the pending update it cleared when a task is started, this includes + // sections for which there's a currently running task. + var type = section.getPendingUpdate(); + if (type == null) { + return 0; + } + + return submitSectionTask(collector, section, type); + } - section.setTaskCancellationToken(job); + private long submitSectionTask(ChunkJobCollector collector, @NotNull RenderSection section, ChunkUpdateType type) { + if (section.isDisposed()) { + return 0; + } + + ChunkBuilderTask task; + if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { + task = this.createSortTask(section, this.frame); + + if (task == null) { + // when a sort task is null it means the render section has no dynamic data and + // doesn't need to be sorted. Nothing needs to be done. + return 0; + } + } else { + task = this.createRebuildTask(section, this.frame); + + if (task == null) { + // if the section is empty or doesn't exist submit this null-task to set the + // built flag on the render section. + // It's important to use a NoData instead of null translucency data here in + // order for it to clear the old data from the translucency sorting system. + // This doesn't apply to sorting tasks as that would result in the section being + // marked as empty just because it was scheduled to be sorted and its dynamic + // data has since been removed. In that case simply nothing is done as the + // rebuild that must have happened in the meantime includes new non-dynamic + // index data. + var result = ChunkJobResult.successfully(new ChunkBuildOutput( + section, this.frame, NoData.forEmptySection(section.getPosition()), + BuiltSectionInfo.EMPTY, Collections.emptyMap())); + this.buildResults.add(result); + + section.setTaskCancellationToken(null); } + } + + var estimatedTaskSize = 0L; + if (task != null) { + var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); + collector.addSubmittedJob(job); + estimatedTaskSize = job.getEstimatedSize(); - section.setLastSubmittedFrame(frame); - section.setPendingUpdate(null); + section.setTaskCancellationToken(job); } + + section.setLastSubmittedFrame(this.frame); + section.clearPendingUpdate(); + return estimatedTaskSize; } public @Nullable ChunkBuilderMeshingTask createRebuildTask(RenderSection render, int frame) { @@ -513,25 +1017,23 @@ private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType typ return null; } - return new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); + var task = new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); + return task; } public ChunkBuilderSortingTask createSortTask(RenderSection render, int frame) { - return ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + var task = ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + if (task != null) { + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); + } + return task; } public void processGFNIMovement(CameraMovement movement) { this.sortTriggering.triggerSections(this::scheduleSort, movement); } - public void markGraphDirty() { - this.needsGraphUpdate = true; - } - - public boolean needsUpdate() { - return this.needsGraphUpdate; - } - public ChunkBuilder getBuilder() { return this.builder; } @@ -539,6 +1041,8 @@ public ChunkBuilder getBuilder() { public void destroy() { this.builder.shutdown(); // stop all the workers, and cancel any tasks + this.asyncCullExecutor.shutdownNow(); + for (var result : this.collectChunkBuildResults()) { result.destroy(); // delete resources for any pending tasks (including those that were cancelled) } @@ -548,7 +1052,8 @@ public void destroy() { } this.sectionsWithGlobalEntities.clear(); - this.resetRenderLists(); + + this.renderLists = SortedRenderLists.empty(); try (CommandList commandList = RenderDevice.INSTANCE.createCommandList()) { this.regions.delete(commandList); @@ -572,25 +1077,57 @@ public int getVisibleChunkCount() { return sections; } + private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { + var current = section.getPendingUpdate(); + type = ChunkUpdateType.getPromotedTypeChange(current, type); + + // if there was no change the upgraded type is null + if (type == null) { + return null; + } + + section.setPendingUpdate(type, this.lastFrameAtTime); + + // when the pending task type changes, and it's important, add it to the list of important tasks + if (type.isImportant()) { + this.importantTasks.get(type.getDeferMode(SodiumClientMod.options().performance.chunkBuildDeferMode)).add(section); + } else { + // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs + if (this.globalTaskTree != null && current == null) { + this.globalTaskTree.markSectionTask(section); + this.needsFrustumTaskListUpdate = true; + + // when a global cull task is already running and has already processed the section, and we mark it with a pending task, + // the section will not be marked as having a task in the then replaced global tree and the derivative frustum tree also won't have it. + // Sections that are marked with a pending task while a task that may replace the global task tree is running are a added to a list from which the new global task tree is populated once it's done. + if (this.pendingGlobalCullTask != null) { + this.concurrentlySubmittedTasks.add(section.getChunkX()); + this.concurrentlySubmittedTasks.add(section.getChunkY()); + this.concurrentlySubmittedTasks.add(section.getChunkZ()); + } + } + } + + return type; + } + public void scheduleSort(long sectionPos, boolean isDirectTrigger) { RenderSection section = this.sectionByPosition.get(sectionPos); if (section != null) { var pendingUpdate = ChunkUpdateType.SORT; var priorityMode = SodiumClientMod.options().performance.getSortBehavior().getPriorityMode(); - if (priorityMode == PriorityMode.ALL - || priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { + if (priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE) || priorityMode == PriorityMode.ALL) { pendingUpdate = ChunkUpdateType.IMPORTANT_SORT; } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); + + if (this.upgradePendingUpdate(section, pendingUpdate) != null) { section.prepareTrigger(isDirectTrigger); } } } - public void scheduleRebuild(int x, int y, int z, boolean important) { + public void scheduleRebuild(int x, int y, int z, boolean playerChanged) { RenderAsserts.validateCurrentThread(); this.sectionCache.invalidate(x, y, z); @@ -600,31 +1137,22 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { if (section != null && section.isBuilt()) { ChunkUpdateType pendingUpdate; - if (allowImportantRebuilds() && (important || this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE))) { + if (playerChanged && this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE)) { pendingUpdate = ChunkUpdateType.IMPORTANT_REBUILD; } else { pendingUpdate = ChunkUpdateType.REBUILD; } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); - - // force update to schedule rebuild task on this section - this.needsGraphUpdate = true; - } + this.upgradePendingUpdate(section, pendingUpdate); } } - private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); - private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); - private boolean shouldPrioritizeTask(RenderSection section, float distance) { - return this.cameraBlockPos != null && section.getSquaredDistance(this.cameraBlockPos) < distance; - } - - private static boolean allowImportantRebuilds() { - return !SodiumClientMod.options().performance.alwaysDeferChunkUpdates; + return this.cameraPosition != null && section.getSquaredDistance( + (float) this.cameraPosition.x(), + (float) this.cameraPosition.y(), + (float) this.cameraPosition.z() + ) < distance; } private float getEffectiveRenderDistance() { @@ -699,22 +1227,66 @@ public Collection getDebugStrings() { list.add(String.format("Geometry Pool: %d/%d MiB (%d buffers)", MathUtil.toMib(deviceUsed), MathUtil.toMib(deviceAllocated), count)); list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString())); - list.add(String.format("Chunk Builder: Permits=%02d (E %03d) | Busy=%02d | Total=%02d", - this.builder.getScheduledJobCount(), this.builder.getScheduledEffort(), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) + list.add(String.format("Chunk Builder: Schd=%02d | Busy=%02d (%04d%%) | Total=%02d", + this.builder.getScheduledJobCount(), this.builder.getBusyThreadCount(), (int) (this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getTotalThreadCount()) ); - list.add(String.format("Chunk Queues: U=%02d (P0=%03d | P1=%03d | P2=%03d)", - this.buildResults.size(), - this.taskLists.get(ChunkUpdateType.IMPORTANT_REBUILD).size() + this.taskLists.get(ChunkUpdateType.IMPORTANT_SORT).size(), - this.taskLists.get(ChunkUpdateType.REBUILD).size() + this.taskLists.get(ChunkUpdateType.SORT).size(), - this.taskLists.get(ChunkUpdateType.INITIAL_BUILD).size()) + list.add(String.format("Tasks: N0=%03d | N1=%03d | Def=%03d, Recv=%03d", + this.thisFrameBlockingTasks, this.nextFrameBlockingTasks, this.deferredTasks, this.buildResults.size()) ); + if (PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment()) { + var meshTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderMeshingTask.class); + var sortTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderSortingTask.class); + list.add(String.format("Duration: Mesh %s, Sort %s", meshTaskParameters, sortTaskParameters)); + + var sizeEstimates = new ReferenceArrayList(); + for (var type : MeshResultSize.SectionCategory.values()) { + sizeEstimates.add(String.format("%s=%s", type, this.meshTaskSizeEstimator.toString(type))); + } + list.add(String.format("Size: %s", String.join(", ", sizeEstimates))); + } + this.sortTriggering.addDebugStrings(list); + var taskSlots = new String[AsyncTaskType.VALUES.length]; + for (var task : this.pendingTasks) { + var type = task.getTaskType(); + taskSlots[type.ordinal()] = type.abbreviation; + } + list.add("Tree Builds: " + Arrays + .stream(taskSlots) + .map(slot -> slot == null ? "_" : slot) + .collect(Collectors.joining(" "))); + return list; } + public String getChunksDebugString() { + // C: visible/total D: distance + return String.format( + "C: %d/%d (%s) D: %d", + this.getVisibleChunkCount(), + this.getTotalSections(), + this.getCullTypeName(), + this.renderDistance); + } + + private String getCullTypeName() { + CullType renderTreeCullType = null; + for (var type : CullType.values()) { + if (this.cullResults.get(type) == this.renderTree) { + renderTreeCullType = type; + break; + } + } + var cullTypeName = "-"; + if (renderTreeCullType != null) { + cullTypeName = renderTreeCullType.abbreviation; + } + return cullTypeName; + } + public @NotNull SortedRenderLists getRenderLists() { return this.renderLists; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java new file mode 100644 index 0000000000..2057621bc1 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java @@ -0,0 +1,78 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public abstract class AsyncRenderTask implements Callable, CancellationToken { + protected final Viewport viewport; + protected final float buildDistance; + protected final int frame; + + private Future future; + private volatile int state; + + private static final int PENDING = 0; + private static final int RUNNING = 1; + private static final int CANCELLED = 2; + + protected AsyncRenderTask(Viewport viewport, float buildDistance, int frame) { + this.viewport = viewport; + this.buildDistance = buildDistance; + this.frame = frame; + } + + public void submitTo(ExecutorService executor) { + this.future = executor.submit(this); + } + + public boolean isDone() { + return this.future.isDone(); + } + + public int getFrame() { + return this.frame; + } + + public boolean isCancelled() { + return this.state == CANCELLED; + } + + @Override + public void setCancelled() { + this.state = CANCELLED; + } + + public boolean cancelIfNotStarted() { + if (this.state == PENDING) { + this.setCancelled(); + return true; + } + return false; + } + + public T getResult() { + try { + return this.future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to get result of render task", e); + } + } + + @Override + public T call() throws Exception { + if (this.state == CANCELLED) { + return null; + } + this.state = RUNNING; + return this.runTask(); + } + + protected abstract T runTask(); + + public abstract AsyncTaskType getTaskType(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java new file mode 100644 index 0000000000..6a0ddb433e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; + +public enum AsyncTaskType { + FRUSTUM_CULL(CullType.FRUSTUM.abbreviation), + REGULAR_CULL(CullType.REGULAR.abbreviation), + WIDE_CULL(CullType.WIDE.abbreviation), + FRUSTUM_TASK_COLLECTION("T"); + + public static final AsyncTaskType[] VALUES = values(); + + public final String abbreviation; + + AsyncTaskType(String abbreviation) { + this.abbreviation = abbreviation; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java new file mode 100644 index 0000000000..cca577f814 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class CullTask extends AsyncRenderTask { + protected final OcclusionCuller occlusionCuller; + protected final boolean useOcclusionCulling; + + protected CullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling) { + super(viewport, buildDistance, frame); + this.occlusionCuller = occlusionCuller; + this.useOcclusionCulling = useOcclusionCulling; + } + + public abstract CullType getCullType(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java new file mode 100644 index 0000000000..6715c2efdb --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; + +public interface FrustumCullResult extends FrustumTaskListsResult { + SectionTree getTree(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java new file mode 100644 index 0000000000..c477787210 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -0,0 +1,64 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public class FrustumCullTask extends CullTask { + private final Level level; + + public FrustumCullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, Level level) { + super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); + this.level = level; + } + + private static final LongArrayList timings = new LongArrayList(); + + @Override + public FrustumCullResult runTask() { + var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM, this.level); + + var start = System.nanoTime(); + + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); + tree.prepareForTraversal(); + + var end = System.nanoTime(); + var time = end - start; + timings.add(time); + if (timings.size() >= 500) { + var average = timings.longStream().average().orElse(0); + System.out.println("Frustum culling took " + (average) / 1000 + "µs over " + timings.size() + " samples"); + timings.clear(); + } + + var frustumTaskLists = tree.getPendingTaskLists(); + + return new FrustumCullResult() { + @Override + public SectionTree getTree() { + return tree; + } + + @Override + public DeferredTaskList getFrustumTaskLists() { + return frustumTaskLists; + } + }; + } + + @Override + public AsyncTaskType getTaskType() { + return AsyncTaskType.FRUSTUM_CULL; + } + + @Override + public CullType getCullType() { + return CullType.FRUSTUM; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java new file mode 100644 index 0000000000..772a712e3c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java @@ -0,0 +1,32 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class FrustumTaskCollectionTask extends AsyncRenderTask { + private final Long2ReferenceMap sectionByPosition; + private final TaskSectionTree globalTaskTree; + + public FrustumTaskCollectionTask(Viewport viewport, float buildDistance, int frame, Long2ReferenceMap sectionByPosition, TaskSectionTree globalTaskTree) { + super(viewport, buildDistance, frame); + this.sectionByPosition = sectionByPosition; + this.globalTaskTree = globalTaskTree; + } + + @Override + public FrustumTaskListsResult runTask() { + var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); + this.globalTaskTree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); + + var frustumTaskLists = collector.getPendingTaskLists(); + return () -> frustumTaskLists; + } + + @Override + public AsyncTaskType getTaskType() { + return AsyncTaskType.FRUSTUM_TASK_COLLECTION; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java new file mode 100644 index 0000000000..439fc230d7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; + +public interface FrustumTaskListsResult { + DeferredTaskList getFrustumTaskLists(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java new file mode 100644 index 0000000000..c3f6f35715 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java @@ -0,0 +1,10 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; + +public interface GlobalCullResult extends FrustumTaskListsResult { + TaskSectionTree getTaskTree(); + + DeferredTaskList getGlobalTaskLists(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java new file mode 100644 index 0000000000..46869001ef --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -0,0 +1,83 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public class GlobalCullTask extends CullTask { + private final Long2ReferenceMap sectionByPosition; + private final CullType cullType; + private final Level level; + + public GlobalCullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, Long2ReferenceMap sectionByPosition, CullType cullType, Level level) { + super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); + this.sectionByPosition = sectionByPosition; + this.cullType = cullType; + this.level = level; + } + + private static final LongArrayList timings = new LongArrayList(); + + @Override + public GlobalCullResult runTask() { + var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType, this.level); + + var start = System.nanoTime(); + + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); + tree.prepareForTraversal(); + + var end = System.nanoTime(); + var time = end - start; + timings.add(time); + if (timings.size() >= 500) { + var average = timings.longStream().average().orElse(0); + System.out.println("Global culling took " + (average) / 1000 + "µs over " + timings.size() + " samples"); + timings.clear(); + } + + var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); + tree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); + + var globalTaskLists = tree.getPendingTaskLists(); + var frustumTaskLists = collector.getPendingTaskLists(); + + return new GlobalCullResult() { + @Override + public TaskSectionTree getTaskTree() { + return tree; + } + + @Override + public DeferredTaskList getFrustumTaskLists() { + return frustumTaskLists; + } + + @Override + public DeferredTaskList getGlobalTaskLists() { + return globalTaskLists; + } + }; + } + + @Override + public AsyncTaskType getTaskType() { + return switch (this.cullType) { + case WIDE -> AsyncTaskType.WIDE_CULL; + case REGULAR -> AsyncTaskType.REGULAR_CULL; + default -> throw new IllegalStateException("Unexpected value: " + this.cullType); + }; + } + + @Override + public CullType getCullType() { + return this.cullType; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java index 102f93727a..89fcc25d5f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java @@ -1,10 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; public abstract class BuilderTaskOutput { public final RenderSection render; public final int submitTime; + private long resultSize = MeshResultSize.NO_DATA; public BuilderTaskOutput(RenderSection render, int buildTime) { this.render = render; @@ -13,4 +15,13 @@ public BuilderTaskOutput(RenderSection render, int buildTime) { public void destroy() { } + + protected abstract long calculateResultSize(); + + public long getResultSize() { + if (this.resultSize == MeshResultSize.NO_DATA) { + this.resultSize = this.calculateResultSize(); + } + return this.resultSize; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java index 88102d1d16..20f075681d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java @@ -40,4 +40,17 @@ public void destroy() { data.getVertexData().free(); } } + + private long getMeshSize() { + long size = 0; + for (var data : this.meshes.values()) { + size += data.getVertexData().getLength(); + } + return size; + } + + @Override + public long calculateResultSize() { + return super.calculateResultSize() + this.getMeshSize(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java index 52236a161e..e782dafb8e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java @@ -54,4 +54,9 @@ public void destroy() { this.indexBuffer.free(); } } + + @Override + protected long calculateResultSize() { + return this.indexBuffer == null ? 0 : this.indexBuffer.getLength(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java new file mode 100644 index 0000000000..02c3a71eb8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java @@ -0,0 +1,89 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.util.MathUtil; + +import java.util.Locale; + +public abstract class Average1DEstimator extends Estimator, Average1DEstimator.ValueBatch, Void, Long, Average1DEstimator.Average> { + private final float newDataRatio; + private final long initialEstimate; + + public Average1DEstimator(float newDataRatio, long initialEstimate) { + this.newDataRatio = newDataRatio; + this.initialEstimate = initialEstimate; + } + + public interface Value extends DataPoint { + long value(); + } + + protected static class ValueBatch implements Estimator.DataBatch> { + private long valueSum; + private long count; + + @Override + public void addDataPoint(Value input) { + this.valueSum += input.value(); + this.count++; + } + + @Override + public void reset() { + this.valueSum = 0; + this.count = 0; + } + + public float getAverage() { + return ((float) this.valueSum) / this.count; + } + } + + @Override + protected ValueBatch createNewDataBatch() { + return new ValueBatch<>(); + } + + protected static class Average implements Estimator.Model, Average> { + private final float newDataRatio; + private boolean hasRealData = false; + private float average; + + public Average(float newDataRatio, float initialValue) { + this.average = initialValue; + this.newDataRatio = newDataRatio; + } + + @Override + public Average update(ValueBatch batch) { + if (batch.count > 0) { + if (this.hasRealData) { + this.average = MathUtil.exponentialMovingAverage(this.average, batch.getAverage(), this.newDataRatio); + } else { + this.average = batch.getAverage(); + this.hasRealData = true; + } + } + + return this; + } + + @Override + public Long predict(Void input) { + return (long) this.average; + } + + @Override + public String toString() { + return String.format(Locale.US, "%.0f", this.average); + } + } + + @Override + protected Average createNewModel() { + return new Average<>(this.newDataRatio, this.initialEstimate); + } + + public Long predict(C category) { + return super.predict(category, null); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java new file mode 100644 index 0000000000..c9abad07a8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java @@ -0,0 +1,91 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import java.util.Map; + +/** + * This generic model learning class that can be used to estimate values based on a set of data points. It performs batch-wise model updates. The actual data aggregation and model updates are delegated to the implementing classes. The estimator stores multiple models in a map, one for each category. + * + * @param The type of the category key + * @param A data point contains a category and one piece of data + * @param A data batch contains multiple data points + * @param The input to the model + * @param The output of the model + * @param The model that is used to predict values + */ +public abstract class Estimator< + C, + D extends Estimator.DataPoint, + B extends Estimator.DataBatch, + I, + O, + M extends Estimator.Model> { + protected final Map models = createMap(); + protected final Map batches = createMap(); + + protected interface DataBatch { + void addDataPoint(D input); + + void reset(); + } + + protected interface DataPoint { + C category(); + } + + protected interface Model> { + M update(B batch); + + O predict(I input); + } + + protected abstract B createNewDataBatch(); + + protected abstract M createNewModel(); + + protected abstract Map createMap(); + + public void addData(D data) { + var category = data.category(); + var batch = this.batches.get(category); + if (batch == null) { + batch = this.createNewDataBatch(); + this.batches.put(category, batch); + } + batch.addDataPoint(data); + } + + private M ensureModel(C category) { + var model = this.models.get(category); + if (model == null) { + model = this.createNewModel(); + this.models.put(category, model); + } + return model; + } + + public void updateModels() { + this.batches.forEach((category, aggregator) -> { + var oldModel = this.ensureModel(category); + + // update the model and store it back if it returned a new model + var newModel = oldModel.update(aggregator); + if (newModel != oldModel) { + this.models.put(category, newModel); + } + + aggregator.reset(); + }); + } + + public O predict(C category, I input) { + return this.ensureModel(category).predict(input); + } + + public String toString(C category) { + var model = this.models.get(category); + if (model == null) { + return "-"; + } + return model.toString(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java new file mode 100644 index 0000000000..0f83a902c9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -0,0 +1,24 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceArrayMap; + +import java.util.Map; + +public class JobDurationEstimator extends Linear2DEstimator> { + public static final int INITIAL_SAMPLE_TARGET = 100; + public static final float NEW_DATA_RATIO = 0.05f; + private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; // 5ms + + public JobDurationEstimator() { + super(NEW_DATA_RATIO, INITIAL_SAMPLE_TARGET, INITIAL_JOB_DURATION_ESTIMATE); + } + + public long estimateJobDuration(Class jobType, long effort) { + return this.predict(jobType, effort); + } + + @Override + protected Map, T> createMap() { + return new Reference2ReferenceArrayMap<>(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java new file mode 100644 index 0000000000..2a35dda343 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public record JobEffort(Class category, long duration, long effort) implements Linear2DEstimator.DataPair> { + public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { + return new JobEffort(effortType,System.nanoTime() - start, effort); + } + + @Override + public long x() { + return this.effort; + } + + @Override + public long y() { + return this.duration; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java new file mode 100644 index 0000000000..159d08f067 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java @@ -0,0 +1,143 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import java.util.Locale; + +public abstract class Linear2DEstimator extends Estimator, Linear2DEstimator.LinearRegressionBatch, Long, Long, Linear2DEstimator.LinearFunction> { + private final float newDataRatio; + private final int initialSampleTarget; + private final long initialOutput; + + public Linear2DEstimator(float newDataRatio, int initialSampleTarget, long initialOutput) { + this.newDataRatio = newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.initialOutput = initialOutput; + } + + public interface DataPair extends DataPoint { + long x(); + + long y(); + } + + protected static class LinearRegressionBatch extends ObjectArrayList> implements Estimator.DataBatch> { + @Override + public void addDataPoint(DataPair input) { + this.add(input); + } + + @Override + public void reset() { + this.clear(); + } + } + + @Override + protected LinearRegressionBatch createNewDataBatch() { + return new LinearRegressionBatch<>(); + } + + protected static class LinearFunction implements Model, LinearFunction> { + // the maximum fraction of the total weight that new data can have + private final float newDataRatioInv; + // how many samples we want to have at least before we start diminishing the new data's weight + private final int initialSampleTarget; + private final long initialOutput; + + private float yIntercept; + private float slope; + + private int gatheredSamples = 0; + private float xMeanOld = 0; + private float yMeanOld = 0; + private float covarianceOld = 0; + private float varianceOld = 0; + + public LinearFunction(float newDataRatio, int initialSampleTarget, long initialOutput) { + this.newDataRatioInv = 1.0f / newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.initialOutput = initialOutput; + } + + @Override + public LinearFunction update(LinearRegressionBatch batch) { + if (batch.isEmpty()) { + return this; + } + + // condition the weight to gather at least the initial sample target, and then weight the new data with a ratio + var newDataSize = batch.size(); + var totalSamples = this.gatheredSamples + newDataSize; + float oldDataWeight; + float totalWeight; + if (totalSamples <= this.initialSampleTarget) { + totalWeight = totalSamples; + oldDataWeight = this.gatheredSamples; + this.gatheredSamples = totalSamples; + } else { + oldDataWeight = newDataSize * this.newDataRatioInv - newDataSize; + totalWeight = oldDataWeight + newDataSize; + } + + var totalWeightInv = 1.0f / totalWeight; + + // calculate the weighted mean along both axes + long xSum = 0; + long ySum = 0; + for (var data : batch) { + xSum += data.x(); + ySum += data.y(); + } + var xMean = (this.xMeanOld * oldDataWeight + xSum) * totalWeightInv; + var yMean = (this.yMeanOld * oldDataWeight + ySum) * totalWeightInv; + + // the covariance and variance are calculated from the differences to the mean + var covarianceSum = 0.0f; + var varianceSum = 0.0f; + for (var data : batch) { + var xDelta = data.x() - xMean; + var yDelta = data.y() - yMean; + covarianceSum += xDelta * yDelta; + varianceSum += xDelta * xDelta; + } + + if (varianceSum == 0) { + return this; + } + + covarianceSum += this.covarianceOld * oldDataWeight; + varianceSum += this.varianceOld * oldDataWeight; + + // negative slopes are clamped to produce a flat line if necessary + this.slope = Math.max(0, covarianceSum / varianceSum); + this.yIntercept = yMean - this.slope * xMean; + + this.xMeanOld = xMean; + this.yMeanOld = yMean; + this.covarianceOld = covarianceSum * totalWeightInv; + this.varianceOld = varianceSum * totalWeightInv; + + return this; + } + + @Override + public Long predict(Long input) { + if (this.gatheredSamples == 0) { + return this.initialOutput; + } + + return (long) (this.yIntercept + this.slope * input); + } + + @Override + public String toString() { + return String.format(Locale.US, "s=%.2f,y=%.0f", this.slope, this.yIntercept); + } + } + + @Override + protected LinearFunction createNewModel() { + return new LinearFunction<>(this.newDataRatio, this.initialSampleTarget, this.initialOutput); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java new file mode 100644 index 0000000000..e4d976bac0 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java @@ -0,0 +1,44 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public record MeshResultSize(SectionCategory category, long resultSize) implements Average1DEstimator.Value { + public static long NO_DATA = -1; + + public enum SectionCategory { + LOW, + UNDERGROUND, + WATER_LEVEL, + SURFACE, + HIGH; + + public static SectionCategory forSection(RenderSection section) { + var sectionY = section.getChunkY(); + if (sectionY < 0) { + return LOW; + } else if (sectionY < 3) { + return UNDERGROUND; + } else if (sectionY == 3) { + return WATER_LEVEL; + } else if (sectionY < 7) { + return SURFACE; + } else { + return HIGH; + } + } + } + + public static MeshResultSize forSection(RenderSection section, long resultSize) { + return new MeshResultSize(SectionCategory.forSection(section), resultSize); + } + + @Override + public SectionCategory category() { + return this.category; + } + + @Override + public long value() { + return this.resultSize; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java new file mode 100644 index 0000000000..67809d61d8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java @@ -0,0 +1,28 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; + +import java.util.EnumMap; +import java.util.Map; + +public class MeshTaskSizeEstimator extends Average1DEstimator { + public static final float NEW_DATA_RATIO = 0.02f; + + public MeshTaskSizeEstimator() { + super(NEW_DATA_RATIO, RenderRegion.SECTION_BUFFER_ESTIMATE); + } + + public long estimateSize(RenderSection section) { + var lastResultSize = section.getLastMeshResultSize(); + if (lastResultSize != MeshResultSize.NO_DATA) { + return lastResultSize; + } + return this.predict(MeshResultSize.SectionCategory.forSection(section)); + } + + @Override + protected Map createMap() { + return new EnumMap<>(MeshResultSize.SectionCategory.class); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index e251f27e89..4b894728c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -20,30 +20,11 @@ import java.util.function.Consumer; public class ChunkBuilder { - /** - * The low and high efforts given to the sorting and meshing tasks, - * respectively. This split into two separate effort categories means more - * sorting tasks, which are faster, can be scheduled compared to mesh tasks. - * These values need to capture that there's a limit to how much data can be - * uploaded per frame. Since sort tasks generate index data, which is smaller - * per quad and (on average) per section, more of their results can be uploaded - * in one frame. This number should essentially be a conservative estimate of - * min((mesh task upload size) / (sort task upload size), (mesh task time) / - * (sort task time)). - */ - public static final int HIGH_EFFORT = 10; - public static final int LOW_EFFORT = 1; - public static final int EFFORT_PER_THREAD_PER_FRAME = HIGH_EFFORT + LOW_EFFORT; - private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_PER_THREAD_PER_FRAME; - static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); private final ChunkJobQueue queue = new ChunkJobQueue(); - private final List threads = new ArrayList<>(); - private final AtomicInteger busyThreadCount = new AtomicInteger(); - private final ChunkBuildContext localContext; public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { @@ -69,16 +50,8 @@ public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { * Returns the remaining effort for tasks which should be scheduled this frame. If an attempt is made to * spawn more tasks than the budget allows, it will block until resources become available. */ - private int getTotalRemainingBudget() { - return Math.max(0, this.threads.size() * EFFORT_PER_THREAD_PER_FRAME - this.queue.getEffortSum()); - } - - public int getHighEffortSchedulingBudget() { - return Math.max(HIGH_EFFORT, (int) (this.getTotalRemainingBudget() * HIGH_EFFORT_BUDGET_FACTOR)); - } - - public int getLowEffortSchedulingBudget() { - return Math.max(LOW_EFFORT, this.getTotalRemainingBudget() - this.getHighEffortSchedulingBudget()); + public long getTotalRemainingDuration(long durationPerThread) { + return Math.max(0, this.threads.size() * durationPerThread - this.queue.getJobDurationSum()); } /** @@ -172,8 +145,8 @@ public int getScheduledJobCount() { return this.queue.size(); } - public int getScheduledEffort() { - return this.queue.getEffortSum(); + public float getBusyFraction(long frameDuration) { + return (float) this.queue.getJobDurationSum() / (frameDuration * this.threads.size()); } public int getBusyThreadCount() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java index a0bcb33505..65dd30a5fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java @@ -8,5 +8,7 @@ public interface ChunkJob extends CancellationToken { boolean isStarted(); - int getEffort(); + long getEstimatedSize(); + + long getEstimatedDuration(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index 69701b0a26..5566a97e06 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -11,25 +11,16 @@ public class ChunkJobCollector { private final Semaphore semaphore = new Semaphore(0); private final Consumer> collector; private final List submitted = new ArrayList<>(); - private int submittedHighEffort = 0; - private int submittedLowEffort = 0; - private final int highEffortBudget; - private final int lowEffortBudget; - private final boolean unlimitedBudget; + private long duration; public ChunkJobCollector(Consumer> collector) { - this.unlimitedBudget = true; - this.highEffortBudget = 0; - this.lowEffortBudget = 0; + this.duration = Long.MAX_VALUE; this.collector = collector; } - public ChunkJobCollector(int highEffortBudget, int lowEffortBudget, - Consumer> collector) { - this.unlimitedBudget = false; - this.highEffortBudget = highEffortBudget; - this.lowEffortBudget = lowEffortBudget; + public ChunkJobCollector(long duration, Consumer> collector) { + this.duration = duration; this.collector = collector; } @@ -56,28 +47,14 @@ public void awaitCompletion(ChunkBuilder builder) { public void addSubmittedJob(ChunkJob job) { this.submitted.add(job); + this.duration -= job.getEstimatedDuration(); + } - if (this.unlimitedBudget) { - return; - } - var effort = job.getEffort(); - if (effort <= ChunkBuilder.LOW_EFFORT) { - this.submittedLowEffort += effort; - } else { - this.submittedHighEffort += effort; - } + public boolean hasBudgetRemaining() { + return this.duration > 0; } - public boolean hasBudgetFor(int effort, boolean ignoreEffortCategory) { - if (this.unlimitedBudget) { - return true; - } - if (ignoreEffortCategory) { - return this.submittedLowEffort + this.submittedHighEffort + effort - <= this.highEffortBudget + this.lowEffortBudget; - } - return effort <= ChunkBuilder.LOW_EFFORT - ? this.submittedLowEffort + effort <= this.lowEffortBudget - : this.submittedHighEffort + effort <= this.highEffortBudget; + public int getSubmittedTaskCount() { + return this.submitted.size(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java index 0e6c2b9aa2..2f7564eef4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java @@ -7,12 +7,12 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; class ChunkJobQueue { private final ConcurrentLinkedDeque jobs = new ConcurrentLinkedDeque<>(); - private final AtomicInteger jobEffortSum = new AtomicInteger(); + private final AtomicLong jobDurationSum = new AtomicLong(); private final Semaphore semaphore = new Semaphore(0); @@ -30,7 +30,7 @@ public void add(ChunkJob job, boolean important) { } else { this.jobs.addLast(job); } - this.jobEffortSum.addAndGet(job.getEffort()); + this.jobDurationSum.addAndGet(job.getEstimatedDuration()); this.semaphore.release(1); } @@ -45,7 +45,7 @@ public ChunkJob waitForNextJob() throws InterruptedException { var job = this.getNextTask(); if (job != null) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } return job; } @@ -58,7 +58,7 @@ public boolean stealJob(ChunkJob job) { var success = this.jobs.remove(job); if (success) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } else { // If we didn't manage to actually steal the task, then we need to release the permit which we did steal this.semaphore.release(1); @@ -89,7 +89,7 @@ public Collection shutdown() { // force the worker threads to wake up and exit this.semaphore.release(Runtime.getRuntime().availableProcessors()); - this.jobEffortSum.set(0); + this.jobDurationSum.set(0); return list; } @@ -98,8 +98,8 @@ public int size() { return this.semaphore.availablePermits(); } - public int getEffortSum() { - return this.jobEffortSum.get(); + public long getJobDurationSum() { + return this.jobDurationSum.get(); } public boolean isEmpty() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java index 5619855494..2a1b98ac54 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java @@ -1,22 +1,29 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.minecraft.ReportedException; public class ChunkJobResult { private final OUTPUT output; private final Throwable throwable; + private final JobEffort jobEffort; - private ChunkJobResult(OUTPUT output, Throwable throwable) { + private ChunkJobResult(OUTPUT output, Throwable throwable, JobEffort jobEffort) { this.output = output; this.throwable = throwable; + this.jobEffort = jobEffort; } public static ChunkJobResult exceptionally(Throwable throwable) { - return new ChunkJobResult<>(null, throwable); + return new ChunkJobResult<>(null, throwable, null); + } + + public static ChunkJobResult successfully(OUTPUT output, JobEffort jobEffort) { + return new ChunkJobResult<>(output, null, jobEffort); } public static ChunkJobResult successfully(OUTPUT output) { - return new ChunkJobResult<>(output, null); + return new ChunkJobResult<>(output, null, null); } public OUTPUT unwrap() { @@ -29,4 +36,8 @@ public OUTPUT unwrap() { return this.output; } + + public JobEffort getJobEffort() { + return this.jobEffort; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java index 78e1242803..97f877fba4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import java.util.function.Consumer; @@ -42,6 +43,7 @@ public void execute(ChunkBuildContext context) { ChunkJobResult result; try { + var start = System.nanoTime(); var output = this.task.execute(context, this); // Task was cancelled while executing @@ -49,7 +51,7 @@ public void execute(ChunkBuildContext context) { return; } - result = ChunkJobResult.successfully(output); + result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, output.getResultSize())); } catch (Throwable throwable) { result = ChunkJobResult.exceptionally(throwable); ChunkBuilder.LOGGER.error("Chunk build failed", throwable); @@ -68,7 +70,12 @@ public boolean isStarted() { } @Override - public int getEffort() { - return this.task.getEffort(); + public long getEstimatedSize() { + return this.task.getEstimatedSize(); + } + + @Override + public long getEstimatedDuration() { + return this.task.getEstimatedDuration(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java index 75b7f7064c..24b50360d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java @@ -7,7 +7,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderCache; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; @@ -228,7 +228,7 @@ private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, Bl } @Override - public int getEffort() { - return ChunkBuilder.HIGH_EFFORT; + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return estimator.estimateSize(this.render); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java index d3178ceb8e..f0fc6ca110 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.Sorter; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicSorter; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import org.joml.Vector3dc; @@ -8,14 +9,13 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; public class ChunkBuilderSortingTask extends ChunkBuilderTask { - private final Sorter sorter; + private final DynamicSorter sorter; - public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, Sorter sorter) { + public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, DynamicSorter sorter) { super(render, frame, absoluteCameraPos); this.sorter = sorter; } @@ -43,7 +43,7 @@ public static ChunkBuilderSortingTask createTask(RenderSection render, int frame } @Override - public int getEffort() { - return ChunkBuilder.LOW_EFFORT; + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return this.sorter.getQuadCount(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java index 1735c23bb6..d30a23a0ee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java @@ -1,5 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import org.joml.Vector3dc; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -26,6 +28,9 @@ public abstract class ChunkBuilderTask impleme protected final Vector3dc absoluteCameraPos; protected final Vector3fc cameraPos; + private long estimatedSize; + private long estimatedDuration; + /** * Constructs a new build task for the given chunk and converts the absolute camera position to a relative position. While the absolute position is stored as a double vector, the relative position is stored as a float vector. * @@ -54,7 +59,20 @@ public ChunkBuilderTask(RenderSection render, int time, Vector3dc absoluteCamera */ public abstract OUTPUT execute(ChunkBuildContext context, CancellationToken cancellationToken); - public abstract int getEffort(); + public abstract long estimateTaskSizeWith(MeshTaskSizeEstimator estimator); + + public void calculateEstimations(JobDurationEstimator jobEstimator, MeshTaskSizeEstimator sizeEstimator) { + this.estimatedSize = this.estimateTaskSizeWith(sizeEstimator); + this.estimatedDuration = jobEstimator.estimateJobDuration(this.getClass(), this.estimatedSize); + } + + public long getEstimatedSize() { + return this.estimatedSize; + } + + public long getEstimatedDuration() { + return this.estimatedDuration; + } @Override public Vector3fc getRelativeCameraPos() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java index 25bf883a4d..bc3342a9f9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java @@ -41,15 +41,15 @@ private BuiltSectionInfo(@NotNull Collection blockRenderPasse int flags = 0; if (!blockRenderPasses.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_BLOCK_GEOMETRY; + flags |= RenderSectionFlags.MASK_HAS_BLOCK_GEOMETRY; } if (!culledBlockEntities.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_BLOCK_ENTITIES; + flags |= RenderSectionFlags.MASK_HAS_BLOCK_ENTITIES; } if (!animatedSprites.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_ANIMATED_SPRITES; + flags |= RenderSectionFlags.MASK_HAS_ANIMATED_SPRITES; } this.flags = flags; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java index e45638b8ad..ea212ae495 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java @@ -73,23 +73,22 @@ public void sortSections(SectionPos cameraPos, int[] sortItems) { } } - public void add(RenderSection render) { + public void add(int localSectionIndex) { if (this.size >= RenderRegion.REGION_SIZE) { throw new ArrayIndexOutOfBoundsException("Render list is full"); } this.size++; - int index = render.getSectionIndex(); - int flags = render.getFlags(); + int flags = this.region.getSectionFlags(localSectionIndex); - this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) index; + this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) localSectionIndex; this.sectionsWithGeometryCount += (flags >>> RenderSectionFlags.HAS_BLOCK_GEOMETRY) & 1; - this.sectionsWithSprites[this.sectionsWithSpritesCount] = (byte) index; + this.sectionsWithSprites[this.sectionsWithSpritesCount] = (byte) localSectionIndex; this.sectionsWithSpritesCount += (flags >>> RenderSectionFlags.HAS_ANIMATED_SPRITES) & 1; - this.sectionsWithEntities[this.sectionsWithEntitiesCount] = (byte) index; + this.sectionsWithEntities[this.sectionsWithEntitiesCount] = (byte) localSectionIndex; this.sectionsWithEntitiesCount += (flags >>> RenderSectionFlags.HAS_BLOCK_ENTITIES) & 1; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java new file mode 100644 index 0000000000..f8209de95c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java @@ -0,0 +1,45 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongCollection; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.minecraft.core.SectionPos; + +public class DeferredTaskList extends LongHeapPriorityQueue { + private final long creationTime; + private final boolean isFrustumTested; + private final int baseOffsetX; + private final int baseOffsetZ; + + public static DeferredTaskList createHeapCopyOf(LongCollection copyFrom, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { + return new DeferredTaskList(new LongArrayList(copyFrom), creationTime, isFrustumTested, baseOffsetX, baseOffsetZ); + } + + private DeferredTaskList(LongArrayList copyFrom, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { + super(copyFrom.elements(), copyFrom.size()); + this.creationTime = creationTime; + this.isFrustumTested = isFrustumTested; + this.baseOffsetX = baseOffsetX; + this.baseOffsetZ = baseOffsetZ; + } + + public float getCollectorPriorityBias(long now) { + // compensate for creation time of the list and whether the sections are in the frustum + return (now - this.creationTime) * PendingTaskCollector.PENDING_TIME_FACTOR + + (this.isFrustumTested ? PendingTaskCollector.WITHIN_FRUSTUM_BIAS : 0); + } + + public RenderSection decodeAndFetchSection(Long2ReferenceMap sectionByPosition, long encoded) { + var localX = (int) (encoded >>> 20) & 0b1111111111; + var localY = (int) (encoded >>> 10) & 0b1111111111; + var localZ = (int) (encoded & 0b1111111111); + + var globalX = localX + this.baseOffsetX; + var globalY = localY + PendingTaskCollector.SECTION_Y_MIN; + var globalZ = localZ + this.baseOffsetZ; + + return sectionByPosition.get(SectionPos.asLong(globalX, globalY, globalZ)); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java new file mode 100644 index 0000000000..70e1d33689 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class FallbackVisibleChunkCollector extends FrustumTaskCollector { + private final VisibleChunkCollectorAsync renderListCollector; + + public FallbackVisibleChunkCollector(Viewport viewport, float buildDistance, Long2ReferenceMap sectionByPosition, RenderRegionManager regions, int frame) { + super(viewport, buildDistance, sectionByPosition); + this.renderListCollector = new VisibleChunkCollectorAsync(regions, frame); + } + + public SortedRenderLists createRenderLists(Viewport viewport) { + return this.renderListCollector.createRenderLists(viewport); + } + + @Override + public void visit(int x, int y, int z) { + super.visit(x, y, z); + this.renderListCollector.visit(x, y, z); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java new file mode 100644 index 0000000000..ed19d03cb9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java @@ -0,0 +1,28 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; + +public class FrustumTaskCollector extends PendingTaskCollector implements SectionTree.VisibleSectionVisitor { + private final Long2ReferenceMap sectionByPosition; + + public FrustumTaskCollector(Viewport viewport, float buildDistance, Long2ReferenceMap sectionByPosition) { + super(viewport, buildDistance, true); + + this.sectionByPosition = sectionByPosition; + } + + @Override + public void visit(int x, int y, int z) { + var section = this.sectionByPosition.get(SectionPos.asLong(x, y, z)); + + if (section == null) { + return; + } + + this.checkForTask(section); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java new file mode 100644 index 0000000000..92fa3f3f0b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -0,0 +1,123 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.minecraft.util.Mth; + +public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { + public static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) + + // tunable parameters for the priority calculation. + // each "gained" point means a reduction in the final priority score (lowest score processed first) + static final float PENDING_TIME_FACTOR = -1.0f / 5_000_000_000.0f; // 1 point gained per 5s + static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum + static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away + static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied + static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // penalty for being CLOSE_DISTANCE or farther away + static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; + + private final LongArrayList pendingTasks = new LongArrayList(); + + protected final boolean isFrustumTested; + protected final int baseOffsetX, baseOffsetY, baseOffsetZ; + + protected final int cameraX, cameraY, cameraZ; + private final float invMaxDistance; + private final long creationTime; + + public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frustumTested) { + this.creationTime = System.nanoTime(); + this.isFrustumTested = frustumTested; + var offsetDistance = Mth.ceil(buildDistance / 16.0f) + 1; + + // the offset applied to section coordinates to encode their position in the octree + var sectionPos = viewport.getChunkCoord(); + var cameraSectionX = sectionPos.getX(); + var cameraSectionY = sectionPos.getY(); + var cameraSectionZ = sectionPos.getZ(); + this.baseOffsetX = cameraSectionX - offsetDistance; + this.baseOffsetY = cameraSectionY - offsetDistance; + this.baseOffsetZ = cameraSectionZ - offsetDistance; + + this.invMaxDistance = PROXIMITY_FACTOR / buildDistance; + + if (frustumTested) { + var blockPos = viewport.getBlockCoord(); + this.cameraX = blockPos.getX(); + this.cameraY = blockPos.getY(); + this.cameraZ = blockPos.getZ(); + } else { + this.cameraX = (cameraSectionX << 4); + this.cameraY = (cameraSectionY << 4); + this.cameraZ = (cameraSectionZ << 4); + } + } + + @Override + public void visit(RenderSection section) { + this.checkForTask(section); + } + + protected void checkForTask(RenderSection section) { + ChunkUpdateType type = section.getPendingUpdate(); + + // collect tasks even if they're important, whether they're actually important is decided later + if (type != null && section.getTaskCancellationToken() == null) { + this.addPendingSection(section, type); + } + } + + protected void addPendingSection(RenderSection section, ChunkUpdateType type) { + // start with a base priority value, lowest priority of task gets processed first + float priority = getSectionPriority(section, type); + + // encode the absolute position of the section + var localX = section.getChunkX() - this.baseOffsetX; + var localY = section.getChunkY() - SECTION_Y_MIN; + var localZ = section.getChunkZ() - this.baseOffsetZ; + long taskCoordinate = (long) (localX & 0b1111111111) << 20 | (long) (localY & 0b1111111111) << 10 | (long) (localZ & 0b1111111111); + + // encode the priority and the section position into a single long such that all parts can be later decoded + this.pendingTasks.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); + } + + private float getSectionPriority(RenderSection section, ChunkUpdateType type) { + float priority = type.getPriorityValue(); + + // calculate the relative distance to the camera + // alternatively: var distance = deltaX + deltaY + deltaZ; + var deltaX = Math.abs(section.getCenterX() - this.cameraX); + var deltaY = Math.abs(section.getCenterY() - this.cameraY); + var deltaZ = Math.abs(section.getCenterZ() - this.cameraZ); + var distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); + priority += distance * this.invMaxDistance; // distance / maxDistance * PROXIMITY_FACTOR + priority += Math.max(distance, CLOSE_DISTANCE) * INV_MAX_DISTANCE_CLOSE; + + // tasks that have been waiting for longer are more urgent + var taskPendingTimeNanos = this.creationTime - section.getPendingUpdateSince(); + priority += taskPendingTimeNanos * PENDING_TIME_FACTOR; // upgraded by one point every second + + // explain how priority was calculated +// System.out.println("Priority " + priority + " from: distance " + distance + " = " + (distance * this.invMaxDistance) + +// ", time " + taskPendingTimeNanos + " = " + (taskPendingTimeNanos * PENDING_TIME_FACTOR) + +// ", type " + type + " = " + type.getPriorityValue() + +// ", frustum " + this.isFrustumTested + " = " + (this.isFrustumTested ? WITHIN_FRUSTUM_BIAS : 0)); + + return priority; + } + + public static float decodePriority(long encoded) { + return MathUtil.comparableIntToFloat((int) (encoded >>> 32)); + } + + public DeferredTaskList getPendingTaskLists() { + return DeferredTaskList.createHeapCopyOf(this.pendingTasks, this.creationTime, this.isFrustumTested, this.baseOffsetX, this.baseOffsetZ); + } + +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java new file mode 100644 index 0000000000..156e66edfa --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java @@ -0,0 +1,54 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public interface RenderListProvider { + ObjectArrayList getUnsortedRenderLists(); + + int[] getCachedSortItems(); + + void setCachedSortItems(int[] sortItems); + + default SortedRenderLists createRenderLists(Viewport viewport) { + // sort the regions by distance to fix rare region ordering bugs + var sectionPos = viewport.getChunkCoord(); + var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; + var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; + var cameraZ = sectionPos.getZ() >> RenderRegion.REGION_LENGTH_SH; + + var unsortedRenderLists = this.getUnsortedRenderLists(); + var size = unsortedRenderLists.size(); + + var sortItems = this.getCachedSortItems(); + if (sortItems.length < size) { + sortItems = new int[size]; + this.setCachedSortItems(sortItems); + } + + for (var i = 0; i < size; i++) { + var region = unsortedRenderLists.get(i).getRegion(); + var x = Math.abs(region.getX() - cameraX); + var y = Math.abs(region.getY() - cameraY); + var z = Math.abs(region.getZ() - cameraZ); + sortItems[i] = (x + y + z) << 16 | i; + } + + IntArrays.unstableSort(sortItems, 0, size); + + var sorted = new ObjectArrayList(size); + for (var i = 0; i < size; i++) { + var key = sortItems[i]; + var renderList = unsortedRenderLists.get(key & 0xFFFF); + sorted.add(renderList); + } + + for (var list : sorted) { + list.sortSections(sectionPos, sortItems); + } + + return new SortedRenderLists(sorted); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java new file mode 100644 index 0000000000..da5eef6d3a --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -0,0 +1,46 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.TraversableForest; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public class TaskSectionTree extends RayOcclusionSectionTree { + private final TraversableForest taskTree; + private boolean taskTreeFinalized = false; + + public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, frame, cullType, level); + + this.taskTree = TraversableForest.createTraversableForest(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); + } + + public void markSectionTask(RenderSection section) { + this.taskTree.add(section); + this.taskTreeFinalized = false; + } + + public void markSectionTask(int x, int y, int z) { + this.taskTree.add(x, y, z); + this.taskTreeFinalized = false; + } + + @Override + protected void addPendingSection(RenderSection section, ChunkUpdateType type) { + super.addPendingSection(section, type); + + this.markSectionTask(section); + } + + public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + if (!this.taskTreeFinalized) { + this.taskTree.prepareForTraversal(); + this.taskTreeFinalized = true; + } + + this.taskTree.traverse(visitor, viewport, distanceLimit); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java deleted file mode 100644 index 89cf22cf78..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.lists; - -import it.unimi.dsi.fastutil.ints.IntArrays; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; -import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; -import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; -import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; - -import java.util.ArrayDeque; -import java.util.EnumMap; -import java.util.Map; -import java.util.Queue; - -/** - * The visible chunk collector is passed to the occlusion graph search culler to - * collect the visible chunks. - */ -public class VisibleChunkCollector implements OcclusionCuller.Visitor { - private final ObjectArrayList sortedRenderLists; - private final EnumMap> sortedRebuildLists; - - private final int frame; - - public VisibleChunkCollector(int frame) { - this.frame = frame; - - this.sortedRenderLists = new ObjectArrayList<>(); - this.sortedRebuildLists = new EnumMap<>(ChunkUpdateType.class); - - for (var type : ChunkUpdateType.values()) { - this.sortedRebuildLists.put(type, new ArrayDeque<>()); - } - } - - @Override - public void visit(RenderSection section) { - // only process section (and associated render list) if it has content that needs rendering - if (section.getFlags() != 0) { - RenderRegion region = section.getRegion(); - ChunkRenderList renderList = region.getRenderList(); - - if (renderList.getLastVisibleFrame() != this.frame) { - renderList.reset(this.frame); - - this.sortedRenderLists.add(renderList); - } - - renderList.add(section); - } - - // always add to rebuild lists though, because it might just not be built yet - this.addToRebuildLists(section); - } - - private void addToRebuildLists(RenderSection section) { - ChunkUpdateType type = section.getPendingUpdate(); - - if (type != null && section.getTaskCancellationToken() == null) { - Queue queue = this.sortedRebuildLists.get(type); - - if (queue.size() < type.getMaximumQueueSize()) { - queue.add(section); - } - } - } - - private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; - - public SortedRenderLists createRenderLists(Viewport viewport) { - // sort the regions by distance to fix rare region ordering bugs - var sectionPos = viewport.getChunkCoord(); - var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; - var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; - var cameraZ = sectionPos.getZ() >> RenderRegion.REGION_LENGTH_SH; - var size = this.sortedRenderLists.size(); - - if (sortItems.length < size) { - sortItems = new int[size]; - } - - for (var i = 0; i < size; i++) { - var region = this.sortedRenderLists.get(i).getRegion(); - var x = Math.abs(region.getX() - cameraX); - var y = Math.abs(region.getY() - cameraY); - var z = Math.abs(region.getZ() - cameraZ); - sortItems[i] = (x + y + z) << 16 | i; - } - - IntArrays.unstableSort(sortItems, 0, size); - - var sorted = new ObjectArrayList(size); - for (var i = 0; i < size; i++) { - var key = sortItems[i]; - var renderList = this.sortedRenderLists.get(key & 0xFFFF); - sorted.add(renderList); - } - - for (var list : sorted) { - list.sortSections(sectionPos, sortItems); - } - - return new SortedRenderLists(sorted); - } - - public Map> getRebuildLists() { - return this.sortedRebuildLists; - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java new file mode 100644 index 0000000000..1dc15df9ab --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -0,0 +1,67 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.LocalSectionIndex; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; + +/** + * The async visible chunk collector is passed into a section tree to collect visible chunks. + */ +public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor, RenderListProvider { + private final RenderRegionManager regions; + private final int frame; + + private final ObjectArrayList sortedRenderLists; + + public VisibleChunkCollectorAsync(RenderRegionManager regions, int frame) { + this.regions = regions; + this.frame = frame; + + this.sortedRenderLists = new ObjectArrayList<>(); + } + + @Override + public void visit(int x, int y, int z) { + var region = this.regions.getForChunk(x, y, z); + + // since this is async, the region might have been removed in the meantime + if (region == null) { + return; + } + + int rX = x & (RenderRegion.REGION_WIDTH - 1); + int rY = y & (RenderRegion.REGION_HEIGHT - 1); + int rZ = z & (RenderRegion.REGION_LENGTH - 1); + var sectionIndex = LocalSectionIndex.pack(rX, rY, rZ); + + ChunkRenderList renderList = region.getRenderList(); + + if (renderList.getLastVisibleFrame() != this.frame) { + renderList.reset(this.frame); + + this.sortedRenderLists.add(renderList); + } + + // flags don't need to be checked here since only sections with contents (RenderSectionFlags.MASK_NEEDS_RENDER) are added to the octree + renderList.add(sectionIndex); + } + + private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; + + @Override + public ObjectArrayList getUnsortedRenderLists() { + return this.sortedRenderLists; + } + + @Override + public int[] getCachedSortItems() { + return sortItems; + } + + @Override + public void setCachedSortItems(int[] sortItems) { + VisibleChunkCollectorAsync.sortItems = sortItems; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java new file mode 100644 index 0000000000..c307cd334d --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -0,0 +1,60 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +/** + * The sync visible chunk collector is passed into the graph search occlusion culler to collect visible chunks. + */ +public class VisibleChunkCollectorSync extends SectionTree implements RenderListProvider { + private final ObjectArrayList sortedRenderLists; + + public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, frame, cullType, level); + this.sortedRenderLists = new ObjectArrayList<>(); + } + + @Override + public void visit(RenderSection section) { + super.visit(section); + + RenderRegion region = section.getRegion(); + ChunkRenderList renderList = region.getRenderList(); + + // Even if a section does not have render objects, we must ensure the render list is initialized and put + // into the sorted queue of lists, so that we maintain the correct order of draw calls. + if (renderList.getLastVisibleFrame() != this.frame) { + renderList.reset(this.frame); + + this.sortedRenderLists.add(renderList); + } + + var index = section.getSectionIndex(); + if (region.sectionNeedsRender(index)) { + renderList.add(index); + } + } + + private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; + + @Override + public ObjectArrayList getUnsortedRenderLists() { + return this.sortedRenderLists; + } + + @Override + public int[] getCachedSortItems() { + return sortItems; + } + + @Override + public void setCachedSortItems(int[] sortItems) { + VisibleChunkCollectorSync.sortItems = sortItems; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java new file mode 100644 index 0000000000..5d34e9a4f5 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java @@ -0,0 +1,39 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import net.minecraft.client.Camera; +import net.minecraft.world.phys.Vec3; + +public class AsyncCameraTimingControl { + private static final double ENTER_SYNC_STEP_THRESHOLD = 32; + private static final double EXIT_SYNC_STEP_THRESHOLD = 20; + + private Vec3 previousPosition; + private boolean isSyncRendering = false; + + public boolean getShouldRenderSync(Camera camera) { + var cameraPosition = camera.getPosition(); + + if (this.previousPosition == null) { + this.previousPosition = cameraPosition; + return true; + } + + // if the camera moved too much, use sync rendering until it stops + var distance = Math.max( + Math.abs(cameraPosition.x - this.previousPosition.x), + Math.max( + Math.abs(cameraPosition.y - this.previousPosition.y), + Math.abs(cameraPosition.z - this.previousPosition.z) + ) + ); + if (this.isSyncRendering && distance <= EXIT_SYNC_STEP_THRESHOLD) { + this.isSyncRendering = false; + } else if (!this.isSyncRendering && distance >= ENTER_SYNC_STEP_THRESHOLD) { + this.isSyncRendering = true; + } + + this.previousPosition = cameraPosition; + + return this.isSyncRendering; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java new file mode 100644 index 0000000000..a925277218 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +public enum CullType { + WIDE("W", 1, false, false), + REGULAR("R", 0, false, false), + FRUSTUM("F", 0, true, true); + + public final String abbreviation; + public final int bfsWidth; + public final boolean isFrustumTested; + public final boolean isFogCulled; + + CullType(String abbreviation, int bfsWidth, boolean isFrustumTested, boolean isFogCulled) { + this.abbreviation = abbreviation; + this.bfsWidth = bfsWidth; + this.isFrustumTested = isFrustumTested; + this.isFogCulled = isFogCulled; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index a6550ff8ca..df88a4e274 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -7,66 +7,116 @@ import net.caffeinemc.mods.sodium.client.util.collections.DoubleBufferedQueue; import net.caffeinemc.mods.sodium.client.util.collections.ReadQueue; import net.caffeinemc.mods.sodium.client.util.collections.WriteQueue; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; import net.minecraft.world.level.Level; -import org.jetbrains.annotations.NotNull; +/* + * TODO idea: traverse octants of the world with separate threads for better performance? + */ public class OcclusionCuller { private final Long2ReferenceMap sections; private final Level level; - private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); + private volatile int tokenSource = 0; + + private int token; + private GraphOcclusionVisitor visitor; + private Viewport viewport; + private float searchDistance; + private boolean useOcclusionCulling; + + // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models + // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon + // to deal with floating point imprecision during a frustum check (see GH#2132). + public static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; + public static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + public static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; + + public interface GraphOcclusionVisitor { + default boolean visitTestVisible(RenderSection section) { + return true; + } + + void visit(RenderSection section); + + default boolean isWithinFrustum(Viewport viewport, RenderSection section) { + return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), + CHUNK_SECTION_RADIUS, CHUNK_SECTION_RADIUS, CHUNK_SECTION_RADIUS); + } + + default int getOutwardDirections(SectionPos origin, RenderSection section) { + int planes = 0; + + planes |= section.getChunkX() <= origin.getX() ? 1 << GraphDirection.WEST : 0; + planes |= section.getChunkX() >= origin.getX() ? 1 << GraphDirection.EAST : 0; + + planes |= section.getChunkY() <= origin.getY() ? 1 << GraphDirection.DOWN : 0; + planes |= section.getChunkY() >= origin.getY() ? 1 << GraphDirection.UP : 0; + + planes |= section.getChunkZ() <= origin.getZ() ? 1 << GraphDirection.NORTH : 0; + planes |= section.getChunkZ() >= origin.getZ() ? 1 << GraphDirection.SOUTH : 0; + + return planes; + } + } + public OcclusionCuller(Long2ReferenceMap sections, Level level) { this.sections = sections; this.level = level; } - public void findVisible(Visitor visitor, + public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, - int frame) - { + CancellationToken cancellationToken) { + this.visitor = visitor; + this.viewport = viewport; + this.searchDistance = searchDistance; + this.useOcclusionCulling = useOcclusionCulling; + final var queues = this.queue; queues.reset(); - this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); + // get a token for this bfs run by incrementing the counter. + // It doesn't need to be atomic since there's no concurrent access, but it needs to be synced to other threads. + this.token = this.tokenSource; + this.tokenSource = this.token + 1; + + this.init(queues.write()); - while (queues.flip()) { - processQueue(visitor, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); + while (this.queue.flip()) { + if (cancellationToken.isCancelled()) { + break; + } + + processQueue(this.queue.read(), this.queue.write()); } - this.addNearbySections(visitor, viewport, searchDistance, frame); + this.addNearbySections(visitor, viewport); + + this.visitor = null; + this.viewport = null; } - private static void processQueue(Visitor visitor, - Viewport viewport, - float searchDistance, - boolean useOcclusionCulling, - int frame, - ReadQueue readQueue, - WriteQueue writeQueue) - { + private void processQueue(ReadQueue readQueue, + WriteQueue writeQueue) { RenderSection section; + // only visible sections are entered into the queue while ((section = readQueue.dequeue()) != null) { - if (!isSectionVisible(section, viewport, searchDistance)) { - continue; - } - - visitor.visit(section); - int connections; { - if (useOcclusionCulling) { + if (this.useOcclusionCulling) { var sectionVisibilityData = section.getVisibilityData(); // occlude paths through the section if it's being viewed at an angle where // the other side can't possibly be seen - sectionVisibilityData &= getAngleVisibilityMask(viewport, section); + sectionVisibilityData &= getAngleVisibilityMask(this.viewport, section); // When using occlusion culling, we can only traverse into neighbors for which there is a path of // visibility through this chunk. This is determined by taking all the incoming paths to this chunk and @@ -79,10 +129,10 @@ private static void processQueue(Visitor visitor, // We can only traverse *outwards* from the center of the graph search, so mask off any invalid // directions. - connections &= getOutwardDirections(viewport.getChunkCoord(), section); + connections &= this.visitor.getOutwardDirections(this.viewport.getChunkCoord(), section); } - visitNeighbors(writeQueue, section, connections, frame); + visitNeighbors(writeQueue, section, connections); } } @@ -110,11 +160,24 @@ private static long getAngleVisibilityMask(Viewport viewport, RenderSection sect return ~angleOcclusionMask; } - private static boolean isSectionVisible(RenderSection section, Viewport viewport, float maxDistance) { - return isWithinRenderDistance(viewport.getTransform(), section, maxDistance) && isWithinFrustum(viewport, section); + private static boolean isWithinRenderDistance(CameraTransform camera, RenderSection section, float maxDistance) { + // origin point of the chunk's bounding box (in view space) + int ox = section.getOriginX() - camera.intX; + int oy = section.getOriginY() - camera.intY; + int oz = section.getOriginZ() - camera.intZ; + + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(ox, ox + 16) - camera.fracX; + float dy = nearestToZero(oy, oy + 16) - camera.fracY; + float dz = nearestToZero(oz, oz + 16) - camera.fracZ; + + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); } - private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int frame) { + private void visitNeighbors(WriteQueue queue, RenderSection section, int outgoing) { // Only traverse into neighbors which are actually present. // This avoids a null-check on each invocation to enqueue, and since the compiler will see that a null // is never encountered (after profiling), it will optimize it away. @@ -129,73 +192,51 @@ private static void visitNeighbors(final WriteQueue queue, Render queue.ensureCapacity(6); if (GraphDirectionSet.contains(outgoing, GraphDirection.DOWN)) { - visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP), frame); + visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.UP)) { - visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN), frame); + visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.NORTH)) { - visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH), frame); + visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.SOUTH)) { - visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH), frame); + visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.WEST)) { - visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST), frame); + visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.EAST)) { - visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST), frame); + visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST)); } } - private static void visitNode(final WriteQueue queue, @NotNull RenderSection render, int incoming, int frame) { - if (render.getLastVisibleFrame() != frame) { - // This is the first time we are visiting this section during the given frame, so we must - // reset the state. - render.setLastVisibleFrame(frame); - render.setIncomingDirections(GraphDirectionSet.NONE); - - queue.enqueue(render); + private void visitNode(WriteQueue queue, RenderSection section, int incoming) { + // isn't usually null, but can be null if the bfs is happening during loading or unloading of chunks + if (section == null) { + return; } - render.addIncomingDirections(incoming); - } - - private static int getOutwardDirections(SectionPos origin, RenderSection section) { - int planes = 0; - - planes |= section.getChunkX() <= origin.getX() ? 1 << GraphDirection.WEST : 0; - planes |= section.getChunkX() >= origin.getX() ? 1 << GraphDirection.EAST : 0; - - planes |= section.getChunkY() <= origin.getY() ? 1 << GraphDirection.DOWN : 0; - planes |= section.getChunkY() >= origin.getY() ? 1 << GraphDirection.UP : 0; - - planes |= section.getChunkZ() <= origin.getZ() ? 1 << GraphDirection.NORTH : 0; - planes |= section.getChunkZ() >= origin.getZ() ? 1 << GraphDirection.SOUTH : 0; - - return planes; - } - - private static boolean isWithinRenderDistance(CameraTransform camera, RenderSection section, float maxDistance) { - // origin point of the chunk's bounding box (in view space) - int ox = section.getOriginX() - camera.intX; - int oy = section.getOriginY() - camera.intY; - int oz = section.getOriginZ() - camera.intZ; - - // coordinates of the point to compare (in view space) - // this is the closest point within the bounding box to the center (0, 0, 0) - float dx = nearestToZero(ox, ox + 16) - camera.fracX; - float dy = nearestToZero(oy, oy + 16) - camera.fracY; - float dz = nearestToZero(oz, oz + 16) - camera.fracZ; + if (section.getLastVisibleSearchToken() != this.token) { + // This is the first time we are visiting this section during the given token, so we must + // reset the state. + section.setLastVisibleSearchToken(this.token); + section.setIncomingDirections(GraphDirectionSet.NONE); + + if (isWithinRenderDistance(this.viewport.getTransform(), section, this.searchDistance) && + this.visitor.isWithinFrustum(this.viewport, section) && + this.visitor.visitTestVisible(section)) { + this.visitor.visit(section); + queue.enqueue(section); + } + } - // vanilla's "cylindrical fog" algorithm - // max(length(distance.xz), abs(distance.y)) - return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); + section.addIncomingDirections(incoming); } @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. @@ -207,12 +248,6 @@ private static int nearestToZero(int min, int max) { return clamped; } - // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models - // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon - // to deal with floating point imprecision during a frustum check (see GH#2132). - private static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; - private static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; - public static boolean isWithinFrustum(Viewport viewport, RenderSection section) { return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE); @@ -220,7 +255,7 @@ public static boolean isWithinFrustum(Viewport viewport, RenderSection section) // this bigger chunk section size is only used for frustum-testing nearby sections with large models private static final float CHUNK_SECTION_SIZE_NEARBY = CHUNK_SECTION_RADIUS + 2.0f /* bigger model extent */ + 0.125f /* epsilon */; - + public static boolean isWithinNearbySectionFrustum(Viewport viewport, RenderSection section) { return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), CHUNK_SECTION_SIZE_NEARBY, CHUNK_SECTION_SIZE_NEARBY, CHUNK_SECTION_SIZE_NEARBY); @@ -231,7 +266,7 @@ public static boolean isWithinNearbySectionFrustum(Viewport viewport, RenderSect // for all neighboring, even diagonally neighboring, sections around the origin to render them // if their extended bounding box is visible, and they may render large models that extend // outside the 16x16x16 base volume of the section. - private void addNearbySections(Visitor visitor, Viewport viewport, float searchDistance, int frame) { + private void addNearbySections(GraphOcclusionVisitor visitor, Viewport viewport) { var origin = viewport.getChunkCoord(); var originX = origin.getX(); var originY = origin.getY(); @@ -247,9 +282,9 @@ private void addNearbySections(Visitor visitor, Viewport viewport, float searchD var section = this.getRenderSection(originX + dx, originY + dy, originZ + dz); // additionally render not yet visited but visible sections - if (section != null && section.getLastVisibleFrame() != frame && isWithinNearbySectionFrustum(viewport, section)) { + if (section != null && section.getLastVisibleSearchToken() != this.token && isWithinNearbySectionFrustum(viewport, section)) { // reset state on first visit, but don't enqueue - section.setLastVisibleFrame(frame); + section.setLastVisibleSearchToken(this.token); visitor.visit(section); } @@ -258,44 +293,45 @@ private void addNearbySections(Visitor visitor, Viewport viewport, float searchD } } - private void init(Visitor visitor, - WriteQueue queue, - Viewport viewport, - float searchDistance, - boolean useOcclusionCulling, - int frame) - { + public boolean graphOriginPresent(Viewport viewport) { var origin = viewport.getChunkCoord(); + var originY = origin.getY(); + return originY < this.level.getMinSectionY() || + originY > this.level.getMaxSectionY() || + this.sections.get(viewport.getChunkCoord().asLong()) != null; + } + + private void init(WriteQueue queue) + { + var origin = this.viewport.getChunkCoord(); if (origin.getY() < this.level.getMinSectionY()) { // below the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, - this.level.getMinSectionY(), GraphDirection.DOWN); + this.initOutsideWorldHeight(queue, this.level.getMinSectionY(), GraphDirection.DOWN); } else if (origin.getY() > this.level.getMaxSectionY()) { // above the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, - this.level.getMaxSectionY(), GraphDirection.UP); + this.initOutsideWorldHeight(queue, this.level.getMaxSectionY(), GraphDirection.UP); } else { - this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, frame); + this.initWithinWorld(queue); } } - private void initWithinWorld(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { - var origin = viewport.getChunkCoord(); + private void initWithinWorld(WriteQueue queue) { + var origin = this.viewport.getChunkCoord(); var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); if (section == null) { return; } - section.setLastVisibleFrame(frame); + section.setLastVisibleSearchToken(this.token); section.setIncomingDirections(GraphDirectionSet.NONE); - visitor.visit(section); + this.visitor.visit(section); int outgoing; - if (useOcclusionCulling) { + if (this.useOcclusionCulling) { // Since the camera is located inside this chunk, there are no "incoming" directions. So we need to instead // find any possible paths out of this chunk and enqueue those neighbors. outgoing = VisibilityEncoding.getConnections(section.getVisibilityData()); @@ -304,35 +340,29 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V outgoing = GraphDirectionSet.ALL; } - visitNeighbors(queue, section, outgoing, frame); + visitNeighbors(queue, section, outgoing); } // Enqueues sections that are inside the viewport using diamond spiral iteration to avoid sorting and ensure a // consistent order. Innermost layers are enqueued first. Within each layer, iteration starts at the northernmost // section and proceeds counterclockwise (N->W->S->E). - private void initOutsideWorldHeight(WriteQueue queue, - Viewport viewport, - float searchDistance, - int frame, - int height, - int direction) - { - var origin = viewport.getChunkCoord(); - var radius = Mth.floor(searchDistance / 16.0f); + private void initOutsideWorldHeight(WriteQueue queue, int height, int direction) { + var origin = this.viewport.getChunkCoord(); + var radius = Mth.floor(this.searchDistance / 16.0f); // Layer 0 - this.tryVisitNode(queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); + this.tryInitNode(queue, origin.getX(), height, origin.getZ(), direction); // Complete layers, excluding layer 0 for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } } @@ -342,41 +372,33 @@ private void initOutsideWorldHeight(WriteQueue queue, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = l; z <= radius; z++) { int x = z - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = radius; z >= l; z--) { int x = layer - z; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } } } - private void tryVisitNode(WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { - RenderSection section = this.getRenderSection(x, y, z); + private void tryInitNode(WriteQueue queue, int x, int y, int z, int direction) { + var section = this.getRenderSection(x, y, z); - if (section == null || !isWithinFrustum(viewport, section)) { - return; - } - - visitNode(queue, section, GraphDirectionSet.of(direction), frame); + visitNode(queue, section, GraphDirectionSet.of(direction)); } private RenderSection getRenderSection(int x, int y, int z) { return this.sections.get(SectionPos.asLong(x, y, z)); } - - public interface Visitor { - void visit(RenderSection section); - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java new file mode 100644 index 0000000000..018b93843b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -0,0 +1,170 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.*; +import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public class RayOcclusionSectionTree extends SectionTree { + private static final float SECTION_HALF_DIAGONAL = (float) Math.sqrt(8 * 8 * 3); + private static final float RAY_MIN_STEP_SIZE_INV = 1.0f / (SECTION_HALF_DIAGONAL * 2); + private static final int RAY_TEST_MAX_STEPS = 12; + private static final int MIN_RAY_TEST_DISTANCE_SQ = (int) Math.pow(16 * 3, 2); + + private final CameraTransform transform; + private final int minSection, maxSection; + + private final Forest portalTree; + + public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, frame, cullType, level); + + this.transform = viewport.getTransform(); + this.portalTree = createPortalTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); + + this.minSection = level.getMinSectionY(); + this.maxSection = level.getMaxSectionY(); + } + + @Override + public boolean visitTestVisible(RenderSection section) { + if (section.needsRender()) { + this.lastSectionKnownEmpty = false; + if (this.isRayBlockedStepped(section)) { + return false; + } + } else { + this.lastSectionKnownEmpty = true; + } + + return super.visitTestVisible(section); + } + + @Override + public void visit(RenderSection section) { + super.visit(section); + this.lastSectionKnownEmpty = false; + + // mark all traversed sections as portals, even if they don't have terrain that needs rendering + this.portalTree.add(section); + } + + private boolean isRayBlockedStepped(RenderSection section) { + // check if this section is visible through all so far traversed sections + var x = (float) section.getCenterX(); + var y = (float) section.getCenterY(); + var z = (float) section.getCenterZ(); + var dX = (float) (this.transform.x - x); + var dY = (float) (this.transform.y - y); + var dZ = (float) (this.transform.z - z); + + var distanceSquared = dX * dX + dY * dY + dZ * dZ; + if (distanceSquared < MIN_RAY_TEST_DISTANCE_SQ) { + return false; + } + + var length = (float) Math.sqrt(distanceSquared); + var steps = Math.min((int) (length * RAY_MIN_STEP_SIZE_INV), RAY_TEST_MAX_STEPS); + + // avoid the last step being in the camera + var stepsInv = 1.0f / steps; + dX *= stepsInv; + dY *= stepsInv; + dZ *= stepsInv; + + for (int i = 1; i < steps; i++) { + x += dX; + y += dY; + z += dZ; + + // if the section is not present in the tree, the path to the camera is blocked + var result = this.blockHasObstruction((int) x, (int) y, (int) z); + if (result == Tree.NOT_PRESENT) { + // also test radius around to avoid false negatives + var radius = SECTION_HALF_DIAGONAL * (steps - i) * stepsInv; + + // this pattern simulates a shape similar to the sweep of the section towards the camera + if (this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) != Tree.NOT_PRESENT || + this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius)) != Tree.NOT_PRESENT) { + continue; + } + + // the path is blocked because there's no visited section that gives a clear line of sight + return true; + } else if (result == Tree.OUT_OF_BOUNDS) { + break; + } + } + + return false; + } + + private int blockHasObstruction(int x, int y, int z) { + x >>= 4; + y >>= 4; + z >>= 4; + + if (y < this.minSection || y > this.maxSection) { + return Tree.OUT_OF_BOUNDS; + } + + return this.portalTree.getPresence(x, y, z); + } + + private static Forest createPortalTree(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { + if (BaseBiForest.checkApplicable(buildDistance, level)) { + return new PortalBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + return new PortalMultiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + private static class PortalBiForest extends BaseBiForest { + public PortalBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new FlatTree(offsetX, offsetY, offsetZ); + } + } + + private static class PortalMultiForest extends BaseMultiForest { + public PortalMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new FlatTree(offsetX, offsetY, offsetZ); + } + + @Override + protected FlatTree[] makeTrees(int length) { + return new FlatTree[length]; + } + } + + protected static class FlatTree extends Tree { + public FlatTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + @Override + public int getPresence(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (isOutOfBounds(x, y, z)) { + return Tree.OUT_OF_BOUNDS; + } + + var bitIndex = interleave6x3(x, y, z); + var mask = 1L << (bitIndex & 0b111111); + return (this.tree[bitIndex >> 6] & mask) == 0 ? Tree.NOT_PRESENT : Tree.PRESENT; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java new file mode 100644 index 0000000000..3372fe00a7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -0,0 +1,137 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.TraversableForest; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.Tree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; + +public class SectionTree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { + private final TraversableForest tree; + + private final int bfsWidth; + + public final float buildDistance; + protected final int frame; + protected boolean lastSectionKnownEmpty = false; + + public interface VisibleSectionVisitor { + void visit(int x, int y, int z); + } + + public SectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, cullType.isFrustumTested); + + this.bfsWidth = cullType.bfsWidth; + this.buildDistance = buildDistance; + this.frame = frame; + + this.tree = TraversableForest.createTraversableForest(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); + } + + public int getFrame() { + return this.frame; + } + + public boolean isValidFor(Viewport viewport, float searchDistance) { + var cameraPos = viewport.getChunkCoord(); + return Math.abs((this.cameraX >> 4) - cameraPos.getX()) <= this.bfsWidth && + Math.abs((this.cameraY >> 4) - cameraPos.getY()) <= this.bfsWidth && + Math.abs((this.cameraZ >> 4) - cameraPos.getZ()) <= this.bfsWidth && + this.buildDistance >= searchDistance; + } + + @Override + public boolean isWithinFrustum(Viewport viewport, RenderSection section) { + return !this.isFrustumTested || super.isWithinFrustum(viewport, section); + } + + @Override + public int getOutwardDirections(SectionPos origin, RenderSection section) { + int planes = 0; + + planes |= section.getChunkX() <= origin.getX() + this.bfsWidth ? 1 << GraphDirection.WEST : 0; + planes |= section.getChunkX() >= origin.getX() - this.bfsWidth ? 1 << GraphDirection.EAST : 0; + + planes |= section.getChunkY() <= origin.getY() + this.bfsWidth ? 1 << GraphDirection.DOWN : 0; + planes |= section.getChunkY() >= origin.getY() - this.bfsWidth ? 1 << GraphDirection.UP : 0; + + planes |= section.getChunkZ() <= origin.getZ() + this.bfsWidth ? 1 << GraphDirection.NORTH : 0; + planes |= section.getChunkZ() >= origin.getZ() - this.bfsWidth ? 1 << GraphDirection.SOUTH : 0; + + return planes; + } + + @Override + public void visit(RenderSection section) { + super.visit(section); + + // discard invisible or sections that don't need to be rendered, + // only perform this test if it hasn't already been done before + if (this.lastSectionKnownEmpty || !section.needsRender()) { + return; + } + + this.markPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + protected void markPresent(int x, int y, int z) { + this.tree.add(x, y, z); + } + + public void prepareForTraversal() { + this.tree.prepareForTraversal(); + } + + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2, NotInTreePredicate notInTreePredicate) { + // check if there's a section at any part of the box + int minX = SectionPos.posToSectionCoord(x1 - 0.5D); + int minY = SectionPos.posToSectionCoord(y1 - 0.5D); + int minZ = SectionPos.posToSectionCoord(z1 - 0.5D); + + int maxX = SectionPos.posToSectionCoord(x2 + 0.5D); + int maxY = SectionPos.posToSectionCoord(y2 + 0.5D); + int maxZ = SectionPos.posToSectionCoord(z2 + 0.5D); + + // check if any of the sections in the box are present, and then check the predicate as it's likely more expensive than fetching from the bitmask + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + if (this.tree.isSectionPresent(x, y, z)) { + return true; + } + } + } + } + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + if (notInTreePredicate.isEmpty(x, y, z)) { + return true; + } + } + } + } + + return false; + } + + @FunctionalInterface + public interface NotInTreePredicate { + boolean isEmpty(int x, int y, int z); + } + + public boolean isSectionVisible(Viewport viewport, RenderSection section) { + // empty sections are not tested against the tree because the tree is not aware of them + return (!section.needsRender() || this.tree.isSectionPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ())) && + this.isWithinFrustum(viewport, section); + } + + public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + this.tree.traverse(visitor, viewport, distanceLimit); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index 29ca64b226..5cd473b47b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -7,19 +7,28 @@ import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.tessellation.GlTessellation; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; +import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.data.SectionRenderDataStorage; import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats; import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.SectionPos; +import net.minecraft.world.level.block.entity.BlockEntity; import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Map; public class RenderRegion { + public static final int SECTION_VERTEX_COUNT_ESTIMATE = 756; + public static final int SECTION_INDEX_COUNT_ESTIMATE = (SECTION_VERTEX_COUNT_ESTIMATE / 4) * 6; + public static final int SECTION_BUFFER_ESTIMATE = SECTION_VERTEX_COUNT_ESTIMATE * ChunkMeshFormats.COMPACT.getVertexFormat().getStride() + SECTION_INDEX_COUNT_ESTIMATE * Integer.BYTES; + public static final int REGION_WIDTH = 8; public static final int REGION_HEIGHT = 4; public static final int REGION_LENGTH = 8; @@ -46,6 +55,10 @@ public class RenderRegion { private final ChunkRenderList renderList; private final RenderSection[] sections = new RenderSection[RenderRegion.REGION_SIZE]; + private final byte[] sectionFlags = new byte[RenderRegion.REGION_SIZE]; + private final BlockEntity[] @Nullable [] globalBlockEntities = new BlockEntity[RenderRegion.REGION_SIZE][]; + private final BlockEntity[] @Nullable [] culledBlockEntities = new BlockEntity[RenderRegion.REGION_SIZE][]; + private final TextureAtlasSprite[] @Nullable [] animatedSprites = new TextureAtlasSprite[RenderRegion.REGION_SIZE][]; private int sectionCount; private final Map sectionRenderData = new Reference2ReferenceOpenHashMap<>(); @@ -165,6 +178,59 @@ public void addSection(RenderSection section) { this.sectionCount++; } + public void setSectionRenderState(int id, BuiltSectionInfo info) { + this.sectionFlags[id] = (byte) (info.flags | RenderSectionFlags.MASK_IS_BUILT); + this.globalBlockEntities[id] = info.globalBlockEntities; + this.culledBlockEntities[id] = info.culledBlockEntities; + this.animatedSprites[id] = info.animatedSprites; + } + + public void clearSectionRenderState(int id) { + this.sectionFlags[id] = RenderSectionFlags.NONE; + this.globalBlockEntities[id] = null; + this.culledBlockEntities[id] = null; + this.animatedSprites[id] = null; + } + + public int getSectionFlags(int id) { + return this.sectionFlags[id]; + } + + public boolean sectionNeedsRender(int id) { + return RenderSectionFlags.needsRender(this.sectionFlags[id]); + } + + /** + * Returns the collection of block entities contained by this rendered chunk, which are not part of its culling + * volume. These entities should always be rendered regardless of the render being visible in the frustum. + * + * @param id The section index + * @return The collection of block entities + */ + public BlockEntity[] getGlobalBlockEntities(int id) { + return this.globalBlockEntities[id]; + } + + /** + * Returns the collection of block entities contained by this rendered chunk. + * + * @param id The section index + * @return The collection of block entities + */ + public BlockEntity[] getCulledBlockEntities(int id) { + return this.culledBlockEntities[id]; + } + + /** + * Returns the collection of animated sprites contained by this rendered chunk section. + * + * @param id The section index + * @return The collection of animated sprites + */ + public TextureAtlasSprite[] getAnimatedSprites(int id) { + return this.animatedSprites[id]; + } + public void removeSection(RenderSection section) { var sectionIndex = section.getSectionIndex(); var prev = this.sections[sectionIndex]; @@ -227,11 +293,8 @@ public static class DeviceResources { public DeviceResources(CommandList commandList, StagingBuffer stagingBuffer) { int stride = ChunkMeshFormats.COMPACT.getVertexFormat().getStride(); - // the magic number 756 for the initial size is arbitrary, it was made up. - var initialVertices = 756; - this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * initialVertices, stride, stagingBuffer); - var initialIndices = (initialVertices / 4) * 6; - this.indexArena = new GlBufferArena(commandList, REGION_SIZE * initialIndices, Integer.BYTES, stagingBuffer); + this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_VERTEX_COUNT_ESTIMATE, stride, stagingBuffer); + this.indexArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_INDEX_COUNT_ESTIMATE, Integer.BYTES, stagingBuffer); } public void updateTessellation(CommandList commandList, GlTessellation tessellation) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 2b4c704256..f4ca4127fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -188,6 +188,12 @@ public RenderRegion createForChunk(int chunkX, int chunkY, int chunkZ) { chunkZ >> RenderRegion.REGION_LENGTH_SH); } + public RenderRegion getForChunk(int chunkX, int chunkY, int chunkZ) { + return this.regions.get(RenderRegion.key(chunkX >> RenderRegion.REGION_WIDTH_SH, + chunkY >> RenderRegion.REGION_HEIGHT_SH, + chunkZ >> RenderRegion.REGION_LENGTH_SH)); + } + @NotNull private RenderRegion create(int x, int y, int z) { var key = RenderRegion.key(x, y, z); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java index 055270f7bf..c6b2cd5466 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java @@ -1,5 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; + public enum SortBehavior { OFF("OFF", SortMode.NONE), STATIC("S", SortMode.STATIC), @@ -12,10 +14,10 @@ public enum SortBehavior { private final String shortName; private final SortBehavior.SortMode sortMode; private final SortBehavior.PriorityMode priorityMode; - private final SortBehavior.DeferMode deferMode; + private final DeferMode deferMode; SortBehavior(String shortName, SortBehavior.SortMode sortMode, SortBehavior.PriorityMode priorityMode, - SortBehavior.DeferMode deferMode) { + DeferMode deferMode) { this.shortName = shortName; this.sortMode = sortMode; this.priorityMode = priorityMode; @@ -26,7 +28,7 @@ public enum SortBehavior { this(shortName, sortMode, null, null); } - SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, SortBehavior.DeferMode deferMode) { + SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, DeferMode deferMode) { this(shortName, SortMode.DYNAMIC, priorityMode, deferMode); } @@ -42,7 +44,7 @@ public SortBehavior.PriorityMode getPriorityMode() { return this.priorityMode; } - public SortBehavior.DeferMode getDeferMode() { + public DeferMode getDeferMode() { return this.deferMode; } @@ -53,8 +55,4 @@ public enum SortMode { public enum PriorityMode { NONE, NEARBY, ALL } - - public enum DeferMode { - ALWAYS, ONE_FRAME, ZERO_FRAMES - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java index cfab09e721..f07668ba5b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java @@ -37,7 +37,7 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { } @Override - public Sorter getSorter() { + public DynamicSorter getSorter() { return new DynamicBSPSorter(this.getQuadCount()); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java index 6c3c69080c..41d5be78f5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java @@ -20,6 +20,8 @@ public SortType getSortType() { return SortType.DYNAMIC; } + public abstract DynamicSorter getSorter(); + public GeometryPlanes getGeometryPlanes() { return this.geometryPlanes; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java index 87539c346d..4ef745f8c5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -abstract class DynamicSorter extends Sorter { +public abstract class DynamicSorter extends Sorter { private final int quadCount; DynamicSorter(int quadCount) { @@ -14,4 +14,8 @@ public void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial) { this.initBufferWithQuadLength(this.quadCount); this.writeSort(cameraPos, initial); } + + public int getQuadCount() { + return this.quadCount; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java index 2156bb3283..ca352b1a64 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java @@ -58,7 +58,7 @@ private DynamicTopoData(SectionPos sectionPos, int vertexCount, TQuad[] quads, } @Override - public Sorter getSorter() { + public DynamicSorter getSorter() { return new DynamicTopoSorter(this.getQuadCount(), this, this.pendingTriggerIsDirect, this.consecutiveTopoSortFailures, this.GFNITrigger, this.directTrigger); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java new file mode 100644 index 0000000000..68826ddeee --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java @@ -0,0 +1,27 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class AbstractTraversableBiForest extends BaseBiForest implements TraversableForest { + public AbstractTraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void prepareForTraversal() { + this.mainTree.prepareForTraversal(); + if (this.secondaryTree != null) { + this.secondaryTree.prepareForTraversal(); + } + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree + this.mainTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + if (this.secondaryTree != null) { + this.secondaryTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java new file mode 100644 index 0000000000..d4be826f60 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -0,0 +1,53 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class AbstractTraversableMultiForest extends BaseMultiForest implements TraversableForest { + public AbstractTraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void prepareForTraversal() { + for (var tree : this.trees) { + if (tree != null) { + tree.prepareForTraversal(); + } + } + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + var cameraPos = viewport.getChunkCoord(); + var cameraSectionX = cameraPos.getX(); + var cameraSectionY = cameraPos.getY(); + var cameraSectionZ = cameraPos.getZ(); + + // sort the trees by distance from the camera by sorting a packed index array. + var items = new int[this.trees.length]; + for (int i = 0; i < this.trees.length; i++) { + var tree = this.trees[i]; + if (tree != null) { + var deltaX = Math.abs(tree.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(tree.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(tree.offsetZ + 32 - cameraSectionZ); + items[i] = (deltaX + deltaY + deltaZ + 1) << 16 | i; + } + } + + IntArrays.unstableSort(items); + + // traverse in sorted front-to-back order for correct render order + for (var item : items) { + if (item == 0) { + continue; + } + var tree = this.trees[item & 0xFFFF]; + if (tree != null) { + tree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + } + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java new file mode 100644 index 0000000000..ddbfe1ae05 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java @@ -0,0 +1,57 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.world.level.Level; + +public abstract class BaseBiForest extends BaseForest { + private static final int SECONDARY_TREE_OFFSET_XZ = 4; + + protected final T mainTree; + protected T secondaryTree; + + public BaseBiForest(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.mainTree = this.makeTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + protected T makeSecondaryTree() { + // offset diagonally to fully encompass the required 65x65 area + return this.makeTree( + this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, + this.baseOffsetY, + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); + } + + @Override + public void add(int x, int y, int z) { + if (this.mainTree.add(x, y, z)) { + return; + } + + if (this.secondaryTree == null) { + this.secondaryTree = this.makeSecondaryTree(); + } + this.secondaryTree.add(x, y, z); + } + + @Override + public int getPresence(int x, int y, int z) { + var result = this.mainTree.getPresence(x, y, z); + if (result != Tree.OUT_OF_BOUNDS) { + return result; + } + + if (this.secondaryTree != null) { + return this.secondaryTree.getPresence(x, y, z); + } + return Tree.OUT_OF_BOUNDS; + } + + public static boolean checkApplicable(float buildDistance, Level level) { + if (buildDistance / 16.0f > 64.0f) { + return false; + } + + return level.getHeight() >> 4 <= 64; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java new file mode 100644 index 0000000000..2501fbd68e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java @@ -0,0 +1,15 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseForest implements Forest { + protected final int baseOffsetX, baseOffsetY, baseOffsetZ; + final float buildDistance; + + protected BaseForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + this.baseOffsetX = baseOffsetX; + this.baseOffsetY = baseOffsetY; + this.baseOffsetZ = baseOffsetZ; + this.buildDistance = buildDistance; + } + + protected abstract T makeTree(int offsetX, int offsetY, int offsetZ); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java new file mode 100644 index 0000000000..ccf62c58f9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -0,0 +1,91 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseMultiForest extends BaseForest { + protected final T[] trees; + protected final int forestDim; + + protected T lastTree; + + public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.forestDim = forestDimFromBuildDistance(buildDistance); + this.trees = this.makeTrees(this.forestDim * this.forestDim * this.forestDim); + } + + public static int forestDimFromBuildDistance(float buildDistance) { + // / 16 (block to chunk) * 2 (radius to diameter) + 1 (center chunk) / 64 (chunks per tree) + return (int) Math.ceil((buildDistance / 8.0 + 1) / 64.0); + } + + protected int getTreeIndex(int localX, int localY, int localZ) { + var treeX = localX >> 6; + var treeY = localY >> 6; + var treeZ = localZ >> 6; + + if (treeX < 0 || treeX >= this.forestDim || + treeY < 0 || treeY >= this.forestDim || + treeZ < 0 || treeZ >= this.forestDim) { + return Tree.OUT_OF_BOUNDS; + } + + return treeX + (treeZ * this.forestDim + treeY) * this.forestDim; + } + + @Override + public void add(int x, int y, int z) { + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return; + } + + var tree = this.trees[treeIndex]; + + if (tree == null) { + var treeOffsetX = this.baseOffsetX + (localX & ~0b111111); + var treeOffsetY = this.baseOffsetY + (localY & ~0b111111); + var treeOffsetZ = this.baseOffsetZ + (localZ & ~0b111111); + tree = this.makeTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees[treeIndex] = tree; + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + @Override + public int getPresence(int x, int y, int z) { + if (this.lastTree != null) { + var result = this.lastTree.getPresence(x, y, z); + if (result != Tree.OUT_OF_BOUNDS) { + return result; + } + } + + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return Tree.OUT_OF_BOUNDS; + } + + var tree = this.trees[treeIndex]; + if (tree != null) { + this.lastTree = tree; + return tree.getPresence(x, y, z); + } + return Tree.OUT_OF_BOUNDS; + } + + protected abstract T[] makeTrees(int length); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java new file mode 100644 index 0000000000..9fbbd3b471 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public interface Forest { + void add(int x, int y, int z); + + default void add(RenderSection section) { + add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + int getPresence(int x, int y, int z); + + default boolean isSectionPresent(int x, int y, int z) { + return this.getPresence(x, y, z) == Tree.PRESENT; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java new file mode 100644 index 0000000000..4729c43b26 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java @@ -0,0 +1,5 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public interface RemovableForest extends TraversableForest { + void remove(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java new file mode 100644 index 0000000000..153a76bdb6 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java @@ -0,0 +1,139 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; + +import java.util.Comparator; + +public class RemovableMultiForest implements RemovableForest { + private final Long2ReferenceLinkedOpenHashMap trees; + private final ReferenceArrayList treeSortList = new ReferenceArrayList<>(); + private RemovableTree lastTree; + + // the removable tree separately tracks if it needs to prepared for traversal because it's not just built once, prepared, and then traversed. Since it can receive updates, it needs to be prepared for traversal again and to avoid unnecessary preparation, it tracks whether it's ready. + private boolean treesAreReady = true; + + public RemovableMultiForest(float buildDistance) { + this.trees = new Long2ReferenceLinkedOpenHashMap<>(getCapacity(buildDistance)); + } + + private static int getCapacity(float buildDistance) { + var forestDim = BaseMultiForest.forestDimFromBuildDistance(buildDistance) + 1; + return forestDim * forestDim * forestDim; + } + + public void ensureCapacity(float buildDistance) { + this.trees.ensureCapacity(getCapacity(buildDistance)); + } + + @Override + public void prepareForTraversal() { + if (this.treesAreReady) { + return; + } + + var it = this.trees.values().iterator(); + while (it.hasNext()) { + var tree = it.next(); + tree.prepareForTraversal(); + if (tree.isEmpty()) { + it.remove(); + if (this.lastTree == tree) { + this.lastTree = null; + } + } + } + + this.treesAreReady = true; + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + + // sort the trees by distance from the camera by sorting a packed index array. + this.treeSortList.clear(); + this.treeSortList.ensureCapacity(this.trees.size()); + this.treeSortList.addAll(this.trees.values()); + for (var tree : this.treeSortList) { + tree.updateSortKeyFor(cameraSectionX, cameraSectionY, cameraSectionZ); + } + + this.treeSortList.unstableSort(Comparator.comparingInt(RemovableTree::getSortKey)); + + // traverse in sorted front-to-back order for correct render order + for (var tree : this.treeSortList) { + // disable distance test in traversal because we don't use it here + tree.traverse(visitor, viewport, 0, 0); + } + } + + @Override + public void add(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + var treeOffsetX = treeX << 6; + var treeOffsetY = treeY << 6; + var treeOffsetZ = treeZ << 6; + tree = new RemovableTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees.put(treeKey, tree); + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + public void remove(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.remove(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + return; + } + + tree.remove(x, y, z); + + this.lastTree = tree; + } + + public void remove(RenderSection section) { + this.remove(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + @Override + public int getPresence(int x, int y, int z) { + // unused operation on removable trees + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java new file mode 100644 index 0000000000..4b7cea63e4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java @@ -0,0 +1,69 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.core.SectionPos; + +public class RemovableTree extends TraversableTree { + private boolean reducedIsValid = true; + private int sortKey; + + public RemovableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public boolean remove(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] &= ~(1L << (bitIndex & 0b111111)); + + this.reducedIsValid = false; + + return true; + } + + @Override + public void prepareForTraversal() { + if (!this.reducedIsValid) { + super.prepareForTraversal(); + this.reducedIsValid = true; + } + } + + @Override + public boolean add(int x, int y, int z) { + var result = super.add(x, y, z); + if (result) { + this.reducedIsValid = false; + } + return result; + } + + public boolean isEmpty() { + return this.treeDoubleReduced == 0; + } + + public long getTreeKey() { + return SectionPos.asLong(this.offsetX, this.offsetY, this.offsetZ); + } + + public void updateSortKeyFor(int cameraSectionX, int cameraSectionY, int cameraSectionZ) { + var deltaX = Math.abs(this.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(this.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(this.offsetZ + 32 - cameraSectionZ); + this.sortKey = deltaX + deltaY + deltaZ + 1; + } + + public int getSortKey() { + return this.sortKey; + } + + @Override + public int getPresence(int i, int i1, int i2) { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java new file mode 100644 index 0000000000..ae75cc441b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java @@ -0,0 +1,12 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public class TraversableBiForest extends AbstractTraversableBiForest { + public TraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java new file mode 100644 index 0000000000..f297ee77b0 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -0,0 +1,19 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public interface TraversableForest extends Forest { + void prepareForTraversal(); + + void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit); + + static TraversableForest createTraversableForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { + if (BaseBiForest.checkApplicable(buildDistance, level)) { + return new TraversableBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + return new TraversableMultiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java new file mode 100644 index 0000000000..d547655de8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public class TraversableMultiForest extends AbstractTraversableMultiForest { + public TraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } + + @Override + protected TraversableTree[] makeTrees(int length) { + return new TraversableTree[length]; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java new file mode 100644 index 0000000000..002059efd8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -0,0 +1,302 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import org.joml.FrustumIntersection; + +/** + * A traversable tree is a tree of sections that can be traversed with a distance limit and a frustum. It traverses the sections in visual front-to-back order, so that they can be directly put into a render list. Note however that ordering regions by adding them to the list the first time one of their sections is visited does not yield the correct order. This is because the sections are traversed in visual order, not ordered by distance from the camera. + */ +public class TraversableTree extends Tree { + private static final int INSIDE_FRUSTUM = 0b01; + private static final int INSIDE_DISTANCE = 0b10; + private static final int FULLY_INSIDE = INSIDE_FRUSTUM | INSIDE_DISTANCE; + + protected final long[] treeReduced = new long[64]; + public long treeDoubleReduced = 0L; + + // set temporarily during traversal + private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private SectionTree.VisibleSectionVisitor visitor; + protected Viewport viewport; + private float distanceLimit; + + public TraversableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public void prepareForTraversal() { + long doubleReduced = 0; + for (int i = 0; i < 64; i++) { + long reduced = 0; + var reducedOffset = i << 6; + for (int j = 0; j < 64; j++) { + reduced |= this.tree[reducedOffset + j] == 0 ? 0L : 1L << j; + } + this.treeReduced[i] = reduced; + doubleReduced |= reduced == 0 ? 0L : 1L << i; + } + this.treeDoubleReduced = doubleReduced; + } + + @Override + public int getPresence(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (isOutOfBounds(x, y, z)) { + return OUT_OF_BOUNDS; + } + + var bitIndex = interleave6x3(x, y, z); + int doubleReducedBitIndex = bitIndex >> 12; + if ((this.treeDoubleReduced & (1L << doubleReducedBitIndex)) == 0) { + return NOT_PRESENT; + } + + int reducedBitIndex = bitIndex >> 6; + return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0 ? PRESENT : NOT_PRESENT; + } + + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + this.visitor = visitor; + this.viewport = viewport; + this.distanceLimit = distanceLimit; + + // + 1 to offset section position to compensate for shifted global offset + // adjust camera block position to account for fractional part of camera position + var sectionPos = viewport.getChunkCoord(); + this.cameraOffsetX = sectionPos.getX() - this.offsetX + 1; + this.cameraOffsetY = sectionPos.getY() - this.offsetY + 1; + this.cameraOffsetZ = sectionPos.getZ() - this.offsetZ + 1; + + // everything is already inside the distance limit if the build distance is smaller + var initialInside = this.distanceLimit >= buildDistance ? INSIDE_DISTANCE : 0; + this.traverse(getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); + + this.visitor = null; + this.viewport = null; + } + + void traverse(int orderModulator, int nodeOrigin, int level, int inside) { + // half of the dimension of a child of this node, in blocks + int childHalfDim = 1 << (level + 3); // * 16 / 2 + + // odd levels (the higher levels of each reduction) need to modulate indexes that are multiples of 8 + if ((level & 1) == 1) { + orderModulator <<= 3; + } + + if (level <= 1) { + // check using the full bitmap + int childOriginBase = nodeOrigin & 0b111111_111111_000000; + long map = this.tree[nodeOrigin >> 6]; + + if (level == 0) { + int startBit = nodeOrigin & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + int sectionOrigin = childOriginBase | childIndex; + int x = deinterleave6(sectionOrigin) + this.offsetX; + int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; + int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; + + if (inside == FULLY_INSIDE || testLeafNode(x, y, z, inside)) { + this.visitor.visit(x, y, z); + } + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); + } + } + } + } else if (level <= 3) { + int childOriginBase = nodeOrigin & 0b111111_000000_000000; + long map = this.treeReduced[nodeOrigin >> 12]; + + if (level == 2) { + int startBit = (nodeOrigin >> 6) & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } + } else { + if (level == 4) { + int startBit = nodeOrigin >> 12; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (0xFFL << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } + } + } + + void testChild(int childOrigin, int childHalfDim, int level, int inside) { + // calculate section coordinates in tree-space + int x = deinterleave6(childOrigin); + int y = deinterleave6(childOrigin >> 1); + int z = deinterleave6(childOrigin >> 2); + + // immediately traverse if fully inside + if (inside == FULLY_INSIDE) { + level--; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + return; + } + + // convert to world-space section origin in blocks, then to camera space + var transform = this.viewport.getTransform(); + int worldX = ((x + this.offsetX) << 4) - transform.intX; + int worldY = ((y + this.offsetY) << 4) - transform.intY; + int worldZ = ((z + this.offsetZ) << 4) - transform.intZ; + + boolean visible = true; + + if ((inside & INSIDE_FRUSTUM) == 0) { + var intersectionResult = this.viewport.getBoxIntersectionDirect( + (worldX + childHalfDim) - transform.fracX, + (worldY + childHalfDim) - transform.fracY, + (worldZ + childHalfDim) - transform.fracZ, + childHalfDim + OcclusionCuller.CHUNK_SECTION_MARGIN); + if (intersectionResult == FrustumIntersection.INSIDE) { + inside |= INSIDE_FRUSTUM; + } else { + visible = intersectionResult == FrustumIntersection.INTERSECT; + } + } + + if ((inside & INSIDE_DISTANCE) == 0) { + // calculate the point of the node closest to the camera + int childFullDim = childHalfDim << 1; + float dx = nearestToZero(worldX, worldX + childFullDim) - transform.fracX; + float dy = nearestToZero(worldY, worldY + childFullDim) - transform.fracY; + float dz = nearestToZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + // check if closest point inside the cylinder + visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + if (visible) { + // if the farthest point is also visible, the node is fully inside + dx = farthestFromZero(worldX, worldX + childFullDim) - transform.fracX; + dy = farthestFromZero(worldY, worldY + childFullDim) - transform.fracY; + dz = farthestFromZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { + inside |= INSIDE_DISTANCE; + } + } + } + + if (visible) { + level--; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + } + } + + boolean testLeafNode(int x, int y, int z, int inside) { + // input coordinates are section coordinates in world-space + + var transform = this.viewport.getTransform(); + + // convert to blocks and move into integer camera space + x = (x << 4) - transform.intX; + y = (y << 4) - transform.intY; + z = (z << 4) - transform.intZ; + + // test frustum if not already inside frustum + if ((inside & INSIDE_FRUSTUM) == 0 && !this.viewport.isBoxVisibleDirect( + (x + 8) - transform.fracX, + (y + 8) - transform.fracY, + (z + 8) - transform.fracZ, + OcclusionCuller.CHUNK_SECTION_RADIUS)) { + return false; + } + + // test distance if not already inside distance + if ((inside & INSIDE_DISTANCE) == 0) { + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(x, x + 16) - transform.fracX; + float dy = nearestToZero(y, y + 16) - transform.fracY; + float dz = nearestToZero(z, z + 16) - transform.fracZ; + + return cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + } + + return true; + } + + static boolean cylindricalDistanceTest(float dx, float dy, float dz, float distanceLimit) { + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (distanceLimit * distanceLimit)) && + (Math.abs(dy) < distanceLimit); + } + + @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. + private static int nearestToZero(int min, int max) { + // this compiles to slightly better code than Math.min(Math.max(0, min), max) + int clamped = 0; + if (min > 0) { + clamped = min; + } + if (max < 0) { + clamped = max; + } + return clamped; + } + + private static int farthestFromZero(int min, int max) { + int clamped = 0; + if (min > 0) { + clamped = max; + } + if (max < 0) { + clamped = min; + } + if (clamped == 0) { + if (Math.abs(min) > Math.abs(max)) { + clamped = min; + } else { + clamped = max; + } + } + return clamped; + } + + int getChildOrderModulator(int x, int y, int z, int childFullSectionDim) { + return (x + childFullSectionDim - this.cameraOffsetX) >>> 31 + | ((y + childFullSectionDim - this.cameraOffsetY) >>> 31) << 1 + | ((z + childFullSectionDim - this.cameraOffsetZ) >>> 31) << 2; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java new file mode 100644 index 0000000000..9128db2721 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java @@ -0,0 +1,54 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class Tree { + public static final int OUT_OF_BOUNDS = -1; + public static final int NOT_PRESENT = 0; + public static final int PRESENT = 1; + + protected final long[] tree = new long[64 * 64]; + protected final int offsetX, offsetY, offsetZ; + + public Tree(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + public static boolean isOutOfBounds(int x, int y, int z) { + return x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0; + } + + protected static int interleave6x3(int x, int y, int z) { + return Tree.interleave6(x) | Tree.interleave6(y) << 1 | Tree.interleave6(z) << 2; + } + + private static int interleave6(int n) { + n &= 0b000000000000111111; + n = (n | n << 4 | n << 8) & 0b000011000011000011; + n = (n | n << 2) & 0b001001001001001001; + return n; + } + + protected static int deinterleave6(int n) { + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; + n = (n | n >> 4 | n >> 8) & 0b000000000000111111; + return n; + } + + public boolean add(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); + + return true; + } + + public abstract int getPresence(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java index be9521ebad..999310be3c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java @@ -41,6 +41,30 @@ public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ, floa ); } + public boolean isBoxVisibleDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { + return this.frustum.testAab( + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, + + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize + ); + } + + public int getBoxIntersectionDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { + return this.frustum.intersectAab( + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, + + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize + ); + } + public CameraTransform getTransform() { return this.transform; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java index 3ec8d16aa6..c7495555dd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java @@ -2,4 +2,6 @@ public interface Frustum { boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); + + int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java index 88ff3b7738..2ada093971 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java @@ -13,4 +13,9 @@ public SimpleFrustum(FrustumIntersection frustumIntersection) { public boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { return this.frustum.testAab(minX, minY, minZ, maxX, maxY, maxZ); } + + @Override + public int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return this.frustum.intersectAab(minX, minY, minZ, maxX, maxY, maxZ); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java index 01d267e128..53d729e089 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java @@ -43,4 +43,12 @@ public static int floatToComparableInt(float f) { public static float comparableIntToFloat(int i) { return Float.intBitsToFloat(i ^ ((i >> 31) & 0x7FFFFFFF)); } + + public static float exponentialMovingAverage(float oldValue, float newValue, float newValueContribution) { + return newValueContribution * newValue + (1 - newValueContribution) * oldValue; + } + + public static long exponentialMovingAverage(long oldValue, long newValue, float newValueContribution) { + return (long) (newValueContribution * newValue) + (long) ((1 - newValueContribution) * oldValue); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java index f018b18e6c..ed7b08edd4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java @@ -5,7 +5,7 @@ import java.util.Arrays; /** - * Originally authored here: https://github.com/CaffeineMC/sodium/blob/ddfb9f21a54bfb30aa876678204371e94d8001db/src/main/java/net/caffeinemc/sodium/util/collections/BitArray.java + * Originally authored here * @author burgerindividual */ public class BitArray { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java index 7ebe731d18..9f98ec3c26 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java @@ -4,4 +4,16 @@ public interface CancellationToken { boolean isCancelled(); void setCancelled(); + + CancellationToken NEVER_CANCELLED = new CancellationToken() { + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void setCancelled() { + throw new UnsupportedOperationException("NEVER_CANCELLED cannot be cancelled"); + } + }; } diff --git a/common/src/main/resources/assets/sodium/lang/en_us.json b/common/src/main/resources/assets/sodium/lang/en_us.json index d1d1e6b6a7..02cbea6f41 100644 --- a/common/src/main/resources/assets/sodium/lang/en_us.json +++ b/common/src/main/resources/assets/sodium/lang/en_us.json @@ -48,8 +48,11 @@ "sodium.options.use_persistent_mapping.tooltip": "For debugging only. If enabled, persistent memory mappings will be used for the staging buffer so that unnecessary memory copies can be avoided. Disabling this can be useful for narrowing down the cause of graphical corruption.\n\nRequires OpenGL 4.4 or ARB_buffer_storage.", "sodium.options.chunk_update_threads.name": "Chunk Update Threads", "sodium.options.chunk_update_threads.tooltip": "Specifies the number of threads to use for chunk building and sorting. Using more threads can speed up chunk loading and update speed, but may negatively impact frame times. The default value is usually good enough for all situations.", - "sodium.options.always_defer_chunk_updates.name": "Always Defer Chunk Updates", - "sodium.options.always_defer_chunk_updates.tooltip": "If enabled, rendering will never wait for chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear.", + "sodium.options.defer_chunk_updates.name": "Chunk Updates", + "sodium.options.defer_chunk_updates.tooltip": "If set to \"Deferred\", rendering will never wait for nearby chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear. \"Immediate\" eliminates visual lag by blocking the frame until chunk updates are complete while \"Soon\" allows at most one frame of visual lag.", + "sodium.options.defer_chunk_updates.always": "Deferred", + "sodium.options.defer_chunk_updates.one_frame": "Soon", + "sodium.options.defer_chunk_updates.zero_frames": "Immediate", "sodium.options.sort_behavior.name": "Translucency Sorting", "sodium.options.sort_behavior.tooltip": "Enables translucency sorting. This avoids glitches in translucent blocks like water and glass when enabled and attempts to correctly present them even when the camera is in motion. This has a small performance impact on chunk loading and update speeds, but is usually not noticeable in frame rates.", "sodium.options.use_no_error_context.name": "Use No Error Context",