9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-19 15:09:15 +00:00

完善剔除线程调度

This commit is contained in:
XiaoMoMi
2025-11-29 04:20:49 +08:00
parent 19d1cb2a21
commit b77dbdfca9
14 changed files with 164 additions and 72 deletions

View File

@@ -84,6 +84,7 @@ import net.momirealms.craftengine.core.plugin.context.NetworkTextReplaceContext;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingThread;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.plugin.network.*;
@@ -128,7 +129,6 @@ import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Predicate;
@@ -462,14 +462,6 @@ public class BukkitNetworkManager implements NetworkManager, Listener, PluginMes
player.getScheduler().runAtFixedRate(plugin.javaPlugin(), (t) -> user.tick(),
() -> {}, 1, 1);
}
// 安排异步tick任务
this.plugin.scheduler().asyncRepeating(t -> {
if (!user.isOnline()) {
t.cancel();
return;
}
user.asyncTick();
}, 50, 50, TimeUnit.MILLISECONDS);
user.sendPacket(TotemAnimationCommand.FIX_TOTEM_SOUND_PACKET, false);
}
}

View File

@@ -507,9 +507,6 @@ public class BukkitServerPlayer extends Player {
this.encoderState = encoderState;
}
private final Queue<Long> recentDurations = new LinkedList<>();
private static final int SAMPLE_SIZE = 100;
@Override
public void tick() {
// not fully online
@@ -569,38 +566,16 @@ public class BukkitServerPlayer extends Player {
}
}
public void asyncTick() {
if (Config.enableEntityCulling()) {
long nano1 = System.nanoTime();
for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) {
CullingData cullingData = cullableObject.cullable.cullingData();
if (cullingData != null) {
Vec3d vec3d = LocationUtils.toVec3d(platformPlayer().getEyeLocation());
boolean visible = this.culling.isVisible(cullingData, vec3d);
cullableObject.setShown(this, visible);
} else {
cullableObject.setShown(this, true);
}
}
long nano2 = System.nanoTime();
long duration = nano2 - nano1;
// 添加到队列
recentDurations.offer(duration);
// 保持队列大小为100
if (recentDurations.size() > SAMPLE_SIZE) {
recentDurations.poll();
}
// 每100次输出一次平均耗时
if (recentDurations.size() == SAMPLE_SIZE) {
double averageMs = recentDurations.stream()
.mapToLong(Long::longValue)
.average()
.orElse(0) / 1_000_000d;
CraftEngine.instance().logger().info("EntityCulling 最近100次平均耗时: " + averageMs + "ms");
@Override
public void entityCullingTick() {
for (VirtualCullableObject cullableObject : this.trackedBlockEntityRenderers.values()) {
CullingData cullingData = cullableObject.cullable.cullingData();
if (cullingData != null) {
Vec3d vec3d = LocationUtils.toVec3d(platformPlayer().getEyeLocation());
boolean visible = this.culling.isVisible(cullingData, vec3d);
cullableObject.setShown(this, visible);
} else {
cullableObject.setShown(this, true);
}
}
}

View File

@@ -557,6 +557,8 @@ client-optimization:
entity-culling:
enable: true
view-distance: 64
# Determining the number of threads to execute these raytrace operations
threads: 1
# Enables or disables debug mode
debug:
@@ -564,4 +566,6 @@ debug:
packet: false
furniture: false
item: false
resource-pack: false
resource-pack: false
block: false
entity-culling: false

View File

@@ -32,7 +32,7 @@ public class TickingBlockEntityImpl<T extends BlockEntity> implements TickingBlo
// 不是合法方块
if (!this.blockEntity.isValidBlockState(state)) {
this.chunk.removeBlockEntity(pos);
Debugger.BLOCK_ENTITY.warn(() -> "Invalid block entity(" + this.blockEntity.getClass().getSimpleName() + ") with state " + state + " found at world " + this.chunk.world().name() + " " + pos, null);
Debugger.BLOCK.warn(() -> "Invalid block entity(" + this.blockEntity.getClass().getSimpleName() + ") with state " + state + " found at world " + this.chunk.world().name() + " " + pos, null);
return;
}
try {

View File

@@ -38,6 +38,8 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract void setClientSideWorld(World world);
public abstract void entityCullingTick();
public abstract float getDestroyProgress(Object blockState, BlockPos pos);
public abstract void setClientSideCanBreakBlock(boolean canBreak);

View File

@@ -26,6 +26,8 @@ import net.momirealms.craftengine.core.plugin.dependency.Dependencies;
import net.momirealms.craftengine.core.plugin.dependency.Dependency;
import net.momirealms.craftengine.core.plugin.dependency.DependencyManager;
import net.momirealms.craftengine.core.plugin.dependency.DependencyManagerImpl;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingManager;
import net.momirealms.craftengine.core.plugin.entityculling.EntityCullingManagerImpl;
import net.momirealms.craftengine.core.plugin.gui.GuiManager;
import net.momirealms.craftengine.core.plugin.gui.category.ItemBrowserManager;
import net.momirealms.craftengine.core.plugin.gui.category.ItemBrowserManagerImpl;
@@ -79,6 +81,7 @@ public abstract class CraftEngine implements Plugin {
protected GlobalVariableManager globalVariableManager;
protected ProjectileManager projectileManager;
protected SeatManager seatManager;
protected EntityCullingManager entityCullingManager;
private final PluginTaskRegistry beforeEnableTaskRegistry = new PluginTaskRegistry();
private final PluginTaskRegistry afterEnableTaskRegistry = new PluginTaskRegistry();
@@ -118,6 +121,8 @@ public abstract class CraftEngine implements Plugin {
this.globalVariableManager = new GlobalVariableManager();
// 初始化物品浏览器
this.itemBrowserManager = new ItemBrowserManagerImpl(this);
// 初始化实体剔除器
this.entityCullingManager = new EntityCullingManagerImpl();
}
public void setUpConfigAndLocale() {
@@ -158,6 +163,7 @@ public abstract class CraftEngine implements Plugin {
this.advancementManager.reload();
this.projectileManager.reload();
this.seatManager.reload();
this.entityCullingManager.reload();
}
private void runDelayTasks(boolean reloadRecipe) {
@@ -349,6 +355,7 @@ public abstract class CraftEngine implements Plugin {
if (this.translationManager != null) this.translationManager.disable();
if (this.globalVariableManager != null) this.globalVariableManager.disable();
if (this.projectileManager != null) this.projectileManager.disable();
if (this.entityCullingManager != null) this.entityCullingManager.disable();
if (this.scheduler != null) this.scheduler.shutdownScheduler();
if (this.scheduler != null) this.scheduler.shutdownExecutor();
if (this.commandManager != null) this.commandManager.unregisterFeatures();

View File

@@ -54,6 +54,8 @@ public class Config {
protected boolean debug$item;
protected boolean debug$furniture;
protected boolean debug$resource_pack;
protected boolean debug$block;
protected boolean debug$entity_culling;
protected boolean resource_pack$remove_tinted_leaves_particle;
protected boolean resource_pack$generate_mod_assets;
@@ -205,6 +207,7 @@ public class Config {
protected boolean client_optimization$entity_culling$enable;
protected int client_optimization$entity_culling$view_distance;
protected int client_optimization$entity_culling$threads;
public Config(CraftEngine plugin) {
this.plugin = plugin;
@@ -308,6 +311,8 @@ public class Config {
debug$item = config.getBoolean("debug.item", false);
debug$furniture = config.getBoolean("debug.furniture", false);
debug$resource_pack = config.getBoolean("debug.resource-pack", false);
debug$block = config.getBoolean("debug.block", false);
debug$entity_culling = config.getBoolean("debug.entity-culling", false);
// resource pack
resource_pack$path = resolvePath(config.getString("resource-pack.path", "./generated/resource_pack.zip"));
@@ -606,12 +611,12 @@ public class Config {
return instance.debug$item;
}
public static boolean debugBlockEntity() {
return false;
public static boolean debugBlock() {
return instance.debug$block;
}
public static boolean debugBlock() {
return false;
public static boolean debugEntityCulling() {
return instance.debug$entity_culling;
}
public static boolean debugFurniture() {
@@ -1167,6 +1172,10 @@ public class Config {
return instance.client_optimization$entity_culling$view_distance;
}
public static int entityCullingThreads() {
return instance.client_optimization$entity_culling$threads;
}
public YamlDocument loadOrCreateYamlData(String fileName) {
Path path = this.plugin.dataFolderPath().resolve(fileName);
if (!Files.exists(path)) {

View File

@@ -93,24 +93,24 @@ public final class EntityCulling {
}
int size = 0;
if (this.dotSelectors[0]) targetPoints[size++].set(minX, minY, minZ);
if (this.dotSelectors[1]) targetPoints[size++].set(maxX, minY, minZ);
if (this.dotSelectors[2]) targetPoints[size++].set(minX, minY, maxZ);
if (this.dotSelectors[3]) targetPoints[size++].set(maxX, minY, maxZ);
if (this.dotSelectors[4]) targetPoints[size++].set(minX, maxY, minZ);
if (this.dotSelectors[5]) targetPoints[size++].set(maxX, maxY, minZ);
if (this.dotSelectors[6]) targetPoints[size++].set(minX, maxY, maxZ);
if (this.dotSelectors[7]) targetPoints[size++].set(maxX, maxY, maxZ);
if (this.dotSelectors[0]) targetPoints[size++].set(minX + 0.05, minY + 0.05, minZ + 0.05);
if (this.dotSelectors[1]) targetPoints[size++].set(maxX - 0.05, minY + 0.05, minZ + 0.05);
if (this.dotSelectors[2]) targetPoints[size++].set(minX + 0.05, minY + 0.05, maxZ - 0.05);
if (this.dotSelectors[3]) targetPoints[size++].set(maxX - 0.05, minY + 0.05, maxZ - 0.05);
if (this.dotSelectors[4]) targetPoints[size++].set(minX + 0.05, maxY - 0.05, minZ + 0.05);
if (this.dotSelectors[5]) targetPoints[size++].set(maxX - 0.05, maxY - 0.05, minZ + 0.05);
if (this.dotSelectors[6]) targetPoints[size++].set(minX + 0.05, maxY - 0.05, maxZ - 0.05);
if (this.dotSelectors[7]) targetPoints[size++].set(maxX - 0.05, maxY - 0.05, maxZ - 0.05);
// 面中心点
double averageX = (minX + maxX) / 2.0;
double averageY = (minY + maxY) / 2.0;
double averageZ = (minZ + maxZ) / 2.0;
if (this.dotSelectors[8]) targetPoints[size++].set(averageX, averageY, minZ);
if (this.dotSelectors[9]) targetPoints[size++].set(averageX, averageY, maxZ);
if (this.dotSelectors[10]) targetPoints[size++].set(minX, averageY, averageZ);
if (this.dotSelectors[11]) targetPoints[size++].set(maxX, averageY, averageZ);
if (this.dotSelectors[12]) targetPoints[size++].set(averageX, minY, averageZ);
if (this.dotSelectors[13]) targetPoints[size].set(averageX, maxY, averageZ);
if (this.dotSelectors[8]) targetPoints[size++].set(averageX, averageY, minZ + 0.05);
if (this.dotSelectors[9]) targetPoints[size++].set(averageX, averageY, maxZ - 0.05);
if (this.dotSelectors[10]) targetPoints[size++].set(minX + 0.05, averageY, averageZ);
if (this.dotSelectors[11]) targetPoints[size++].set(maxX - 0.05, averageY, averageZ);
if (this.dotSelectors[12]) targetPoints[size++].set(averageX, minY + 0.05, averageZ);
if (this.dotSelectors[13]) targetPoints[size].set(averageX, maxY - 0.05, averageZ);
return isVisible(cameraPos, this.targetPoints, size);
}
@@ -194,9 +194,9 @@ public final class EntityCulling {
// 预计算每单位距离在各方块边界上的步进增量
// 这些值表示射线穿过一个方块所需的时间分数
double stepIncrementX = 1.0 / (absDeltaX + 1e-10); // 避免除0
double stepIncrementY = 1.0 / (absDeltaY + 1e-10);
double stepIncrementZ = 1.0 / (absDeltaZ + 1e-10);
double stepIncrementX = 1.0 / absDeltaX;
double stepIncrementY = 1.0 / absDeltaY;
double stepIncrementZ = 1.0 / absDeltaZ;
// 射线将穿过的总方块数量(包括起点和终点)
int totalBlocksToCheck = 1;
@@ -286,8 +286,8 @@ public final class EntityCulling {
int currentBlockY = startingY;
int currentBlockZ = startingZ;
// 遍历射线路径上的所有方块(跳过最后一个目标方块)
for (; remainingSteps > 1; remainingSteps--) {
// 遍历射线路径上的所有方块
for (; remainingSteps > 0; remainingSteps--) {
// 检查当前方块是否遮挡视线
if (isOccluding(currentBlockX, currentBlockY, currentBlockZ)) {

View File

@@ -0,0 +1,6 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.plugin.Manageable;
public interface EntityCullingManager extends Manageable {
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.plugin.config.Config;
import java.util.ArrayList;
import java.util.List;
public class EntityCullingManagerImpl implements EntityCullingManager {
private final List<EntityCullingThread> threads = new ArrayList<>();
@Override
public void load() {
if (Config.enableEntityCulling()) {
int threads = Math.min(64, Math.max(Config.entityCullingThreads(), 1));
for (int i = 0; i < threads; i++) {
EntityCullingThread thread = new EntityCullingThread(i, threads);
this.threads.add(thread);
thread.start();
}
}
}
@Override
public void unload() {
if (!this.threads.isEmpty()) {
for (EntityCullingThread thread : this.threads) {
thread.stop();
}
this.threads.clear();
}
}
}

View File

@@ -0,0 +1,65 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class EntityCullingThread {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final AtomicBoolean isRunning = new AtomicBoolean(false);
private final int id;
private final int threads;
public EntityCullingThread(int id, int threads) {
this.id = id;
this.threads = threads;
}
public void start() {
// 错开线程启动时间,避免所有线程同时执行
long initialDelay = this.id * (50L / this.threads);
this.scheduler.scheduleAtFixedRate(this::scheduleTask, initialDelay, 50, TimeUnit.MILLISECONDS);
}
private void scheduleTask() {
// 使用CAS操作更安全
if (!this.isRunning.compareAndSet(false, true)) {
return;
}
this.scheduler.execute(() -> {
try {
int processed = 0;
long startTime = System.currentTimeMillis();
for (Player player : CraftEngine.instance().networkManager().onlineUsers()) {
// 使用绝对值确保非负,使用 threads 而不是 threads-1 确保均匀分布
if (Math.abs(player.uuid().hashCode()) % this.threads == this.id) {
player.entityCullingTick();
processed++;
}
}
long duration = System.currentTimeMillis() - startTime;
if (duration > 45) {
String value = String.format("EntityCullingThread-%d processed %d players in %dms (over 45ms)",
this.id, processed, duration);
Debugger.ENTITY_CULLING.debug(() -> value);
}
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to run entity culling tick: " + e.getMessage());
} finally {
this.isRunning.set(false);
}
});
}
public void stop() {
this.scheduler.shutdown();
}
}

View File

@@ -12,7 +12,7 @@ public enum Debugger {
RESOURCE_PACK(Config::debugResourcePack),
ITEM(Config::debugItem),
BLOCK(Config::debugBlock),
BLOCK_ENTITY(Config::debugBlockEntity);
ENTITY_CULLING(Config::debugEntityCulling);
private final Supplier<Boolean> condition;

View File

@@ -462,7 +462,7 @@ public class CEChunk {
BlockPos pos = blockEntity.pos();
ImmutableBlockState blockState = this.getBlockState(pos);
if (!blockState.hasBlockEntity()) {
Debugger.BLOCK_ENTITY.debug(() -> "Failed to add invalid block entity " + blockEntity.saveAsTag() + " at " + pos);
Debugger.BLOCK.debug(() -> "Failed to add invalid block entity " + blockEntity.saveAsTag() + " at " + pos);
return;
}
// 设置方块实体所在世界

View File

@@ -38,7 +38,7 @@ public final class DefaultBlockEntitySerializer {
Key id = Key.of(data.getString("id"));
BlockEntityType<?> type = BuiltInRegistries.BLOCK_ENTITY_TYPE.getValue(id);
if (type == null) {
Debugger.BLOCK_ENTITY.debug(() -> "Unknown block entity type: " + id);
Debugger.BLOCK.debug(() -> "Unknown block entity type: " + id);
} else {
BlockPos pos = BlockEntity.readPosAndVerify(data, chunk.chunkPos());
ImmutableBlockState blockState = chunk.getBlockState(pos);