From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sat, 15 Jun 2019 08:54:33 -0700 Subject: [PATCH] Fix World#isChunkGenerated calls Optimize World#loadChunk() too This patch also adds a chunk status cache on region files (note that its only purpose is to cache the status on DISK) diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java index fa6d91886d433c0b7d77c1d059a23ee194ae14a4..56e292e6550b19d0cae9ebad369da730ca1cabd8 100644 --- a/src/main/java/net/minecraft/server/level/ChunkHolder.java +++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java @@ -111,6 +111,19 @@ public class ChunkHolder { Either either = (Either) statusFuture.getNow(null); return (either == null) ? null : (LevelChunk) either.left().orElse(null); } + + public ChunkAccess getAvailableChunkNow() { + // TODO can we just getStatusFuture(EMPTY)? + for (ChunkStatus curr = ChunkStatus.FULL, next = curr.getPreviousStatus(); curr != next; curr = next, next = next.getPreviousStatus()) { + CompletableFuture> future = this.getFutureIfPresentUnchecked(curr); + Either either = future.getNow(null); + if (either == null || !either.left().isPresent()) { + continue; + } + return either.left().get(); + } + return null; + } // CraftBukkit end public CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus leastStatus) { diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java index 25826b41812f2d9ac29806ad7a9242e5674376fc..dd7de2c3db10d9d606d47c52eba40e71034fc11a 100644 --- a/src/main/java/net/minecraft/server/level/ChunkMap.java +++ b/src/main/java/net/minecraft/server/level/ChunkMap.java @@ -84,6 +84,7 @@ import net.minecraft.world.level.chunk.ProtoChunk; import net.minecraft.world.level.chunk.UpgradeData; import net.minecraft.world.level.chunk.storage.ChunkSerializer; import net.minecraft.world.level.chunk.storage.ChunkStorage; +import net.minecraft.world.level.chunk.storage.RegionFile; import net.minecraft.world.level.entity.ChunkStatusUpdateListener; import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureManager; @@ -1083,12 +1084,61 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider // Paper end @Nullable - private CompoundTag readChunk(ChunkPos pos) throws IOException { + public CompoundTag readChunk(ChunkPos pos) throws IOException { // Paper - private -> public CompoundTag nbttagcompound = this.read(pos); + // Paper start - Cache chunk status on disk + if (nbttagcompound == null) { + return null; + } + + nbttagcompound = this.getChunkData(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, pos, level); // CraftBukkit + if (nbttagcompound == null) { + return null; + } + + this.updateChunkStatusOnDisk(pos, nbttagcompound); + + return nbttagcompound; + // Paper end + } + + // Paper start - chunk status cache "api" + public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) { + RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); - return nbttagcompound == null ? null : this.getChunkData(this.level.getTypeKey(), this.overworldDataStorage, nbttagcompound, pos, level); // CraftBukkit + return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); } + public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException { + RegionFile regionFile = regionFileCache.getFile(chunkPos, true); + + if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) { + return null; + } + + ChunkStatus status = regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + + if (status != null) { + return status; + } + + this.readChunk(chunkPos); + + return regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); + } + + public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { + RegionFile regionFile = regionFileCache.getFile(chunkPos, false); + + regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound)); + } + + public ChunkAccess getUnloadingChunk(int chunkX, int chunkZ) { + ChunkHolder chunkHolder = this.pendingUnloads.get(ChunkPos.asLong(chunkX, chunkZ)); + return chunkHolder == null ? null : chunkHolder.getAvailableChunkNow(); + } + // Paper end + boolean noPlayersCloseForSpawning(ChunkPos chunkPos) { // Spigot start return this.isOutsideOfRange(chunkPos, false); diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java index 77bb2fb280293da4a38f9acc6503e84997ecb399..c47d1772044913475a60292162ef4be594bed4c6 100644 --- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java +++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java @@ -52,10 +52,10 @@ public class ServerChunkCache extends ChunkSource { private static final List CHUNK_STATUSES = ChunkStatus.getStatusList(); public static final List getPossibleChunkStatuses() { return ServerChunkCache.CHUNK_STATUSES; } // Paper - OBFHELPER private final DistanceManager distanceManager; public final ChunkGenerator generator; - final ServerLevel level; - public final Thread mainThread; // Paper - package-private -> public - final ThreadedLevelLightEngine lightEngine; - public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; // Paper - private -> public + private final ServerLevel level; + public final Thread mainThread; // Paper - private -> public + private final ThreadedLevelLightEngine lightEngine; + public final ServerChunkCache.MainThreadExecutor mainThreadProcessor; // Paper private -> public public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; private long lastInhabitedUpdate; @@ -324,6 +324,7 @@ public class ServerChunkCache extends ChunkSource { } // Paper end // Paper start - async chunk io + @Nullable public ChunkAccess getChunkAtImmediately(int x, int z) { ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); if (holder == null) { @@ -922,7 +923,7 @@ public class ServerChunkCache extends ChunkSource { return this.lastSpawnState; } - private final class MainThreadExecutor extends BlockableEventLoop { + public final class MainThreadExecutor extends BlockableEventLoop { // Paper - package -> public MainThreadExecutor(Level world) { super("Chunk source main thread executor for " + world.dimension().location()); diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java index 6e0cf8ee76143301c939fc4af5eeb091abdcbc5c..066f03ee7b4feda9ec2b0984ee7cf63fa0b9e4fc 100644 --- a/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java +++ b/src/main/java/net/minecraft/world/level/chunk/ChunkStatus.java @@ -204,6 +204,7 @@ public class ChunkStatus { return this.name; } + public ChunkStatus getPreviousStatus() { return this.getParent(); } // Paper - OBFHELPER public ChunkStatus getParent() { return this.parent; } @@ -224,6 +225,17 @@ public class ChunkStatus { return this.chunkType; } + // Paper start + public static ChunkStatus getStatus(String name) { + try { + // We need this otherwise we return EMPTY for invalid names + ResourceLocation key = new ResourceLocation(name); + return Registry.CHUNK_STATUS.getOptional(key).orElse(null); + } catch (Exception ex) { + return null; // invalid name + } + } + // Paper end public static ChunkStatus byName(String id) { return (ChunkStatus) Registry.CHUNK_STATUS.get(ResourceLocation.tryParse(id)); } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index 2621739b8dd11860084ea574c243cb8ba167ac40..fc320450878279a6aa48019fbde35bb183f5f06e 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -545,6 +545,17 @@ public class ChunkSerializer { return nbttagcompound; } + // Paper start + public static ChunkStatus getStatus(CompoundTag compound) { + if (compound == null) { + return null; + } + + // Note: Copied from below + return ChunkStatus.getStatus(compound.getCompound("Level").getString("Status")); + } + // Paper end + public static ChunkStatus.ChunkType getChunkTypeFromTag(@Nullable CompoundTag nbt) { if (nbt != null) { ChunkStatus chunkstatus = ChunkStatus.byName(nbt.getCompound("Level").getString("Status")); diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java index 357da4846344d1182ab7149c4d352d5019384715..832392cc2adc94e1fcb1055d91eb465529da1e92 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java @@ -23,6 +23,7 @@ import java.nio.file.StandardOpenOption; import javax.annotation.Nullable; import net.minecraft.Util; import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -49,6 +50,30 @@ public class RegionFile implements AutoCloseable { protected final RegionBitmap usedSectors; public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper + // Paper start - Cache chunk status + private final ChunkStatus[] statuses = new ChunkStatus[32 * 32]; + + private boolean closed; + + // invoked on write/read + public void setStatus(int x, int z, ChunkStatus status) { + if (this.closed) { + // We've used an invalid region file. + throw new IllegalStateException("RegionFile is closed"); + } + this.statuses[getChunkLocation(x, z)] = status; + } + + public ChunkStatus getStatusIfCached(int x, int z) { + if (this.closed) { + // We've used an invalid region file. + throw new IllegalStateException("RegionFile is closed"); + } + final int location = getChunkLocation(x, z); + return this.statuses[location]; + } + // Paper end + public RegionFile(File file, File directory, boolean dsync) throws IOException { this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync); } @@ -395,6 +420,7 @@ public class RegionFile implements AutoCloseable { return this.getOffset(pos) != 0; } + private static int getChunkLocation(int x, int z) { return (x & 31) + (z & 31) * 32; } // Paper - OBFHELPER - sort of, mirror of logic below private static int getOffsetIndex(ChunkPos pos) { return pos.getRegionLocalX() + pos.getRegionLocalZ() * 32; } @@ -405,6 +431,7 @@ public class RegionFile implements AutoCloseable { synchronized (this) { try { // Paper end + this.closed = true; // Paper try { this.padToFullSector(); } finally { diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java index 211ab6cffe78c61fcff12ef7ffba904c4cae57b2..1bee455235ece8aa299a2baeede027d251e6ff57 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -143,6 +143,7 @@ public class RegionFileStorage implements AutoCloseable { // Paper - no final try { NbtIo.write(nbt, (DataOutput) dataoutputstream); + regionfile.setStatus(pos.x, pos.z, ChunkSerializer.getStatus(nbt)); // Paper - cache status on disk } catch (Throwable throwable) { if (dataoutputstream != null) { try { @@ -205,3 +206,4 @@ public class RegionFileStorage implements AutoCloseable { // Paper - no final } } + diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java index 98b2d054b6436e3fdb8fadd03369a65cf4156843..f9c58de7fa8b3c2ab5ac78cf0b366df69e0b40df 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; import java.util.stream.Collectors; import net.minecraft.core.BlockPos; @@ -418,8 +419,22 @@ public class CraftWorld implements World { @Override public boolean isChunkGenerated(int x, int z) { + // Paper start - Fix this method + if (!Bukkit.isPrimaryThread()) { + return CompletableFuture.supplyAsync(() -> { + return CraftWorld.this.isChunkGenerated(x, z); + }, world.getChunkSource().mainThreadProcessor).join(); + } + ChunkAccess chunk = world.getChunkSource().getChunkAtImmediately(x, z); + if (chunk == null) { + chunk = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); + } + if (chunk != null) { + return chunk instanceof ImposterProtoChunk || chunk instanceof net.minecraft.world.level.chunk.LevelChunk; + } try { - return this.world.getChunkSource().getChunkAtIfCachedImmediately(x, z) != null || this.world.getChunkSource().chunkMap.read(new ChunkPos(x, z)) != null; // Paper (TODO check if the first part can be removed) + return world.getChunkSource().chunkMap.getChunkStatusOnDisk(new ChunkPos(x, z)) == ChunkStatus.FULL; + // Paper end } catch (IOException ex) { throw new RuntimeException(ex); } @@ -530,20 +545,48 @@ public class CraftWorld implements World { @Override public boolean loadChunk(int x, int z, boolean generate) { org.spigotmc.AsyncCatcher.catchOp("chunk load"); // Spigot - ChunkAccess chunk = this.world.getChunkSource().getChunk(x, z, generate || isChunkGenerated(x, z) ? ChunkStatus.FULL : ChunkStatus.EMPTY, true); // Paper + // Paper start - Optimize this method + ChunkPos chunkPos = new ChunkPos(x, z); - // If generate = false, but the chunk already exists, we will get this back. - if (chunk instanceof ImposterProtoChunk) { - // We then cycle through again to get the full chunk immediately, rather than after the ticket addition - chunk = this.world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); - } + if (!generate) { + ChunkAccess immediate = world.getChunkSource().getChunkAtImmediately(x, z); + if (immediate == null) { + immediate = world.getChunkSource().chunkMap.getUnloadingChunk(x, z); + } + if (immediate != null) { + if (!(immediate instanceof ImposterProtoChunk) && !(immediate instanceof net.minecraft.world.level.chunk.LevelChunk)) { + return false; // not full status + } + world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE); + world.getChunk(x, z); // make sure we're at ticket level 32 or lower + return true; + } - if (chunk instanceof net.minecraft.world.level.chunk.LevelChunk) { - this.world.getChunkSource().addRegionTicket(TicketType.PLUGIN, new ChunkPos(x, z), 1, Unit.INSTANCE); - return true; + net.minecraft.world.level.chunk.storage.RegionFile file; + try { + file = world.getChunkSource().chunkMap.regionFileCache.getFile(chunkPos, false); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + + ChunkStatus status = file.getStatusIfCached(x, z); + if (!file.hasChunk(chunkPos) || (status != null && status != ChunkStatus.FULL)) { + return false; + } + + ChunkAccess chunk = world.getChunkSource().getChunk(x, z, ChunkStatus.EMPTY, true); + if (!(chunk instanceof ImposterProtoChunk) && !(chunk instanceof net.minecraft.world.level.chunk.LevelChunk)) { + return false; + } + + // fall through to load + // we do this so we do not re-read the chunk data on disk } - return false; + world.getChunkSource().addRegionTicket(TicketType.PLUGIN, chunkPos, 1, Unit.INSTANCE); + world.getChunkSource().getChunk(x, z, ChunkStatus.FULL, true); + return true; + // Paper end } @Override