diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb2b3ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Gradle 构建产物 +.gradle/ +build/ +out/ + +# IDE +.idea/ +.vscode/ +*.iml + +# 运行时数据 +runs/ +logs/ +*.log + +# 系统文件 +.DS_Store +Thumbs.db diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7402740 --- /dev/null +++ b/build.gradle @@ -0,0 +1,99 @@ +plugins { + id 'java-library' + id 'maven-publish' + id 'net.neoforged.gradle.userdev' version '7.0.170' +} + +tasks.named('wrapper', Wrapper).configure { + distributionType = Wrapper.DistributionType.BIN + gradleVersion = '8.10' +} + +version = mod_version +group = mod_group_id + +base { + archivesName = mod_id +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + withSourcesJar() +} + +//minecraft.accessTransformers.file rootProject.file('src/main/resources/META-INF/accesstransformer.cfg') +//minecraft.accessTransformers.publish rootProject.file('src/main/resources/META-INF/accesstransformer.cfg') + +runs { + configureEach { + systemProperty 'forge.logging.markers', 'REGISTRIES' + //systemProperty 'forge.logging.console.level', 'debug' + systemProperty 'mixin.debug.verbose', 'true' + systemProperty 'mixin.debug.export', 'true' + modSource project.sourceSets.main + } + + client { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + // TEMP-DEBUG: auto-join world for deadlock repro (REMOVE AFTER DEBUG) + argument '--quickPlaySingleplayer' + argument '新的世界' + // 关闭 JVM 热路径堆栈省略,便于定位并行子任务中反复抛出的 ClassCastException + jvmArguments '-XX:-OmitStackTraceInFastThrow' + } + + server { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + argument '--nogui' + } + + gameTestServer { + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + arguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } +} + +sourceSets.main.resources { + srcDir 'src/generated/resources' +} + +dependencies { + implementation "net.neoforged:neoforge:${neo_version}" +} + +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [ + minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_range : neo_version_range, + loader_version_range: loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : mod_version, + mod_authors : mod_authors, + mod_description : mod_description + ] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..65f25d3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,55 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +# This is required to provide enough memory for the Minecraft decompilation process. +# 本机 JDK 路径(机器特定,已从仓库中注释掉)。如本地构建报"无法找到 JDK",请取消注释并填你本机的 JDK 路径。 +#org.gradle.java.home=C:\\Program Files\\Java\\jdk-21 +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true + +# Environment Properties +# You can setup the workspace by running: gradle sync +# Note: the non-default values are documented in the NeoGradle user docs. You should probably be using them. +neo_version=21.1.244 +# Parchment is an unofficial project maintained by ParchmentMC. It aims to provide better parameter names and javadocs. +# You should use the latest version matching your Minecraft version. +# parchment_minecraft_version=1.21.1 +# parchment_mappings_version=2024.11.17 + +# The Minecraft version must agree with the NeoForge version to get a valid artifact +minecraft_version=1.21.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning semantics. +minecraft_version_range=[1.21.1,1.22) +# The NeoForge version must agree with the Minecraft version to get a valid artifact +neo_version_range=[21.1.0,21.2) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) + +## Mod Properties + +# The unique mod identifier for the mod. Must be lowercase, alphanumeric separated by underscores. +# This is used to identify the mod in the mod list, for dependencies, and in the file name. +mod_id=dimensionalripper +# The human-readable display name for the mod. +mod_name=Dimensional Ripper +# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. +mod_license=All-Rights-Reserved +# The mod version. See https://semver.org/ +mod_version=1.0.0 +# The group ID for the mod. It is only important when publishing build artifacts to Maven. +mod_group_id=dev.modzuozhi +# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_authors=YourNameHere +# The description of the mod. This is a simple text string that is used for display purposes in the mod list. +mod_description=Multi-threaded dimension parallel optimization: independent per-dimension tick loops, fine-grained parallel ticking for entities/block entities/chunks, collision optimization, and asynchronous portal teleportation support (vanilla nether/end and mod dimensions like Twilight Forest/Aether). + +# Proxy settings for VPN / system proxy +# 说明:VPN 开启(系统代理模式)时取消下面 4 行注释;直连可访问时保持注释状态 +#systemProp.http.proxyHost=127.0.0.1 +#systemProp.http.proxyPort=57777 +#systemProp.https.proxyHost=127.0.0.1 +#systemProp.https.proxyPort=57777 +#systemProp.http.nonProxyHosts=localhost|127.*|192.168.*|10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.20.*|172.21.*|172.22.*|172.23.*|172.24.*|172.25.*|172.26.*|172.27.*|172.28.*|172.29.*|172.30.*|172.31.* diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..9355b41 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..de98174 --- /dev/null +++ b/gradlew @@ -0,0 +1,38 @@ +#!/bin/sh +# +# Gradle start up script for POSIX generated by the Gradle Wrapper project. +# + +# Resolve APP_HOME +APP_HOME=$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P ) +APP_NAME="Gradle" +APP_BASE_NAME=$(basename "$0") + +# Add default JVM options here. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use. +if [ -n "$JAVA_HOME" ]; then + JAVA_CMD="$JAVA_HOME/bin/java" +else + JAVA_CMD="java" +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then + MAX_FD_LIMIT=$(ulimit -H -n) + if [ $? -eq 0 ]; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then + MAX_FD=$MAX_FD_LIMIT + fi + ulimit -n $MAX_FD + fi +fi + +exec "$JAVA_CMD" $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..50d06f3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c16ef86 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + maven { url = 'https://maven.neoforged.net/releases' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +rootProject.name = 'dimensionalripper' diff --git a/src/main/java/dev/modzuozhi/ModZuozhi.java b/src/main/java/dev/modzuozhi/ModZuozhi.java new file mode 100644 index 0000000..3be8544 --- /dev/null +++ b/src/main/java/dev/modzuozhi/ModZuozhi.java @@ -0,0 +1,45 @@ +package dev.modzuozhi; + +import com.mojang.logging.LogUtils; +import dev.modzuozhi.command.MasterCommand; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.event.RegisterCommandsEvent; +import org.slf4j.Logger; + +@Mod(ModZuozhi.MOD_ID) +public class ModZuozhi { + public static final String MOD_ID = "dimensionalripper"; + public static final Logger LOGGER = LogUtils.getLogger(); + + /** + * 全局主开关:false 时整个模组退化为原版(所有并行环节关闭),运行时可由 + * {@code /dimensionalripper on|off} 切换,无需修改配置或重启。 + */ + public static volatile boolean ENABLED = true; + + /** 全局主开关是否开启。所有 mixin 的并行入口都应先判断此开关。 */ + public static boolean isEnabled() { + return ENABLED; + } + + /** + * 是否禁用实体间碰撞(Mob Collision Off 模式,释放大量 TPS 红利)。 + * 默认开启(已长时间测试稳定),可由 {@code /dimensionalripper nocollide on|off} 运行时切换。 + */ + public static volatile boolean MOB_COLLISION_OFF = true; + + public ModZuozhi(IEventBus modEventBus) { + LOGGER.info("[ModZuozhi] 初始化多线程模组(dimthreads 式维度并行)..."); + } + + @EventBusSubscriber(modid = MOD_ID) + public static class CommandRegistration { + @SubscribeEvent + public static void onRegisterCommands(RegisterCommandsEvent event) { + MasterCommand.register(event.getDispatcher()); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/command/MasterCommand.java b/src/main/java/dev/modzuozhi/command/MasterCommand.java new file mode 100644 index 0000000..b3fe7c3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/command/MasterCommand.java @@ -0,0 +1,111 @@ +package dev.modzuozhi.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FaultGuard; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.TpsTracker; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; + +import static net.minecraft.commands.Commands.literal; + +/** + * modzuozhi 管理命令。 + *

+ * 全局多线程优化默认启用,不提供全局开关(运行时切换全局并行风险高,容易引发 + * 状态不一致问题)。若触发自动降级(见 {@link FaultGuard}),默认退回原版串行,重启游戏 + * 即恢复启用,也可执行 {@code /dimensionalripper fine on} 手动复位。 + *

+ * /dimensionalripper fine on|off - 单独开关"细粒度并行"(维度内 TE/区块/实体并行,默认开启)。 + * /dimensionalripper nocollide on|off - 单独开关"禁用实体间碰撞"(Mob Collision Off 模式,默认开启)。 + * /dimensionalripper status - 查看当前运行状态、降级情况与故障原因。 + */ +public final class MasterCommand { + private MasterCommand() { + } + + public static void register(CommandDispatcher dispatcher) { + dispatcher.register( + literal("dimensionalripper") + .then(literal("status").executes(MasterCommand::status)) + .then(literal("fine") + .then(literal("on").executes(ctx -> setFine(ctx, true))) + .then(literal("off").executes(ctx -> setFine(ctx, false)))) + .then(literal("nocollide") + .then(literal("on").executes(ctx -> setNoCollide(ctx, true))) + .then(literal("off").executes(ctx -> setNoCollide(ctx, false))))); + } + + private static int setFine(CommandContext ctx, boolean enabled) { + if (enabled && !ModZuozhi.isEnabled()) { + // 全局关闭时单独开细粒度没有意义(维度 loop 已停、并行入口被 isActive 拦截), + // 还会留下"全局关但细粒度开"的不一致状态。这里自动恢复全局开启,下一 tick + // 主线程会重新拉起维度 loop(killAllLoops 已清空 loops map,computeIfAbsent 重建)。 + ModZuozhi.ENABLED = true; + ctx.getSource().sendSuccess(() -> + Component.literal("全局此前已关闭,已随细粒度一并恢复为开启"), false); + ModZuozhi.LOGGER.info("[ModZuozhi] 全局随 /dimensionalripper fine on 自动恢复为开启"); + } + FineGrainScheduler.setEnabled(enabled); + if (!enabled) { + // 关闭细粒度后必须先排空 SUB_POOL 残留子任务:此时维度 loop 已退化为串行 tick, + // 残留实体子任务会与它并发改 EntitySection(靠 isLockDisabled 的残留保护仍持锁互斥, + // 见 EntityTickParallel),等排空后锁才回落到零开销纯串行。 + FineGrainScheduler.awaitIdle(5000L); + } + if (enabled) { + FaultGuard.reset(); + } + ctx.getSource().sendSuccess(() -> + Component.literal("细粒度并行已" + (enabled ? "开启" : "关闭")), false); + return 1; + } + + private static int setNoCollide(CommandContext ctx, boolean enabled) { + ModZuozhi.MOB_COLLISION_OFF = enabled; + ctx.getSource().sendSuccess(() -> + Component.literal("实体间碰撞已" + (enabled ? "禁用" : "恢复")), false); + ModZuozhi.LOGGER.info("[ModZuozhi] 实体间碰撞已切换为 {}", enabled ? "禁用" : "恢复"); + return 1; + } + + private static int status(CommandContext ctx) { + String fine = FineGrainScheduler.isEnabled() ? "开启" : "关闭"; + String noCollide = ModZuozhi.MOB_COLLISION_OFF ? "开启" : "关闭"; + String fineMark = FaultGuard.isFineDegraded() ? "(已自动降级)" : ""; + // 全局始终默认启用,不提供开关;FaultGuard 若触发全局降级,其信息已写入 reason。 + String reason = FaultGuard.getReason(); + + ServerLevel level = ctx.getSource().getLevel(); + ResourceKey dimension = level.dimension(); + // 维度并行激活时:维度 loop 的真实 MSPT(recordDimension 持续更新); + // 全局被 FaultGuard 降级关闭(退回原版串行,维度 loop 已停、recordDimension 不再更新)时: + // 用主线程 tick 间隔样本,它才是当前真实 tick 周期。 + double tps = DimThreadCore.MANAGER.isActive(level.getServer()) + ? TpsTracker.tps(dimension) + : TpsTracker.tps(); + + String msg = String.format( + "modzuozhi 状态:细粒度=%s%s,禁碰撞=%s,故障原因:%s\n当前维度:%s,TPS:%.1f", + fine, fineMark, noCollide, reason, + dimensionName(dimension), tps); + ctx.getSource().sendSuccess(() -> Component.literal(msg), false); + return 1; + } + + /** 把维度 key 映射成玩家可读的中文名。 */ + private static String dimensionName(ResourceKey dimension) { + return switch (dimension.location().getPath()) { + case "overworld" -> "主世界"; + case "the_nether" -> "下界"; + case "the_end" -> "末地"; + default -> dimension.location().toString(); + }; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java new file mode 100644 index 0000000..2ce961b --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLong2ObjectMap.java @@ -0,0 +1,193 @@ +package dev.modzuozhi.core.collection; + +import it.unimi.dsi.fastutil.longs.AbstractLongSet; +import it.unimi.dsi.fastutil.longs.Long2ObjectFunction; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongSet; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.LongFunction; + +/** + * 第11步:SECTION_LOCK → 并发集合——并发的 {@link Long2ObjectMap} 实现(借鉴 MCMT 思路,独立重写)。 + *

+ * {@code EntitySectionStorage.sections} 字段类型是 fastutil {@code Long2ObjectMap},mixin 无法改字段类型, + * 只能替换实例。fastutil 没有现成的并发 {@code Long2ObjectMap},因此继承 + * {@link Long2ObjectOpenHashMap}(保持字段类型兼容),内部改用 {@link ConcurrentHashMap} 作为实际存储, + * 仅覆盖被 {@code EntitySectionStorage} 实际调用的方法(get/remove/computeIfAbsent/keySet/size/...)。 + *

+ * 弱一致性说明:{@link #keySet()} 返回的 {@code LongSet} 是 {@link ConcurrentHashMap#keySet()} 的 + * 弱一致视图,并发增删期间迭代不会抛 {@code ConcurrentModificationException},可安全用于 + * {@code getAllChunksWithExistingSections} 的结构遍历。 + *

+ * 未覆盖的方法(entrySet/long2ObjectEntrySet/values/putAll 等)会落到父类的内部 fastutil 数组(恒空), + * 因 {@code EntitySectionStorage} 从不对 {@code sections} 调用它们,故无影响(若未来使用需补充覆盖)。 + */ +public final class ConcurrentLong2ObjectMap extends Long2ObjectOpenHashMap { + private static final long serialVersionUID = 1L; + + /** 实际存储:并发哈希表。 */ + private final Map backing = new ConcurrentHashMap<>(); + + @Override + public V get(long key) { + V v = backing.get(key); + return (v == null && !backing.containsKey(key)) ? defaultReturnValue() : v; + } + + @Override + public V get(Object key) { + return (key instanceof Long) ? get((long) (Long) key) : defaultReturnValue(); + } + + @Override + public boolean containsKey(long key) { + return backing.containsKey(key); + } + + @Override + public boolean containsKey(Object key) { + return key instanceof Long && backing.containsKey(key); + } + + @Override + public V put(long key, V value) { + return backing.put(key, value); + } + + @Override + public V put(Long key, V value) { + return backing.put(key, value); + } + + @Override + public V remove(long key) { + return backing.remove(key); + } + + @Override + public V remove(Object key) { + return backing.remove(key); + } + + @Override + public boolean remove(long key, Object value) { + return backing.remove(key, value); + } + + @Override + public boolean remove(Object key, Object value) { + return key instanceof Long && backing.remove(key, value); + } + + @Override + public V computeIfAbsent(long key, Long2ObjectFunction mappingFunction) { + V v = backing.computeIfAbsent(key, l -> mappingFunction.get(l)); + return v == null ? defaultReturnValue() : v; + } + + @Override + public V computeIfAbsent(long key, LongFunction mappingFunction) { + V v = backing.computeIfAbsent(key, l -> mappingFunction.apply(l)); + return v == null ? defaultReturnValue() : v; + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public void clear() { + backing.clear(); + } + + @Override + public LongSet keySet() { + return new BackedKeySet(backing.keySet()); + } + + /** {@link ConcurrentHashMap#keySet()} 的 fastutil 弱一致 {@code LongSet} 视图。 */ + private static final class BackedKeySet extends AbstractLongSet { + private static final long serialVersionUID = 1L; + + private final Set backing; + + BackedKeySet(Set backing) { + this.backing = backing; + } + + @Override + public LongIterator iterator() { + return new IteratorView(backing.iterator()); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean add(long k) { + return backing.add(k); + } + + @Override + public boolean remove(long k) { + return backing.remove(k); + } + + @Override + public boolean contains(long k) { + return backing.contains(k); + } + + @Override + public boolean contains(Object o) { + return backing.contains(o); + } + + @Override + public boolean remove(Object o) { + return backing.remove(o); + } + + @Override + public void clear() { + backing.clear(); + } + } + + /** 把 {@code java.util.Iterator} 包装成 fastutil 原语迭代器。 */ + private static final class IteratorView implements LongIterator { + private final java.util.Iterator delegate; + + IteratorView(java.util.Iterator delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public long nextLong() { + return delegate.next(); + } + + @Override + public Long next() { + return delegate.next(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java new file mode 100644 index 0000000..e6c8c23 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/collection/ConcurrentLongSortedSet.java @@ -0,0 +1,156 @@ +package dev.modzuozhi.core.collection; + +import it.unimi.dsi.fastutil.longs.AbstractLongBidirectionalIterator; +import it.unimi.dsi.fastutil.longs.AbstractLongSortedSet; +import it.unimi.dsi.fastutil.longs.LongBidirectionalIterator; +import it.unimi.dsi.fastutil.longs.LongComparator; +import it.unimi.dsi.fastutil.longs.LongSortedSet; + +import java.util.NavigableSet; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * 第11步:SECTION_LOCK → 并发集合——并发的 {@link LongSortedSet} 实现(借鉴 MCMT 思路,独立重写)。 + *

+ * {@code EntitySectionStorage.sectionIds} 字段类型是 fastutil {@code LongSortedSet},mixin 无法改字段类型, + * 只能替换实例。fastutil 没有现成的并发 {@code LongSortedSet},因此实现 + * {@link LongSortedSet},内部用 {@link ConcurrentSkipListSet}(有序、弱一致迭代、无 CME)。 + *

+ * 与 MCMT 参考实现的关键差异:{@link #subSet}/{@link #headSet}/{@link #tailSet} 返回的是 + * 同一 {@link ConcurrentSkipListSet} 子集视图的弱一致包装(不拷贝快照),避免每查询一次 + * {@code forEachAccessibleNonEmptySection} 就分配一棵新 AVL 树的 GC 开销;迭代器弱一致, + * 并发增删期间迭代不会抛 {@code ConcurrentModificationException}。 + *

+ * {@code EntitySectionStorage.createSection} 先 {@code sectionIds.add} 再建 {@code EntitySection}, + * 读方在 subSet 迭代中可能遇到尚未写入 {@code sections} 的 key——原版 + * {@code forEachAccessibleNonEmptySection} 已有 {@code sections.get(...) != null} 检查,天然安全。 + */ +public final class ConcurrentLongSortedSet extends AbstractLongSortedSet { + private static final long serialVersionUID = 1L; + + /** 实际存储:有序并发跳表。 */ + private final NavigableSet back; + + public ConcurrentLongSortedSet() { + this.back = new ConcurrentSkipListSet<>(); + } + + private ConcurrentLongSortedSet(NavigableSet back) { + this.back = back; + } + + @Override + public LongBidirectionalIterator iterator() { + return new SortedIteratorView(back.iterator()); + } + + @Override + public LongBidirectionalIterator iterator(long fromElement) { + return new SortedIteratorView(back.tailSet(fromElement, true).iterator()); + } + + @Override + public int size() { + return back.size(); + } + + @Override + public boolean isEmpty() { + return back.isEmpty(); + } + + @Override + public boolean add(long k) { + return back.add(k); + } + + @Override + public boolean add(Long k) { + return back.add(k); + } + + @Override + public boolean remove(long k) { + return back.remove(k); + } + + @Override + public boolean remove(Object o) { + return back.remove(o); + } + + @Override + public boolean contains(long k) { + return back.contains(k); + } + + @Override + public boolean contains(Object o) { + return back.contains(o); + } + + @Override + public void clear() { + back.clear(); + } + + @Override + public long firstLong() { + return back.first(); + } + + @Override + public long lastLong() { + return back.last(); + } + + @Override + public LongSortedSet subSet(long fromElement, long toElement) { + // ConcurrentSkipListSet 的 2 参 subSet 返回 SortedSet;用 4 参重载([from, to) 语义一致)拿到 NavigableSet 视图 + return new ConcurrentLongSortedSet(back.subSet(fromElement, true, toElement, false)); + } + + @Override + public LongSortedSet headSet(long toElement) { + return new ConcurrentLongSortedSet(back.headSet(toElement, false)); + } + + @Override + public LongSortedSet tailSet(long fromElement) { + return new ConcurrentLongSortedSet(back.tailSet(fromElement, true)); + } + + @Override + public LongComparator comparator() { + return null; + } + + /** 把 {@code java.util.Iterator} 包装成 fastutil 双向原语迭代器(previous 不用于本集合场景)。 */ + private static final class SortedIteratorView extends AbstractLongBidirectionalIterator { + private final java.util.Iterator delegate; + + SortedIteratorView(java.util.Iterator delegate) { + this.delegate = delegate; + } + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public long nextLong() { + return delegate.next(); + } + + @Override + public boolean hasPrevious() { + return false; + } + + @Override + public long previousLong() { + throw new UnsupportedOperationException(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java b/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java new file mode 100644 index 0000000..bb8f8cb --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/collection/Int2ObjectConcurrentHashMap.java @@ -0,0 +1,111 @@ +package dev.modzuozhi.core.collection; + +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.AbstractObjectCollection; +import it.unimi.dsi.fastutil.objects.ObjectCollection; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ObjectIterators; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第14步(对齐 Async 零锁路线):并发的 {@code Int2ObjectMap} 实现(借鉴 MCMT/Async 思路,独立重写)。 + *

+ * {@code EntityLookup.byId} 字段类型是 fastutil {@code Int2ObjectMap},mixin 无法改字段类型, + * 只能替换实例。fastutil 无现成并发版,因此继承 {@link Int2ObjectOpenHashMap}(保持字段类型兼容), + * 内部用 {@link ConcurrentHashMap} 作为实际存储,仅覆盖 {@code EntityLookup} 用到的 + * get/put/remove/values/size 等方法。 + *

+ * {@link #values()} 返回 {@link ConcurrentHashMap#values()} 的弱一致视图(迭代并发增删不抛 CME), + * 供 {@code EntityLookup.getEntities/getAllEntities} 安全遍历。 + */ +public final class Int2ObjectConcurrentHashMap extends Int2ObjectOpenHashMap { + private static final long serialVersionUID = 1L; + + /** 实际存储:并发哈希表。 */ + private final Map backing = new ConcurrentHashMap<>(); + + @Override + public V get(int key) { + V v = backing.get(key); + return (v == null && !backing.containsKey(key)) ? defaultReturnValue() : v; + } + + @Override + public V get(Object key) { + return (key instanceof Integer) ? get((int) (Integer) key) : defaultReturnValue(); + } + + @Override + public boolean containsKey(int key) { + return backing.containsKey(key); + } + + @Override + public boolean containsKey(Object key) { + return key instanceof Integer && backing.containsKey(key); + } + + @Override + public V put(int key, V value) { + return backing.put(key, value); + } + + @Override + public V put(Integer key, V value) { + return backing.put(key, value); + } + + @Override + public V remove(int key) { + return backing.remove(key); + } + + @Override + public V remove(Object key) { + return backing.remove(key); + } + + @Override + public int size() { + return backing.size(); + } + + @Override + public boolean isEmpty() { + return backing.isEmpty(); + } + + @Override + public void clear() { + backing.clear(); + } + + @Override + public ObjectCollection values() { + return new ValuesView(backing.values()); + } + + /** {@link ConcurrentHashMap#values()} 的弱一致视图(迭代安全)。 */ + private static final class ValuesView extends AbstractObjectCollection { + private static final long serialVersionUID = 1L; + + private final Collection backing; + + ValuesView(Collection backing) { + this.backing = backing; + } + + @Override + public ObjectIterator iterator() { + return ObjectIterators.asObjectIterator(backing.iterator()); + } + + @Override + public int size() { + return backing.size(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java b/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java new file mode 100644 index 0000000..24c4387 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ChunkLock.java @@ -0,0 +1,95 @@ +package dev.modzuozhi.core.dimthread; + +import net.minecraft.core.BlockPos; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +/** + * 按区块坐标的细粒度可重入锁(借鉴 MCMT 的 ChunkLock 思路,写我们自己的实现)。 + *

+ * 细粒度并行(subtask)下,多个子任务可能并发访问同一区块内的方块实体 / 方块 / + * getChunk / 光照等结构。用「区块坐标 → {@link ReentrantLock}」做互斥:同一区块内的子任务 + * 串行,不同区块并行,既保证安全又保留并行度。 + *

+ * 第5步(6b):radius 多区块锁。某些子任务会跨区块访问(如破坏方块、爆炸、 + * 大型实体),单区块锁不足。提供 {@link #lock(BlockPos, int)} / {@link #lock(int, int, int)}, + * 锁定以目标区块为中心 {@code (1+2r)²} 个区块的正方形范围:先对目标区块坐标排序 + * 再逐个加锁(排序保证多线程以相同顺序取锁,杜绝循环等待死锁),解锁时逆序释放。 + * 仍保留单区块 {@link #lock(BlockPos)} 供普通子任务使用(等价 radius=0)。 + *

+ * 线程安全:锁表用 {@link ConcurrentHashMap},{@code computeIfAbsent} 原子取锁,避免并发创建。 + * 锁按需惰性创建;为控制内存,锁数量与活跃区块数同量级,可接受(如需要可在后续版本加入过期回收)。 + */ +public final class ChunkLock { + private static final ConcurrentHashMap LOCKS = new ConcurrentHashMap<>(); + + private ChunkLock() { + } + + /** 锁定包含该世界坐标的区块(返回后必须 {@code close()} 释放)。 */ + public static AutoCloseable lock(BlockPos pos) { + return lock(pos.getX() >> 4, pos.getZ() >> 4); + } + + /** 锁定指定区块(返回后必须 {@code close()} 释放)。 */ + public static AutoCloseable lock(int chunkX, int chunkZ) { + long key = ((long) chunkX << 32) ^ (chunkZ & 0xFFFFFFFFL); + ReentrantLock lock = LOCKS.computeIfAbsent(key, k -> new ReentrantLock()); + lock.lock(); + return lock::unlock; + } + + /** + * 锁定包含该世界坐标区块及其周围 radius 圈的区块({@code (1+2r)²} 个)。 + * 排序加锁、逆序释放,防死锁。返回的 {@link ChunkLockHandle} 用后必须 {@code close()}。 + * + * @param radius 区块半径(0=仅本区块,1=3×3=9 个区块) + */ + public static ChunkLockHandle lock(BlockPos pos, int radius) { + return lock(pos.getX() >> 4, pos.getZ() >> 4, radius); + } + + /** + * 锁定指定区块及其周围 radius 圈的区块({@code (1+2r)²} 个)。 + * 排序加锁、逆序释放,防死锁。返回的 {@link ChunkLockHandle} 用后必须 {@code close()}。 + */ + public static ChunkLockHandle lock(int chunkX, int chunkZ, int radius) { + long[] targets = new long[(1 + radius * 2) * (1 + radius * 2)]; + int idx = 0; + for (int dx = -radius; dx <= radius; dx++) { + for (int dz = -radius; dz <= radius; dz++) { + targets[idx++] = ((long) (chunkX + dx) << 32) ^ ((chunkZ + dz) & 0xFFFFFFFFL); + } + } + // 排序保证所有线程以相同顺序取锁,避免循环等待死锁 + Arrays.sort(targets); + for (long key : targets) { + LOCKS.computeIfAbsent(key, k -> new ReentrantLock()).lock(); + } + return new ChunkLockHandle(targets); + } + + /** + * 多区块锁的句柄:持有已加锁的区块 key 数组,{@link #close()} 逆序释放。 + */ + public static final class ChunkLockHandle implements AutoCloseable { + private final long[] targets; + + private ChunkLockHandle(long[] targets) { + this.targets = targets; + } + + @Override + public void close() { + // 逆序释放(与加锁方向相反) + for (int i = targets.length - 1; i >= 0; i--) { + ReentrantLock lock = LOCKS.get(targets[i]); + if (lock != null) { + lock.unlock(); + } + } + } + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java b/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java new file mode 100644 index 0000000..89ebc2a --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ChunkPrefetcher.java @@ -0,0 +1,174 @@ +package dev.modzuozhi.core.dimthread; + +import com.mojang.logging.LogUtils; +import dev.modzuozhi.ModZuozhi; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.Unit; +import net.minecraft.world.level.ChunkPos; +import org.slf4j.Logger; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 阶段 A(步骤 0):自适应区块预取。 + *

+ * 原理:在玩家 tick(主线程 {@code playerList.tick()} → {@code ServerPlayer.doTick()})时, + * 根据玩家移动方向,对前方尚未加载的区块投递一个临时的 FULL 级 ticket,让原版 + * 区块生成管线提前把该区块生成好;玩家到达时直接命中已生成缓存,无需现场等待。 + *

+ * 安全 / 无泄漏: + *

+ *

+ * 不碰生成管线、不碰地形生成 mod:这里只负责"提前下票触发原版生成",生成逻辑完全交给原版。 + */ +public final class ChunkPrefetcher { + + /** 预取总开关(与 {@link ModZuozhi#isEnabled()} 叠加,一键回退)。 */ + public static volatile boolean ENABLED = true; + + private static final Logger LOGGER = LogUtils.getLogger(); + + /** 预取 ticket 存活时长(tick),到期自动回收。10s 覆盖通常的前视时间窗。 */ + private static final int PREFETCH_TIMEOUT_TICKS = 200; + + /** 每次重算至少要移动这么多格(平方距离)才触发,过滤原地抖动。 */ + private static final double MIN_MOVE_SQ = 1.0; + + /** 预取重算间隔(tick),降低每个玩家的逐 tick 开销。 */ + private static final int PREFETCH_INTERVAL_TICKS = 5; + + /** 预取深度:在玩家视距之外,再向前预取的区块数(收敛为 4~6 格,避免过度预取浪费算力)。 */ + private static final int PREFETCH_DEPTH = 6; + + /** 自定义预取 ticket:FULL 级别 + 有限存活时间。 */ + private static final TicketType PREFETCH_TICKET = + TicketType.create("dimensionalripper_prefetch", (a, b) -> 0, PREFETCH_TIMEOUT_TICKS); + + /** 每玩家轻量状态(上次位置 / 上次所在 chunk / 上次重算游戏刻)。 */ + private static final ConcurrentHashMap STATES = new ConcurrentHashMap<>(); + + /** 状态表容量兜底,超出即整体清空(丢失仅损失一次方向估计,无害)。 */ + private static final int STATE_CAPACITY = 512; + + private ChunkPrefetcher() { + } + + /** 每次玩家 tick 调用一次(主线程)。 */ + public static void onPlayerTick(ServerPlayer player) { + if (!ModZuozhi.isEnabled() || !ENABLED) { + return; + } + try { + tick(player); + } catch (Throwable t) { + // 预取是纯增益:任何异常都不应影响玩家 tick 或服务器稳定性 + } + } + + private static void tick(ServerPlayer player) { + ServerLevel level = player.serverLevel(); + ServerChunkCache source = level.getChunkSource(); + + State state = STATES.computeIfAbsent(player.getUUID(), u -> new State()); + if (STATES.size() > STATE_CAPACITY) { + STATES.clear(); + } + + // 首次记录位置快照,不产生方向估计 + if (!state.initialized) { + state.init(player, level.getGameTime()); + return; + } + + long gameTime = level.getGameTime(); + if (gameTime - state.lastGameTime < PREFETCH_INTERVAL_TICKS) { + return; + } + state.lastGameTime = gameTime; + + double x = player.getX(); + double z = player.getZ(); + double dx = x - state.lastX; + double dz = z - state.lastZ; + state.lastX = x; + state.lastZ = z; + + if (dx * dx + dz * dz < MIN_MOVE_SQ) { + return; // 原地不动,不预取 + } + + ChunkPos chunk = player.chunkPosition(); + int dirX = Integer.compare(chunk.x, state.lastChunkX); + int dirZ = Integer.compare(chunk.z, state.lastChunkZ); + state.lastChunkX = chunk.x; + state.lastChunkZ = chunk.z; + + if (dirX == 0 && dirZ == 0) { + return; // 尚未跨 chunk,方向不可定 + } + + // 原版把区块生成到 FULL(完整)的半径由“视距 viewDistance”决定,不是模拟距离 simDistance。 + // 因此只对【视距外 1~PREFETCH_DEPTH 格】的固定窗口投递临时 ticket:视距内的区块已被玩家 + // 自身 ticket 加载到 FULL(hasChunk 恒真、下不出去票),窗口内已 FULL 的跳过即可。 + // 固定窗口(不再向前扫描)是为了把预取前沿钉死在 view+PREFETCH_DEPTH,避免生成速度过快时 + // 前沿被自己持续向前推进到十几二十格外,导致无谓的算力浪费。 + int viewDist = level.getServer().getPlayerList().getViewDistance(); + int simDist = level.getServer().getPlayerList().getSimulationDistance(); + int from = viewDist + 1; + int to = viewDist + PREFETCH_DEPTH; + int prefetched = 0; + for (int k = from; k <= to; k++) { + if (prefetch(source, chunk.x + dirX * k, chunk.z + dirZ * k)) { + prefetched++; + } + } + if (state.firstLog || gameTime - state.lastLogGameTime >= 20) { + state.firstLog = false; + state.lastLogGameTime = gameTime; + LOGGER.info("[ChunkPrefetcher] 玩家 {} 方向 ({},{}), view={}, sim={}, 预取区间 [{}~{}], 投递 {} 个ticket", + player.getGameProfile().getName(), dirX, dirZ, viewDist, simDist, from, to, prefetched); + } + } + + private static boolean prefetch(ServerChunkCache source, int chunkX, int chunkZ) { + if (source.hasChunk(chunkX, chunkZ)) { + return false; // 已 FULL 加载,无需预取 + } + // radius=0 → FULL 级别 ticket;到期自动回收,不阻塞、不泄漏 + source.addRegionTicket(PREFETCH_TICKET, new ChunkPos(chunkX, chunkZ), 0, Unit.INSTANCE); + return true; + } + + private static final class State { + boolean initialized; + double lastX; + double lastZ; + int lastChunkX; + int lastChunkZ; + long lastGameTime; + long lastLogGameTime; + boolean firstLog; + + void init(ServerPlayer player, long gameTime) { + this.initialized = true; + this.lastX = player.getX(); + this.lastZ = player.getZ(); + ChunkPos c = player.chunkPosition(); + this.lastChunkX = c.x; + this.lastChunkZ = c.z; + this.lastGameTime = gameTime; + this.firstLog = true; + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java b/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java new file mode 100644 index 0000000..ae91740 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/DimThreadCore.java @@ -0,0 +1,63 @@ +package dev.modzuozhi.core.dimthread; + +import dev.modzuozhi.ModZuozhi; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.slf4j.Logger; + +/** + * dimthreads 式核心入口:临时主线程交换 + worker 识别 + 线程池托管。 + *

+ * 核心思想:不固定"每维度专属线程",而是所有维度共用一个线程池,主线程每 tick 把 + * 每个 {@code level.tick()} 分发到池中并行,然后 {@code awaitCompletion()} 等待全部完成。 + * 运行某维度 tick 前,将该维度的 {@code thread} 与 {@code chunkSource.mainThread} 临时 + * 设为当前 worker,使原版线程检查自然通过,从根上消除 supplyAsync/join 死锁。 + */ +public final class DimThreadCore { + public static final String MOD_ID = ModZuozhi.MOD_ID; + public static final Logger LOGGER = ModZuozhi.LOGGER; + public static final ServerManager MANAGER = new ServerManager(); + + private DimThreadCore() { + } + + public static ThreadPool getThreadPool(MinecraftServer server) { + return MANAGER.getThreadPool(server); + } + + /** + * 临时把一批对象(Level / ServerChunkCache)的 mainThread 换成当前线程,执行任务后还原。 + * 必须在 try/finally 中调用,保证 worker 线程异常时也能还原。 + */ + public static void swapThreadsAndRun(Runnable task, Object... threadedObjects) { + Thread currentThread = Thread.currentThread(); + Thread[] oldThreads = new Thread[threadedObjects.length]; + + for (int i = 0; i < oldThreads.length; i++) { + oldThreads[i] = ((IMutableMainThread) threadedObjects[i]).dimThreads$getMainThread(); + ((IMutableMainThread) threadedObjects[i]).dimThreads$setMainThread(currentThread); + } + + try { + task.run(); + } finally { + for (int i = 0; i < oldThreads.length; i++) { + ((IMutableMainThread) threadedObjects[i]).dimThreads$setMainThread(oldThreads[i]); + } + } + } + + /** 给 worker 线程命名,便于在崩溃报告中识别。 */ + public static void attach(Thread thread, String name) { + thread.setName(MOD_ID + "_server_" + name); + } + + public static void attach(Thread thread, ServerLevel world) { + attach(thread, world.dimension().location().getPath()); + } + + /** 判断线程是否为维度 worker(通过名字前缀识别)。 */ + public static boolean owns(Thread thread) { + return thread.getName().startsWith(MOD_ID + "_server_"); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java new file mode 100644 index 0000000..59f4929 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/DimensionChangeQueue.java @@ -0,0 +1,57 @@ +package dev.modzuozhi.core.dimthread; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * 跨维度传送延期队列:维度 worker 上触发的 {@code changeDimension} 由本队列承接, + * 不依赖 {@code server.execute()}(跨 tick 消费),而是由主线程在维度 tick 收口后、 + * {@code playerList.tick()} 之前统一处理,保证传送在同一 tick 内完成。 + *

+ * 线程安全:维度 worker 写({@link #schedule}),主线程读({@link #flush}), + * 且主线程 flush 时所有维度 worker 已 awaitCompletion 完成,无竞争。 + */ +public final class DimensionChangeQueue { + private static final Deque queue = new ArrayDeque<>(); + + private DimensionChangeQueue() { + } + + /** 入队一个待执行的维度变更(由维度 worker 调用)。 */ + public static void schedule(Entity entity, DimensionTransition transition) { + queue.add(new PendingChange(entity, transition)); + } + + /** 是否有待处理的维度变更。 */ + public static boolean hasPending() { + return !queue.isEmpty(); + } + + /** + * 在主线程上执行所有排队的维度变更(由 {@code tickChildren} 收口处调用)。 + *

+ * 直接调用 {@link Entity#changeDimension},该调用在主线程上执行时不会被 + * {@code EntityChangeDimensionMixin} 拦截(主线程不是维度 worker), + * 完整的维度变更逻辑(移除旧维度、添加新维度、发数据包)同步完成。 + */ + @Nullable + public static Entity flush() { + PendingChange change; + Entity result = null; + while ((change = queue.pollFirst()) != null) { + try { + result = change.entity.changeDimension(change.transition); + } catch (Throwable t) { + DimThreadCore.LOGGER.error("[DimensionChangeQueue] 维度变更异常", t); + } + } + return result; + } + + private record PendingChange(Entity entity, DimensionTransition transition) { + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java new file mode 100644 index 0000000..9cf868a --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/EntityTickParallel.java @@ -0,0 +1,189 @@ +package dev.modzuozhi.core.dimthread; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Consumer; + +/** + * 实体 tick 细粒度并行的上下文管理(借鉴 MCMT/Async 思路,写我们自己的实现)。 + *

+ * 实体 tick 并行与方块实体/区块环境并行的关键区别:{@code ServerLevel.tick} 里对每个实体 + * 的 {@code guardEntityTick(...)} 调用位于 {@code entityTickList.forEach(...)} 的 lambda 内部, + * 无法像 TE/区块那样在循环体内 {@code @Redirect}。因此这里用 ThreadLocal 屏障在 + * {@code ServerLevelEntityTickMixin} 的 {@code forEach} 前后建立/收口,再由 + * {@code EntityTickParallelMixin} 在 {@code guardEntityTick} 处读取当前屏障并提交并行。 + *

+ * 第14步(对齐 Async 零锁路线):实体移动、生成、移除的共享状态({@code EntityLookup}、 + * {@code knownUuids}、{@code chunkVisibility}、section 存储)已全部并发化,不再需要全局锁 + * {@link #SECTION_LOCK} 保护。实体生命周期写入现在无锁并行,碰撞查询也走并行。 + * 仅保留 {@code EntitySection} 内容({@code ClassInstanceMultiMap})的分片读写锁保护内容遍历 + * 与增删的互斥(Async 未去掉分片锁,保留为安全边界;分片竞争在 256 片下极低,不是瓶颈)。 + */ +public final class EntityTickParallel { + /** 当前维度 worker 线程正在并行 tick 的实体屏障(仅 worker 线程非 null)。 */ + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + /** 本 tick 收集的待并行实体批(仅并行窗口 worker 线程非 null)。 */ + private static final ThreadLocal BATCH = new ThreadLocal<>(); + + /** 分片锁数量(2 的幂)。越大锁竞争越小,内存开销约 256×对象头,可忽略。 */ + private static final int STRIPE_COUNT = 256; + /** 分片读写锁数组:保护单个 {@code EntitySection} 的内容读写({@code ClassInstanceMultiMap})。 */ + private static final ReentrantReadWriteLock[] SECTION_LOCKS = new ReentrantReadWriteLock[STRIPE_COUNT]; + + /** + * 当前线程的锁配对栈:记录每次 lock 调用时是否真正加了锁,unlock 时弹出对应决定。 + *

+ * 背景:{@link #isLockDisabled()} 在「细粒度关闭且无残留子任务」时为 true(零锁纯串行)。 + * 但 {@code /dimensionalripper fine on|off} 运行期切换瞬间,该判定会在一次 lock→unlock 之间翻转: + * lock 时跳过加锁、unlock 时却执行解锁 → 对未持有的锁 unlock,抛 + * {@code IllegalMonitorStateException: attempt to unlock read lock}(实测:大量村民 tick 时 + * 执行 {@code /dimensionalripper fine on} 触发)。用本栈把 lock 时的决定记录下来,unlock 严格按 + * 记录配对(支持嵌套,LIFO),无论 {@code isLockDisabled()} 是否翻转都保持一致。 + */ + private static final ThreadLocal> LOCK_STACK = ThreadLocal.withInitial(ArrayDeque::new); + + static { + for (int i = 0; i < STRIPE_COUNT; i++) { + SECTION_LOCKS[i] = new ReentrantReadWriteLock(); + } + } + + private EntityTickParallel() { + } + + /** + * 将 section key({@code SectionPos.asLong(x,y,z)})映射到分片索引。 + * 使用乘散列取高位,保证相邻 section 均匀分布到不同分片。 + */ + private static int stripe(long sectionKey) { + long hash = sectionKey * 0x9E3779B97F4A7C15L; + return (int) (hash >>> 32) & (STRIPE_COUNT - 1); + } + + /** 对指定 section 分片加写锁(内容增删,独占);记录配对决定。 */ + public static void lockSection(long sectionKey) { + boolean locked = !isLockDisabled(); + LOCK_STACK.get().push(locked); + if (locked) { + SECTION_LOCKS[stripe(sectionKey)].writeLock().lock(); + } + } + + /** 对指定 section 分片解写锁;严格按 lock 时的配对决定解锁。 */ + public static void unlockSection(long sectionKey) { + if (popLocked()) { + SECTION_LOCKS[stripe(sectionKey)].writeLock().unlock(); + } + } + + /** 对指定 section 分片加读锁(内容遍历/查询,共享);记录配对决定。 */ + public static void lockSectionRead(long sectionKey) { + boolean locked = !isLockDisabled(); + LOCK_STACK.get().push(locked); + if (locked) { + SECTION_LOCKS[stripe(sectionKey)].readLock().lock(); + } + } + + /** 对指定 section 分片解读锁;严格按 lock 时的配对决定解锁。 */ + public static void unlockSectionRead(long sectionKey) { + if (popLocked()) { + SECTION_LOCKS[stripe(sectionKey)].readLock().unlock(); + } + } + + /** 弹出本线程最近一次 lock 的配对决定(LIFO);栈空视为未加锁(防御)。 */ + private static boolean popLocked() { + Deque stack = LOCK_STACK.get(); + Boolean locked = stack.pollFirst(); + if (stack.isEmpty()) { + LOCK_STACK.remove(); + } + return Boolean.TRUE.equals(locked); + } + + /** + * 是否应跳过锁(零开销纯串行)。 + *

+ * 锁的唯一目的是保护并行子任务对 {@code EntitySection} 的并发增删。而子任务是否 + * 并行由 {@link FineGrainScheduler#isEnabled()} 决定({@code submitBatch} 关闭时同步串行)。 + * 因此锁的启用必须与细粒度并行开关一致:若这里跟随全局 {@code ModZuozhi.ENABLED}, + * 会出现"全局 off → 锁被跳过,但细粒度仍开 → 子任务并行增删而主线程无锁遍历"的 + * {@code ConcurrentModificationException} 竞态(已实测崩溃)。 + *

+ * 残留保护:关闭细粒度({@code /dimensionalripper fine off}、{@code /dimensionalripper off}、 + * FaultGuard 降级)瞬间,{@code SUB_POOL} 里可能仍有已提交未跑完的实体子任务; + * 它们与退化为串行的维度 tick / 主线程并发改 {@code EntitySection}。因此只要还存在残留 + * 子任务({@link FineGrainScheduler#activeSubTasks()} > 0),锁就必须保持启用,直到 + * {@code awaitIdle} 把它们全部排空后才回到零锁纯串行。 + *

+ * 注意:本判定可能随时间变化({@code fine on|off} 切换)。lock/unlock 的配对一致性 + * 由 {@link #LOCK_STACK} 保证——lock 时快照本判定,unlock 严格按快照执行,不会因判定 + * 在两次调用间翻转而对未持有的锁 unlock。 + */ + public static boolean isLockDisabled() { + return !FineGrainScheduler.isEnabled() && FineGrainScheduler.activeSubTasks() == 0; + } + + /** 当前线程的实体屏障(worker 端);无则 null。 */ + public static FineGrainScheduler.Barrier current() { + return CURRENT.get(); + } + + /** + * 本 tick 收集的一批待并行实体(同一 consumer,即 {@code tickNonPassenger})。 + * 收集代替逐实体提交:循环结束后统一按线程池大小切成块提交,把每实体一次的 + * 调度/屏障计数开销从 O(实体数) 降到 O(线程数)。 + */ + public static final class Batch { + public final MinecraftServer server; + public final Consumer consumer; + public final List entities = new ArrayList<>(); + + public Batch(MinecraftServer server, Consumer consumer) { + this.server = server; + this.consumer = consumer; + } + } + + /** 收集一个待并行 tick 的实体(在 {@code guardEntityTick} 中调用,替代逐实体提交)。 */ + public static void collect(MinecraftServer server, Consumer consumer, Entity entity) { + Batch batch = BATCH.get(); + if (batch == null) { + batch = new Batch(server, consumer); + BATCH.set(batch); + } + batch.entities.add(entity); + } + + /** 实体循环开始:为本维度 tick 建立实体子任务屏障(仅 worker 线程调用)。 */ + public static void begin() { + CURRENT.set(FineGrainScheduler.beginBarrier()); + } + + /** + * 实体循环结束(finally 中调用):先把收集的实体批按块提交并行,再等待全部子任务完成 + * 并执行延迟回调,清除上下文。 + */ + public static void end() { + FineGrainScheduler.Barrier barrier = CURRENT.get(); + if (barrier == null) { + return; + } + CURRENT.remove(); + Batch batch = BATCH.get(); + BATCH.remove(); + if (batch != null && !batch.entities.isEmpty()) { + barrier.submitBatch(batch.server, batch.entities, batch.consumer); + } + barrier.await(); + barrier.drain(); + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java b/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java new file mode 100644 index 0000000..3dcdd8f --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/FaultGuard.java @@ -0,0 +1,148 @@ +package dev.modzuozhi.core.dimthread; + +import dev.modzuozhi.ModZuozhi; + +/** + * 第9步后半:故障监控与自动回退(fail-safe / graceful degradation)。 + *

+ * 目标:运行期检测到并行故障时安全降级而非崩溃。采用两级回退,先关细粒度、再退回原版串行: + *

+ * 计数均为连续语义(成功/按时完成会清零),容忍偶发抖动,只有持续故障才触发回退。 + * 手动 {@code /dimensionalripper on|fine on} 重新启用时调用 {@link #reset()} 清除降级状态。 + */ +public final class FaultGuard { + /** 子任务连续异常达到该次数,自动关闭细粒度并行。 */ + private static final int FINE_FAULT_THRESHOLD = 5; + /** 维度 tick 连续异常达到该次数,自动回退原版串行。 */ + private static final int DIM_FAULT_THRESHOLD = 3; + /** 维度 tick 连续超时达到该次数,自动回退原版串行。 */ + private static final int AWAIT_TIMEOUT_THRESHOLD = 3; + /** 看门狗:维度 tick 最晚完成时限(主线程等待上限)。 */ + public static final long AWAIT_TIMEOUT_MS = 30_000L; + + /** 细粒度是否已被自动关闭。 */ + private static volatile boolean fineDegraded = false; + /** 维度并行是否已被自动回退。 */ + private static volatile boolean dimDegraded = false; + /** 最近的降级原因(status 命令展示)。 */ + private static volatile String reason = "正常"; + /** 连续失败计数(仅写入线程同步读写,读取方为 status/日志,volatile 已足够)。 */ + private static int fineFailures; + private static int dimFailures; + private static int awaitTimeouts; + + private FaultGuard() { + } + + /** 上报一次子任务异常;达阈值自动关闭细粒度并行。 */ + public static void onSubTaskFailure() { + if (fineDegraded) { + return; + } + fineFailures++; + if (fineFailures >= FINE_FAULT_THRESHOLD) { + fineDegraded = true; + reason = "子任务连续异常 " + fineFailures + " 次,已自动关闭细粒度并行(保留维度并行)"; + ModZuozhi.LOGGER.error("[FaultGuard] {}", reason); + FineGrainScheduler.setEnabled(false); + } + } + + /** 上报维度 tick 异常;达阈值自动回退原版串行。 */ + public static void onDimTickFailure(Throwable t) { + if (dimDegraded) { + return; + } + dimFailures++; + ModZuozhi.LOGGER.error("[FaultGuard] 维度 tick 异常(第 {} 次)", dimFailures, t); + if (dimFailures >= DIM_FAULT_THRESHOLD) { + dimDegraded = true; + reason = "维度 tick 连续异常 " + dimFailures + " 次,已自动回退原版串行"; + ModZuozhi.LOGGER.error("[FaultGuard] {}", reason); + // 关闭顺序与 setMaster 一致:先关细粒度并排空残留子任务(此时 ENABLED 仍 true, + // section 锁与碰撞拦截仍生效,残留任务安全跑完),再关全局,最后熄灭维度 loop + // (否则主线程退回原版串行 tick 时仍会经 mixin 提交并行子任务 → 双 tick/CME 竞态)。 + FineGrainScheduler.setEnabled(false); + FineGrainScheduler.awaitIdle(5000L); + ModZuozhi.ENABLED = false; + DimThreadCore.MANAGER.killAllLoops(); + } + } + + /** 上报子任务屏障等待超时(子任务卡死/死锁):立即降级,避免维度 worker 永久阻塞导致整服冻结。 */ + public static void onBarrierTimeout() { + if (dimDegraded) { + return; + } + dimDegraded = true; + reason = "子任务屏障等待超时(疑似死锁),已自动回退原版串行"; + ModZuozhi.LOGGER.error("[FaultGuard] {}", reason); + // 与 onDimTickFailure 一致:先关细粒度并尽力排空残留子任务,再关全局, + // 最后熄灭维度 loop,释放可能被卡死子任务持有的锁,恢复主线程原版串行。 + FineGrainScheduler.setEnabled(false); + FineGrainScheduler.awaitIdle(2000L); + ModZuozhi.ENABLED = false; + DimThreadCore.MANAGER.killAllLoops(); + } + + /** + * 看门狗:主线程等待维度 tick 完成后上报结果。 + * + * @param done 是否在 {@link #AWAIT_TIMEOUT_MS} 内完成 + * @return 本次是否触发维度并行回退(调用方需在返回 true 时等待残留 worker 收口) + */ + public static boolean onAwaitCompleted(boolean done) { + if (dimDegraded) { + return false; + } + if (!done) { + awaitTimeouts++; + ModZuozhi.LOGGER.error("[FaultGuard] 维度 tick 超时(第 {} 次,>{}/tick)", awaitTimeouts, AWAIT_TIMEOUT_MS); + if (awaitTimeouts >= AWAIT_TIMEOUT_THRESHOLD) { + dimDegraded = true; + reason = "维度 tick 连续超时 " + awaitTimeouts + " 次,已自动回退原版串行"; + ModZuozhi.LOGGER.error("[FaultGuard] {}", reason); + // 与 onDimTickFailure 一致:先关细粒度并排空残留子任务,再关全局, + // 避免主线程串行 tick 时仍提交并行子任务(双 tick / section 无锁遍历 CME 竞态)。 + FineGrainScheduler.setEnabled(false); + FineGrainScheduler.awaitIdle(5000L); + ModZuozhi.ENABLED = false; + return true; + } + } else { + // 按时完成:重置连续超时计数 + awaitTimeouts = 0; + } + return false; + } + + public static boolean isFineDegraded() { + return fineDegraded; + } + + public static boolean isDimDegraded() { + return dimDegraded; + } + + public static String getReason() { + return reason; + } + + /** 手动重新启用(/dimensionalripper on|fine on)时重置所有降级状态与计数。 */ + public static void reset() { + fineDegraded = false; + dimDegraded = false; + reason = "正常"; + fineFailures = 0; + dimFailures = 0; + awaitTimeouts = 0; + ModZuozhi.LOGGER.info("[FaultGuard] 降级状态已重置"); + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java new file mode 100644 index 0000000..b241a4d --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/FineGrainScheduler.java @@ -0,0 +1,304 @@ +package dev.modzuozhi.core.dimthread; + +import dev.modzuozhi.ModZuozhi; +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.entity.Entity; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.function.Consumer; + +import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID; + +/** + * 细粒度并行调度器(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 在现有 dimthreads 式「维度级并行」基础上,提供维度内更细粒度并行的能力: + * 维度 worker 在本维度的 tick 内部,把可并行的子单元(方块实体、区块环境、白名单实体) + * 提交到独立的细粒度子任务池并行执行,再用本维度自己的屏障 {@link Barrier} 收口, + * 保持该维度 tick 内部的顺序边界,同时兼容外层主线程的 {@code awaitCompletion()} 收口。 + *

+ * 设计要点: + *

+ */ +public final class FineGrainScheduler { + + /** + * 细粒度并行总开关(默认开启,已长时间测试稳定;出错时 {@link FaultGuard} 会自动降级关闭)。 + * 可由 {@code /dimensionalripper fine on|off} 运行时切换。 + */ + private static volatile boolean enabled = true; + + /** 细粒度子任务的独立线程池(daemon,固定 CPU 线程数)。 */ + private static final ThreadPoolExecutor SUB_POOL = new ThreadPoolExecutor( + Math.max(2, Runtime.getRuntime().availableProcessors()), + Math.max(2, Runtime.getRuntime().availableProcessors()), + 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName(MOD_ID + "_subtask"); + return t; + }); + + /** 当前并行子任务的延迟执行队列(仅并行子任务线程内非 null)。 */ + private static final ThreadLocal CURRENT_QUEUE = new ThreadLocal<>(); + + /** + * 正在 {@link #SUB_POOL} 中运行或排队的子任务总数(含残留排空期)。 + *

+ * 用于两点: + *

    + *
  1. {@link #awaitIdle} 判断是否排空(比 {@code getActiveCount()+queue} 更准,覆盖排队任务);
  2. + *
  3. {@link EntityTickParallel#isLockDisabled} 在「细粒度已关但仍有残留子任务」期间保持 + * 分片锁启用,保证残留子任务与退化为串行的维度 tick 并发改 {@code EntitySection} 时仍互斥, + * 否则 `fine off`/`off` 瞬间锁被禁用 + 残留子任务无锁运行 → CME(已实测)。
  4. + *
+ */ + private static final java.util.concurrent.atomic.AtomicInteger ACTIVE_SUBTASKS = + new java.util.concurrent.atomic.AtomicInteger(); + + private FineGrainScheduler() { + } + + /** 当前仍在运行/排队的子任务数(关闭细粒度后的残留排空期也计入)。 */ + public static int activeSubTasks() { + return ACTIVE_SUBTASKS.get(); + } + + public static boolean isEnabled() { + return enabled; + } + + public static void setEnabled(boolean value) { + enabled = value; + // 子任务使用独立 SUB_POOL,无需再扩容维度线程池;只需让线程数目标在下次 tick 重算。 + DimThreadCore.MANAGER.invalidateThreadCount(); + ModZuozhi.LOGGER.info("[FineGrain] 细粒度并行已切换为 {}", value ? "开启" : "关闭"); + } + + /** 是否处于某个并行子任务上下文(用于拦截跨线程的 Level 结构写入)。 */ + public static boolean inSubTask() { + return CURRENT_QUEUE.get() != null; + } + + /** + * 等待已提交的细粒度子任务全部完成(全局关闭时调用,带超时兜底)。 + *

+ * 目的:{@code /dimensionalripper off} 或自动降级后,{@link #SUB_POOL} 里可能残留上一拍提交、 + * 尚未跑完的实体子任务;此时主线程已退回原版串行 tick 同一批实体,会与残留子任务 + * 并发 tick 同一实体导致异常(表现为"关闭时反复出现子任务异常")。先排空 + * SUB_POOL 再让主线程接管,消除该竞态。超时则强制继续,避免异常场景下永久阻塞。 + */ + public static void awaitIdle(long timeoutMs) { + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs); + while (System.nanoTime() < deadline) { + if (ACTIVE_SUBTASKS.get() == 0) { + return; + } + LockSupport.parkNanos(100_000L); // 0.1ms 退避 + } + ModZuozhi.LOGGER.warn("[FineGrain] 等待子任务排空超时(>{}ms),强制继续", timeoutMs); + } + + /** 当前并行子任务的延迟执行队列;非子任务上下文返回 null。 */ + public static PostExecuteQueue currentQueue() { + return CURRENT_QUEUE.get(); + } + + /** 为本维度 tick 创建一个新的子任务屏障。 */ + public static Barrier beginBarrier() { + return new Barrier(); + } + + /** + * 维度内子任务屏障:提交多个可并行子任务,{@link #await()} 等待全部完成。 + *

+ * 仅在 {@link FineGrainScheduler#isEnabled()} 为 true 时真正并行;关闭时 + * {@link #submit} 直接在当前线程同步执行,保证开关无关的正确性。 + */ + public static final class Barrier { + private final CountDownLatch latch = new CountDownLatch(1); + private final PostExecuteQueue postQueue = new PostExecuteQueue(); + private int pending; + + /** + * 子任务屏障等待超时(毫秒)。一批子任务(方块实体/区块/实体)正常应在几十 ms 内完成, + * 超时意味着某个子任务卡死(例如阻塞在另一个线程持有的 {@link ChunkLock} 或 section 锁上)。 + * 此时无条件 {@code latch.await()} 会让维度 worker 永久阻塞,进而死锁主线程保存流程。 + */ + private static final long AWAIT_TIMEOUT_MS = 10_000L; + + /** + * 提交一个细粒度子任务。 + * + * @param server 当前服务器(用于取线程池) + * @param task 子任务(如某个方块实体/实体的 tick) + */ + public void submit(MinecraftServer server, Runnable task) { + submit(server, task, null); + } + + /** + * 提交一个细粒度子任务,可选地锁定其所在区块(防止同一区块内并发访问)。 + * + * @param server 当前服务器(保留参数,当前子任务使用独立线程池) + * @param task 子任务(如某个方块实体/实体的 tick) + * @param pos 子任务所在世界坐标;非 null 时执行前锁定所在区块 + */ + public void submit(MinecraftServer server, Runnable task, BlockPos pos) { + submit(server, task, pos, 0); + } + + /** + * 提交一个细粒度子任务,可选地锁定其所在区块及其周围 radius 圈区块。 + * + * @param server 当前服务器(保留参数,当前子任务使用独立线程池) + * @param task 子任务(如某个方块实体/实体的 tick) + * @param pos 子任务所在世界坐标;非 null 时执行前锁定区块 + * @param radius 区块锁半径:0=仅本区块;>0 时锁定 {@code (1+2r)²} 个区块 + * (排序加锁防死锁),供跨区块访问的子任务使用 + */ + public void submit(MinecraftServer server, Runnable task, BlockPos pos, int radius) { + if (!FineGrainScheduler.enabled) { + task.run(); + return; + } + synchronized (this) { + pending++; + } + ACTIVE_SUBTASKS.incrementAndGet(); + SUB_POOL.execute(() -> { + AutoCloseable chunkLock = (pos == null) + ? null + : (radius > 0 ? ChunkLock.lock(pos, radius) : ChunkLock.lock(pos)); + PostExecuteQueue prev = CURRENT_QUEUE.get(); + CURRENT_QUEUE.set(postQueue); + try { + task.run(); + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t); + FaultGuard.onSubTaskFailure(); + } finally { + CURRENT_QUEUE.set(prev); + if (chunkLock != null) { + try { + chunkLock.close(); + } catch (Exception ignore) { + } + } + synchronized (this) { + pending--; + if (pending == 0) { + latch.countDown(); + } + } + ACTIVE_SUBTASKS.decrementAndGet(); + } + }); + } + + /** + * 批量提交待并行实体(借鉴 Async 批量化思路,独立实现)。 + *

+ * 按线程池大小把实体列表切成块,每块作为一个子任务提交并行执行,把调度/屏障计数 + * 开销从 O(实体数) 降到 O(线程数)。块内逐实体 tick 并保持异常隔离。 + * + * @param server 当前服务器 + * @param entities 待并行实体列表 + * @param consumer 实体 tick 函数(如 {@code tickNonPassenger}) + */ + public void submitBatch(MinecraftServer server, List entities, Consumer consumer) { + if (entities.isEmpty()) { + return; + } + if (!FineGrainScheduler.enabled) { + // 细粒度关闭:当前线程逐实体串行 tick(行为等价原版) + for (Entity entity : entities) { + if (!entity.isRemoved()) { + consumer.accept(entity); + } + } + return; + } + int poolSize = SUB_POOL.getCorePoolSize(); + int chunkSize = Math.max(1, (entities.size() + poolSize - 1) / poolSize); + int chunkCount = (entities.size() + chunkSize - 1) / chunkSize; + synchronized (this) { + pending += chunkCount; + } + ACTIVE_SUBTASKS.addAndGet(chunkCount); + for (int i = 0; i < entities.size(); i += chunkSize) { + int end = Math.min(i + chunkSize, entities.size()); + List chunk = entities.subList(i, end); + SUB_POOL.execute(() -> modzuozhi_runChunk(chunk, consumer)); + } + } + + /** 块内逐实体 tick:单实体异常隔离(等价原逐实体提交语义),异常计入 FaultGuard。 */ + private void modzuozhi_runChunk(List chunk, Consumer consumer) { + PostExecuteQueue prev = CURRENT_QUEUE.get(); + CURRENT_QUEUE.set(postQueue); + try { + for (Entity entity : chunk) { + try { + if (!entity.isRemoved()) { + consumer.accept(entity); + } + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[FineGrain] 子任务异常", t); + FaultGuard.onSubTaskFailure(); + } + } + } finally { + CURRENT_QUEUE.set(prev); + synchronized (this) { + pending--; + if (pending == 0) { + latch.countDown(); + } + } + ACTIVE_SUBTASKS.decrementAndGet(); + } + } + + /** 阻塞等待本维度所有已提交子任务完成;超时触发 FaultGuard 降级,避免永久死锁。 */ + public void await() { + // 关键:若从未提交任何子任务(pending==0),latch 永远不会 countDown, + // 无条件 await 会永久死锁。因此先检查 pending,为 0 直接返回。 + synchronized (this) { + if (pending == 0) { + return; + } + } + boolean done; + try { + done = latch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + if (!done) { + ModZuozhi.LOGGER.error("[FineGrain] 子任务屏障等待超时(>{}ms),存在卡死的子任务,触发降级", AWAIT_TIMEOUT_MS); + FaultGuard.onBarrierTimeout(); + } + } + + /** 维度 worker 收口处:串行执行子任务投递的延迟回调。 */ + public void drain() { + postQueue.drain(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java b/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java new file mode 100644 index 0000000..dee55d1 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/IChunkEnvTick.java @@ -0,0 +1,17 @@ +package dev.modzuozhi.core.dimthread; + +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.chunk.LevelChunk; + +/** + * 区块环境 tick(随机 tick / 流体)的并行版本入口,由 {@code ServerLevelChunkTickMixin} 注入。 + *

+ * 并行子任务需要在线程本地随机源上执行随机 tick,但原版 {@code ServerLevel.tickChunk} 直接访问 + * {@code Level.random}({@code public final} 共享字段)。由于在 NeoForge 开发环境无法对 + * Minecraft 混淆字段做 {@code @Redirect}(无 refmap),这里改为在 {@code ServerLevel} 上提供 + * 一个等价的自定义实现 {@link #modzuozhi_tickChunk},它接收线程本地随机源作为参数, + * 由 {@code ServerChunkCacheMixin} 在并行子任务中调用,从而完全绕开共享随机源。 + */ +public interface IChunkEnvTick { + void modzuozhi_tickChunk(LevelChunk chunk, int randomTickSpeed, RandomSource random); +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java b/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java new file mode 100644 index 0000000..5cdf6f2 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/IMutableMainThread.java @@ -0,0 +1,14 @@ +package dev.modzuozhi.core.dimthread; + +/** + * 可迁移主线程引用的对象(Level.thread / ServerChunkCache.mainThread)。 + *

+ * dimthreads 模型:运行某维度 tick 前,先把该维度 {@code Level.thread} 与 + * {@code ServerChunkCache.mainThread} 临时设成当前 worker 线程,使原版所有 + * {@code currentThread() == mainThread} 判断自然成立,杜绝 supplyAsync/join 死锁。 + */ +public interface IMutableMainThread { + Thread dimThreads$getMainThread(); + + void dimThreads$setMainThread(Thread thread); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java new file mode 100644 index 0000000..81fff09 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/IPlayerPacketQueue.java @@ -0,0 +1,12 @@ +package dev.modzuozhi.core.dimthread; + +/** + * 第2步:网络包按玩家分派到维度线程——{@code ServerLevel} 的每维度玩家包队列访问接口。 + *

+ * 由 {@code ServerLevelPlayerPacketMixin} 注入实现,供 {@code PacketUtilsDimMixin} + * (包处理重定向)与 {@code ServerLevelPlayerPacketMixin}(tick 收口 drain)使用。 + */ +public interface IPlayerPacketQueue { + /** 获取本维度的玩家包处理队列。 */ + PlayerPacketQueue modzuozhi$playerPacketQueue(); +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java b/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java new file mode 100644 index 0000000..a4799a5 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ISectionKeyAccessor.java @@ -0,0 +1,18 @@ +package dev.modzuozhi.core.dimthread; + +/** + * 供 {@code EntitySectionMixin} 注入的 section key 访问器。 + *

+ * {@code EntitySection} 自身不持有其在 {@code EntitySectionStorage} 中的 section key, + * 细粒度分片锁需要该 key 定位锁片。本接口由 {@code EntitySectionMixin} 实现,注入 + * {@code @Unique} 字段保存 key,并由 {@code EntitySectionStorageMixin} 在 + * {@code createSection} 时写入。 + *

+ * 注意:该接口不能放在 mixin 包内——Mixin 禁止从被转换类直接引用 mixin 包中的 + * 类型({@code IllegalClassLoadError})。因此放在核心包 {@code core.dimthread}。 + */ +public interface ISectionKeyAccessor { + void modzuozhi_setSectionKey(long key); + + long modzuozhi_getSectionKey(); +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java b/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java new file mode 100644 index 0000000..2269df3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/LevelTickLoop.java @@ -0,0 +1,288 @@ +package dev.modzuozhi.core.dimthread; + +import com.mojang.logging.LogUtils; +import dev.modzuozhi.ModZuozhi; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.neoforged.neoforge.event.EventHooks; +import org.slf4j.Logger; + +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +/** + * 维度独立 tick 循环(借鉴 Chlorophyll 的 {LevelTickLoop 思路,用我们自己的 + * {@link DimThreadCore#swapThreadsAndRun} 机制独立重写)。 + *

+ * 目标:每个 {@link ServerLevel} 一个常驻循环,各自按 tick 预算独立排下一次 tick。 + * 某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再像旧「有界屏障」模型那样由 + * 主线程 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。 + *

+ * 每个循环在一"拍"里打包三件事(对应原版 {@code tickChildren} 里逐维度的部分): + *

    + *
  1. 本维度 {@code level.tick()};
  2. + *
  3. 本维度玩家的连接 tick(原版玩家 {@code doTick()} 走的就是网络连接路径);
  4. + *
  5. 本维度玩家的区块发送 + 刷新恢复。
  6. + *
+ * 全局部分(命令函数、全局连接监听、playerList 延迟广播、自动保存)仍留在主线程, + * 见 {@code MinecraftServerDimMixin} 的后续改造。 + */ +public final class LevelTickLoop implements Runnable, java.util.concurrent.Executor { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final ServerLevel level; + private final ScheduledThreadPoolExecutor masterPool; + + /** 当前维度的玩家连接集合(玩家登录/断开时增删,见连接路由改造)。 */ + private final Set connections = ConcurrentHashMap.newKeySet(); + + /** 从主线程/跨线程投递回本 loop 执行的任务队列。 */ + private final Queue taskScope = new ConcurrentLinkedQueue<>(); + + /** 本 loop 正在调度中的下一次 tick 任务。 */ + private volatile Future currentTickTask; + /** 是否还在运行(false 表示已申请停止)。 */ + private volatile boolean running = true; + /** 是否已启动过自排程(防止重复 schedule 出多个并列循环)。 */ + private volatile boolean scheduled = false; + /** 是否正在执行一次 tick(health 检查用)。 */ + private volatile boolean ticking = false; + + /** 本循环独立的 tick 计数(用于每 20 tick 的时间同步)。 */ + private int tickCount; + + public LevelTickLoop(ServerLevel level, ScheduledThreadPoolExecutor masterPool) { + this.level = level; + this.masterPool = masterPool; + } + + public ServerLevel getLevel() { + return this.level; + } + + public boolean isTicking() { + return this.ticking; + } + + public boolean isRunning() { + return this.running; + } + + /** 是否已被主线程 {@code tickChildren} 启动过(即该 loop 已接管本维度 tick)。 */ + public boolean isScheduled() { + return this.scheduled; + } + + /** 申请停止自排程(维度卸载 / 服务器停机时调用)。 */ + public void kill() { + this.running = false; + Future task = this.currentTickTask; + if (task != null && !task.isCancelled()) { + task.cancel(true); + } + } + + /** 首次启动(或外部主动重排)本循环。 */ + public void schedule() { + this.currentTickTask = this.masterPool.submit(this); + } + + /** 每 tick 由主线程调用:保证本 loop 只启动一次自排程。 */ + public void scheduleIfNeeded() { + if (!this.scheduled) { + this.scheduled = true; + this.schedule(); + } + } + + public void addConnection(Connection connection) { + this.connections.add(connection); + } + + public void removeConnection(Connection connection) { + this.connections.remove(connection); + } + + @Override + public void execute(java.lang.Runnable command) { + this.taskScope.offer(command); + } + + @Override + public void run() { + DimThreadCore.attach(Thread.currentThread(), this.level); + this.ticking = true; + + MinecraftServer server = this.level.getServer(); + long nspt = server.tickRateManager().nanosecondsPerTick(); + if (server.tickRateManager().isSprinting()) { + nspt = 0L; + } + + long tickStart = System.nanoTime(); + long deadline = tickStart + nspt; + try { + this.processPendingTasks(); + DimThreadCore.swapThreadsAndRun(() -> { + long workStart = System.nanoTime(); + this.internalTick(); + // 只统计真实 tick 工作量(MSPT),不含后面 drainChunkTasks 的按预算轮询等待, + // 避免把定时器唤醒延迟等「非负载时间」算进 TPS(否则空闲也会显示 19.7)。 + TpsTracker.recordDimension(this.level.dimension(), System.nanoTime() - workStart); + this.drainChunkTasks(deadline); + }, this.level, this.level.getChunkSource()); + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 异常", this.level.dimension().location(), t); + FaultGuard.onDimTickFailure(t); + } finally { + this.ticking = false; + long remaining = Math.max(0L, deadline - System.nanoTime()); + if (this.running) { + this.currentTickTask = this.masterPool.schedule(this, remaining, TimeUnit.NANOSECONDS); + } else { + ModZuozhi.LOGGER.info("[DimThread] 维度 {} 独立循环已停止", this.level.dimension().location()); + } + } + } + + /** 执行由 {@link #execute} 投递回来的主线程任务。 */ + private void processPendingTasks() { + Runnable task; + while ((task = this.taskScope.poll()) != null) { + try { + task.run(); + } catch (Throwable t) { + ModZuozhi.LOGGER.error("[DimThread] 维度 {} 主线程任务异常", this.level.dimension().location(), t); + } + } + } + + /** + * 排空本维度区块源任务(距离更新 + 状态提升 + 生成调度 + 光照调度 + 队列)。 + *

+ * 原版主线程在 {@code waitUntilNextTick → pollTaskInternal → ServerChunkCache.pollTask} 期间会 + * 连续排空这些任务,是区块加载吞吐量的主要来源。切到维度 loop 后主线程那份被 + * {@code ServerChunkCachePollMixin} 跳过,改由本 worker 独占;这里在本 tick 的剩余预算内等价地 + * 连续排空,直到无更多任务或预算耗尽,避免地形加载掉速、卡在未加载区块。 + */ + private void drainChunkTasks(long deadline) { + ServerChunkCache chunkSource = this.level.getChunkSource(); + long now = System.nanoTime(); + // 低 TPS 时 internalTick 可能已超预算(deadline 已过),若直接退出会与区块加载形成「死亡螺旋」: + // 区块不加载 → 实体/光照等待区块 → tick 更慢 → 更没有预算排空区块。因此给一个最低保证窗口。 + long effectiveDeadline = Math.max(deadline, now + 2_000_000L); // 最低 2ms + boolean overBudget = now >= deadline; + while (this.running && System.nanoTime() < effectiveDeadline) { + if (chunkSource.pollTask()) { + continue; // 有就绪任务,继续排空 + } + // 区块任务空档期补排空一次玩家包(移动/交互等非命令包),减轻其在高负载下的延迟。 + ((IPlayerPacketQueue) this.level).modzuozhi$playerPacketQueue().drain(); + if (overBudget) { + break; // 超预算窗口内不可忙等,避免继续占用 worker + } + // 正常预算内:区块生成是异步的,几毫秒内可能有后台任务完成回调;极短退避持续轮询, + // 避免错过异步结果导致管线饥饿(等价原版主线程 managedBlock)。 + LockSupport.parkNanos(100_000L); // 0.1ms + } + } + + /** + * 一"拍"的维度内部 tick:排空玩家包(命令)→ 挂起刷新 → 时间同步 → level.tick → + * 连接 tick(玩家 doTick)→ 区块发送 + 恢复刷新。注意:本阶段暂不把自动保存下沉到 loop, + * 仍由主线程全局保存,避免与 {@code MinecraftServerSaveMixin} 双保存。 + */ + private void internalTick() { + // 先排空玩家包队列(命令等),再进沉重的 level.tick:实体/AI 卡顿时命令仍能及时执行, + // 不会像原来那样被饿死到 level.tick 结束(RETURN)才处理。 + ((IPlayerPacketQueue) this.level).modzuozhi$playerPacketQueue().drain(); + + MinecraftServer server = this.level.getServer(); + this.tickCount++; + + for (Connection connection : this.connections) { + ((ServerGamePacketListenerImpl) connection.getPacketListener()).suspendFlushing(); + } + + if (this.tickCount % 20 == 0) { + ((dev.modzuozhi.mixin.MinecraftServerTimeSyncAccessor) server).modzuozhi$invokeSynchronizeTime(this.level); + } + + EventHooks.fireLevelTickPre(this.level, () -> true); + try { + this.level.tick(() -> true); + } catch (Throwable throwable) { + ModZuozhi.LOGGER.error("[DimThread] 维度 {} tick 抛异常", this.level.dimension().location(), throwable); + } + EventHooks.fireLevelTickPost(this.level, () -> true); + + this.tickConnections(); + + for (Connection connection : this.connections) { + ServerGamePacketListenerImpl handler = (ServerGamePacketListenerImpl) connection.getPacketListener(); + handler.chunkSender.sendNextChunks(handler.player); + handler.resumeFlushing(); + } + } + + /** tick 本维度玩家的连接(正是这里触发原版 {@code ServerPlayer.doTick()})。 */ + private void tickConnections() { + Iterator iterator = this.connections.iterator(); + while (iterator.hasNext()) { + Connection connection = iterator.next(); + if (connection.isConnected()) { + try { + connection.tick(); + } catch (Exception e) { + // 区块未就绪类瞬时异常(玩家 tick 请求尚未加载的位置区块,常见于首次传送进 + // 全新维度、玩家位置 ticket 尚未登记):跳过本拍,下一 tick 区块就绪后自愈, + // 不执行断线(原行为会把玩家踢成 "Internal server error")。 + if (isChunkNotReady(e)) { + ModZuozhi.LOGGER.warn("维度 {} 玩家 tick 跳过(区块未就绪):{}", + this.level.dimension().location(), e.getMessage()); + continue; + } + ModZuozhi.LOGGER.warn("维度 {} 玩家连接 tick 失败 {}", + this.level.dimension().location(), connection.getLoggableAddress(serverTelemetry()), e); + Component message = Component.literal("Internal server error"); + connection.send(new ClientboundDisconnectPacket(message), + PacketSendListener.thenRun(() -> connection.disconnect(message))); + connection.setReadOnly(); + } + } else { + iterator.remove(); + connection.handleDisconnection(); + } + } + } + + /** 是否为"区块未就绪"(Chunk not there when requested)瞬时异常(遍历 cause 链)。 */ + private static boolean isChunkNotReady(Throwable t) { + while (t != null) { + if (t instanceof IllegalStateException + && t.getMessage() != null + && t.getMessage().contains("Chunk not there when requested")) { + return true; + } + t = t.getCause(); + } + return false; + } + + private boolean serverTelemetry() { + return this.level.getServer().logIPs(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java new file mode 100644 index 0000000..58b6149 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/PlayerPacketQueue.java @@ -0,0 +1,58 @@ +package dev.modzuozhi.core.dimthread; + +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 第2步:网络包按玩家分派到维度线程的每维度任务队列。 + *

+ * 借鉴 Chlorophyll(Unlicense)的 EntityTaskScheduler 思路:把玩家发来的游戏包处理任务 + * 投递到玩家所在维度的队列,由该维度 worker 在维度 tick 收口处统一执行(见 + * {@code ServerLevelPlayerPacketMixin})。主线程不再逐一执行所有玩家的包 handle,减轻主线程 + * 瓶颈;多维度下各维度的包处理与维度 tick 并行。 + *

+ * 线程安全:{@link ConcurrentLinkedQueue} 支持 netty 读线程 / 服务器主线程并发投递; + * {@link #drain()} 仅由本维度 tick 线程(维度 worker 或单机主线程串行路径)调用一次。 + */ +public final class PlayerPacketQueue { + /** 标记当前线程正在执行玩家包任务(handle 展开中)。 */ + private static final ThreadLocal EXECUTING = new ThreadLocal<>(); + + private final ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue<>(); + + /** 投递一个待处理的玩家包任务(任意线程安全)。 */ + public void offer(Runnable task) { + tasks.add(task); + } + + /** + * 串行执行并清空队列(仅本维度 tick 线程调用)。 + *

+ * 执行期间设置 {@link #EXECUTING} 标记:玩家包 handle(如 + * {@code ServerGamePacketListenerImpl.handleMovePlayer})开头会再次调用 + * {@code PacketUtils.ensureRunningOnSameThread},此时应直接通过(等价原版 + * "已在正确线程"),否则会把包再次投回队列造成无限循环。 + */ + public void drain() { + Runnable task; + while ((task = tasks.poll()) != null) { + EXECUTING.set(Boolean.TRUE); + try { + task.run(); + } catch (Throwable t) { + // 包任务异常不应中断维度 tick 收口;记录后继续处理剩余包 + DimThreadCore.LOGGER.warn("[PlayerPacketQueue] 玩家包处理异常", t); + } finally { + EXECUTING.remove(); + } + } + } + + /** 当前线程是否正在执行玩家包任务(handle 展开中)。 */ + public static boolean isExecuting() { + return Boolean.TRUE.equals(EXECUTING.get()); + } + + public boolean isEmpty() { + return tasks.isEmpty(); + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java b/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java new file mode 100644 index 0000000..8f6f83f --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/PostExecuteQueue.java @@ -0,0 +1,40 @@ +package dev.modzuozhi.core.dimthread; + +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * 跨线程延迟执行队列(借鉴 MCMT 的 PostExecutePool 思路,写我们自己的实现)。 + *

+ * 细粒度并行下,子任务(如方块实体 tick)运行在其它 worker 线程上,可能触发对 + * {@code Level} 内部非线程安全结构(如 {@code pendingBlockEntityTickers}、 + * {@code pendingFreshBlockEntities})的写入。为保证安全,这些写入不直接发生, + * 而是投递到本队列,由维度 worker 线程在本维度 tick 的收口处串行执行 + * (此时所有并行子任务已 {@code await} 完成、无并发)。 + *

+ * 线程安全:{@link ConcurrentLinkedQueue} 支持多线程并发投递;{@link #drain()} + * 只由维度 worker 调用一次。 + */ +public final class PostExecuteQueue { + private final ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); + + /** 投递一个跨线程延迟执行的回调(任意线程安全)。 */ + public void post(Runnable action) { + queue.add(action); + } + + /** 串行执行并清空队列(仅维度 worker 在收口处调用)。 */ + public void drain() { + Runnable action; + while ((action = queue.poll()) != null) { + try { + action.run(); + } catch (Throwable t) { + // 延迟回调异常不应中断维度 tick 收口 + } + } + } + + public boolean isEmpty() { + return queue.isEmpty(); + } +} diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java b/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java new file mode 100644 index 0000000..c20fa9e --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ServerManager.java @@ -0,0 +1,222 @@ +package dev.modzuozhi.core.dimthread; + +import dev.modzuozhi.ModZuozhi; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 每服务器激活状态与线程池管理(dimthreads 模型)。 + *

+ * 线程池按服务器懒创建(每个 {@code MinecraftServer} 实例一个),并在服务器停止时关闭清理。 + *

+ * 是否启用维度并行,由两个条件共同决定:全局开关 {@link ModZuozhi#isEnabled()}(可通过 + * {@code /dimensionalripper on|off} 运行时切换)+ 服务器级激活标记。任一为 false 即退回原版串行。 + */ +public class ServerManager { + /** CPU 核数是常量,仅在首个服务器创建线程池前计算一次。 */ + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + + /** + * 使用 {@link ConcurrentHashMap}:其 {@code computeIfAbsent} 是原子的,保证同一 + * {@code MinecraftServer} 只会有一个线程池实例。若用 {@code synchronizedMap} 包装, + * {@code computeIfAbsent} 默认实现非原子,维度 worker 并发调用 {@link #getThreadPool} 时会 + * 竞态创建出多个池实例,导致"子任务提交进池 A、主线程 awaitCompletion 却等在池 B"的错配死锁。 + */ + private final Map actives = new ConcurrentHashMap<>(); + public final Map threadPools = new ConcurrentHashMap<>(); + /** 缓存上次同步的维度数,维度数未变化时跳过遍历与线程调整,把每 tick 开销降到最低。 */ + private volatile int lastDimCount = -1; + + /** 每维度独立 tick 循环(维度隔离模型,见 {@link LevelTickLoop})。key 为维度。 */ + private final Map loops = new ConcurrentHashMap<>(); + /** 承载所有维度 loop 自排程的调度池({@code schedule(task, nspt)} 需要 Scheduled 能力)。 */ + private volatile ScheduledThreadPoolExecutor masterPool; + /** 玩家连接当前所属维度(key 为 Connection,value 为维度),供跨维度/登录时幂等迁移连接。 */ + private final Map connectionRoutes = new ConcurrentHashMap<>(); + + public boolean isActive(MinecraftServer server) { + // 每次调用都叠加全局开关,保证 /dimensionalripper off 立即退回原版 + return ModZuozhi.isEnabled() && this.actives.computeIfAbsent(server, s -> true); + } + + public void setActive(MinecraftServer server, boolean value) { + this.actives.put(server, value); + } + + public ThreadPool getThreadPool(MinecraftServer server) { + // 初始尺寸取 CPU 核数兜底,实际线程数由 syncThreadCount 每 tick 贴合维度数 + return this.threadPools.computeIfAbsent(server, s -> { + ModZuozhi.LOGGER.info("[DimThread] 新建维度线程池 server@{}", System.identityHashCode(s)); + return new ThreadPool(CPU_COUNT); + }); + } + + /** + * 回收不再属于当前活跃服务器的僵尸线程池。 + *

+ * 启动 / 世界加载阶段可能创建多个 {@code MinecraftServer} 实例,每个都会建一套维度线程池; + * 旧的池不再被 {@code tickWorlds} 使用却永远占用线程(每维度一个 worker),造成线程数量失控。 + * 主线程 {@code tickWorlds} 每 tick 调用本方法,把不属于当前 {@code current} 的旧池 shutdown 并移除, + * 使维度线程收敛回「每维度 1 个 worker」。 + */ + public void reapStalePools(MinecraftServer current) { + this.threadPools.forEach((server, pool) -> { + if (server != current) { + pool.shutdown(); + this.threadPools.remove(server, pool); + } + }); + } + + /** + * 让该服务器的线程池线程数贴合当前活跃维度数量(每维度 1 个 worker)。 + *

+ * 注意:细粒度子任务运行在 {@code FineGrainScheduler.SUB_POOL}(独立线程池),维度池 + * 只需承载「每维度 1 个 worker」的维度 tick,无需随 CPU 扩容。早期版本曾在此按 + * {@code fine} 开关把维度池扩容到 CPU 核数,但独立子任务池上线后该逻辑已冗余,会造成 + * 大量闲置的维度线程。统一收缩到维度数即可。 + *

+ * 目标值未变化时(绝大多数 tick)直接返回,不做遍历、不调整线程池,几乎零开销。 + */ + public void syncThreadCount(MinecraftServer server) { + int dims = 0; + for (var ignored : server.getAllLevels()) { + dims++; + } + int target = Math.max(1, Math.min(dims, CPU_COUNT)); + if (target == this.lastDimCount) { + return; + } + this.lastDimCount = target; + ThreadPool pool = getThreadPool(server); + pool.setThreadCount(target); + } + + /** 使线程数缓存失效,下次 tick 强制重算(开关切换时调用)。 */ + public void invalidateThreadCount() { + this.lastDimCount = -1; + } + + /** + * 全局调度池(承载所有维度 loop 的自排程)。懒创建,线程为 daemon,避免阻止服务器停机。 + * 线程数取 CPU 核数,保证每个活跃维度 loop 都有独立线程跑、不互相拖累。 + */ + public ScheduledThreadPoolExecutor getMasterPool() { + ScheduledThreadPoolExecutor pool = this.masterPool; + if (pool == null || pool.isShutdown()) { + synchronized (this) { + pool = this.masterPool; + if (pool == null || pool.isShutdown()) { + AtomicInteger seq = new AtomicInteger(); + pool = new ScheduledThreadPoolExecutor(CPU_COUNT, r -> { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName(DimThreadCore.MOD_ID + "_server_dimloop_" + seq.getAndIncrement()); + return t; + }); + pool.setRemoveOnCancelPolicy(true); + this.masterPool = pool; + } + } + } + return pool; + } + + /** 获取(懒创建)某维度的独立 tick 循环。 */ + public LevelTickLoop getLoop(ServerLevel level) { + return this.loops.computeIfAbsent(level, l -> new LevelTickLoop(l, getMasterPool())); + } + + /** + * 该维度 loop 是否已启动接管({@code tickChildren → scheduleIfNeeded} 之后才为 true)。 + *

+ * 启动阶段 {@code prepareLevels} 在 {@code tickChildren} 之前运行,此时 loop 尚未启动, + * 服务器主线程必须仍走原版 {@code ServerChunkCache.pollTask → runDistanceManagerUpdates} + * 来驱动出生点区块生成;直到 loop 接管后才由 {@code ServerChunkCachePollMixin} 跳过主线程 poll。 + */ + public boolean isLoopScheduled(ServerLevel level) { + LevelTickLoop loop = this.loops.get(level); + return loop != null && loop.isScheduled(); + } + + /** + * 把玩家的网络连接路由到其所属维度的 loop(幂等)。 + *

+ * 用 {@link #connectionRoutes} 记录连接当前所属维度:目标维度与记录一致时是 no-op;不一致 + * (登录首次 / 跨维度传送 / 重生换维度)时先从旧 loop 移除、再加入新 loop。线程安全,可由 + * 主线程(reconcile)与 loop 线程并发调用。 + */ + public void routeConnection(Connection connection, ServerLevel level) { + ServerLevel old = this.connectionRoutes.put(connection, level); + if (old != level) { + if (old != null) { + LevelTickLoop oldLoop = this.loops.get(old); + if (oldLoop != null) { + oldLoop.removeConnection(connection); + } + } + this.getLoop(level).addConnection(connection); + } + } + + /** 玩家下线时彻底移除连接路由(幂等;loop 也会在 {@code tickConnections} 自清理断开连接)。 */ + public void unrouteConnection(Connection connection) { + ServerLevel old = this.connectionRoutes.remove(connection); + if (old != null) { + LevelTickLoop loop = this.loops.get(old); + if (loop != null) { + loop.removeConnection(connection); + } + } + } + + /** + * 主线程每 tick 调用:把每个在线玩家的连接路由到其当前维度 loop。 + *

+ * 登录、跨维度传送、重生换维度三大路径的时序复杂,这里用一个幂等的「回收」统一收口:遍历 + * {@code playerList.getPlayers()},按玩家当前 {@code serverLevel()} 调用 {@link #routeConnection}。 + * 已在正确 loop 中的连接是 no-op,仅发生实际迁移时才增删。单机/少量玩家时开销可忽略。 + */ + public void reconcileConnections(MinecraftServer server) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (player.connection != null) { + routeConnection(player.connection.getConnection(), player.serverLevel()); + } + } + } + + /** 移除并停止某维度的 loop(维度卸载时调用)。 */ + public void removeLoop(ServerLevel level) { + LevelTickLoop loop = this.loops.remove(level); + if (loop != null) { + loop.kill(); + } + } + + /** 停止所有维度 loop 并关闭调度池(服务器停机 / 全局开关关闭时调用)。 */ + public void killAllLoops() { + this.loops.values().forEach(LevelTickLoop::kill); + this.loops.clear(); + // 清空连接路由:重新启用时 routeConnection 会把连接当作「首次」重新挂到新 loop。 + // 否则 old == level 会误判为 no-op,导致重启后玩家连接丢失、不再被任何 loop tick。 + this.connectionRoutes.clear(); + ScheduledThreadPoolExecutor pool = this.masterPool; + if (pool != null) { + pool.shutdownNow(); + this.masterPool = null; + } + } + + public void clear() { + actives.clear(); + threadPools.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java b/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java new file mode 100644 index 0000000..a429012 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/ThreadPool.java @@ -0,0 +1,206 @@ +package dev.modzuozhi.core.dimthread; + +import java.util.Iterator; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.*; + +import static dev.modzuozhi.core.dimthread.DimThreadCore.MOD_ID; + +/** + * 维度共享线程池(dimthreads 模型)。 + *

+ * 所有维度共用一个线程池,通过 {@code activeCount} 计数 + {@link #awaitCompletion()} + * 让主线程在每 tick 分发完所有维度后等待全部完成。线程均为 daemon,避免阻止服务器停止。 + *

+ * 线程数可动态调整:{@link #setThreadCount(int)} 会随当前活跃维度数上下浮动, + * 保证每个维度都有线程并行跑,也能适配运行期由其它 mod 新建的维度。 + */ +public class ThreadPool { + private ThreadPoolExecutor executor; + private volatile int threadCount; + private final IntLatch activeCount = new IntLatch(); + + public ThreadPool() { + this(Runtime.getRuntime().availableProcessors()); + } + + public ThreadPool(int threadCount) { + this.threadCount = Math.max(1, threadCount); + this.restart(); + } + + public int getThreadCount() { + return this.threadCount; + } + + /** + * 动态调整线程数(至少 1)。使用无界队列,多余任务排队等待,绝不因任务数超过线程数而拒绝。 + *

+ * 顺序很重要:{@code setCorePoolSize(c)} 要求 {@code c <= maximumPoolSize},否则抛 + * {@code IllegalArgumentException}。因此增大线程数时先扩 {@code max} 再扩 + * {@code core};缩小时先缩 {@code core} 再缩 {@code max}。 + */ + public void setThreadCount(int count) { + int c = Math.max(1, count); + if (c == this.threadCount) { + return; + } + this.threadCount = c; + if (this.executor != null && !this.executor.isShutdown()) { + if (c > this.executor.getMaximumPoolSize()) { + this.executor.setMaximumPoolSize(c); + this.executor.setCorePoolSize(c); + } else { + this.executor.setCorePoolSize(c); + this.executor.setMaximumPoolSize(c); + } + } + } + + public int getActiveCount() { + return this.activeCount.getCount(); + } + + public ThreadPoolExecutor getExecutor() { + return this.executor; + } + + public void execute(Runnable action) { + this.activeCount.increment(); + try { + this.executor.execute(() -> { + try { + action.run(); + } finally { + this.activeCount.decrement(); + } + }); + } catch (RejectedExecutionException e) { + // 池已 shutdown(服务器停机/回收旧池)时会拒绝新任务;若不回滚计数, + // awaitCompletion 会永久等待一个永远不会被 decrement 的计数 → 保存流程死锁。 + this.activeCount.decrement(); + throw e; + } + } + + public void execute(Iterator iterator, Consumer action) { + iterator.forEachRemaining(t -> this.execute(() -> action.accept(t))); + } + + public void execute(Iterable iterable, Consumer action) { + iterable.forEach(t -> this.execute(() -> action.accept(t))); + } + + public void execute(T[] array, Consumer action) { + for (T t : array) this.execute(() -> action.accept(t)); + } + + public void awaitFreeThread() { + this.waitFor(value -> value < this.getThreadCount()); + } + + public void awaitCompletion() { + this.waitFor(value -> value == 0); + } + + /** + * 带超时的完成等待(看门狗用)。 + * + * @param timeoutMillis 最长等待毫秒数 + * @return true 表示在超时前全部完成;false 表示超时(仍有任务未完成) + */ + public boolean awaitCompletion(long timeoutMillis) { + return this.waitFor(value -> value == 0, timeoutMillis); + } + + public void waitFor(IntPredicate condition) { + try { + this.activeCount.waitUntil(condition); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + public boolean waitFor(IntPredicate condition, long timeoutMillis) { + try { + return this.activeCount.waitUntil(condition, timeoutMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + public void restart() { + if (this.executor == null || this.executor.isShutdown()) { + this.executor = new ThreadPoolExecutor(this.threadCount, this.threadCount, + 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(), r -> { + Thread t = new Thread(r); + t.setDaemon(true); + t.setName(MOD_ID + "_server_" + "unassigned"); + return t; + }); + } + } + + public void shutdown() { + // 仅 shutdown() 只会拒绝新任务、不回收 core 线程,旧池线程会一直挂在 take() 上占用资源。 + // 因此先归零 core、再用 shutdownNow 中断闲置线程,使其真正退出(旧池此时必然闲置,安全)。 + if (this.executor != null) { + this.executor.setCorePoolSize(0); + this.executor.shutdownNow(); + } + } + + public boolean isShutdown() { + return this.executor.isShutdown(); + } + + private static class IntLatch { + private CountDownLatch latch; + + private IntLatch() { + this(0); + } + + private IntLatch(int count) { + this.latch = new CountDownLatch(count); + } + + private synchronized int getCount() { + return (int) this.latch.getCount(); + } + + private synchronized void decrement() { + this.latch.countDown(); + this.notifyAll(); + } + + private synchronized void increment() { + this.latch = new CountDownLatch((int) this.latch.getCount() + 1); + this.notifyAll(); + } + + private synchronized void waitUntil(IntPredicate predicate) throws InterruptedException { + while (!predicate.test(this.getCount())) { + this.wait(); + } + } + + private synchronized boolean waitUntil(IntPredicate predicate, long timeoutMillis) + throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMillis; + while (!predicate.test(this.getCount())) { + long remaining = deadline - System.currentTimeMillis(); + if (remaining <= 0) { + return false; + } + this.wait(remaining); + } + return true; + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java b/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java new file mode 100644 index 0000000..48cad28 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/dimthread/TpsTracker.java @@ -0,0 +1,125 @@ +package dev.modzuozhi.core.dimthread; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 服务器 TPS 估算(供 {@code /dimensionalripper status} 展示,验证优化效果)。 + *

+ * 维度独立 tick 循环改造后,主线程退化为「协调者」,恒按 20 拍运行;真实负载体现在每个维度 loop + * 的单次 tick 实际耗时(MSPT)上。因此这里按维度记录每次 {@code internalTick} 的真实耗时, + * {@code TPS = min(20, 1e9 / 平均MSPT)}: + *

    + *
  • 空闲时 MSPT 远小于 50ms → TPS = 20;
  • + *
  • 某维度单 tick 超过 50ms 预算 → TPS < 20(真实掉 TPS)。
  • + *
+ * 这比「记录相邻 tick 的墙钟间隔」更准确——后者会把自排程定时器的唤醒延迟(每拍约 0.5~0.8ms) + * 一并计入,导致完全空闲时仍显示 19.7。 + *

+ * 主线程样本 {@link #tick()} / {@link #tps()} 仅在维度并行关闭(退回原版串行)时有对比意义, + * 此时主线程间隔即真实 tick 周期。 + */ +public final class TpsTracker { + /** 采样窗口(tick 数)。 */ + private static final int WINDOW = 100; + + /** 主线程最近 N 个 tick 间隔(纳秒)。 */ + private static final long[] INTERVALS = new long[WINDOW]; + private static int index; + private static boolean primed; + private static long lastTick = -1L; + + /** 各维度的独立 MSPT 采样(key 为维度,如 {@code minecraft:overworld})。 */ + private static final Map, Sample> DIMENSIONS = new ConcurrentHashMap<>(); + + private TpsTracker() { + } + + /** 服务器主线程每 tick 调用一次。 */ + public static void tick() { + long now = System.nanoTime(); + if (lastTick >= 0L) { + long interval = now - lastTick; + synchronized (TpsTracker.class) { + INTERVALS[index] = interval; + index = (index + 1) % WINDOW; + primed = true; + } + } + lastTick = now; + } + + /** 某维度 loop 每次完成 {@code internalTick} 后调用,上报本次真实 tick 耗时(纳秒)。 */ + public static void recordDimension(ResourceKey dimension, long tickNanos) { + Sample sample = DIMENSIONS.computeIfAbsent(dimension, k -> new Sample()); + sample.record(tickNanos); + } + + /** 主线程 (协调者) TPS。数据不足时返回 20。 */ + public static double tps() { + synchronized (TpsTracker.class) { + if (!primed) { + return 20.0; + } + long sum = 0L; + for (long interval : INTERVALS) { + if (interval > 0L) { + sum += interval; + } + } + return sum <= 0L ? 20.0 : tpsFromAvg(sum / (double) WINDOW); + } + } + + /** 指定维度的真实 TPS。无采样时返回 20。 */ + public static double tps(ResourceKey dimension) { + Sample sample = DIMENSIONS.get(dimension); + return sample == null ? 20.0 : sample.tps(); + } + + /** 按维度名排序返回每维度的 TPS,供 {@code /dimensionalripper status} 展示。 */ + public static Map allDimensionTps() { + Map result = new TreeMap<>(); + for (Map.Entry, Sample> entry : DIMENSIONS.entrySet()) { + result.put(entry.getKey().location().toString(), entry.getValue().tps()); + } + return result; + } + + private static double tpsFromAvg(double avgNanos) { + if (avgNanos <= 0.0) { + return 20.0; + } + return Math.min(20.0, 1_000_000_000.0 / avgNanos); + } + + /** 单个维度的独立 MSPT 采样窗口。只在所属维度 loop 线程写入,读走 {@code synchronized}。 */ + private static final class Sample { + private final long[] work = new long[WINDOW]; + private int index; + private int count; + + synchronized void record(long nanos) { + work[index] = nanos; + index = (index + 1) % WINDOW; + if (count < WINDOW) { + count++; + } + } + + synchronized double tps() { + if (count == 0) { + return 20.0; + } + long sum = 0L; + for (int i = 0; i < count; i++) { + sum += work[i]; + } + return tpsFromAvg(sum / (double) count); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java b/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java new file mode 100644 index 0000000..1683b88 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/filter/EntityTickFilter.java @@ -0,0 +1,120 @@ +package dev.modzuozhi.core.filter; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 实体 tick 并行的白名单过滤器(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 实体 tick 远比方块实体复杂:实体互相交互、移动会更新 {@code EntitySectionStorage}、 + * 可能繁殖/转换产生新实体。因此第4步采用保守白名单——先以最复杂的村民(Brain AI) + * 验证通过,再扩展为所有原版生物。 + *

+ * 判定规则(按优先级): + *

    + *
  1. 命中黑名单 {@link #BLACKLIST} → 串行(已知有问题的类型,可运行时扩展排除);
  2. + *
  3. 命中白名单 {@link #WHITELIST} → 并行(显式放行,运行时可用 {@link #add} 增补);
  4. + *
  5. 默认:{@code LivingEntity} 且非玩家且原版生物 → 并行;其余(modded 生物、 + * 掉落物、经验球、箭、TNT、盔甲架等非生物实体)→ 串行。
  6. + *
+ * modded 生物默认串行是借鉴 Async 的思路(其用 {@code @AsyncCompatible} 注解显式放行): + * 跨模组生物行为不可控,保守串行以降低并行下的兼容风险;确需并行时可用 {@link #add} 显式放行。 + * 玩家始终由 {@link PlayerList} 单独处理,不参与本并行路径。 + */ +public final class EntityTickFilter { + /** 显式放行名单:命中即并行(村民已验证,作为兜底显式保留)。 */ + private static final Set WHITELIST = ConcurrentHashMap.newKeySet(); + + /** 显式排除名单:命中即强制串行(覆盖默认规则,可用于运行时隔离问题生物)。 */ + private static final Set BLACKLIST = ConcurrentHashMap.newKeySet(); + + /** + * 并行判定缓存:{@code EntityType → 是否并行}。 + *

+ * 避免每个实体每 tick 都 {@code EntityType.getKey(...).toString()} 构建字符串 + * (高实体量下是显著开销)。实体类型数量有限,首次遇到时计算一次并缓存; + * 黑/白名单运行时变化时 {@link #PARALLEL_CACHE} 清空。 + */ + private static final ConcurrentHashMap, Boolean> PARALLEL_CACHE = new ConcurrentHashMap<>(); + + static { + // 第4步首期验证通过的最难实体(Brain AI),显式保留在放行名单。 + WHITELIST.add("minecraft:villager"); + + // 第5步(6b/6c):爆炸/破坏方块类实体强制串行。 + // 它们在 tick 中会调用 {@code Level.setBlock} 破坏方块(苦力怕自爆、凋灵、末影龙、 + // 末影水晶),这会写 LevelChunk 的 PalettedContainer(非线程安全)。若在并行子任务线程 + // 执行,会与主线程的玩家操作(放置方块)并发写同一区块,导致数据竞争: + // 放置的方块立即消失、被破坏的方块在存档中"复活"(重进后 TNT 再次出现并爆炸)。 + // 因此这些实体必须走原版串行路径,与玩家操作在主线程时序上互斥。 + BLACKLIST.add("minecraft:creeper"); + BLACKLIST.add("minecraft:end_crystal"); + BLACKLIST.add("minecraft:ender_dragon"); + BLACKLIST.add("minecraft:wither"); + BLACKLIST.add("minecraft:tnt_minecart"); + } + + private EntityTickFilter() { + } + + /** 判断实体是否可并行 tick。 */ + public static boolean isParallel(Entity entity) { + // 快路径:非生物或玩家一定不并行(黑名单中的末影水晶/TNT矿车非生物,白名单只有村民)。 + if (!(entity instanceof LivingEntity) || entity instanceof Player) { + return false; + } + EntityType type = entity.getType(); + Boolean cached = PARALLEL_CACHE.get(type); + if (cached != null) { + return cached; + } + boolean result = computeParallel(type); + PARALLEL_CACHE.put(type, result); + return result; + } + + /** 首次遇到某实体类型时计算一次判定(含黑/白名单与命名空间判断)。 */ + private static boolean computeParallel(EntityType type) { + String key = EntityType.getKey(type).toString(); + if (BLACKLIST.contains(key)) { + return false; + } + if (WHITELIST.contains(key)) { + return true; + } + // 默认:原版(minecraft 命名空间)生物并行;modded 生物串行(借鉴 Async 思路)。 + return EntityType.getKey(type).getNamespace().equals("minecraft"); + } + + /** 显式放行某实体类型(运行时扩展)。 */ + public static void add(String entityType) { + WHITELIST.add(entityType); + BLACKLIST.remove(entityType); + PARALLEL_CACHE.clear(); + } + + /** 显式排除某实体类型(运行时隔离问题生物)。 */ + public static void exclude(String entityType) { + BLACKLIST.add(entityType); + WHITELIST.remove(entityType); + PARALLEL_CACHE.clear(); + } + + public static boolean remove(String entityType) { + PARALLEL_CACHE.clear(); + return WHITELIST.remove(entityType); + } + + public static boolean contains(String entityType) { + return WHITELIST.contains(entityType); + } + + public static boolean isExcluded(String entityType) { + return BLACKLIST.contains(entityType); + } +} diff --git a/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java b/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java new file mode 100644 index 0000000..bb9a4b4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/filter/ISerDesFilter.java @@ -0,0 +1,21 @@ +package dev.modzuozhi.core.filter; + +import net.minecraft.world.level.block.entity.TickingBlockEntity; + +/** + * SerDes 过滤器接口(借鉴 MCMT 的 ISerDesFilter)。 + *

+ * 决定某个方块实体 tick 是否可以在并行 worker 线程中执行。 + * 返回 {@code true} 表示可并行;返回 {@code false} 表示该方块实体线程不安全, + * 需回退到主线程串行 tick。 + */ +@FunctionalInterface +public interface ISerDesFilter { + /** + * 该方块实体 tick 是否可并行执行。 + * + * @param ticker 待 tick 的方块实体({@link TickingBlockEntity}) + * @return true = 可并行;false = 回退主线程串行 + */ + boolean isParallel(TickingBlockEntity ticker); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java b/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java new file mode 100644 index 0000000..0cfaa95 --- /dev/null +++ b/src/main/java/dev/modzuozhi/core/filter/SerDesFilter.java @@ -0,0 +1,62 @@ +package dev.modzuozhi.core.filter; + +import net.minecraft.world.level.block.entity.TickingBlockEntity; + +import java.util.Set; + +/** + * 默认 SerDes 过滤器。 + *

+ * 基于方块实体注册名({@code TickingBlockEntity.getType()},形如 {@code minecraft:furnace})判断: + *

    + *
  • 黑名单(强制串行):已知线程不安全、会与主线程交互的方块实体 + * (如移动的活塞/活塞头,涉及跨区块移除与实体交互)。
  • + *
  • modded 方块实体(非 {@code minecraft:} 前缀)默认串行,保守兜底; + * 可通过 {@link #setLockModded(boolean)} 关闭该保守行为。
  • + *
  • 其余 vanilla 方块实体默认可并行。
  • + *
+ */ +public final class SerDesFilter implements ISerDesFilter { + /** 全局单例({@link #isParallel} 为实例方法)。 */ + public static final SerDesFilter INSTANCE = new SerDesFilter(); + + /** 强制串行的方块实体注册名(黑名单)。 */ + private static final Set BLACKLIST = Set.of( + "minecraft:piston_head", + "minecraft:moving_piston", + // 会跨 5×5 范围访问其他容器/方块并触发跨区块交互,worker 线程并行时 + // 目标区块可能被并发修改(如 InvWrapper.getInv() 返回 null),强制串行 + "minecraft:hopper", + "minecraft:dropper", + "minecraft:dispenser", + "minecraft:chiseled_bookshelf", + "minecraft:crafter", + // serverTick 会访问 Level 级共享随机源(LegacyRandomSource),多个蜂巢分布在不同 + // 区块被并行 tick 时会跨线程访问同一随机源,触发 ThreadingDetector 异常,强制串行 + "minecraft:beehive", + "minecraft:bee_nest" + ); + + /** 是否对非 vanilla 的 modded 方块实体强制串行(保守兜底,默认开启)。 */ + private static volatile boolean lockModded = true; + + public static void setLockModded(boolean lockModded) { + SerDesFilter.lockModded = lockModded; + } + + public static boolean isLockModded() { + return lockModded; + } + + @Override + public boolean isParallel(TickingBlockEntity ticker) { + String type = ticker.getType(); + if (BLACKLIST.contains(type)) { + return false; + } + if (lockModded && !type.startsWith("minecraft:")) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java b/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java new file mode 100644 index 0000000..a0755cb --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/AttributeInstanceMixin.java @@ -0,0 +1,39 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 属性修饰符并发化(借鉴 Async 思路,独立实现)。 + *

+ * {@code AttributeInstance} 的修饰符表在并行 tick 下被跨线程读写(增益/减益结算、伤害计算、 + * 传感器查询)。原版用普通 Map,替换为并发容器;{@code getModifiers} 返回的按操作分类子表 + * 也并发化,保证 computeIfAbsent 原子且子表写入安全。 + */ +@Mixin(AttributeInstance.class) +public abstract class AttributeInstanceMixin { + + @Shadow + private final Map> modifiersByOperation = + new ConcurrentHashMap<>(); + + @Shadow + private final Map modifierById = new ConcurrentHashMap<>(); + + @Shadow + private final Map permanentModifiers = new ConcurrentHashMap<>(); + + @WrapMethod(method = "getModifiers(Lnet/minecraft/world/entity/ai/attributes/AttributeModifier$Operation;)Ljava/util/Map;") + private Map modzuozhi_getModifiers( + AttributeModifier.Operation operation, Operation> original) { + return this.modifiersByOperation.computeIfAbsent(operation, op -> new ConcurrentHashMap<>()); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java b/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java new file mode 100644 index 0000000..886c999 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/AttributeMapMixin.java @@ -0,0 +1,33 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeInstance; +import net.minecraft.world.entity.ai.attributes.AttributeMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 属性表并发化(借鉴 Async 思路,独立实现)。 + *

+ * 并行 tick 下,传感器/寻路/目标选择会跨线程读取其它实体的属性 + * ({@code getAttribute → getInstance}、{@code getValue}),属性被修改时还会写入 + * {@code attributesToUpdate/attributesToSync}。原版 {@code AttributeMap} 用普通 Map/Set, + * 跨线程读写存在数据竞争。全部替换为并发容器,读写安全。 + */ +@Mixin(AttributeMap.class) +public abstract class AttributeMapMixin { + + @Shadow + private final Map, AttributeInstance> attributes = new ConcurrentHashMap<>(); + + @Shadow + private final Set attributesToSync = ConcurrentHashMap.newKeySet(); + + @Shadow + private final Set attributesToUpdate = ConcurrentHashMap.newKeySet(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java new file mode 100644 index 0000000..669db76 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BabyFollowAdultGuardMixin.java @@ -0,0 +1,74 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.util.valueproviders.UniformInt; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BabyFollowAdult; +import net.minecraft.world.entity.ai.behavior.EntityTracker; +import net.minecraft.world.entity.ai.behavior.OneShot; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.memory.WalkTarget; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; + +import java.util.function.Function; + +/** + * 跨维度安全:{@link BabyFollowAdult} 添加维度检查。 + *

+ * 原版 1.21.1 的 {@code create()} 不做维度检查,当幼年生物追踪的成年目标在另一个维度时, + * 幼年生物会尝试跨维度寻路。并行环境下需确保幼年生物只跟随同一维度的目标。 + *

+ * 直接覆盖 1.21.1 实际存在的 {@code create(UniformInt, Function)} 重载(此为底层实现, + * {@code create(UniformInt, float)} 委托给此方法),增加维度检查。 + */ +@Mixin(BabyFollowAdult.class) +public class BabyFollowAdultGuardMixin { + + /** + * @reason 添加维度守卫,防止幼年生物跨维度跟随成年目标 + */ + @Overwrite + public static @NotNull OneShot create( + UniformInt uniformInt, + Function function + ) { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.NEAREST_VISIBLE_ADULT), + instance.registered(MemoryModuleType.LOOK_TARGET), + instance.absent(MemoryModuleType.WALK_TARGET) + ).apply(instance, (memoryAccessor, memoryAccessor2, memoryAccessor3) -> + (serverLevel, livingEntity, l) -> { + if (!livingEntity.isBaby()) { + return false; + } + LivingEntity livingEntity2 = instance.get(memoryAccessor); + + // 维度守卫:成年目标不在同一维度则清除记忆并跳过 + if (livingEntity2.level() != livingEntity.level()) { + memoryAccessor.erase(); + return true; + } + + if (livingEntity.closerThan(livingEntity2, + (double) (uniformInt.getMaxValue() + 1)) + && !livingEntity.closerThan(livingEntity2, + (double) uniformInt.getMinValue())) { + WalkTarget walkTarget = new WalkTarget( + new EntityTracker(livingEntity2, false), + function.apply(livingEntity), + uniformInt.getMinValue() - 1 + ); + memoryAccessor2.set(new EntityTracker(livingEntity2, true)); + memoryAccessor3.set(walkTarget); + return true; + } + return false; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java b/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java new file mode 100644 index 0000000..ffe8898 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BinaryHeapMixin.java @@ -0,0 +1,59 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.level.pathfinder.BinaryHeap; +import net.minecraft.world.level.pathfinder.Node; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第7步:寻路/AI 并发安全——{@code BinaryHeap} 守卫(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code BinaryHeap} 是 A* 寻路的开放集(open set),{@code Node.heapIdx} 记录节点在堆中的 + * 下标(-1 表示不在堆中)。每个 {@code PathFinder} 拥有独立的 {@code BinaryHeap}, + * 并行实体各自寻路时堆对象不跨实体共享,因此不会并发读写同一堆。 + *

+ * 但并行寻路下仍有防御性需要:若节点重复 {@code insert}(其 {@code heapIdx} 已在堆中), + * 原版直接抛 {@code IllegalStateException("OW KNOWS!")};若 {@code upHeap/downHeap} 收到 + * {@code heapIdx == -1} 的节点会数组越界。这些异常在正常串行下不会发生,但在并行子任务的 + * 寻路中,若某处对同一节点的堆状态判定出现竞态,可能触发。为让并行寻路失败不崩溃, + * 这里加守卫:已入堆的节点直接返回(不重复插入);堆下标为 -1 时跳过堆调整。 + *

+ * 语义影响:仅忽略"重复入堆/对已出堆节点的堆调整"这类非法调用,正常寻路路径完全不受影响。 + */ +@Mixin(BinaryHeap.class) +public abstract class BinaryHeapMixin { + + /** + * {@code insert}:若节点已在堆中({@code heapIdx >= 0}),跳过插入,避免重复入堆破坏堆结构。 + */ + @WrapMethod(method = "insert") + private Node modzuozhi_guardInsert(Node node, Operation original) { + if (node.heapIdx >= 0) { + return node; + } + return original.call(node); + } + + /** + * {@code upHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。 + */ + @WrapMethod(method = "upHeap") + private void modzuozhi_guardUpHeap(int index, Operation original) { + if (index < 0) { + return; + } + original.call(index); + } + + /** + * {@code downHeap}:堆下标为 -1(节点已出堆)时跳过,避免数组越界。 + */ + @WrapMethod(method = "downHeap") + private void modzuozhi_guardDownHeap(int index, Operation original) { + if (index < 0) { + return; + } + original.call(index); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java b/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java new file mode 100644 index 0000000..b0ca95e --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BlockDropCaptureMixin.java @@ -0,0 +1,57 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.block.Block; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.util.ArrayList; +import java.util.List; + +/** + * 让 NeoForge 的方块掉落捕获对多线程安全。 + *

+ * NeoForge 在 {@code Block.dropResources} 中通过全局静态字段 {@code Block.capturedDrops} + * 临时捕获本方块掉落的 {@code ItemEntity}(详见 {@code beginCapturingDrops}/{@code stopCapturingDrops}, + * 注释明确要求「只在服务端线程调用」)。维度并行 / 细粒度并行下,多个线程并发执行随机 tick + * (树叶凋零、方块燃烧等)都会进入 {@code dropResources},并发读写该静态字段会互相覆盖: + * 某个线程的 {@code stopCapturingDrops()} 会读到已被另一线程清空的 {@code null},导致 + * {@code BlockDropsEvent.getDrops()} 为 null 而 NPE(日志中 {@code CommonHooks.handleBlockDrops} + * 的崩溃即源于此),并连带产生 {@code ClassCastException}。 + *

+ * 这里把掉落捕获改为线程本地:每个线程维护独立的捕获列表,从根上消除跨线程污染。 + * 语义与原版一致(仅在捕获开始到结束之间拦截掉落),但并行安全。 + */ +@Mixin(Block.class) +public abstract class BlockDropCaptureMixin { + /** 线程本地掉落捕获列表,替代原版全局静态 {@code Block.capturedDrops}。 */ + @Unique + private static final ThreadLocal> modzuozhi_capturedDrops = new ThreadLocal<>(); + + @Redirect(method = "dropResources", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;beginCapturingDrops()V")) + private static void modzuozhi_beginCapture() { + modzuozhi_capturedDrops.set(new ArrayList<>()); + } + + @Redirect(method = "dropResources", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Block;stopCapturingDrops()Ljava/util/List;")) + private static List modzuozhi_stopCapture() { + List drops = modzuozhi_capturedDrops.get(); + modzuozhi_capturedDrops.remove(); + return drops; + } + + @Redirect(method = "popResource(Lnet/minecraft/world/level/Level;Ljava/util/function/Supplier;Lnet/minecraft/world/item/ItemStack;)V", + at = @At(value = "FIELD", + target = "Lnet/minecraft/world/level/block/Block;capturedDrops:Ljava/util/List;", + opcode = Opcodes.GETSTATIC)) + private static List modzuozhi_capturedGet() { + return modzuozhi_capturedDrops.get(); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java b/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java new file mode 100644 index 0000000..212f4cc --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BlockableEventLoopPollMixin.java @@ -0,0 +1,61 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.util.thread.BlockableEventLoop; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Queue; + +/** + * 修复 {@link BlockableEventLoop#pollTask()} 的 peek→remove 并发竞态。 + *

+ * 原版 {@code pollTask()} 先 {@code peek()}(非空判断)再 {@code remove()},两步非原子。 + * 维度 loop 模型下,本模组维度 worker({@code LevelTickLoop.drainChunkTasks} 每拍、以及 + * {@code managedBlock})与服务器主线程 {@code pollTaskInternal}(瞬态窗口内)可能并发 poll + * 同一维度的 {@code pendingRunnables}:线程 A {@code peek()} 到元素后、{@code remove()} 前, + * 线程 B 已 {@code remove()},A 的 {@code remove()} 空队列抛 {@code NoSuchElementException} + * (实测:下界维度 tick 异常,{@code FaultGuard} 计数 1 次)。 + *

+ * 仅在本模组维度 worker线程上用 {@code poll()}(空返回 null,不抛异常)替代 + * {@code remove()}:并发被抢走时返回 false(跳过本拍,下一拍自愈)。其它线程保持原版行为不变。 + */ +@Mixin(BlockableEventLoop.class) +public abstract class BlockableEventLoopPollMixin { + + @Shadow + @Final + private Queue pendingRunnables; + + @Shadow + private int blockingCount; + + @Shadow + protected abstract boolean shouldRun(R runnable); + + @Shadow + protected abstract void doRunTask(R task); + + @WrapMethod(method = "pollTask") + private boolean modzuozhi_safePollTask(Operation original) { + if (!DimThreadCore.owns(Thread.currentThread())) { + return original.call(); + } + R r = this.pendingRunnables.peek(); + if (r == null) { + return false; + } else if (this.blockingCount == 0 && !this.shouldRun(r)) { + return false; + } else { + R removed = this.pendingRunnables.poll(); + if (removed == null) { + return false; // 并发被抢走,跳过本拍(自愈) + } + this.doRunTask(removed); + return true; + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/BrainMixin.java b/src/main/java/dev/modzuozhi/mixin/BrainMixin.java new file mode 100644 index 0000000..4cd88f0 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/BrainMixin.java @@ -0,0 +1,70 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.ai.Brain; +import net.minecraft.world.entity.ai.memory.ExpirableValue; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.memory.MemoryStatus; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.Optional; + +/** + * 跨线程访问生物大脑内存的同步加固(借鉴 Async/MCMT 思路,独立实现)。 + *

+ * 细粒度并行时,多个实体的 tick 在子任务线程上并发执行。某个生物的大脑内存 + * ({@code Brain.memories})不仅被它自己的 tick 读写,还可能被其它实体的 + * 传感器/寻路并发访问(如 {@code NearestLivingEntitySensor} 读取它的记忆)。 + * 原版 {@code Brain} 用普通 HashMap,跨线程读写存在数据竞争(潜在错误行为而非崩溃)。 + *

+ * 这里对记忆读写入口加 {@code synchronized(this)}:单对象锁、逐方法加解锁、无嵌套, + * 同一大脑同一时刻只允许一个线程访问,结构上无死锁;未竞争时偏向锁开销可忽略。 + * 相比给整段 villager tick 加锁,仅同步记忆访问点,保留并行收益。 + */ +@Mixin(Brain.class) +public abstract class BrainMixin { + + @WrapMethod(method = "getMemory") + private Optional modzuozhi_getMemory(MemoryModuleType type, Operation> original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "getMemoryInternal") + private Optional modzuozhi_getMemoryInternal(MemoryModuleType type, Operation> original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "hasMemoryValue") + private boolean modzuozhi_hasMemoryValue(MemoryModuleType type, Operation original) { + synchronized (this) { + return original.call(type); + } + } + + @WrapMethod(method = "checkMemory") + private boolean modzuozhi_checkMemory(MemoryModuleType type, MemoryStatus status, Operation original) { + synchronized (this) { + return original.call(type, status); + } + } + + @WrapMethod(method = "setMemoryInternal") + private void modzuozhi_setMemoryInternal( + MemoryModuleType type, Optional> value, Operation original) { + synchronized (this) { + original.call(type, value); + } + } + + @WrapMethod(method = "clearMemories") + private void modzuozhi_clearMemories(Operation original) { + synchronized (this) { + original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java b/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java new file mode 100644 index 0000000..2e055e2 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ChunkMapLoadOffloadMixin.java @@ -0,0 +1,58 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2ByteMaps; +import net.minecraft.Util; +import net.minecraft.server.level.ChunkMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.concurrent.Executor; + +/** + * 阶段 A(下一步):把「区块反序列化」从主线程下放到后台线程。 + *

+ * 原版 {@code ChunkMap.scheduleChunkLoad} 里,磁盘读 + datafix 已经异步(见 + * {@code readChunk}),但 {@code ChunkSerializer.read}(NBT → ProtoChunk,含方块状态/生物群系 + * 调色板解析,CPU 密集)仍跑在 {@code this.mainThreadExecutor}。这正是进图 / 快速跑图 / 传送时 + * 主线程卡顿的元凶之一——本 mixin 把这一步的执行器切换到 {@link Util#backgroundExecutor()}。 + *

+ * 配套加固:下放后 {@code markPosition}/{@code createEmptyChunk} 会从后台线程写 + * {@code chunkTypeCache}(仅用于调试的 chunk-type 缓存,非世界数据),而主线程仍在读/删它, + * 因此把该缓存包装为同步 Map,消除并发写冲突。 + */ +@Mixin(ChunkMap.class) +public abstract class ChunkMapLoadOffloadMixin { + + /** 反序列化下放开关(与 {@link ModZuozhi#isEnabled()} 叠加,可一键回退)。Mixin 禁止非私有静态字段。 */ + private static volatile boolean ENABLED = true; + + @Shadow + @Final + @Mutable + private Long2ByteMap chunkTypeCache; + + @Inject(method = "", at = @At("RETURN")) + private void modzuozhi$makeChunkTypeCacheConcurrent(CallbackInfo ci) { + this.chunkTypeCache = Long2ByteMaps.synchronize(this.chunkTypeCache); + } + + @ModifyArg( + method = "scheduleChunkLoad", + at = @At( + value = "INVOKE", + target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;" + ), + index = 1 + ) + private Executor modzuozhi$deserializeOffMainThread(Executor executor) { + return (ModZuozhi.isEnabled() && ENABLED) ? Util.backgroundExecutor() : executor; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java b/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java new file mode 100644 index 0000000..425e758 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ClassInstanceMultiMapMixin.java @@ -0,0 +1,44 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.util.ClassInstanceMultiMap; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第10步:{@code ClassInstanceMultiMap} 内部结构线程安全化(修共享读锁下的 CME)。 + *

+ * {@code EntitySection.getEntities(EntityTypeTest,...)} 走 {@code ClassInstanceMultiMap.find}, + * 其内部执行 {@code byClass.computeIfAbsent(...)} 懒加载类型列表——这是写操作。 + * 第10步把 section 内容查询改成共享读锁后,同一 section 的两个并发查询会同时 + * {@code computeIfAbsent} 同一个普通 {@code HashMap} → {@code ConcurrentModificationException} + * (日志已证实:村民大脑传感器在 1 分钟内触发 5 次子任务异常导致自动降级)。 + *

+ * 修复:把 {@code byClass} 换成 {@link ConcurrentHashMap}——其 {@code computeIfAbsent} 原子且 + * 线程安全(同一 key 的映射函数只执行一次),并发查询互不干扰。既保留共享读锁的并行度,又根除 CME。 + * {@code allInstances} 的读写仍由 section 分片锁(写锁 vs 读锁)互斥,无需改动。 + *

+ * 注:字段 final 已由 access transformer({@code accesstransformer.cfg})移除,本类在 + * 构造器 RETURN 处替换为并发映射并恢复 {@code baseClass → allInstances} 注册。 + */ +@Mixin(ClassInstanceMultiMap.class) +public abstract class ClassInstanceMultiMapMixin { + @Shadow + private Map, List> byClass; + @Shadow + private Class baseClass; + @Shadow + private List allInstances; + + @Inject(method = "", at = @At("RETURN")) + private void modzuozhi_concurrentByClass(CallbackInfo ci) { + this.byClass = new ConcurrentHashMap<>(); + this.byClass.put(this.baseClass, this.allInstances); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java b/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java new file mode 100644 index 0000000..0d0c185 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/CollectingNeighborUpdaterMixin.java @@ -0,0 +1,78 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.redstone.CollectingNeighborUpdater; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * 修复并行随机 tick 触发邻居更新时的并发崩溃(NoSuchElementException)。 + *

+ * 根因:第3步区块环境 tick 并行中,藤蔓等 {@code randomTick} 会在子任务线程调用 + * {@code Level.setBlockAndUpdate},进而触发 {@code CollectingNeighborUpdater}。它内部用 + * 非线程安全的 {@code ArrayDeque} 作为邻居更新栈({@code stack}/{@code addedThisLayer}), + * 多个子任务线程并发访问同一实例时会抛 {@code NoSuchElementException}(日志已证实)。 + *

+ * 修复:对 {@code CollectingNeighborUpdater} 的每个公开入口方法加实例级重入锁。 + * 同一维度的邻居更新串行执行;不同维度(不同实例)互不干扰。锁为可重入锁,内部递归 + * 更新({@code executeUpdate → setBlock → 再入 addAndRun})不会死锁。 + */ +@Mixin(CollectingNeighborUpdater.class) +public abstract class CollectingNeighborUpdaterMixin { + /** 实例级重入锁:串行化同一维度内并发子任务对邻居更新栈的访问。 */ + @Unique + private final ReentrantLock modzuozhi_neighborLock = new ReentrantLock(); + + @WrapMethod(method = "shapeUpdate") + private void modzuozhi_shapeUpdate(Direction direction, BlockState state, BlockPos pos, + BlockPos neighborPos, int updateFlags, int updateLimit, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(direction, state, pos, neighborPos, updateFlags, updateLimit); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "neighborChanged(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/core/BlockPos;)V") + private void modzuozhi_neighborChanged(BlockPos pos, Block block, BlockPos neighborPos, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(pos, block, neighborPos); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "neighborChanged(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/core/BlockPos;Z)V") + private void modzuozhi_neighborChangedState(BlockState state, BlockPos pos, Block block, + BlockPos neighborPos, boolean movedByPiston, + Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(state, pos, block, neighborPos, movedByPiston); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } + + @WrapMethod(method = "updateNeighborsAtExceptFromFacing") + private void modzuozhi_updateNeighborsAtExceptFromFacing(BlockPos pos, Block block, + Direction skipDir, Operation original) { + this.modzuozhi_neighborLock.lock(); + try { + original.call(pos, block, skipDir); + } finally { + this.modzuozhi_neighborLock.unlock(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java b/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java new file mode 100644 index 0000000..68c96f5 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/DistanceManagerMixin.java @@ -0,0 +1,118 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.Ticket; +import net.minecraft.world.level.ChunkPos; +import org.spongepowered.asm.mixin.Mixin; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * {@code DistanceManager}(区块 ticket 调度)并发保护。 + *

+ * 维度并行下,维度 worker / 细粒度 subtask 的 {@code getChunk} 会在同步路径里调用 + * {@code addTicket/removeTicket} 修改 ticket,而主线程 executor 里的 + * {@code PlayerTicketTracker.onLevelChange}(玩家移动/跨维度传送触发)也在修改同一 + * {@code DistanceManager},两者并发导致内部 {@code SortedArraySet.first()} 读到空集等 + * NPE 竞态。 + *

+ * 这里的锁覆盖所有 ticket 写操作,使「维度侧 getChunk 的 ticket 修改」与「主线程玩家的 + * ticket 变更」互斥,从根上消除 DistanceManager 并发竞态。ticket 操作不频繁,串行化代价低。 + */ +@Mixin(DistanceManager.class) +public class DistanceManagerMixin { + private static final ReentrantLock LOCK = new ReentrantLock(); + + @WrapMethod(method = "addTicket") + private void modzuozhi_lockAddTicket(long chunkPos, Ticket ticket, Operation original) { + LOCK.lock(); + try { + original.call(chunkPos, ticket); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removeTicket") + private void modzuozhi_lockRemoveTicket(long chunkPos, Ticket ticket, Operation original) { + LOCK.lock(); + try { + original.call(chunkPos, ticket); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "updateChunkForced") + private void modzuozhi_lockUpdateChunkForced(ChunkPos pos, boolean add, Operation original) { + LOCK.lock(); + try { + original.call(pos, add); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removeTicketsOnClosing") + private void modzuozhi_lockRemoveTicketsOnClosing(Operation original) { + LOCK.lock(); + try { + original.call(); + } finally { + LOCK.unlock(); + } + } + + /** + * 维度循环模型下,主线程 {@code ServerChunkCache.pollTask} 与维度 worker 的 + * {@code level.tick → ServerChunkCache.tick} 会并发调用 {@code runAllUpdates}, + * 遍历 {@code chunksToUpdateFutures}(普通 HashSet)时抛 {@code ConcurrentModificationException}。 + * 用同一把锁串行化,彻底消除该竞态。 + */ + @WrapMethod(method = "runAllUpdates") + private boolean modzuozhi_lockRunAllUpdates(ChunkMap chunkMap, Operation original) { + LOCK.lock(); + try { + return original.call(chunkMap); + } finally { + LOCK.unlock(); + } + } + + /** {@code purgeStaleTickets} 无锁遍历 + 修改 {@code tickets},与主线程 ticket 写并发时需要串行化。 */ + @WrapMethod(method = "purgeStaleTickets") + private void modzuozhi_lockPurgeStaleTickets(Operation original) { + LOCK.lock(); + try { + original.call(); + } finally { + LOCK.unlock(); + } + } + + /** 玩家登录/跨维度在 worker 或主线程改 {@code playersPerChunk},与 runAllUpdates 并发时串行化。 */ + @WrapMethod(method = "addPlayer") + private void modzuozhi_lockAddPlayer(SectionPos pos, ServerPlayer player, Operation original) { + LOCK.lock(); + try { + original.call(pos, player); + } finally { + LOCK.unlock(); + } + } + + @WrapMethod(method = "removePlayer") + private void modzuozhi_lockRemovePlayer(SectionPos pos, ServerPlayer player, Operation original) { + LOCK.lock(); + try { + original.call(pos, player); + } finally { + LOCK.unlock(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java new file mode 100644 index 0000000..e2fe82d --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityChangeDimensionMixin.java @@ -0,0 +1,45 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import javax.annotation.Nullable; + +/** + * dimthreads 式跨维度传送调度。 + *

+ * 各维度 tick 并行运行在不同 worker 上,实体容器 {@code Level.entityStorage} 非线程安全。 + * 若某维度 worker 直接调用 {@code changeDimension} 迁移实体到另一维度,会与其他 worker + * 并发修改实体容器/区块生命周期而崩溃或死锁。因此当调用线程是维度 worker 时,把真正的 + * 迁移登记到 {@link DimensionChangeQueue},由主线程在维度 tick 收口后、 + * {@code playerList.tick()} 之前统一执行(见 {@code MinecraftServerDimMixin})。 + *

+ * 与 dimthreads 原版用 {@code server.execute()} 的区别:execute 依赖 {@code runAllTasks} + * 在 tick 结束后才消费,传送会延迟到下一 tick,且如果期间玩家走开会被 stale 传送拉回 + * 传送门(反复拉扯)。本队列在同一 tick 收口处消费,传送即时完成,杜绝拉扯。 + */ +@Mixin(Entity.class) +public abstract class EntityChangeDimensionMixin { + + @Shadow + @Nullable + public abstract Entity changeDimension(DimensionTransition transition); + + @Inject(method = "changeDimension", at = @At("HEAD"), cancellable = true, remap = false) + public void modzuozhi_moveToWorld(DimensionTransition transition, CallbackInfoReturnable cir) { + if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) { + return; + } + if (DimThreadCore.owns(Thread.currentThread())) { + DimensionChangeQueue.schedule((Entity) (Object) this, transition); + cir.setReturnValue(null); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java new file mode 100644 index 0000000..ca57826 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityGetEntityCollisionsMixin.java @@ -0,0 +1,55 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; +import java.util.List; + +/** + * 第11步(可选特性):禁用实体间碰撞(Mob Collision Off 模式,独立实现)。 + *

+ * 两个注入点(对齐 mob-collision-off 模组的 EntityCollideMixin + EntityMixin): + *

    + *
  • {@code @Redirect} {@code Entity.collide(Vec3)} 里的 {@code getEntityCollisions(...)}: + * 实体移动时不查询/计算附近实体的碰撞形状(消除 O(n²) 热点)。
  • + *
  • {@code @Inject} {@code Entity.push(Entity)} HEAD 取消:实体重叠时互相推开的 + * 独立推动机制也一并关闭(否则即使移动碰撞为空,重叠实体仍会被 push 挤开,表现为 + * "碰撞箱还在")。攻击击退走 {@code push(double,double,double)} 重载,不受影响。
  • + *
+ * 方块碰撞不受影响(走另一条路径 {@code BlockCollisions})。开关:{@code /dimensionalripper nocollide on|off}, + * 遵守全局开关:{@code /dimensionalripper off} 一并退化为原版。 + *

+ * 第14步(对齐 Async):第13步的"碰撞降频"(每 2 tick 评估一次碰撞)已回退——移除全局写锁、 + * 并发化实体存储后,碰撞开启也能并行跑,不再需要降频拐杖。本特性保留为可选开关(默认关闭 + * 碰撞以获取最大 TPS;需要生物交互时开碰撞)。 + */ +@Mixin(Entity.class) +public abstract class EntityGetEntityCollisionsMixin { + @Redirect(method = "collide(Lnet/minecraft/world/phys/Vec3;)Lnet/minecraft/world/phys/Vec3;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;getEntityCollisions(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/phys/AABB;)Ljava/util/List;")) + private List modzuozhi_noMobCollision(Level level, @Nullable Entity entity, AABB aabb) { + // 只受 nocollide 开关控制,不随全局开关失效:全局 /dimensionalripper off 后若恢复真实碰撞, + // 实体密集场景会回归 O(n²) 碰撞热点,主线程被拖到几乎无响应(压测巨量实体时实测卡死)。 + if (ModZuozhi.MOB_COLLISION_OFF) { + return List.of(); + } + return level.getEntityCollisions(entity, aabb); + } + + @Inject(method = "push(Lnet/minecraft/world/entity/Entity;)V", at = @At("HEAD"), cancellable = true) + private void modzuozhi_noPush(Entity other, CallbackInfo ci) { + if (ModZuozhi.MOB_COLLISION_OFF) { + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java new file mode 100644 index 0000000..19d8262 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityLookupMixin.java @@ -0,0 +1,38 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.collection.Int2ObjectConcurrentHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntityLookup; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第14步(对齐 Async 零锁路线):{@code EntityLookup}(可见实体表)并发化。 + *

+ * {@code EntityLookup} 是 {@code PersistentEntitySectionManager.visibleEntityStorage}: + *

    + *
  • {@code byId}({@code Int2ObjectLinkedOpenHashMap})→ {@link Int2ObjectConcurrentHashMap} + * (内部 {@code ConcurrentHashMap},弱一致迭代);
  • + *
  • {@code byUuid}(普通 {@code HashMap})→ {@link ConcurrentHashMap}。
  • + *
+ * 实体并行 tick 下,{@code startTracking/stopTracking}(实体可见性变更,经 onMove/updateStatus/ + * addEntityWithoutEvent 触发)会从多个并行子任务线程并发读写该表。此前由全局写锁 + * {@code SECTION_LOCK} 串行化;第14步移除全局写锁后,改由并发集合承载,读写均线程安全。 + * {@code getEntities/getAllEntities} 遍历 {@code byId.values()}(弱一致),并发增删不抛 CME。 + * 用 {@code @Shadow} 初始值替换模式换成并发实例(与 {@code PoiSectionMixin}/{@code SectionStorageMixin} + * 相同的已验证模式,无需 AT)。 + */ +@Mixin(EntityLookup.class) +public abstract class EntityLookupMixin { + + @Shadow + private final Int2ObjectMap byId = new Int2ObjectConcurrentHashMap<>(); + + @Shadow + private final Map byUuid = new ConcurrentHashMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java new file mode 100644 index 0000000..ddac043 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionManagerMixin.java @@ -0,0 +1,40 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.PersistentEntitySectionManager; +import net.minecraft.world.level.entity.Visibility; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Collections; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第14步(对齐 Async 零锁路线):{@code PersistentEntitySectionManager} 共享状态并发化。 + *

+ * 第4步起,实体生命周期写入({@code addEntityWithoutEvent}/{@code onMove}/{@code onRemove})由全局写锁 + * {@code SECTION_LOCK} 串行化;第14步移除该全局写锁,改由并发集合承载,使实体移动/生成可真正并行: + *

    + *
  • {@code knownUuids}(普通 {@code HashSet})→ 并发 {@code Set}({@code ConcurrentHashMap} 视图), + * 保护 {@code addEntityUuid}/{@code isLoaded}/{@code onRemove} 的并发读写;
  • + *
  • {@code chunkVisibility}({@code Long2ObjectOpenHashMap})→ {@link ConcurrentLong2ObjectMap}, + * 保护 {@code createSection}/{@code updateStatus}/{@code canPositionTick} 对区块可见性的并发读 + * (写主要在服务器线程 {@code updateChunkStatus})。
  • + *
+ * 用 {@code @Shadow} 初始值替换模式(与 {@code PoiSectionMixin}/{@code SectionStorageMixin} 相同,无需 AT)。 + */ +@Mixin(PersistentEntitySectionManager.class) +public abstract class EntitySectionManagerMixin { + + /** 已注册实体 UUID 集合:并发的 {@code Set}(替换原 {@code HashSet},读写线程安全)。 */ + @Shadow + final Set knownUuids = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + /** 区块可见性表:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读线程安全)。 */ + @Shadow + private final Long2ObjectMap chunkVisibility = new ConcurrentLong2ObjectMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java new file mode 100644 index 0000000..ebed5d4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionMixin.java @@ -0,0 +1,115 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.dimthread.ISectionKeyAccessor; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import net.minecraft.util.AbortableIterationConsumer; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntityTypeTest; +import net.minecraft.world.phys.AABB; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** + * 第4/5步:实体 tick 并行——{@code EntitySection} 内部存储的并发读写互斥(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code EntitySection.storage} 是 {@code ClassInstanceMultiMap}(内部 {@code HashMap}/{@code ArrayList}, + * 非线程安全)。并行子任务中:村民大脑传感器({@code NearestLivingEntitySensor} → {@code getEntitiesOfClass}) + * 和移动碰撞检测({@code getEntityCollisions})会遍历 {@code storage}({@code getEntities}), + * 而实体移动({@code onMove})与区块加载({@code addEntityWithoutEvent})会在任意线程(子任务、维度 worker、 + * 区块加载线程、服务器主线程)增删该 {@code storage},二者并发即抛 {@code ConcurrentModificationException}, + * 中断实体 tick 导致实体卡进方块/遁地/窒息。 + *

+ * 第5步细粒度化:将第4步的全局锁 {@link EntityTickParallel#SECTION_LOCK} 降级为分片锁。 + * 本类注入 {@link #modzuozhi_sectionKey}(由 {@code EntitySectionStorageMixin} 在 createSection 时写入), + * 对 add/remove/getEntities 用该 key 定位分片锁——不同区域的实体 section 互不阻塞,真正并行。 + * 结构级操作(storage 遍历/创建/删除、manager、callback)仍持全局锁,锁序固定为 + * 全局锁 → 分片锁,无反向路径,不会死锁。 + * 用 {@link WrapMethod} 整方法包装,避免 {@code @WrapOperation at HEAD} 对含分支的方法注入失败。 + */ +@Mixin(EntitySection.class) +public abstract class EntitySectionMixin implements ISectionKeyAccessor { + /** 本 section 在 {@code EntitySectionStorage} 中的 key(SectionPos.asLong),由 storage 注入。 */ + @Unique + private long modzuozhi_sectionKey; + + @Override + @Unique + public void modzuozhi_setSectionKey(long key) { + this.modzuozhi_sectionKey = key; + } + + @Override + @Unique + public long modzuozhi_getSectionKey() { + return this.modzuozhi_sectionKey; + } + + @WrapMethod(method = "add") + private void modzuozhi_lockedAdd(EntityAccess entity, Operation original) { + // 内容写入:分片写锁(独占) + EntityTickParallel.lockSection(this.modzuozhi_sectionKey); + try { + original.call(entity); + } finally { + EntityTickParallel.unlockSection(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "remove") + private boolean modzuozhi_lockedRemove(EntityAccess entity, Operation original) { + EntityTickParallel.lockSection(this.modzuozhi_sectionKey); + try { + return original.call(entity); + } finally { + EntityTickParallel.unlockSection(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "getEntities(Lnet/minecraft/world/phys/AABB;Lnet/minecraft/util/AbortableIterationConsumer;)Lnet/minecraft/util/AbortableIterationConsumer$Continuation;") + private AbortableIterationConsumer.Continuation modzuozhi_lockedGetEntities( + AABB aabb, AbortableIterationConsumer consumer, + Operation original + ) { + // 内容遍历/查询:分片读锁(共享,传感器/碰撞查询并发执行) + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call(aabb, consumer); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } + + @WrapMethod(method = "getEntities(Lnet/minecraft/world/level/entity/EntityTypeTest;Lnet/minecraft/world/phys/AABB;Lnet/minecraft/util/AbortableIterationConsumer;)Lnet/minecraft/util/AbortableIterationConsumer$Continuation;") + private AbortableIterationConsumer.Continuation modzuozhi_lockedGetEntitiesTyped( + EntityTypeTest typeTest, AABB aabb, + AbortableIterationConsumer consumer, + Operation original + ) { + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call(typeTest, aabb, consumer); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } + + /** + * {@code getEntities()} 返回的 {@code storage.stream()} 是惰性流,锁无法覆盖其消费阶段。 + * 消费方({@code PersistentEntitySectionManager.updateChunkStatus/storeChunkSections})可能在子任务 + * 并发增删时迭代底层 {@code ArrayList} 而抛 CME。这里在锁内收集成快照再返回,消费时必然安全。 + */ + @WrapMethod(method = "getEntities()Ljava/util/stream/Stream;") + private Stream modzuozhi_lockedGetEntitiesStream(Operation> original) { + EntityTickParallel.lockSectionRead(this.modzuozhi_sectionKey); + try { + return original.call().collect(Collectors.toList()).stream(); + } finally { + EntityTickParallel.unlockSectionRead(this.modzuozhi_sectionKey); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java b/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java new file mode 100644 index 0000000..87660f4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntitySectionStorageMixin.java @@ -0,0 +1,61 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.collection.ConcurrentLong2ObjectMap; +import dev.modzuozhi.core.collection.ConcurrentLongSortedSet; +import dev.modzuozhi.core.dimthread.ISectionKeyAccessor; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.LongSortedSet; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 第4/11步:实体 tick 并行——{@code EntitySectionStorage} 结构存储并发化(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 第11步(SECTION_LOCK → 并发集合):把 {@code EntitySectionStorage.sections} + * ({@code Long2ObjectOpenHashMap})与 {@code sectionIds}({@code LongAVLTreeSet})替换为 + * {@link ConcurrentLong2ObjectMap}/{@link ConcurrentLongSortedSet}(弱一致、无 CME)。 + * 由此删除结构级读路径的全部全局读锁: + *

    + *
  • 读:{@code forEachAccessibleNonEmptySection} 的 {@code sectionIds.subSet().iterator()} + + * {@code sections.get} 现在无锁安全(原版源码对 {@code sections.get} 已有 null 检查, + * 并发创建 section 时读到 null 天然跳过);
  • + *
  • 写:{@code getOrCreateSection → computeIfAbsent} 原子、{@code createSection} 内部 + * {@code sectionIds.add} 与 {@code sections.put} 由 {@code ConcurrentHashMap.computeIfAbsent} + * 串行化,不会重复建 section。
  • + *
+ * 结构写(实体生命周期 {@code onMove/onRemove/addEntityWithoutEvent})仍由 + * {@link dev.modzuozhi.core.dimthread.EntityTickParallel#SECTION_LOCK} 全局写锁保护(本步不触碰), + * 因此实体移动与 section 内容(分片锁)的既有锁序不变。 + *

+ * 用 {@link WrapMethod} 整方法包装,避免 {@code @WrapOperation at HEAD} 对含分支的方法注入失败。 + *

+ * 第5步细粒度化:在 {@code createSection} 中把 section key 写入新创建的 + * {@code EntitySection}(见 {@link ISectionKeyAccessor}),供 {@code EntitySection} 的分片锁定位锁片。 + */ +@Mixin(EntitySectionStorage.class) +public abstract class EntitySectionStorageMixin { + + /** 结构存储:并发的 {@code Long2ObjectMap}(替换原 {@code Long2ObjectOpenHashMap},读路径无锁安全)。 */ + @Shadow + private final Long2ObjectMap> sections = new ConcurrentLong2ObjectMap<>(); + + /** 有序 section key 集:并发的 {@code LongSortedSet}(替换原 {@code LongAVLTreeSet},subSet 弱一致)。 */ + @Shadow + private final LongSortedSet sectionIds = new ConcurrentLongSortedSet(); + + /** + * {@code createSection(long)} 是私有方法,创建 {@code EntitySection} 时把 section key 注入 + * 给新 section,使 {@code EntitySection} 的 add/remove/getEntities 能用分片锁定位锁片。 + */ + @WrapMethod(method = "createSection") + private EntitySection modzuozhi_createSection(long sectionKey, Operation original) { + EntitySection section = original.call(sectionKey); + ((ISectionKeyAccessor) (Object) section).modzuozhi_setSectionKey(sectionKey); + return section; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java new file mode 100644 index 0000000..94822c2 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityTickListMixin.java @@ -0,0 +1,44 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 第4步:实体 tick 并行——子任务线程对 {@code EntityTickList} 的写入延迟执行 + * (借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code EntityTickList} 的 {@code active} 字段在 {@code forEach} 期间被迭代。 + * 并行子任务中实体可能被移除/生成,触发 {@code EntityCallbacks.onTickingEnd/onTickingStart} + * 进而调用 {@code entityTickList.add/remove}。这些写入发生在子任务线程(非迭代线程), + * 直接修改 {@code active} 会破坏并发迭代。因此拦截 add/remove,在子任务上下文中投递到 + * {@link PostExecuteQueue},由维度 worker 在所有实体子任务完成后统一执行。 + *

+ * 非子任务上下文(串行/维度 worker 线程)行为不变,零开销。 + */ +@Mixin(EntityTickList.class) +public abstract class EntityTickListMixin { + + @Inject(method = "add", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferAdd(Entity entity, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> ((EntityTickList) (Object) this).add(entity)); + ci.cancel(); + } + } + + @Inject(method = "remove", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferRemove(Entity entity, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> ((EntityTickList) (Object) this).remove(entity)); + ci.cancel(); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java b/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java new file mode 100644 index 0000000..b4f2356 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/EntityTickParallelMixin.java @@ -0,0 +1,78 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.filter.EntityTickFilter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Consumer; + +/** + * 第4步:实体 tick 并行——拦截 {@code Level.guardEntityTick},把白名单实体的 tick 提交到 + * 实体子任务屏障并行执行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 玩家始终走串行:{@code EntityTickFilter.isParallel} 对玩家恒为 false,因此玩家 + * 在维度 worker 的实体循环里按原版顺序 tick,绝不进入并行批。 + *

+ * 注意:不能再在 worker 上跳过玩家的实体 tick。1.21.1 的 + * {@code PlayerList.tick()} 并不调用 {@code ServerPlayer.tick()}(其只做延迟统计广播), + * 玩家实体 tick 唯一路径就是 {@code ServerLevel.tick} 的实体循环(维度 worker 执行)。 + * 若在这里跳过玩家,{@code ServerPlayer.tick()}(含 {@code AbstractContainerMenu + * .broadcastChanges()}、传送门、状态推进)将从不执行 → 熔炉/漏斗等容器 UI 进度条与 + * 物品数量永不刷新(实测),玩家相关游戏逻辑全部停滞。 + */ +@Mixin(Level.class) +public abstract class EntityTickParallelMixin { + + @Inject(method = "guardEntityTick", at = @At("HEAD"), cancellable = true) + private void modzuozhi_guardEntityTick(Consumer consumer, Entity entity, CallbackInfo ci) { + if (EntityTickParallel.current() == null || !EntityTickFilter.isParallel(entity)) { + // 非并行窗口或非白名单实体(含玩家):走原版串行(guardEntityTick 原文) + return; + } + // 首次启动 / 区块生成高峰:实体所在区块尚未 FULL 时跳过本 tick。 + // 原因:并行子任务线程对未生成区块做碰撞检测会拿到空气占位(见 ServerChunkCacheMixin + // 的非阻塞占位设计,为避免 join 生成死锁),村民会被持续下坠拉穿地面遁地;区块 FULL + // 后下一 tick 恢复正常。此处既不收集并行、也不走串行(串行在 worker 上 join 等生成 + // 会死锁),直接跳过是唯一安全选择。 + if (!modzuozhi_entityChunkReady((ServerLevel) (Object) this, entity)) { + ci.cancel(); + return; + } + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + // 收集到本 tick 的实体批,循环结束统一按块提交并行(减少逐实体提交的调度/屏障开销) + EntityTickParallel.collect(server, consumer, entity); + ci.cancel(); + } + + /** + * 实体所在区块是否已 FULL(线程无关,直接查 ChunkHolder 的 FULL future)。 + * 与 {@link ServerChunkCacheMixin} 的 subtask 缓存同源,不依赖共享 lastChunk 缓存(防撕裂误判)。 + */ + @Unique + private static boolean modzuozhi_entityChunkReady(ServerLevel level, Entity entity) { + long key = ChunkPos.asLong(entity.getBlockX() >> 4, entity.getBlockZ() >> 4); + try { + ServerChunkCache cache = level.getChunkSource(); + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) cache) + .modzuozhi_getVisibleChunkIfPresent(key); + if (holder == null) { + return false; + } + return holder.getFullChunkFuture() + .getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null) != null; + } catch (Throwable t) { + return false; // 保守:拿不到就跳过本 tick + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java b/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java new file mode 100644 index 0000000..cf1d308 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GameEventDispatcherMixin.java @@ -0,0 +1,35 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.gameevent.GameEventListenerRegistry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * 兜底修复 {@code GameEventDispatcher.post} 中 + * {@code chunkaccess.getListenerRegistry(j2)} 返回 null 导致的 NPE。 + *

+ * 背景:细粒度并行子任务线程(或维度 worker)执行实体 tick 时,GlowSquid 等实体的移动会触发 + * {@code GameEventDispatcher.post}。此时读取到的区块对象可能处于"正在生成/加载中"的不一致状态, + * 其 {@code getListenerRegistry(int)} 可能返回 null,随后 {@code visitInRangeListeners(...)} 直接 + * NPE,进而被 {@code FaultGuard} 计为子任务异常并自动降级关闭细粒度并行(生产环境多次复现: + * "…because the return value of …ChunkAccess.getListenerRegistry(int) is null")。 + *

+ * 修复:包装该调用,null 时回退到 {@link GameEventListenerRegistry#NOOP}(空实现,行为等同无监听器), + * 保证任何路径下都不会 NPE,细粒度并行不再因该问题自动降级。 + */ +@Mixin(net.minecraft.world.level.gameevent.GameEventDispatcher.class) +public abstract class GameEventDispatcherMixin { + + @WrapOperation( + method = "post", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/chunk/ChunkAccess;getListenerRegistry(I)Lnet/minecraft/world/level/gameevent/GameEventListenerRegistry;")) + private GameEventListenerRegistry modzuozhi_noopIfNull( + ChunkAccess chunk, int sectionY, Operation original) { + GameEventListenerRegistry registry = original.call(chunk, sectionY); + return registry != null ? registry : GameEventListenerRegistry.NOOP; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java new file mode 100644 index 0000000..ea141aa --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GoToPotentialJobSiteGuardMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.ai.behavior.GoToPotentialJobSite; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.npc.Villager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 跨维度安全:{@link GoToPotentialJobSite} 添加维度检查。 + *

+ * 原版 {@code checkExtraStartConditions}(1.21.1)只检查 {@code Activity.WORK},不做维度检查。 + * 当村民的 {@code POTENTIAL_JOB_SITE} 存了另一个维度的 {@code GlobalPos} 时,村民会启动行为 + * 并尝试在当前维度坐标下寻路到该位置(坐标语义错乱)。并行环境下(维度并行/细粒度并行)也需 + * 确保村民只寻路当前维度的 POI。 + *

+ * 在 {@code checkExtraStartConditions} 头部注入守卫:POTENTIAL_JOB_SITE 维度不匹配时直接返回 + * false,阻止行为启动(不覆盖原版逻辑,仅在维度不匹配时短路)。 + */ +@Mixin(GoToPotentialJobSite.class) +public class GoToPotentialJobSiteGuardMixin { + + @Inject(method = "checkExtraStartConditions(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/npc/Villager;)Z", + at = @At("HEAD"), cancellable = true) + private void modzuozhi_guardDimension( + ServerLevel serverLevel, Villager villager, CallbackInfoReturnable cir + ) { + if (villager.getBrain().getMemory(MemoryModuleType.POTENTIAL_JOB_SITE) + .map(gp -> gp.dimension() != villager.level().dimension()) + .orElse(false)) { + cir.setReturnValue(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java b/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java new file mode 100644 index 0000000..c3d4e45 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/GossipContainerMixin.java @@ -0,0 +1,23 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.entity.ai.gossip.GossipContainer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 流言表并发化(借鉴 Async 思路,独立实现)。 + *

+ * 并行 tick 下,村民之间交换流言({@code transferFrom})或读取声望时会跨线程读写彼此的 + * {@code GossipContainer.gossips}(如判断是否信任某个玩家、村民间声望值)。原版用普通 + * HashMap,跨线程读写存在数据竞争。替换为 {@link ConcurrentHashMap},读写均线程安全。 + */ +@Mixin(GossipContainer.class) +public abstract class GossipContainerMixin { + + @Shadow + private final Map gossips = new ConcurrentHashMap<>(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java b/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java new file mode 100644 index 0000000..57bffb9 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ILevelMixin.java @@ -0,0 +1,19 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.IMutableMainThread; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * 让 {@link Level} 实现 {@link IMutableMainThread},暴露其 {@code thread} 字段, + * 供 {@code DimThreadCore.swapThreadsAndRun} 临时迁移主线程引用。 + */ +@Mixin(Level.class) +public interface ILevelMixin extends IMutableMainThread { + @Accessor("thread") + void dimThreads$setMainThread(Thread t); + + @Accessor("thread") + Thread dimThreads$getMainThread(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java new file mode 100644 index 0000000..0af2d08 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelChunkMixin.java @@ -0,0 +1,32 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.gameevent.GameEventListenerRegistry; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 区块游戏事件监听器注册表并发访问加固。 + *

+ * 细粒度并行时,实体 tick 触发 {@code gameEvent → GameEventDispatcher.post → + * getListenerRegistry},子任务线程与维度 worker/服务器线程会并发访问 + * {@code LevelChunk.gameEventListenerRegistrySections}(fastutil + * {@code Int2ObjectOpenHashMap},非线程安全)。并发 {@code computeIfAbsent} 会在 rehash 时 + * 损坏哈希表(实测抛 {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}), + * 导致服务器崩溃。 + *

+ * 修复:把 {@code getListenerRegistry} 整体加 {@code synchronized(this)}(单块锁、逐调用、 + * 无嵌套,无死锁;gameEvent 发布频率低,开销可忽略),串行化该 map 的所有读写入口 + * ({@code GameEventDispatcher} 与 {@code LevelChunk.add/removeGameEventListener} 都走此方法)。 + */ +@Mixin(LevelChunk.class) +public abstract class LevelChunkMixin { + + @WrapMethod(method = "getListenerRegistry") + private GameEventListenerRegistry modzuozhi_getListenerRegistry(int sectionY, Operation original) { + synchronized (this) { + return original.call(sectionY); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java new file mode 100644 index 0000000..e6351ab --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelExplodeMixin.java @@ -0,0 +1,49 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.ExplosionDamageCalculator; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +/** + * 爆炸全链路串行化(借鉴 Async 思路,独立实现)。 + *

+ * 爆炸会批量写入区块方块状态({@code Explosion.explode} → {@code Level.setBlock} 写 + * 非线程安全的 {@code PalettedContainer})。并行环境下,若两个爆炸并发执行(或多个爆炸实体/ + * TNT 方块/指令/下界床等不同触发源同时引爆),会并发写同一/相邻区块 → 方块状态撕裂损坏 + * (放置方块消失、被破坏方块"复活")。爆炸实体已由 {@code EntityTickFilter} 黑名单强制串行, + * 但爆炸还有 TNT 方块、下界床/重生锚、指令等其它触发源,需在方法入口兜底。 + *

+ * 原理:{@code Level} 上所有 {@code explode(...)} 重载与 {@code ServerLevel} 的覆写最终都 + * 委托到 13 参重载(唯一真正创建 {@code Explosion} 并执行 {@code explode()}+ + * {@code finalizeExplosion()} 的方法)。因此只需给该终端重载加一把静态全局锁,所有 + * 触发源的爆炸便全部互斥串行。爆炸低频、锁竞争可忽略;该方法不递归调用 + * {@code Level.explode},静态单锁无锁序环,无死锁风险。 + */ +@Mixin(Level.class) +public abstract class LevelExplodeMixin { + + /** 全局爆炸互斥锁(跨维度/跨触发源统一串行)。 */ + @Unique + private static final Object MODZUOZHI_EXPLODE_LOCK = new Object(); + + @WrapMethod(method = "explode(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/world/damagesource/DamageSource;Lnet/minecraft/world/level/ExplosionDamageCalculator;DDDFZLnet/minecraft/world/level/Level$ExplosionInteraction;ZLnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/particles/ParticleOptions;Lnet/minecraft/core/Holder;)Lnet/minecraft/world/level/Explosion;") + private Explosion modzuozhi_explode( + Entity source, DamageSource damageSource, ExplosionDamageCalculator damageCalculator, + double x, double y, double z, float radius, boolean fire, Level.ExplosionInteraction interaction, + boolean spawnParticles, ParticleOptions smallExplosionParticles, ParticleOptions largeExplosionParticles, + Holder explosionSound, Operation original) { + synchronized (MODZUOZHI_EXPLODE_LOCK) { + return original.call(source, damageSource, damageCalculator, x, y, z, radius, fire, interaction, + spawnParticles, smallExplosionParticles, largeExplosionParticles, explosionSound); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java new file mode 100644 index 0000000..03e7add --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelGetChunkMixin.java @@ -0,0 +1,40 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ImposterProtoChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 让 {@code Level.getChunk(int, int)} 对「区块未就绪」场景健壮。 + *

+ * 原版 {@code Level.getChunk(int, int)} 直接 {@code (LevelChunk) this.getChunk(x, z, FULL)}。 + * 服务端主线程下,若目标区块尚未 FULL,会先同步完成生成再返回真实 {@code LevelChunk},强转不会失败。 + *

+ * 但细粒度并行 / 维度并行下,子任务线程(伪装成主线程走 {@code ServerChunkCache} 主线程路径)请求 + * 一个正被主线程生成的相邻区块时,返回的是 {@code ImposterProtoChunk}(世界生成占位块, + * {@code extends ProtoChunk},并非 {@code LevelChunk} 子类),强转失败抛 + * {@code ClassCastException}(例如草/泥土蔓延 {@code SpreadingSnowyDirtBlock.randomTick} 通过 + * {@code getBlockState} 访问相邻未就绪区块时)。 + *

+ * 这里在强转前,若结果是 {@code ImposterProtoChunk},则取其 {@code getWrapped()}(其内部委托的真实 + * {@code LevelChunk},其 {@code getBlockState}/{@code getSections} 均转发到 wrapped),既避免崩溃, + * 语义也与原版一致。单线程场景该分支从不触发,是空操作。 + */ +@Mixin(Level.class) +public abstract class LevelGetChunkMixin { + @Redirect(method = "getChunk(II)Lnet/minecraft/world/level/chunk/LevelChunk;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/Level;getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;)Lnet/minecraft/world/level/chunk/ChunkAccess;")) + private ChunkAccess modzuozhi_getRealChunk(Level level, int x, int z, ChunkStatus status) { + ChunkAccess chunk = level.getChunk(x, z, status); + if (chunk instanceof ImposterProtoChunk imposter) { + return imposter.getWrapped(); + } + return chunk; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelMixin.java new file mode 100644 index 0000000..9f1ce60 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelMixin.java @@ -0,0 +1,56 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * dimthreads 式 {@code Level.getBlockEntity} 线程守卫伪装。 + *

+ * 原版 {@code Level.getBlockEntity} 有线程守卫:服务端且当前线程 != 世界构造时捕获的 + * {@code thread} 时返回 null。维度并行下,整个 {@code ServerLevel.tick} 运行在池中 worker 上, + * 该守卫会把 worker 的一切 {@code getBlockEntity} 返回 null,导致漏斗/箱子合并/比较器等 + * 依赖 {@code getBlockEntity} 的逻辑失效(漏斗 NPE 等)。 + *

+ * 在 {@code swapThreadsAndRun} 期间本维度的 {@code thread} 已被设为当前 worker,守卫本应通过; + * 这里额外处理「另一维度 worker 访问本维度方块实体」的情况:当调用者(t)与属主(this.thread) + * 都是维度 worker 时,把当前线程伪装成属主线程,使守卫通过(属主 worker 此刻 tick 本维度, + * 是唯一写入者,读方直接同步读取安全)。 + */ +@Mixin(Level.class) +public abstract class LevelMixin { + @Shadow + public Thread thread; + + /** + * 将 {@code Level.random}(及 {@code randValue})初始化为线程安全随机源。 + *

+ * 原版 {@code Level.random = RandomSource.create()} 得到 {@code LegacyRandomSource}, + * 其内部通过 {@code ThreadingDetector} 检测跨线程访问并抛异常。维度并行/细粒度并行下, + * 实体 AI(村民、群游鱼等)与服务器主线程会并发访问 {@code getRandom()},导致 + * "Accessing LegacyRandomSource from multiple threads"。这里重定向构造期间的 + * {@code RandomSource.create()} 为 {@code createThreadSafe()}({@code ThreadSafeLegacyRandomSource}, + * 基于 AtomicLong CAS,无线程检测),使并发访问安全。 + */ + @Redirect(method = "", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/RandomSource;create()Lnet/minecraft/util/RandomSource;")) + private static RandomSource modzuozhi_threadSafeRandom() { + return RandomSource.createThreadSafe(); + } + + @Redirect(method = "getBlockEntity", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + private Thread modzuozhi_recallForDimthreads() { + Thread t = Thread.currentThread(); + // 维度 worker 访问本维度方块实体时,伪装成属主线程,使守卫通过; + // 细粒度子任务线程(subtask)同样需要伪装,否则 getBlockEntity 返回 null + boolean callerIsActive = DimThreadCore.owns(t) || FineGrainScheduler.inSubTask(); + boolean ownerIsActive = DimThreadCore.owns(this.thread); + return callerIsActive && ownerIsActive ? this.thread : t; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java b/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java new file mode 100644 index 0000000..662e3e1 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LevelTicksMixin.java @@ -0,0 +1,47 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import net.minecraft.core.BlockPos; +import net.minecraft.world.ticks.LevelTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第6步:区块调度({@code LevelTicks})并发安全——并行子任务中的 tick 调度写入延迟到 + * 维度 worker 收口串行执行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 时序背景:{@code ServerLevel.tick} 中 {@code blockTicks.tick / fluidTicks.tick}(收集并执行 + * 计划内 tick)与 {@code runBlockEvents} 都运行在维度 worker 线程(串行阶段), + * 不在细粒度并行窗口内,天然线程安全。 + *

+ * 风险点:并行子任务(方块实体 tick、区块随机 tick)里某些方块/方块实体会调用 + * {@code level.scheduleTick(...)} → {@code LevelTicks.schedule},多个子任务线程并发写 + * {@code LevelTicks} 内部的 {@code Long2ObjectOpenHashMap}/{@code PriorityQueue}(非线程安全), + * 会损坏调度结构,导致计划内 tick 丢失、方块行为异常。 + *

+ * 修复:在并行子任务上下文({@link FineGrainScheduler#inSubTask()})中,把 {@code schedule} + * 投递到 {@link PostExecuteQueue},由维度 worker 在本维度 tick 收口处(所有并行子任务已 + * {@code await} 完成、无并发)串行执行。由于收口发生在 {@code blockTicks.tick} 之后,新增 + * 调度会排到下个 tick执行,与原版"当前 tick 之后"语义一致。关闭并行时零开销(串行)。 + *

+ * 说明:读方法({@code hasScheduledTick}/{@code willTickThisTick})在子任务中几乎不被调用, + * 且读多写少、正确性可容忍偶发旧值,不做延迟(保持原样)。 + */ +@Mixin(LevelTicks.class) +public abstract class LevelTicksMixin { + + @WrapMethod(method = "schedule") + private void modzuozhi_deferSchedule(ScheduledTick tick, Operation original) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue == null) { + // 非并行子任务上下文:原版直接调度 + original.call(tick); + return; + } + // 并行子任务:延迟到维度 worker 收口串行调度,避免并发写 LevelTicks 内部结构 + queue.post(() -> original.call(tick)); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java b/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java new file mode 100644 index 0000000..83a6db8 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/LivingEntityGetInBlockStateMixin.java @@ -0,0 +1,29 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 防御:{@code Entity.getInBlockState()} 空值兜底。 + *

+ * 罕见 NPE 根因:并行子任务线程上,{@code getInBlockState()} 读取实体所在位置方块状态时 + * 偶发返回 {@code null}(区块读路径的边缘竞态),随后该值被传给 + * {@code CommonHooks.isLivingOnLadder(state, ...)} 并直接 {@code state.isLadder(...)} → NPE + * (日志:[FineGrain] 子任务异常 ... because "state" is null,50 分钟约 1 次,被屏障捕获不降级)。 + *

+ * 注意:{@code getInBlockState()} 定义在 {@link Entity}(父类)而非 {@code LivingEntity}, + * 因此 {@code @WrapMethod} 必须目标 {@code Entity.class}(mixin 只包装目标类声明的方法)。 + * 这里对返回值做空值兜底(null → 空气方块),从根源上杜绝该路径的 NPE;正常情况零改动。 + */ +@Mixin(Entity.class) +public abstract class LivingEntityGetInBlockStateMixin { + @WrapMethod(method = "getInBlockState") + private BlockState modzuozhi_nullSafeInBlockState(Operation original) { + BlockState state = original.call(); + return state == null ? Blocks.AIR.defaultBlockState() : state; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java new file mode 100644 index 0000000..2b71381 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerDimMixin.java @@ -0,0 +1,88 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.TpsTracker; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.function.BooleanSupplier; + +/** + * 维度独立 tick 循环分发(替代原「有界屏障 awaitCompletion」模型,借鉴 Chlorophyll 思路)。 + *

+ * 目标:每个 {@link ServerLevel} 一个常驻 {@code LevelTickLoop},各自按 tick 预算 + * 独立排下一次 tick。某个维度 tick 慢(低 TPS)只会推迟它自己的下一次 tick,不再由主线程 + * 统一 {@code awaitCompletion()} 拖累其它维度,从而实现维度隔离。 + *

+ * 主线程 {@code tickChildren} 退化为「协调者」:命令函数、基础连接(登录/状态/配置)、 + * {@code playerList.tick()}、tickables 仍留在主线程;每个维度的「level.tick + 玩家连接 tick + * + 区块发送/刷新」由该维度 loop 在 worker 线程独立完成(见 {@code ServerConnectionListenerMixin} + * 跳过游戏连接、{@code LevelTickLoop.tickConnections} 承接游戏连接 tick,杜绝玩家双 tick)。 + *

+ * 全局关闭或未激活时,原方法体照常执行(退回原版串行,零副作用)。 + */ +@Mixin(value = MinecraftServer.class, priority = 1010) +public abstract class MinecraftServerDimMixin { + + @Shadow + @Final + private List tickables; + + /** + * 主线程每 tick 的协调入口:只做全局部分 + 启动各维度 loop。 + * 命中了维度循环模型的场景才 {@code cancel()} 原方法体。 + */ + @Inject(method = "tickChildren", at = @At("HEAD"), cancellable = true) + private void modzuozhi_worldizedTickChildren(BooleanSupplier hasTimeLeft, CallbackInfo ci) { + // 无论开关状态,每 tick 记录 TPS(供 /dimensionalripper status 对比优化效果) + TpsTracker.tick(); + + MinecraftServer self = (MinecraftServer) (Object) this; + if (!DimThreadCore.MANAGER.isActive(self)) { + return; // 未开启:走原版串行 tickChildren + } + + // 1. 命令函数(主线程全局) + self.getFunctions().tick(); + + // 2. 启动每个维度的独立 loop(只启动一次,loop 此后自排程) + for (ServerLevel level : self.getAllLevels()) { + DimThreadCore.MANAGER.getLoop(level).scheduleIfNeeded(); + } + + // 3. 跨维度传送收口(loop 线程登记的传送,由主线程执行,见 EntityChangeDimensionMixin) + DimensionChangeQueue.flush(); + + // 4. 基础连接(登录/状态/配置)——游戏连接已由维度 loop tick(ServerConnectionListenerMixin) + self.getConnection().tick(); + + // 4.5 连接回收:登录/跨维度/重生后,把每个在线玩家的连接路由到其当前维度 loop(幂等) + DimThreadCore.MANAGER.reconcileConnections(self); + + // 5. playerList.tick(统计/延迟,主线程) + self.getPlayerList().tick(); + + // 6. tickables(服务器 GUI 刷新等) + for (Runnable tickable : this.tickables) { + tickable.run(); + } + + ci.cancel(); + } + + /** 服务器停止时:先停维度 loop,再关闭线程池,防止挂起。 */ + @Inject(method = "stopServer", at = @At("HEAD")) + public void modzuozhi_shutdownThreadpool(CallbackInfo ci) { + DimThreadCore.MANAGER.killAllLoops(); + DimThreadCore.MANAGER.threadPools.forEach((server, pool) -> pool.shutdown()); + DimThreadCore.MANAGER.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java new file mode 100644 index 0000000..22adaf4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerSaveMixin.java @@ -0,0 +1,110 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FaultGuard; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.ThreadPool; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.storage.LevelStorageSource; +import net.minecraft.world.level.storage.ServerLevelData; +import net.minecraft.world.level.storage.WorldData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 多维度并行保存(dimthreads 式)。 + *

+ * 原版 {@code MinecraftServer.saveAllChunks} 在 for 循环里串行遍历所有维度, + * 逐个调用 {@code serverlevel.save(...)} 写区块/实体。当维度多且区块实体量大时,保存耗时线性叠加, + * 主线程被同步写盘阻塞数秒,表现为 "Can't keep up"。 + *

+ * 本 mixin 用 {@code @Inject HEAD + cancellable} 完整接管 {@code saveAllChunks}: + *

    + *
  1. 把每个维度的 {@code serverlevel.save(...)} 分发到线程池并行执行(各维度目录独立,写盘互不干扰,安全);
  2. + *
  3. {@code awaitCompletion()} 等待全部维度写完;
  4. + *
  5. 主线程串行补齐原方法下半段的全局保存(worldData / boss events / level.dat 标签), + * 这些共享状态不能并行。
  6. + *
+ * 维度越多、区块实体越多,加速越明显。 + */ +@Mixin(value = MinecraftServer.class, priority = 1010) +public abstract class MinecraftServerSaveMixin { + @Shadow + @Final + protected WorldData worldData; + @Shadow + @Final + protected LevelStorageSource.LevelStorageAccess storageSource; + + @Inject(method = "saveAllChunks", at = @At("HEAD"), cancellable = true) + public void modzuozhi_parallelSaveAllChunks(boolean suppressLog, boolean flush, boolean forced, + CallbackInfoReturnable cir) { + MinecraftServer self = (MinecraftServer) (Object) this; + if (!DimThreadCore.MANAGER.isActive(self)) { + return; // 未开启:走原版串行路径 + } + + // 保存前先排空细粒度子任务,避免残留子任务与实体/区块保存并发访问(残留任务此刻 + // 若卡死,Barrier 超时降级已在维度 loop 侧触发,这里再给一个短等待兜底)。 + FineGrainScheduler.awaitIdle(2000L); + + // 1. 并行保存每个维度的区块/实体(独立写盘) + List levels = new ArrayList<>(); + self.getAllLevels().forEach(levels::add); + + ThreadPool pool = DimThreadCore.getThreadPool(self); + AtomicReference crash = new AtomicReference<>(); + + for (ServerLevel level : levels) { + pool.execute(() -> { + DimThreadCore.attach(Thread.currentThread(), level); + DimThreadCore.swapThreadsAndRun(() -> { + try { + // 等价于原版 serverlevel.save(null, flush, noSave) + level.save(null, flush, level.noSave && !forced); + } catch (Throwable t) { + crash.set(t); + } + }, level, level.getChunkSource()); + }); + } + // 带超时等待:某维度保存任务若被卡死的线程持锁阻塞,绝不永久挂起主线程(否则 + // 表现为「正在保存中」死锁无法退出)。 + if (!pool.awaitCompletion(FaultGuard.AWAIT_TIMEOUT_MS)) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存超时(>{}ms),跳过等待继续主线程全局保存", FaultGuard.AWAIT_TIMEOUT_MS); + } + + Throwable t = crash.get(); + if (t != null) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存维度异常", t); + } + + // 2. 主线程串行补齐全局保存(与原版 saveAllChunks 下半段完全一致) + ServerLevel overworld = self.overworld(); + ServerLevelData serverleveldata = this.worldData.overworldData(); + serverleveldata.setWorldBorder(overworld.getWorldBorder().createSettings()); + this.worldData.setCustomBossEvents(self.getCustomBossEvents().save(self.registryAccess())); + this.storageSource.saveDataTag(self.registryAccess(), this.worldData, + self.getPlayerList().getSingleplayerData()); + + if (flush) { + for (ServerLevel serverlevel : levels) { + ModZuozhi.LOGGER.info("ThreadedAnvilChunkStorage ({}): All chunks are saved", + serverlevel.getChunkSource().chunkMap.getStorageName()); + } + ModZuozhi.LOGGER.info("ThreadedAnvilChunkStorage: All dimensions are saved"); + } + + cir.setReturnValue(true); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java b/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java new file mode 100644 index 0000000..1135696 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/MinecraftServerTimeSyncAccessor.java @@ -0,0 +1,16 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * 暴露原版私有的 {@code MinecraftServer.synchronizeTime(ServerLevel)},供维度 loop 在每次 + * 时间同步(每 20 tick)时用原版逻辑(含 NeoForge 自定义时间载荷)广播时间包。 + */ +@Mixin(MinecraftServer.class) +public interface MinecraftServerTimeSyncAccessor { + @Invoker("synchronizeTime") + void modzuozhi$invokeSynchronizeTime(ServerLevel level); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java b/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java new file mode 100644 index 0000000..5c0a2ba --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/NavigatingMobsMixin.java @@ -0,0 +1,34 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Mob; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 第7步:寻路/AI 并发安全——{@code ServerLevel.navigatingMobs} 并发化(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * {@code navigatingMobs} 记录当前正在寻路的 {@link Mob},原为 {@code ObjectOpenHashSet}(非线程安全): + *

    + *
  • 写入:{@code onTrackingStart/onTrackingEnd}(实体追踪变化时 {@code add}/{@code remove}), + * 可能来自并行实体子任务线程(实体 tick 触发追踪变化)、维度 worker、区块加载线程;
  • + *
  • 遍历:{@code sendBlockUpdated}(方块变化时)遍历所有 mob 的 navigation 判断是否重算路径, + * 同样可能被任意线程触发。
  • + *
+ * 两者并发会抛 {@code ConcurrentModificationException} 或破坏集合,导致寻路异常/崩溃。 + *

+ * 修复:替换为 {@link ConcurrentHashMap#newKeySet()} 背书的并发集合(线程安全、迭代弱一致、 + * 不抛 CME)。原版串行行为不变,零语义影响。 + */ +@Mixin(ServerLevel.class) +public abstract class NavigatingMobsMixin { + @Shadow + @Final + @Mutable + private Set navigatingMobs = ConcurrentHashMap.newKeySet(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java b/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java new file mode 100644 index 0000000..1c4a0af --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/NearestLivingEntitySensorMixin.java @@ -0,0 +1,72 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.sensing.NearestLivingEntitySensor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * 第10步:{@code NearestLivingEntitySensor} 排序快照化,消除并发 TimSort 异常。 + *

+ * 原版 {@code doTick} 里 {@code list.sort(Comparator.comparingDouble(entity::distanceToSqr))} + * 的比较器会实时读取被排序实体的可变坐标。并行 tick 下,其它子任务线程正同时移动这些 + * 实体,导致比较器前后不一致(破坏传递性)→ {@code TimSort} 抛 + * {@code IllegalArgumentException: Comparison method violates its general contract}(日志已证实)。 + *

+ * 修复:排序前先对每个实体快照一次到锚点(被 tick 实体)的距离,按快照距离稳定排序并 + * 回写——比较器全程一致,既保留"按距离排序"语义,又不触发 TimSort 异常。 + *

+ * 线程安全:每个 {@code NearestLivingEntitySensor} 实例属于单个实体的大脑,仅被该实体自己的 + * 子任务线程调用,字段无跨线程竞争。 + */ +@Mixin(NearestLivingEntitySensor.class) +public abstract class NearestLivingEntitySensorMixin { + /** 当前 tick 的锚点(被 tick 的实体)。 */ + @Unique + private LivingEntity modzuozhi_anchor; + + @Inject(method = "doTick", at = @At("HEAD")) + private void modzuozhi_captureAnchor(ServerLevel level, LivingEntity entity, CallbackInfo ci) { + this.modzuozhi_anchor = entity; + } + + @Redirect(method = "doTick", + at = @At(value = "INVOKE", target = "Ljava/util/List;sort(Ljava/util/Comparator;)V")) + private void modzuozhi_snapshotSort(List list, Comparator comparator) { + LivingEntity anchor = this.modzuozhi_anchor; + if (anchor == null) { + // 兜底:正常不会触发 + list.sort(comparator); + this.modzuozhi_anchor = null; + return; + } + int n = list.size(); + double[] dist = new double[n]; + for (int i = 0; i < n; i++) { + dist[i] = list.get(i).distanceToSqr(anchor); + } + Integer[] order = new Integer[n]; + for (int i = 0; i < n; i++) { + order[i] = i; + } + // 快照距离固定 → 比较器一致 → 不会抛 TimSort 异常(稳定排序,等距保持原顺序) + Arrays.sort(order, (a, b) -> Double.compare(dist[a], dist[b])); + List sorted = new ArrayList<>(n); + for (int index : order) { + sorted.add(list.get(index)); + } + list.clear(); + list.addAll(sorted); + this.modzuozhi_anchor = null; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java b/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java new file mode 100644 index 0000000..29da5d1 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PacketUtilsDimMixin.java @@ -0,0 +1,111 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.IPlayerPacketQueue; +import dev.modzuozhi.core.dimthread.PlayerPacketQueue; +import net.minecraft.ReportedException; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketUtils; +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; +import net.minecraft.network.protocol.game.ServerboundChatCommandSignedPacket; +import net.minecraft.network.protocol.game.ServerboundChatPacket; +import net.minecraft.server.RunningOnDifferentThreadException; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.thread.BlockableEventLoop; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 第2步:网络包按玩家分派到维度线程——包处理入口重定向(1.21.1 API 适配版)。 + *

+ * 1.21.1 的 {@code PacketUtils.ensureRunningOnSameThread} 有 3 参数重载: + * {@code (Packet, T, ServerLevel)} 和 {@code (Packet, T, BlockableEventLoop)}。 + * 真实包链路(1.21.1):Netty IO 线程 {@code Connection.channelRead0 → genericsFtw → + * packet.handle(listener)} 直接调用 handle;而 {@code ServerGamePacketListenerImpl} 几乎 + * 每个 {@code handleXxx} 方法开头都会调 {@code ensureRunningOnSameThread(ServerLevel)}, + * 非主线程时投递到主线程队列并抛 {@code RunningOnDifferentThreadException}(由 + * {@code genericsFtw} 吞掉),主线程队列执行时才真正跑 handle 逻辑。 + *

+ * 借鉴 Chlorophyll(Unlicense 1.21.10 Fabric)的 {@code PacketUtilsMixin} 思路,但适配 + * 1.21.1 NeoForge 签名:全局开启且是游戏包时,把 handle 任务投递到该玩家所在维度的 + * {@code PlayerPacketQueue}(替代投主线程队列),由维度 worker 在 tick 收口处执行。 + * 登录/配置/状态包仍走原版 {@code BlockableEventLoop} 重载。 + *

+ * 防无限循环(必须):包投到维度队列后,worker drain 执行 {@code packet.handle}, + * handle 开头会再次调本方法。因此用 {@link PlayerPacketQueue#isExecuting()} 标记(drain 中 + * 设置)区分两种状态: + *

    + *
  • Netty 线程首次调度(未在展开中、非主线程)→ 投玩家维度队列 + 抛异常;
  • + *
  • 维度 worker / 主线程 drain 中执行 handle({@code isExecuting()} 或主线程)→ + * 直接返回,让 handle 继续执行,避免包被无限投回队列。
  • + *
+ *

+ * 死锁评估:{@code ServerGamePacketListenerImpl} 无 {@code executeBlocking};唯一的 + * {@code ServerCommonPacketListenerImpl.disconnect} 的 {@code executeBlocking} 已由 + * {@code ServerCommonPacketListenerDimMixin} 异步化,worker 不阻塞。 + */ +@Mixin(PacketUtils.class) +public abstract class PacketUtilsDimMixin { + + @Shadow + @Final + private static Logger LOGGER; + + @Shadow + public static ReportedException makeReportedException(Exception exception, Packet packet, T packetListener) { + throw new AssertionError(); + } + + /** + * @reason 把游戏包处理按玩家分派到其所在维度线程(第2步) + */ + @Overwrite + public static void ensureRunningOnSameThread( + Packet packet, T packetListener, ServerLevel serverLevel + ) throws RunningOnDifferentThreadException { + if (packetListener instanceof ServerGamePacketListenerImpl && ModZuozhi.isEnabled()) { + // 命令/聊天包改走原版主线程(BlockableEventLoop): + // 维度 loop 一旦被实体/区块 tick 阻塞,命令会跟着延迟 1-2 分钟才执行;而主线程只做 + // 协调(不 tick 实体),能保持约 50ms 一拍,命令可秒响应。聊天广播是全局的,卡顿维度 + // 的聊天包被拖住会连累其它维度的玩家延迟收到消息,故聊天/命令都走主线程。 + boolean routeMainThread = packet instanceof ServerboundChatCommandPacket + || packet instanceof ServerboundChatCommandSignedPacket + || packet instanceof ServerboundChatPacket; + if (!routeMainThread) { + // 两种情况不拦截,直接让 handle 继续执行(等价原版"已在正确线程"): + // 1. PlayerPacketQueue.drain() 正在执行包任务(维度 worker / 主线程串行) + // 2. 当前已在主线程(原版主线程队列执行路径) + if (PlayerPacketQueue.isExecuting() || serverLevel.getServer().isSameThread()) { + return; + } + // 首次调度(Netty IO 线程):投递到玩家所在维度的队列,由该维度线程在 tick 收口执行 + ServerLevel playerLevel = (ServerLevel) ((ServerGamePacketListenerImpl) packetListener).player.level(); + ((IPlayerPacketQueue) playerLevel).modzuozhi$playerPacketQueue().offer(() -> { + if (packetListener.shouldHandleMessage(packet)) { + try { + packet.handle(packetListener); + } catch (Exception e) { + if (e instanceof ReportedException reportedException + && reportedException.getCause() instanceof OutOfMemoryError) { + throw makeReportedException(e, packet, packetListener); + } + packetListener.onPacketError(packet, e); + } + } else { + LOGGER.debug("Ignoring packet due to disconnection: {}", packet); + } + }); + throw RunningOnDifferentThreadException.RUNNING_ON_DIFFERENT_THREAD; + } + // routeMainThread == true:落到下方走原版 BlockableEventLoop(主线程)路径 + } + + // 登录/配置/状态包,或全局关闭:走原版 BlockableEventLoop 重载 + PacketUtils.ensureRunningOnSameThread(packet, packetListener, (BlockableEventLoop) serverLevel.getServer()); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java b/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java new file mode 100644 index 0000000..32c4960 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PlayerListSaveMixin.java @@ -0,0 +1,95 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FaultGuard; +import dev.modzuozhi.core.dimthread.ThreadPool; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.PlayerList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 第3步:自动保存分布化——玩家保存按维度分发到各维度线程并行执行。 + *

+ * 原版 {@code PlayerList.saveAll()} 在主线程串行遍历所有玩家,逐个 + * {@code playerIo.save(player)} 写玩家数据(+ 统计 + 进度)。玩家多时主线程被同步写盘 + * 阻塞数秒。玩家数据按 UUID 写独立文件,维度间、玩家间均无共享写盘目标,可安全并行。 + *

+ * 本 mixin 用 {@code @Inject HEAD + cancellable} 接管 {@code saveAll}(维度并行激活时): + *

    + *
  1. 把在线玩家按所在维度分组;
  2. + *
  3. 每个维度在对应维度 worker 上串行保存该维度玩家(维度内保序、维度间并行);
  4. + *
  5. {@code awaitCompletion()} 等待全部维度写完。
  6. + *
+ * 与 {@code MinecraftServerSaveMixin}(维度区块/实体保存已并行化)配合,自动保存的 + * savePlayers + saveLevel 都落在各维度线程上,主线程不再亲自执行保存逻辑。 + *

+ * 关服时 {@code MinecraftServerDimMixin.stopServer} 已先 shutdown 线程池并 clear,本 mixin + * 经 {@code DimThreadCore.MANAGER.isActive} 判定失效,自动退回原版串行(安全)。 + */ +@Mixin(PlayerList.class) +public abstract class PlayerListSaveMixin { + + @Shadow + protected abstract void save(ServerPlayer player); + + @Inject(method = "saveAll", at = @At("HEAD"), cancellable = true) + private void modzuozhi_parallelSaveAll(CallbackInfo ci) { + PlayerList self = (PlayerList) (Object) this; + MinecraftServer server = self.getServer(); + if (!DimThreadCore.MANAGER.isActive(server)) { + return; // 未开启 / 关服收尾:走原版串行 + } + + // 按玩家所在维度分组(等价 save() 内部的 connection==null 提前返回) + Map> byDimension = new LinkedHashMap<>(); + for (ServerPlayer player : self.getPlayers()) { + if (player.connection == null) { + continue; + } + byDimension.computeIfAbsent(player.serverLevel(), key -> new ArrayList<>()).add(player); + } + if (byDimension.isEmpty()) { + return; // 无在线玩家:直接走原版(saveAll 空循环,无副作用) + } + + ThreadPool pool = DimThreadCore.getThreadPool(server); + AtomicReference crash = new AtomicReference<>(); + + byDimension.forEach((level, players) -> pool.execute(() -> { + DimThreadCore.attach(Thread.currentThread(), level); + DimThreadCore.swapThreadsAndRun(() -> { + try { + for (ServerPlayer player : players) { + this.save(player); + } + } catch (Throwable t) { + crash.set(t); + } + }, level); + })); + + if (!pool.awaitCompletion(FaultGuard.AWAIT_TIMEOUT_MS)) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存玩家超时(>{}ms),跳过等待继续", FaultGuard.AWAIT_TIMEOUT_MS); + } + + Throwable t = crash.get(); + if (t != null) { + ModZuozhi.LOGGER.error("[DimThread] 异步保存玩家异常", t); + } + + ci.cancel(); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java new file mode 100644 index 0000000..59ba56c --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiCompetitorScanGuardMixin.java @@ -0,0 +1,69 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.PoiCompetitorScan; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.Villager; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +/** + * 跨维度安全:{@link PoiCompetitorScan} 添加维度检查。 + *

+ * 原版 {@code PoiCompetitorScan.create()} 不做维度检查,当村民的 {@code JOB_SITE} 存了 + * 另一个维度的 {@code GlobalPos} 时,会跨维度访问 POI 管理器。并行环境下(维度线程/子任务线程) + * 跨维度访问虽已由 POI 数据结构加固保障安全,但 AI 逻辑本身不应跨维度竞争工作站。 + *

+ * 借鉴 Chlorophyll(Unlicense)的 {@code PoiCompetitorScanMixin} 思路,在竞争逻辑前 + * 增加 {@code globalPos.dimension() != villager.level().dimension()} 检查,维度不匹配时 + * 直接跳过(不参与竞争)。 + */ +@Mixin(PoiCompetitorScan.class) +public abstract class PoiCompetitorScanGuardMixin { + + @Shadow + private static boolean competesForSameJobsite(GlobalPos globalPos, Holder holder, Villager villager) { + throw new AssertionError(); + } + + @Shadow + private static Villager selectWinner(Villager villager, Villager villager2) { + throw new AssertionError(); + } + + /** + * @reason 添加维度守卫,防止跨维度竞争工作站 + */ + @Overwrite + public static BehaviorControl create() { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.JOB_SITE), + instance.present(MemoryModuleType.NEAREST_LIVING_ENTITIES) + ).apply(instance, (memoryAccessor, memoryAccessor2) -> + (serverLevel, villager, l) -> { + GlobalPos globalPos = instance.get(memoryAccessor); + // 维度守卫:工作站不在当前维度则不参与竞争 + if (globalPos.dimension() != villager.level().dimension()) { + return true; + } + serverLevel.getPoiManager().getType(globalPos.pos()).ifPresent((holder) -> + instance.get(memoryAccessor2).stream() + .filter((livingEntity) -> livingEntity instanceof Villager && livingEntity != villager) + .map(entity -> (Villager) entity) + .filter(LivingEntity::isAlive) + .filter((villagerx) -> competesForSameJobsite(globalPos, holder, villagerx)) + .reduce(villager, (v1, v2) -> selectWinner(v1, v2)) + ); + return true; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java new file mode 100644 index 0000000..9c4a2af --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiManagerDistanceTrackerMixin.java @@ -0,0 +1,32 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import org.spongepowered.asm.mixin.Mixin; + +/** + * POI 距离追踪器并发访问加固(村民 POI 并发访问加固)。 + *

+ * {@code PoiManager.DistanceTracker}(继承 {@code SectionTracker → DynamicGraphMinFixedPoint → + * LeveledPriorityQueue})内部用普通 {@code LongLinkedOpenHashSet} 队列。细粒度并行下: + *

    + *
  • 服务器线程每 tick 调用 {@code PoiManager.tick → distanceTracker.runAllUpdates()};
  • + *
  • 子任务线程的村民传感器调用 {@code PoiManager.sectionsToVillage → distanceTracker.runAllUpdates()}。
  • + *
+ * 两路并发改写同一队列,fastutil 哈希表 rehash 时损坏(实测抛 + * {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}),导致崩溃。 + *

+ * 修复:把 {@code runAllUpdates}(两路唯一入口,内部完成全部队列处理)加 + * {@code synchronized(this)},单对象锁、逐调用、无嵌套,无死锁;开销集中在有更新时的 + * 队列处理阶段,可忽略。 + */ +@Mixin(targets = "net.minecraft.world.entity.ai.village.poi.PoiManager$DistanceTracker") +public abstract class PoiManagerDistanceTrackerMixin { + + @WrapMethod(method = "runAllUpdates") + private void modzuozhi_runAllUpdates(Operation original) { + synchronized (this) { + original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java b/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java new file mode 100644 index 0000000..64666eb --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PoiSectionMixin.java @@ -0,0 +1,36 @@ +package dev.modzuozhi.mixin; + +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMaps; +import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.ai.village.poi.PoiRecord; +import net.minecraft.world.entity.ai.village.poi.PoiSection; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Map; +import java.util.Set; + +/** + * POI 区块内部表线程安全化(村民 POI 并发访问加固)。 + *

+ * 并行 tick 下,村民传感器(找床/找工作点)在子任务线程遍历 {@code PoiSection} 的 + * {@code records}/{@code byType},而方块变化会在 worker 线程触发 {@code add/remove} 修改同一 + * 结构。普通 HashMap/fastutil 哈希表并发读写存在数据竞争(迭代时被修改 → CME 或撕裂读)。 + *

+ * 修复:{@code records} 换成 {@link Short2ObjectMaps#synchronize 同步包装}, + * {@code byType} 换成 {@link ConcurrentHashMap}(弱一致迭代安全)。 + */ +@Mixin(PoiSection.class) +public abstract class PoiSectionMixin { + + @Shadow + private final Short2ObjectMap records = + Short2ObjectMaps.synchronize(new Short2ObjectOpenHashMap<>()); + + @Shadow + private final Map, Set> byType = Maps.newConcurrentMap(); +} diff --git a/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java b/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java new file mode 100644 index 0000000..918e69f --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/PortalProcessorMixin.java @@ -0,0 +1,247 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.PortalProcessor; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.NetherPortalBlock; +import net.minecraft.world.level.block.Portal; +import net.minecraft.world.level.dimension.DimensionType; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 通用传送门延后修复(覆盖原版下界门与模组传送门,如暮色森林)。 + *

+ * 根因:{@code Entity.handlePortal → PortalProcessor.getPortalDestination → + * portal.getPortalDestination} 在维度 worker 线程(实体 tick 上下文) + * 执行,会对目标维度区块做大量读写。目标区块尚未 FULL 加载时: + *

    + *
  • 下界门({@link NetherPortalBlock} → {@code PortalForcer}):getBlockState 等 + * required=false 读不到真实地形 → createPortal 失败 → transition 为 null → 传送失效;
  • + *
  • 模组门(如 {@code twilightforest.block.TFPortalBlock} → {@code TFTeleporter}): + * 直接 {@code ServerLevel.getChunk(x,z)}(required=true)→ worker 在同步路径 + * {@code mainThreadProcessor.managedBlock} 上忙等目标区块生成,暮色区块生成慢, + * 表现为卡约一分钟;且首次生成中数据不完整,传送门位置算错 → 被传到虚空。
  • + *
+ *

+ * 修复:在统一入口 {@code PortalProcessor.getPortalDestination} 上,若当前是维度 worker / + * 细粒度子任务线程,且目标维度对应区块尚未 FULL 加载,则先投递 PORTAL ticket 触发目标 + * 维度异步生成,清传送冷却(否则 handlePortal 已设置的 300tick 冷却会阻断重试),并返回 + * null 把传送延后一 tick。玩家持续站在传送门中,portalTime 保持满值, + * 下一 tick 自动重试——目标区块 FULL 后走原版逻辑:getChunk 命中完整缓存(不阻塞、位置 + * 正确),正常创建传送门并传送。全程不阻塞、不忙等、不刷日志,杜绝死锁与虚空。 + *

+ * 目标维度通过 portal 实例推导:下界门 ↔ 主世界/下界;暮色门 ↔ 主世界/暮色(用类名反射, + * 避免编译期依赖模组类,未安装暮色时自然跳过)。 + */ +@Mixin(PortalProcessor.class) +public abstract class PortalProcessorMixin { + + /** 暮色传送门类名(反射判断,避免编译期依赖模组类)。 */ + @Unique + private static final String TF_PORTAL_CLASS = "twilightforest.block.TFPortalBlock"; + /** 暮色维度 key。 */ + @Unique + private static final ResourceKey TF_DIMENSION = + ResourceKey.create(Registries.DIMENSION, ResourceLocation.fromNamespaceAndPath("twilightforest", "twilight_forest")); + + @Shadow + private Portal portal; + + @Shadow + private int portalTime; + + @Shadow + private BlockPos entryPosition; + + /** + * 预加载目标维度区块:玩家站在传送门中、portalTime 累积期间(暮色默认 60tick)每 tick + * 投递 PORTAL ticket,让目标区块提前异步生成。这样玩家 portalTime 满、真正触发 + * {@code getPortalDestination} 时目标区块大概率已 FULL → 直接传送,无需延后等待。 + * ticket 幂等(DistanceManager 按 type+pos+radius 合并),重复投递无害。 + */ + @Inject(method = "processPortalTeleportation", at = @At("HEAD")) + private void modzuozhi_preloadPortalTarget( + ServerLevel level, Entity entity, boolean canUse, CallbackInfoReturnable cir) { + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return; + } + ServerLevel target = modzuozhi_guessTargetLevel(level); + if (target == null) { + return; + } + double scale = DimensionType.getTeleportationScale(level.dimensionType(), target.dimensionType()); + BlockPos targetPos = target.getWorldBorder() + .clampToBounds(entity.getX() * scale, entity.getY(), entity.getZ() * scale); + ServerChunkCache cache = target.getChunkSource(); + int cx = targetPos.getX() >> 4; + int cz = targetPos.getZ() >> 4; + if (modzuozhi_needTargetChunks(cache, cx, cz)) { + cache.addRegionTicket(TicketType.PORTAL, new ChunkPos(cx, cz), modzuozhi_portalRadius(), targetPos); + } + } + + /** + * 包裹 {@code portal.getPortalDestination(...)}(实际执行 PortalForcer / TFTeleporter)。 + *

+ * 放行(目标区块已 FULL)后,暮色 TFTeleporter 在玩家路径(默认进度锁定 + * {@code ENFORCED_PROGRESSION_RULE=true})会走 {@code moveToSafeCoords → + * scanIntoSafeBiomes} 扫描 128 格,读取预加载范围之外的未生成区块 → + * {@code getChunk(required=true)} 抛 {@code IllegalStateException: Chunk not there} → + * 传播到玩家 tick 被 {@code LevelTickLoop.tickConnections} 捕获跳过 → 每 tick 循环 + * "放行→抛异常→跳过" → 玩家永久卡在传送门(生物不查进度走安全路径故能传,丢生物后 + * 目标区域已生成故玩家第二次成功)。 + *

+ * 这里捕获该异常:投递更大范围 PORTAL ticket 预加载 + 清冷却 + portalTime 防衰减 + + * 返回 null 延后一 tick。下一 tick 重试时目标区域渐进生成,最终 TFTeleporter 找到安全 + * 位置完成传送。不掩盖其它真实异常。 + */ + @WrapOperation(method = "getPortalDestination", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/Portal;getPortalDestination(Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/portal/DimensionTransition;")) + private DimensionTransition modzuozhi_wrapPortalDestination( + Portal portal, ServerLevel level, Entity entity, BlockPos pos, Operation original) { + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return original.call(portal, level, entity, pos); + } + try { + return original.call(portal, level, entity, pos); + } catch (Throwable t) { + if (!modzuozhi_isChunkNotReady(t)) { + throw t; // 非区块未就绪异常:照常抛出,不掩盖真实 bug + } + // 目标区块未充分生成(暮色 TFTeleporter 玩家路径 scanIntafeBiomes 读远处区块)。 + // 不做大范围预加载(radius 6 会造成暮色 worker 疯狂生成 → 游戏冻结), + // 由 ServerChunkCacheGetChunkFallbackMixin 对"被请求的单块"投 PORTAL ticket 渐进生成; + // 这里仅清冷却 + portalTime 防衰减 + 延后一 tick,让下一 tick 重试时渐进推进。 + if (modzuozhi_logged.add("chunkMiss@" + entity.getId() + "@" + level.dimension().location())) { + DimThreadCore.LOGGER.info("[PortalProcessor] 目标区块未充分生成(Chunk not there),延后渐进生成: 原因={}", t.getMessage()); + } + entity.setPortalCooldown(0); + this.portalTime = 1_000_000_000; + return null; // 延后,下一 tick 重试(单块渐进生成后最终成功) + } + } + + @Inject(method = "getPortalDestination", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferPortalUntilTargetChunkLoaded( + ServerLevel level, Entity entity, CallbackInfoReturnable cir) { + // 仅维度 worker / 细粒度子任务线程需要延后(主线程/串行可同步生成目标区块,保持原版语义) + if (!DimThreadCore.owns(Thread.currentThread()) && !FineGrainScheduler.inSubTask()) { + return; + } + ServerLevel target = modzuozhi_guessTargetLevel(level); + if (target == null) { + if (modzuozhi_logged.add("noTarget@" + level.dimension().location() + "@" + this.portal.getClass().getName())) { + DimThreadCore.LOGGER.info("[PortalProcessor] 无法确定目标维度,不干预: portal={}, 当前维度={}", + this.portal.getClass().getName(), level.dimension().location()); + } + return; // 无法确定目标维度(未知传送门),不干预 + } + double scale = DimensionType.getTeleportationScale(level.dimensionType(), target.dimensionType()); + BlockPos targetPos = target.getWorldBorder() + .clampToBounds(entity.getX() * scale, entity.getY(), entity.getZ() * scale); + ServerChunkCache cache = target.getChunkSource(); + int cx = targetPos.getX() >> 4; + int cz = targetPos.getZ() >> 4; + // 目标区块(及 5x5 周围区域,暮色 TFTeleporter 会同步 getChunk 5x5)尚未 FULL 加载: + // 投递 PORTAL ticket 触发异步生成,本 tick 延后传送 + if (modzuozhi_needTargetChunks(cache, cx, cz)) { + cache.addRegionTicket(TicketType.PORTAL, new ChunkPos(cx, cz), modzuozhi_portalRadius(), targetPos); + // 关键1:handlePortal 在 getPortalDestination 之前已调用 setPortalCooldown()(300tick 冷却)。 + // 若不清冷却,玩家持续站传送门时 setAsInsidePortal 走冷却分支、不再置 + // insidePortalThisTick=true → processPortalTeleportation 返回 false → getPortalDestination + // 不再重试 → 只能等冷却结束、脱离再进传送门才成功。清冷却后下 tick 保留 portalProcess + // 并重试 getPortalDestination,目标区块 FULL 后立即传送。 + entity.setPortalCooldown(0); + // 关键2:延后期间 handlePortal 与 entityInside 的执行顺序导致 insidePortalThisTick + // 隔 tick 才 true → processPortalTeleportation 每 2 tick 触发、每 2 tick decayTick -4 + // 而 portalTime++ 只 +1 → portalTime 净衰减 → 衰减到 < transitionTime(玩家 60) → + // processPortalTeleportation false → hasExpired → portalProcess 被清除 → 重试彻底中断 + // (玩家第一次进传送门失效的真因;村民 transitionTime=0 恒 >=0 不衰减故能持续重试)。 + // 把 portalTime 拉到安全大值,确保延后期间永不衰减失效,区块 FULL 后必然传送成功。 + this.portalTime = 1_000_000_000; + if (modzuozhi_logged.add("defer@" + entity.getId() + "@" + cx + "," + cz)) { + DimThreadCore.LOGGER.info("[PortalProcessor] 传送门延后: 目标维度={}, 区块=({},{}), 等待目标区块FULL后自动重试", + target.dimension().location(), cx, cz); + } + cir.setReturnValue(null); + } else if (modzuozhi_logged.add("ready@" + entity.getId() + "@" + cx + "," + cz)) { + // 目标区块已 FULL:放行走原逻辑(诊断用,确认延后最终放行) + DimThreadCore.LOGGER.info("[PortalProcessor] 目标区块已就绪放行: 目标维度={}, 区块=({},{}), 走原版传送逻辑", + target.dimension().location(), cx, cz); + } + } + + /** 目标区块 5x5(±2)范围内是否有未 FULL 加载的区块(暮色 TFTeleporter 需同步读 5x5)。 */ + @Unique + private static boolean modzuozhi_needTargetChunks(ServerChunkCache cache, int cx, int cz) { + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + if (cache.getChunkNow(cx + dx, cz + dz) == null) { + return true; + } + } + } + return false; + } + + /** 已打印过的诊断日志(防止刷屏)。 */ + @Unique + private static final java.util.Set modzuozhi_logged = java.util.concurrent.ConcurrentHashMap.newKeySet(); + + /** 依据 portal 实例推导目标维度;未知传送门返回 null(不干预)。 */ + @Unique + private ServerLevel modzuozhi_guessTargetLevel(ServerLevel level) { + MinecraftServer server = level.getServer(); + ResourceKey dim = level.dimension(); + if (this.portal instanceof NetherPortalBlock) { + return server.getLevel(dim == Level.NETHER ? Level.OVERWORLD : Level.NETHER); + } + if (this.portal.getClass().getName().equals(TF_PORTAL_CLASS)) { + return server.getLevel(dim == TF_DIMENSION ? Level.OVERWORLD : TF_DIMENSION); + } + return null; + } + + /** + * 预加载半径:暮色传送门较大(TFTeleporter 玩家路径 scanIntoSafeBiomes 会扫 128 格, + * 覆盖半径约 6 区块),下界门只需中心区域(半径 2)。 + */ + @Unique + private int modzuozhi_portalRadius() { + return this.portal.getClass().getName().equals(TF_PORTAL_CLASS) ? 6 : 2; + } + + /** 是否为"区块未就绪"(Chunk not there when requested)瞬时异常(遍历 cause 链)。 */ + @Unique + private static boolean modzuozhi_isChunkNotReady(Throwable t) { + while (t != null) { + if (t instanceof IllegalStateException + && t.getMessage() != null + && t.getMessage().contains("Chunk not there when requested")) { + return true; + } + t = t.getCause(); + } + return false; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java new file mode 100644 index 0000000..eee6ad6 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/SectionStorageMixin.java @@ -0,0 +1,31 @@ +package dev.modzuozhi.mixin; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMaps; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.world.level.chunk.storage.SectionStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +/** + * POI 区块存储表线程安全化(村民 POI 并发访问加固)。 + *

+ * {@code SectionStorage} 是 {@code PoiManager} 的基类,其内部 {@code storage}( + * {@code Long2ObjectOpenHashMap})在细粒度并行下会被子任务线程(村民传感器查询 POI → + * {@code getOrLoad})与区块加载线程({@code readColumn → storage.put})并发读写。 + * fastutil 的普通哈希表并发 put 会在 rehash 时损坏(实测抛 + * {@code ArrayIndexOutOfBoundsException: Index -1 out of bounds ... rehash}), + * 进而引发 chunk 加载失败、连锁异常。 + *

+ * 修复:把 {@code storage} 替换为 {@link Long2ObjectMaps#synchronize 同步包装},所有 + * get/put/remove/values 访问线程安全,从根上消除该数据竞争。 + */ +@Mixin(SectionStorage.class) +public abstract class SectionStorageMixin { + + @Shadow + private final Long2ObjectMap> storage = + Long2ObjectMaps.synchronize(new Long2ObjectOpenHashMap<>()); +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java new file mode 100644 index 0000000..0bf5c4c --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheAccessor.java @@ -0,0 +1,24 @@ +package dev.modzuozhi.mixin; + +import java.util.concurrent.CompletableFuture; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +/** + * 暴露 {@code ServerChunkCache} 的私有方法,供 worker 线程非阻塞地读取已加载区块, + * 以及供 {@link ServerChunkCacheGetChunkFallbackMixin} 构建"已生成但 ticket 尚未对齐"区块的回退。 + */ +@Mixin(ServerChunkCache.class) +public interface ServerChunkCacheAccessor { + @Invoker("getVisibleChunkIfPresent") + ChunkHolder modzuozhi_getVisibleChunkIfPresent(long chunkPos); + + @Invoker("getChunkFutureMainThread") + CompletableFuture> modzuozhi_getChunkFutureMainThread( + int x, int z, ChunkStatus status, boolean required); +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java new file mode 100644 index 0000000..bebb5e7 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheGetChunkFallbackMixin.java @@ -0,0 +1,126 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import java.util.concurrent.CompletableFuture; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ChunkResult; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.TicketType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * 修复 {@code ServerChunkCache.getChunk} 的 "{@code Chunk not there when requested}" 崩溃。 + *

+ * 维度线程并行驱动区块生命周期后,玩家移动进入新生成的区块时,会出现一种竞态: + * 该区块已生成完毕、{@code getChunkNow} 能拿到完整 {@link LevelChunk}(数据在),但其 + * vanilla ticket 级别尚未对齐到 FULL(玩家位置 ticket 往往在 {@code doTick} 之后的网络 + * tick 才登记)。此时 {@code getChunk(x, z, FULL, true)} 的 + * {@code getChunkFutureMainThread} 会因 {@code chunkAbsent} 返回 + * {@code UNLOADED_CHUNK_FUTURE},进而在第 163 行抛 + * {@code IllegalStateException: Chunk not there when requested: Unloaded chunk}。 + *

+ * 本 mixin 在 {@code getChunk} 中:当 required 的 future 结果为 fail 时,回退到 + * {@code getChunkNow}——若该区块的完整 {@link LevelChunk} 仍在内存中,直接返回它(数据 + * 正确,返回它比抛异常合理得多),彻底消除这一崩溃类;若确实不在内存(真正未加载), + * 维持原版抛异常语义。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheGetChunkFallbackMixin { + + /** + * 包并 {@code getChunk} 里的 {@code getChunkFutureMainThread(...)} 调用点, + * 把返回的 future 换成"fail 时回退 getChunkNow"的语义。 + *

+ * {@code required=false}(碰撞检测 {@code BlockCollisions → getChunkForCollisions → getChunk(x,z,FULL,false)}) + * 同样需要该回退:区块已生成但 ticket 尚未对齐 FULL 时,原版会返回 null → 碰撞被整体跳过 → + * 实体穿墙、遁地、卡进方块窒息。回退到内存中的完整区块后,碰撞检测即可拿到真实方块形状。 + */ + @Redirect( + method = "getChunk", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerChunkCache;getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture;") + ) + private CompletableFuture> modzuozhi_getChunkFallback( + ServerChunkCache instance, int x, int z, ChunkStatus status, boolean required) { + CompletableFuture> fut = + ((ServerChunkCacheAccessor) (Object) instance).modzuozhi_getChunkFutureMainThread(x, z, status, required); + // 无论 required 与否,future 以 fail 结束时都尝试返回仍驻留内存的完整区块: + // required=true 避免 "Chunk not there" 崩溃; + // required=false 避免碰撞检测拿到 null 而遁地/穿墙。 + return fut.handle((ChunkResult res, Throwable t) -> { + if (res != null && res.isSuccess()) { + return res; + } + if (t != null) { + return ChunkResult.error(t.getMessage() == null ? "Unloaded chunk" : t.getMessage()); + } + LevelChunk now = modzuozhi_tryGetChunkNow(instance, x, z); + if (now != null) { + return ChunkResult.of(now); + } + // 区块确实不在内存(未加载):required 时投递单块 PORTAL ticket 触发异步生成。 + // 用途:维度 worker / 子任务线程执行传送门逻辑(如暮色 TFTeleporter 玩家路径 + // scanIntafeBiomes 扫描 128 格)时,会同步 getChunk(required=true) 请求远处未生成 + // 区块 → future fail → 本 fallback 仍失败 → 原版抛 "Chunk not there" → 传送被 + // 中断。这里只给被请求的这一块投 ticket,单块渐进生成(不冻结), + // 上层 PortalProcessorMixin 捕获异常后延后重试,下一 tick 该块已生成/生成中 → + // 渐进推进直到传送门逻辑所需的全部区块就绪。 + if (required && modzuozhi_shouldProgressiveGenerate(instance)) { + instance.addRegionTicket(TicketType.PORTAL, new ChunkPos(x, z), 1, + new BlockPos(x << 4, 0, z << 4)); + } + return res != null ? res : ChunkResult.error("Unloaded chunk"); + }); + } + + /** 是否为维度并行激活下的 worker / 子任务线程(此时需要渐进生成而非同步等待)。 */ + @Unique + private static boolean modzuozhi_shouldProgressiveGenerate(ServerChunkCache cache) { + return DimThreadCore.MANAGER.isActive(cache.level.getServer()) + && (DimThreadCore.owns(Thread.currentThread()) || FineGrainScheduler.inSubTask()); + } + + /** + * required 时安全读取内存中的完整区块;未加载返回 null,绝不抛异常。 + *

+ * 不能只依赖 {@code cache.getChunkNow}:它要求 {@code Thread.currentThread() == mainThread}, + * 而本 fallback 的 {@code handle} 回调可能运行在"补全该 future 的 worker 线程"上(区块仍在 + * 生成/打光,future 未完成),此时 getChunkNow 直接返回 null,即使完整区块已在内存中也会 + * 误判为未加载 → 仍抛 "Chunk not there"。故这里线程无关地直接读 + * {@link ChunkHolder#getFullChunkFuture()},绕过 mainThread 线程校验。 + */ + @Unique + private static LevelChunk modzuozhi_tryGetChunkNow(ServerChunkCache cache, int x, int z) { + // 1. 主线程优先:getChunkNow(含 lastChunk 缓存命中) + try { + LevelChunk c = cache.getChunkNow(x, z); + if (c != null) { + return c; + } + } catch (Throwable ignored) { + // 继续 + } + // 2. 线程无关回退:直接查 ChunkHolder 的 FULL future + try { + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) cache).modzuozhi_getVisibleChunkIfPresent(ChunkPos.asLong(x, z)); + if (holder != null) { + LevelChunk lc = holder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null); + if (lc != null) { + return lc; + } + } + } catch (Throwable ignored) { + // 继续 + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java new file mode 100644 index 0000000..6d859c4 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheMixin.java @@ -0,0 +1,347 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.IChunkEnvTick; +import dev.modzuozhi.core.dimthread.IMutableMainThread; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkHolder; +import net.minecraft.server.level.ServerChunkCache; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.EmptyLevelChunk; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * dimthreads 式 {@code ServerChunkCache} 线程伪装。 + *

+ * 在 {@code swapThreadsAndRun} 期间,本维度的 {@code mainThread} 已被临时设为当前 worker, + * 因此 worker 上 {@code Thread.currentThread() == mainThread} 自然成立,{@code getChunk} + * 走同步路径({@code managedBlock} 驱动本维度 chunkMap),不会投递回服务器主线程并 + * {@code join} 死锁。 + *

+ * 当某个维度 worker 需要访问其它维度的区块(如下界门查找目标传送门)时, + * 当前线程(A worker)!= 目标维度 mainThread(B worker),原版会走 + * {@code getChunkFutureMainThread + managedBlock + join} 慢路径。这里把 + * {@code Thread.currentThread()} 伪装成 {@code this.mainThread},使 A 直接同步读取 B 维度 + * 已加载区块,避免跨维度 supplyAsync/join 死锁。同时实现 {@link IMutableMainThread}, + * 供 {@code swapThreadsAndRun} 临时迁移主线程引用。 + */ +@Mixin(value = ServerChunkCache.class, priority = 1001) +public abstract class ServerChunkCacheMixin implements IMutableMainThread { + @Shadow + public Thread mainThread; + @Shadow + @Final + public ServerLevel level; + + @Override + @Unique + public Thread dimThreads$getMainThread() { + return this.mainThread; + } + + @Override + @Unique + public void dimThreads$setMainThread(Thread thread) { + this.mainThread = thread; + } + + @WrapOperation(method = "getChunk", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + public Thread modzuozhi_currentThread(Operation original) { + return modzuozhi_threadDisguise(original); + } + + /** + * {@code getChunkNow} 同样有 {@code Thread.currentThread() != mainThread → return null} 的线程守卫。 + * 碰撞检测({@code BlockCollisions})通过 {@code Level.getChunkForCollisions} 走 {@code getChunkNow}, + * 子任务线程若被守卫拦截返回 null,实体的方块碰撞被整体跳过 → 穿墙、遁地、卡进方块窒息。 + * 这里与 {@code getChunk} 一样伪装当前线程,使子任务线程能读取到已加载区块的完整碰撞形状。 + */ + @WrapOperation(method = "getChunkNow", + at = @At(value = "INVOKE", target = "Ljava/lang/Thread;currentThread()Ljava/lang/Thread;")) + public Thread modzuozhi_currentThreadNow(Operation original) { + return modzuozhi_threadDisguise(original); + } + + /** + * 维度 worker / 细粒度子任务线程伪装成该 cache 的主线程,走同步路径: + *

    + *
  • {@code getChunk}:避免 supplyAsync/join 死锁;
  • + *
  • {@code getChunkNow}:避免碰撞检测被线程守卫拦截返回 null(穿墙/遁地根因)。
  • + *
+ */ + @Unique + private Thread modzuozhi_threadDisguise(Operation original) { + Thread thread = original.call(); + if (DimThreadCore.MANAGER.isActive(this.level.getServer()) + && (DimThreadCore.owns(thread) || FineGrainScheduler.inSubTask())) { + return this.mainThread; + } + return thread; + } + + // ===== 并行子任务:绕过撕裂的共享 lastChunk 缓存 ===== + // + // 根因:细粒度并行时,多个子任务线程 + 维度 worker 会并发读写 + // ServerChunkCache 的 4 槽 lastChunkPos/lastChunkStatus/lastChunk 缓存(普通数组,非线程安全)。 + // storeInCache 的移位写入与 getChunk 的读取交错时会产生"撕裂读":getChunk(x,z) 命中了 + // pos/status 槽但拿到的是另一个坐标的区块对象。碰撞检测(BlockCollisions → + // ChunkSource.getBlockState)拿着错块去读方块状态,会把脚下的实心地面读成空气 → 村民穿地/遁地/ + // 卡进方块窒息。诊断(getChunk(FULL,false) 返回"wrong 错块"计数持续增长)已证实。 + // + // 修复:子任务线程上完全绕过共享缓存——getChunk / getChunkNow 直接按坐标从 ChunkHolder + // (visibleChunkMap 是 volatile 快照式引用,get 线程安全)读正确区块;storeInCache 对子任务 + // 线程为 no-op,避免污染共享缓存。主线程 / 维度 worker / 串行场景行为不变(零开销)。 + + /** + * 子任务线程的 {@code getChunk}:绕过 lastChunk 缓存,直接读正确坐标的 FULL 区块。 + *

+ * 第10步(修复撕裂回退 + 修复死锁): + *

    + *
  • FULL 命中:走 {@link #modzuozhi_subtaskCacheLookup},绝不用共享 4 槽 lastChunk 缓存 + * (多线程并发写会撕裂,拿到其它坐标区块 → 碰撞把地面读成空气 → 村民遁地)。
  • + *
  • FULL 未命中(区块尚未 FULL,多为加载/生成中):非阻塞占位返回,绝不能等待—— + * 区块生成管线由维度 worker 的 {@code chunkSource.tick} / 服务器主线程驱动,而这两者此刻 + * 分别卡在「实体屏障 await」和「维度 awaitCompletion」上;等待生成 future 会形成 + * 循环等待 → 整服死锁(日志:[FaultGuard] 维度 tick 超时 >30000ms,玩家侧表现为游戏冻结)。 + * 返回占位块后,区块生成完的下一 tick 命中缓存、碰撞恢复正常,短暂下坠自愈,绝不冻结。
  • + *
+ */ + @WrapMethod(method = "getChunk(IILnet/minecraft/world/level/chunk/status/ChunkStatus;Z)Lnet/minecraft/world/level/chunk/ChunkAccess;") + private ChunkAccess modzuozhi_subtaskSafeGetChunk( + int x, int z, ChunkStatus status, boolean required, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + return original.call(x, z, status, required); + } + if (status != ChunkStatus.FULL) { + return original.call(x, z, status, required); + } + LevelChunk full = modzuozhi_subtaskCacheLookup(x, z); + if (full != null) { + return full; + } + // 未 FULL:统一返回实心占位(石头),不再区分 required。 + // 根因:并行子任务线程对未生成区块的碰撞检测(required=false → 原返回 null → 碰撞整体 + // 跳过)与寻路/方块读取(required=true → 原返回空气占位)都把未生成区块当作"空气/无地面", + // 村民会持续下坠穿入未生成区块 → 加载高峰期遁地(已实测:fine off 串行时 getChunk 会 + // join 同步生成拿真实方块故不遁地,仅并行路径有此问题)。 + // 实心占位让未生成区块表现为"实心墙":村民被挡在生成边界,区块 FULL 后恢复正常。 + // 仍保持非阻塞(不等待生成 future,避免维度 worker 被 barrier 卡住的循环等待死锁)。 + return modzuozhi_solidChunk(x, z); + } + + /** 构造一个所有方块读作石头的实心占位区块(非阻塞、线程安全,避免生成等待死锁)。 */ + @Unique + private LevelChunk modzuozhi_solidChunk(int x, int z) { + Holder plains = this.level.registryAccess() + .registryOrThrow(Registries.BIOME).getHolderOrThrow(Biomes.PLAINS); + return new ModZuozhiSolidChunk(this.level, new ChunkPos(x, z), plains); + } + + /** + * 未 FULL 区块的实心占位:重写 {@code getBlockState} 恒返回石头。 + * 并行子任务线程把未生成区块表现为实心墙,防止实体下坠穿入(遁地)。 + */ + @Unique + private static final class ModZuozhiSolidChunk extends EmptyLevelChunk { + private ModZuozhiSolidChunk(ServerLevel level, ChunkPos pos, Holder biome) { + super(level, pos, biome); + } + + @Override + public BlockState getBlockState(BlockPos pos) { + return Blocks.STONE.defaultBlockState(); + } + } + + /** + * 子任务线程的 {@code getChunkNow}:同样绕过 lastChunk 缓存(fallback 的 + * {@code modzuozhi_tryGetChunkNow} 也依赖它),避免撕裂读返回错块。 + */ + @WrapMethod(method = "getChunkNow(II)Lnet/minecraft/world/level/chunk/LevelChunk;") + private LevelChunk modzuozhi_subtaskSafeGetChunkNow(int x, int z, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + return original.call(x, z); + } + return modzuozhi_subtaskCacheLookup(x, z); + } + + /** + * 子任务线程的 {@code storeInCache}:no-op,避免污染(撕裂)主线程/worker 共享的 lastChunk 缓存。 + */ + @WrapMethod(method = "storeInCache(JLnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/chunk/status/ChunkStatus;)V") + private void modzuozhi_subtaskSafeStoreInCache( + long pos, ChunkAccess chunk, ChunkStatus status, Operation original) { + if (!FineGrainScheduler.inSubTask()) { + original.call(pos, chunk, status); + } + // 子任务线程:不写共享缓存 + } + + // ===== 第5步(6c):子任务线程专用并发区块缓存 ===== + // + // 目标:减少子任务线程高频 getChunkNow/getChunk(FULL)(碰撞检测、AI 传感器)的 + // ChunkHolder future 解析开销与共享缓存撕裂风险。设计要点: + // 1. 只缓存 FULL 状态的 LevelChunk(碰撞/AI 只读完整区块); + // 2. 用 ConcurrentHashMap(线程安全)按 chunk pos 缓存,天然与维度隔离(per-cache 字段); + // 3. 命中前先校验 ChunkHolder 仍可见(区块未卸载),杜绝返回已卸载/失效区块; + // 4. 位置校验(x/z 匹配)双保险,防止任何错块路径(吸取 lastChunk 撕裂教训); + // 5. 超过阈值时清空,避免长期运行内存膨胀(区块生命周期短,全清可接受)。 + + /** 子任务线程专用 FULL 区块缓存:chunkPos(long) → LevelChunk。 */ + @Unique + private final ConcurrentHashMap modzuozhi_subtaskChunkCache = new ConcurrentHashMap<>(); + + /** 缓存条目上限,超过即整表清空。 */ + @Unique + private static final int MODZUOZHI_CACHE_MAX = 8192; + + /** + * 子任务线程读取 FULL 区块:优先命中缓存,未命中则从 ChunkHolder 读取并回填缓存。 + *

+ * 正确性关键:本方法不仅被「只读」路径(碰撞检测、AI 传感器)调用,也会被 + * 写入路径(爆炸破坏方块 → {@code Level.setBlock → getChunkAt → getChunk(FULL)}) + * 调用。因此返回的 {@code LevelChunk} 必须是 ChunkHolder 当前活动对象,否则 + * {@code setBlockState} 会写到已过期(重载前)的对象上,导致方块状态丢失/复活(例如 + * 爆炸破坏的 TNT 在存档中复活并再次爆炸)。 + *

+ * 校验策略:命中缓存后,用 {@code holder.getFullChunkFuture().getNow(...)} 解析当前活动 + * 对象,与缓存做对象身份(==)比较——一致才返回缓存(对象未变,安全); + * 不一致说明区块已重载,返回并回填新对象。completed future 的 {@code getNow} 开销可忽略, + * 但彻底杜绝"写到过期对象"这一致数据损坏路径。 + */ + @Unique + private LevelChunk modzuozhi_subtaskCacheLookup(int x, int z) { + long key = ChunkPos.asLong(x, z); + ChunkHolder holder = ((ServerChunkCacheAccessor) (Object) this) + .modzuozhi_getVisibleChunkIfPresent(key); + if (holder == null) { + // 区块不在可见列表:不缓存(避免缓存已卸载区块),直接返回 null + return null; + } + // 解析当前活动 FULL 区块(completed future,开销极低) + LevelChunk full = holder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).orElse(null); + if (full == null) { + return null; + } + // 身份校验:缓存对象与当前活动对象一致才可复用(防止写已过期对象) + LevelChunk cached = this.modzuozhi_subtaskChunkCache.get(key); + if (cached != full) { + this.modzuozhi_subtaskChunkCache.put(key, full); + if (this.modzuozhi_subtaskChunkCache.size() > MODZUOZHI_CACHE_MAX) { + this.modzuozhi_subtaskChunkCache.clear(); + } + } + return full; + } + + // ===== 第3步:区块环境 tick(随机 tick / 流体)跨 chunk 并行 ===== + + /** 并行开启且处于维度 worker 线程时才收集待并行的区块。 */ + @Unique + private boolean modzuozhi_chunkParallel; + /** 收集的待并行区块:(ServerLevel, LevelChunk, randomTickSpeed)。 */ + @Unique + private final List modzuozhi_pendingChunks = new ArrayList<>(); + /** 本次 chunk tick 的子任务屏障。 */ + @Unique + private FineGrainScheduler.Barrier modzuozhi_chunkBarrier; + + @Unique + private boolean modzuozhi_chunkShouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + MinecraftServer server = this.level.getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + @Inject(method = "tickChunks", at = @At("HEAD")) + private void modzuozhi_chunkBegin(CallbackInfo ci) { + this.modzuozhi_chunkParallel = modzuozhi_chunkShouldParallel(); + this.modzuozhi_pendingChunks.clear(); + } + + /** + * 拦截 {@code ServerChunkCache.tickChunks} 里对每个区块的 {@code level.tickChunk(...)}: + * 并行开启时先收集到 {@link #modzuozhi_pendingChunks},由 {@link #modzuozhi_chunkFlush} + * 在广播前统一提交并行执行;关闭时直接串行执行,行为与原版完全一致。 + */ + @Redirect(method = "tickChunks", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/level/ServerLevel;tickChunk(Lnet/minecraft/world/level/chunk/LevelChunk;I)V")) + private void modzuozhi_chunkTick(ServerLevel level, LevelChunk chunk, int randomTickSpeed) { + if (this.modzuozhi_chunkParallel) { + this.modzuozhi_pendingChunks.add(new Object[]{level, chunk, randomTickSpeed}); + } else { + level.tickChunk(chunk, randomTickSpeed); + } + } + + /** + * 在 {@code tickChunks} 的 {@code list.forEach(... broadcastChanges ...)} 之前收口: + * 把收集的区块并行执行随机 tick / 流体,并 {@code await} 全部完成,确保方块状态变化 + * 在广播前已应用。 + */ + @WrapOperation(method = "tickChunks", + at = @At(value = "INVOKE", + target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V")) + private void modzuozhi_chunkFlush(List list, Consumer consumer, Operation original) { + this.modzuozhi_flushChunkTicks(); + original.call(list, consumer); + } + + @Unique + private void modzuozhi_flushChunkTicks() { + if (!this.modzuozhi_chunkParallel || this.modzuozhi_pendingChunks.isEmpty()) { + this.modzuozhi_chunkParallel = false; + return; + } + MinecraftServer server = this.level.getServer(); + this.modzuozhi_chunkBarrier = FineGrainScheduler.beginBarrier(); + for (Object[] entry : this.modzuozhi_pendingChunks) { + ServerLevel level = (ServerLevel) entry[0]; + LevelChunk chunk = (LevelChunk) entry[1]; + int randomTickSpeed = (Integer) entry[2]; + this.modzuozhi_chunkBarrier.submit(server, () -> { + // 线程本地随机源,避免并行子任务跨线程访问共享 Level.random + ((IChunkEnvTick) level).modzuozhi_tickChunk(chunk, randomTickSpeed, RandomSource.create()); + }, chunk.getPos().getWorldPosition()); + } + this.modzuozhi_pendingChunks.clear(); + this.modzuozhi_chunkBarrier.await(); + this.modzuozhi_chunkBarrier.drain(); + this.modzuozhi_chunkBarrier = null; + this.modzuozhi_chunkParallel = false; + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java new file mode 100644 index 0000000..3fc6750 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCachePollMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.server.level.ServerChunkCache; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 维度循环模型:把区块源「主线程」任务收口到维度 loop,禁止服务器主线程二次 poll。 + *

+ * 原版里 {@code ServerChunkCache.pollTask()} 由 {@code MinecraftServer.pollTaskInternal}(主线程 + * waitUntilNextTick 期间)调用,负责距离更新({@code runDistanceManagerUpdates})+ 光照调度 + + * 主线程队列排空。切到维度 loop 后,本维度 {@code level.tick()} 已在 worker 上跑 + * {@code ServerChunkCache.tick → runDistanceManagerUpdates},若服务器主线程仍为该维度 + * {@code pollTask},两个线程会并发改写同一 {@code DistanceManager}/{@code ChunkMap}(本次启动即 + * 因此在 {@code DistanceManager.runAllUpdates} 抛了 ConcurrentModificationException)。 + *

+ * 因此:当监控开启且当前线程不是维度 worker(即服务器主线程/其它线程)时跳过 poll, + * 本维度的区块源任务统一由 {@code LevelTickLoop.processMainThreadTasks()} 在 worker 上排空。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCachePollMixin { + + @WrapMethod(method = "pollTask") + private boolean modzuozhi_skipServerThreadChunkPoll(Operation original) { + ServerChunkCache self = (ServerChunkCache) (Object) this; + // 仅当维度 loop 已启动接管(tickChildren 之后)才跳过主线程 poll;启动阶段 prepareLevels + // 仍依赖主线程 poll 驱动出生点区块生成,跳过会卡死在「Preparing start region」。 + if (DimThreadCore.MANAGER.isActive(self.level.getServer()) + && DimThreadCore.MANAGER.isLoopScheduled(self.level) + && !DimThreadCore.owns(Thread.currentThread())) { + return false; + } + return original.call(); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java new file mode 100644 index 0000000..590729e --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerChunkCacheRunDistanceMixin.java @@ -0,0 +1,34 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.server.level.ServerChunkCache; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 串行化同一维度的区块距离更新(修复并发 {@code runGenerationTasks} 的 CME 与 claim/release + * 计数异常)。 + *

+ * {@code runDistanceManagerUpdates}(内部:{@code DistanceManager.runAllUpdates} → + * {@code scheduleGenerationTask} 向 {@code ChunkMap.pendingGenerationTasks}(普通 ArrayList) + * add;以及 {@code ChunkMap.runGenerationTasks} 遍历 + clear,并对其中的生成任务 claim/release) + * 必须单线程执行。维度 loop 模型下,该维度 worker({@code level.tick → ServerChunkCache + * .tick})与保存线程({@code save → runDistanceManagerUpdates})等路径并发调用时: + * 同时遍历/修改 {@code pendingGenerationTasks} 抛 {@code ConcurrentModificationException}, + * 且对同一 {@code GenerationChunkHolder} 重复 claim/release 抛 + * {@code IllegalStateException: More releases than claims}(实测:下界维度 tick 异常 + + * Worker 线程异常)。 + *

+ * 用 {@code synchronized(this)}(每 {@code ServerChunkCache} 一把锁)把所有调用者串行化: + * 生成任务 add/遍历/claim 互斥,release(ChunkMap 生成线程异步)与 claim 自然配对,计数平衡。 + */ +@Mixin(ServerChunkCache.class) +public abstract class ServerChunkCacheRunDistanceMixin { + + @WrapMethod(method = "runDistanceManagerUpdates") + private boolean modzuozhi_syncRunDistanceUpdates(Operation original) { + synchronized (this) { + return original.call(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java new file mode 100644 index 0000000..072efde --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerCommonPacketListenerDimMixin.java @@ -0,0 +1,37 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.ModZuozhi; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +/** + * 第2步:网络包按玩家分派到维度线程——断开流程死锁防护。 + *

+ * 原版 {@code ServerCommonPacketListenerImpl.disconnect} 用 + * {@code MinecraftServer.executeBlocking} 把清理任务提交到主线程并阻塞等待。当包处理 + * 已移到维度线程(worker 在 {@code ServerLevel.tick} 收口执行包)时,主线程正阻塞在 + * {@code awaitCompletion} 等待维度 worker 完成——worker 再 executeBlocking 等主线程会 + * 互相等待死锁。因此全局开启且为游戏连接时,把断开清理改为异步投递主线程 + * ({@code execute}),不阻塞 worker,杜绝死锁。 + */ +@Mixin(ServerCommonPacketListenerImpl.class) +public abstract class ServerCommonPacketListenerDimMixin { + + @WrapOperation(method = "disconnect(Lnet/minecraft/network/DisconnectionDetails;)V", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/server/MinecraftServer;executeBlocking(Ljava/lang/Runnable;)V")) + private void modzuozhi_asyncDisconnect(MinecraftServer server, Runnable task, Operation original) { + if (ModZuozhi.isEnabled() && ((Object) this) instanceof ServerGamePacketListenerImpl) { + // 异步投主线程,worker 不阻塞 → 防与主线程 awaitCompletion 互相等待死锁 + server.execute(task); + } else { + original.call(server, task); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java new file mode 100644 index 0000000..4769ee3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerConnectionListenerMixin.java @@ -0,0 +1,79 @@ +package dev.modzuozhi.mixin; + +import com.mojang.logging.LogUtils; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.network.Connection; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerConnectionListener; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Iterator; +import java.util.List; + +/** + * 维度循环模型:主线程的连接监听不再 tick 游戏连接(防止玩家被双 tick)。 + *

+ * 原版 {@code ServerConnectionListener.tick()} 会 tick 所有连接(游戏连接触发 + * {@code ServerPlayer.doTick()})。维度 loop 的 {@code tickConnections()} 已经 tick 了它 + * 名下的游戏连接,因此这里对 {@link ServerGamePacketListenerImpl}(游戏连接)只做「断开时 + * 从监听列表移除」,不 tick、不重复清理(断开清理由 loop 完成)。登录/状态/配置连接仍在本处 tick。 + */ +@Mixin(ServerConnectionListener.class) +public abstract class ServerConnectionListenerMixin { + + @Shadow + @Final + private List connections; + + @Shadow + @Final + private MinecraftServer server; + + /** + * @reason 游戏连接交维度 loop tick;主线程只兜底登录/状态/配置连接与列表清理。 + * 全局开关关闭(isActive=false)时恢复原版:所有连接(含游戏连接)都回主线程 tick。 + */ + @Overwrite + public void tick() { + boolean active = DimThreadCore.MANAGER.isActive(this.server); + synchronized (this.connections) { + Iterator iterator = this.connections.iterator(); + while (iterator.hasNext()) { + Connection connection = iterator.next(); + if (connection.isConnecting()) { + continue; + } + boolean game = connection.getPacketListener() instanceof ServerGamePacketListenerImpl; + if (connection.isConnected()) { + if (game && active) { + continue; // 由所属维度 loop tick + } + try { + connection.tick(); + } catch (Exception e) { + Logger logger = LogUtils.getLogger(); + logger.warn("Failed to handle packet for {}", + connection.getLoggableAddress(this.server.logIPs()), e); + Component component = Component.literal("Internal server error"); + connection.send(new ClientboundDisconnectPacket(component), + PacketSendListener.thenRun(() -> connection.disconnect(component))); + connection.setReadOnly(); + } + } else { + iterator.remove(); + if (!game || !active) { + connection.handleDisconnection(); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java new file mode 100644 index 0000000..0dc9da3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelBlockEventMixin.java @@ -0,0 +1,47 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import org.spongepowered.asm.mixin.Mixin; + +/** + * Block Event(方块事件)队列并发访问加固。 + *

+ * 方块事件(音符盒/活塞/发射器/铃等延迟动作)经 {@code ServerLevel.blockEvent} 入队、 + * 每 tick {@code runBlockEvents} 出队执行并广播。原版 {@code blockEvents} 是普通非线程安全 + * 的 {@code ObjectLinkedOpenHashSet}。细粒度并行下,并行实体子任务一旦触发 {@code blockEvent} + * (入队写),会与维度 worker/服务器线程正在 {@code runBlockEvents}(出队删)并发读写 + * 同一哈希集 → CME 或哈希表损坏(fastutil 并发损坏崩溃)。 + *

+ * 修复:对访问队列的 3 个方法({@code blockEvent}/{@code runBlockEvents}/{@code clearBlockEvents}) + * 加 {@code synchronized(this)}:单对象锁、逐调用、无嵌套,无死锁(方块事件低频,开销可忽略); + * 同时保留原版 Set 的去重 + 插入序语义,行为零变化。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelBlockEventMixin { + + @WrapMethod(method = "blockEvent") + private void modzuozhi_blockEvent(BlockPos pos, Block block, int type, int data, Operation original) { + synchronized (this) { + original.call(pos, block, type, data); + } + } + + @WrapMethod(method = "runBlockEvents") + private void modzuozhi_runBlockEvents(Operation original) { + synchronized (this) { + original.call(); + } + } + + @WrapMethod(method = "clearBlockEvents") + private void modzuozhi_clearBlockEvents(BoundingBox box, Operation original) { + synchronized (this) { + original.call(box); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java new file mode 100644 index 0000000..fda3e8d --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelChunkTickMixin.java @@ -0,0 +1,116 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.IChunkEnvTick; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.DifficultyInstance; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LightningBolt; +import net.minecraft.world.entity.animal.horse.SkeletonHorse; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.LightningRodBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +/** + * 区块环境随机 tick / 流体 tick 的并行版本(自定义实现,绕开共享 {@code Level.random})。 + *

+ * 细粒度并行(fine)下多个区块的随机 tick 由不同子任务线程执行。原版 + * {@code ServerLevel.tickChunk} 直接读取 {@code Level.random}({@code public final} 共享字段), + * 并行时会跨线程冲突;且 NeoForge 开发环境无法对混淆字段做 {@code @Redirect}(无 refmap)。 + *

+ * 因此本 mixin 在 {@code ServerLevel} 上注入一个等价的自定义实现 + * {@link #modzuozhi_tickChunk}(实现 {@link IChunkEnvTick}),接收线程本地随机源为参数, + * 复刻原版 {@code tickChunk} 的 thunder / ice-and-snow / 随机 tick + 流体逻辑。随机源全部来自 + * 参数 {@code random},由 {@code ServerChunkCacheMixin} 在并行子任务中调用,完全绕开共享字段。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelChunkTickMixin implements IChunkEnvTick { + /** {@code findLightningTargetAround} 为 protected,需 shadow 后在本实现内调用。 */ + @Shadow + protected abstract BlockPos findLightningTargetAround(BlockPos pos); + + /** + * 等价于原版 {@code ServerLevel.tickChunk},但使用传入的线程本地随机源 {@code random}。 + */ + @Override + @Unique + public void modzuozhi_tickChunk(LevelChunk chunk, int randomTickSpeed, RandomSource random) { + ServerLevel level = (ServerLevel) (Object) this; + ChunkPos chunkpos = chunk.getPos(); + boolean raining = level.isRaining(); + int minX = chunkpos.getMinBlockX(); + int minZ = chunkpos.getMinBlockZ(); + + // thunder:用线程本地随机源判断是否产生闪电 + if (raining && level.isThundering() && random.nextInt(100000) == 0) { + BlockPos target = this.findLightningTargetAround(randomPos(minX, 0, minZ, 15, random)); + if (level.isRainingAt(target)) { + DifficultyInstance difficulty = level.getCurrentDifficultyAt(target); + boolean trapHorse = level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) + && random.nextDouble() < (double) difficulty.getEffectiveDifficulty() * 0.01 + && !(level.getBlockState(target.below()).getBlock() instanceof LightningRodBlock); + if (trapHorse) { + SkeletonHorse horse = EntityType.SKELETON_HORSE.create(level); + if (horse != null) { + horse.setTrap(true); + horse.setAge(0); + horse.setPos((double) target.getX(), (double) target.getY(), (double) target.getZ()); + level.addFreshEntity(horse); + } + } + LightningBolt bolt = EntityType.LIGHTNING_BOLT.create(level); + if (bolt != null) { + bolt.moveTo(Vec3.atBottomCenterOf(target)); + bolt.setVisualOnly(trapHorse); + level.addFreshEntity(bolt); + } + } + } + + // ice and snow(降水 / 结冰) + for (int i = 0; i < randomTickSpeed; i++) { + if (random.nextInt(48) == 0) { + level.tickPrecipitation(randomPos(minX, 0, minZ, 15, random)); + } + } + + // 随机 tick + 流体 tick + if (randomTickSpeed > 0) { + LevelChunkSection[] sections = chunk.getSections(); + for (int idx = 0; idx < sections.length; idx++) { + LevelChunkSection section = sections[idx]; + if (section.isRandomlyTicking()) { + int sectionY = chunk.getSectionYFromSectionIndex(idx); + int baseY = SectionPos.sectionToBlockCoord(sectionY); + for (int l = 0; l < randomTickSpeed; l++) { + BlockPos pos = randomPos(minX, baseY, minZ, 15, random); + BlockState state = section.getBlockState(pos.getX() - minX, pos.getY() - baseY, pos.getZ() - minZ); + if (state.isRandomlyTicking()) { + state.randomTick(level, pos, random); + } + FluidState fluid = state.getFluidState(); + if (fluid.isRandomlyTicking()) { + fluid.randomTick(level, pos, random); + } + } + } + } + } + } + + /** 等价于原版 {@code ServerLevel.getBlockRandomPos},但使用传入的随机源。 */ + @Unique + private static BlockPos randomPos(int x, int y, int z, int mask, RandomSource random) { + return new BlockPos(x + random.nextInt(mask + 1), y, z + random.nextInt(mask + 1)); + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java new file mode 100644 index 0000000..6ac9127 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityCallbacksMixin.java @@ -0,0 +1,72 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.world.entity.Entity; +import org.spongepowered.asm.mixin.Mixin; + +/** + * 第14步(对齐 Async 零锁路线):{@code ServerLevel$EntityCallbacks} 的可见性/生命周期回调加 + * {@code synchronized(this)},替代原全局写锁 {@code SECTION_LOCK} 对这些共享状态的串行保护。 + *

+ * 移除全局写锁后,实体 tick 并行子任务会并发触发这些回调(经 + * {@code startTracking/stopTracking/onDestroyed/onSectionChange}): + *

    + *
  • {@code onTrackingStart/onTrackingEnd}:写 {@code ServerChunkCache} 实体追踪表、玩家表、 + * {@code dragonParts}、游戏事件监听器、NeoForge 事件——非线程安全,需串行;
  • + *
  • {@code onDestroyed}:写记分板;
  • + *
  • {@code onSectionChange}:更新游戏事件监听器位置。
  • + *
+ * 用 {@code synchronized(this)}(同一 {@code EntityCallbacks} 实例,即每 ServerLevel 一把轻量锁) + * 把这些快速回调串行化,锁持有时长远小于整段 onMove,实体移动/生成本体仍无锁并行。 + * {@code onTickingStart/onTickingEnd}(EntityTickList 增删)已在 {@link EntityTickListMixin} + * 中按子任务上下文延迟执行,此处一并加同步兜底。 + *

+ * 服务器线程与维度 worker 的其它区块追踪访问不经过本回调,与移除前一致(本回调原本也未与之互斥), + * 不引入新的竞争面。 + */ +@Mixin(targets = "net.minecraft.server.level.ServerLevel$EntityCallbacks") +public abstract class ServerLevelEntityCallbacksMixin { + + @WrapMethod(method = "onTrackingStart") + private void modzuozhi_syncTrackingStart(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTrackingEnd") + private void modzuozhi_syncTrackingEnd(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onDestroyed") + private void modzuozhi_syncDestroyed(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onSectionChange") + private void modzuozhi_syncSectionChange(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTickingStart") + private void modzuozhi_syncTickingStart(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } + + @WrapMethod(method = "onTickingEnd") + private void modzuozhi_syncTickingEnd(Entity entity, Operation original) { + synchronized (this) { + original.call(entity); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java new file mode 100644 index 0000000..d5502d7 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelEntityTickMixin.java @@ -0,0 +1,73 @@ +package dev.modzuozhi.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.EntityTickParallel; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.entity.EntityTickList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.function.Consumer; + +/** + * 第4步:实体 tick 并行——在 {@code ServerLevel.tick} 的 {@code entityTickList.forEach} 前后 + * 建立/收口实体子任务屏障(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 原版实体循环({@code entityTickList.forEach(lambda)}) 的每实体处理:isRemoved 检查 → + * checkDespawn → 载具处理 → {@code guardEntityTick(tickNonPassenger, entity)}。其中除 + * {@code guardEntityTick} 外的逻辑为串行预检(改动小、需按序),只有最终实体的 + * {@code tick} 是真正的重负载,适合并行。 + *

+ * 由于 {@code guardEntityTick} 调用位于 lambda 内部(合成方法,无法稳定引用),本 mixin 用 + * {@code @WrapOperation} 包住整个 {@code forEach} 调用:进入前建立 ThreadLocal 屏障 + * ({@link EntityTickParallel#begin()}),离开时(finally)收口等待全部实体子任务完成 + * ({@link EntityTickParallel#end()})。真正的实体 tick 由 {@link EntityTickParallelMixin} + * 在 {@code guardEntityTick} 处读取该屏障并提交并行。 + *

+ * 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效; + * 关闭时完全退化为原版串行(零开销)。 + *

+ * 第14步(对齐 Async):第12步曾因"并行实体 tick + 碰撞开启"的锁竞争而改为碰撞开启时 + * 强制串行实体 tick;第14步移除全局写锁、并发化 {@code EntityLookup/knownUuids/chunkVisibility}、 + * 并同步实体回调后,实体 tick 无条件走并行(碰撞开启也并行),不再需要该串行回退。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelEntityTickMixin { + + @Unique + private boolean modzuozhi_entityShouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + /** + * 包住整个实体 tick 循环:进入前建立实体子任务屏障,离开时(含异常)收口等待完成。 + */ + @WrapOperation(method = "tick(Ljava/util/function/BooleanSupplier;)V", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/entity/EntityTickList;forEach(Ljava/util/function/Consumer;)V")) + private void modzuozhi_entityLoop(EntityTickList list, Consumer consumer, Operation original) { + boolean parallel = modzuozhi_entityShouldParallel(); + if (parallel) { + EntityTickParallel.begin(); + } + try { + original.call(list, consumer); + } finally { + if (parallel) { + EntityTickParallel.end(); + } + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java new file mode 100644 index 0000000..4b2e9a3 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerLevelPlayerPacketMixin.java @@ -0,0 +1,48 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.ModZuozhi; +import dev.modzuozhi.core.dimthread.IPlayerPacketQueue; +import dev.modzuozhi.core.dimthread.PlayerPacketQueue; +import net.minecraft.server.level.ServerLevel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.BooleanSupplier; + +/** + * 第2步:网络包按玩家分派到维度线程——每维度包队列的挂载与 tick 收口。 + *

+ * 在 {@code ServerLevel} 上挂一个 {@link PlayerPacketQueue}(玩家发来的包处理任务队列), + * 并在 {@code ServerLevel.tick} 开头(HEAD)统一执行。两种 tick 路径都覆盖: + *

    + *
  • 维度并行:维度 worker 在 {@code swapThreadsAndRun} 内 tick,包任务在 worker + * 身份下执行(getBlockEntity 等线程守卫自然通过),与其它维度并行。
  • + *
  • 主线程串行(dimCount <= 1 / 维度并行关闭):主线程直接 tick,包任务在主线程 + * 执行,行为等价原版。
  • + *
+ * 全局关闭时 {@code drain} 不执行(空转,零副作用)。 + */ +@Mixin(ServerLevel.class) +public abstract class ServerLevelPlayerPacketMixin implements IPlayerPacketQueue { + + @Unique + private final PlayerPacketQueue modzuozhi_playerPacketQueue = new PlayerPacketQueue(); + + @Override + @Unique + public PlayerPacketQueue modzuozhi$playerPacketQueue() { + return this.modzuozhi_playerPacketQueue; + } + + /** 维度 tick 开头:先排空本维度玩家排队的包处理任务(命令等),再进沉重的实体/AI tick, + * 避免指令在高负载(实体多/地形慢)时被饿死到 level.tick 结束才执行(仅全局开启时)。 */ + @Inject(method = "tick(Ljava/util/function/BooleanSupplier;)V", at = @At("HEAD")) + private void modzuozhi_drainPlayerPackets(BooleanSupplier hasTimeLeft, CallbackInfo ci) { + if (ModZuozhi.isEnabled()) { + this.modzuozhi_playerPacketQueue.drain(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java new file mode 100644 index 0000000..e1e20cf --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerPlayerChangeDimensionMixin.java @@ -0,0 +1,41 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimensionChangeQueue; +import dev.modzuozhi.core.dimthread.DimThreadCore; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.portal.DimensionTransition; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 维度循环模型:玩家跨维度传送收口到主线程。 + *

+ * 旧的「有界屏障」模型里,玩家实体 tick 只在主线程({@code EntityTickParallelMixin} 跳过维度 + * worker 上的玩家),因此下界门触发的 {@code ServerPlayer.changeDimension} 也在主线程执行,天然安全。 + * 切到维度循环后,玩家 {@code doTick()} 由本维度 loop 的 {@code tickConnections()} 在 worker 上触发, + * 于是 {@code changeDimension} 会在 worker 上直接 {@code removePlayerImmediately} 旧维度 + + * {@code addDuringTeleport} 新维度,与新维度 worker 并发改写实体容器 → 竞态崩溃。 + *

+ * 因此这里与 {@code EntityChangeDimensionMixin}(针对 {@code Entity.changeDimension},非玩家实体) + * 平行:当调用线程是维度 worker 时,把真正的变更登记到 {@link DimensionChangeQueue},由主线程在维度 + * tick 收口后({@code MinecraftServerDimMixin} 的 {@code DimensionChangeQueue.flush()})统一执行。 + * 主线程上直接调用(如命令 {@code /execute in})不经本拦截,行为与原版一致。 + */ +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerChangeDimensionMixin { + + /** 在 worker 上触发跨维度时退避到主线程队列,避免并发改写两个维度的实体容器。 */ + @Inject(method = "changeDimension", at = @At("HEAD"), cancellable = true, remap = false) + private void modzuozhi_deferPlayerDimChange(DimensionTransition transition, CallbackInfoReturnable cir) { + if (!DimThreadCore.MANAGER.isActive(transition.newLevel().getServer())) { + return; + } + if (DimThreadCore.owns(Thread.currentThread())) { + DimensionChangeQueue.schedule((Entity) (Object) this, transition); + cir.setReturnValue(null); + } + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java b/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java new file mode 100644 index 0000000..ecb5c99 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/ServerPlayerPrefetchMixin.java @@ -0,0 +1,24 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.ChunkPrefetcher; +import net.minecraft.server.level.ServerPlayer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * 阶段 A(步骤 0):挂载区块预取到玩家 tick。 + *

+ * 玩家实体 tick 只由主线程 {@code playerList.tick()} → {@code ServerPlayer.doTick()} 驱动 + * (维度 worker 已跳过玩家 tick,见 {@link EntityTickParallelMixin}),因此本注入天然运行在 + * 主线程,与维度并行无冲突。预取逻辑本身是纯增益、全 try/catch,任何异常都不影响玩家。 + */ +@Mixin(ServerPlayer.class) +public abstract class ServerPlayerPrefetchMixin { + + @Inject(method = "doTick", at = @At("HEAD")) + private void modzuozhi_prefetchChunks(CallbackInfo ci) { + ChunkPrefetcher.onPlayerTick((ServerPlayer) (Object) this); + } +} \ No newline at end of file diff --git a/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java b/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java new file mode 100644 index 0000000..72b0e0b --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/TickingBlockEntityParallelMixin.java @@ -0,0 +1,113 @@ +package dev.modzuozhi.mixin; + +import dev.modzuozhi.core.dimthread.DimThreadCore; +import dev.modzuozhi.core.dimthread.FineGrainScheduler; +import dev.modzuozhi.core.dimthread.PostExecuteQueue; +import dev.modzuozhi.core.filter.SerDesFilter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Collection; + +/** + * 方块实体(TE)细粒度并行(借鉴 MCMT 思路,写我们自己的实现)。 + *

+ * 在 {@code Level.tickBlockEntities()} 的 while 循环中,把线程安全(经 + * {@link SerDesFilter} 判定可并行)的 TE tick 提交到 {@link FineGrainScheduler.Barrier} + * 并行执行,其余(黑名单/非 vanilla)串行;循环结束后 {@code await} 收口,再 + * {@code drain} 子任务投递的延迟回调。 + *

+ * 线程安全关键:并行子任务运行在其它 worker 线程,可能并发写 {@code pendingBlockEntityTickers} + * 等普通集合。这里拦截 {@code addBlockEntityTicker} / {@code addFreshBlockEntities}, + * 在并行子任务上下文中把写入投递到 {@link PostExecuteQueue},由维度 worker 串行执行。 + *

+ * 仅在 {@code /dimensionalripper fine on} 且维度并行激活、当前线程为维度 worker 时生效; + * 关闭时完全退化为原版串行。 + */ +@Mixin(Level.class) +public abstract class TickingBlockEntityParallelMixin { + @Shadow + private void addBlockEntityTicker(TickingBlockEntity ticker) { + } + + @Shadow + private void addFreshBlockEntities(Collection entities) { + } + + @Unique + private FineGrainScheduler.Barrier modzuozhi_teBarrier; + @Unique + private boolean modzuozhi_teParallel; + + @Unique + private boolean modzuozhi_shouldParallel() { + if (!FineGrainScheduler.isEnabled()) { + return false; + } + if (!((Object) this instanceof ServerLevel serverLevel)) { + return false; + } + MinecraftServer server = serverLevel.getServer(); + return server != null + && DimThreadCore.MANAGER.isActive(server) + && DimThreadCore.owns(Thread.currentThread()); + } + + @Inject(method = "tickBlockEntities", at = @At("HEAD")) + private void modzuozhi_teBegin(CallbackInfo ci) { + this.modzuozhi_teParallel = modzuozhi_shouldParallel(); + if (this.modzuozhi_teParallel) { + this.modzuozhi_teBarrier = FineGrainScheduler.beginBarrier(); + } + } + + @Redirect(method = "tickBlockEntities", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/world/level/block/entity/TickingBlockEntity;tick()V")) + private void modzuozhi_teTick(TickingBlockEntity ticker) { + if (this.modzuozhi_teParallel && SerDesFilter.INSTANCE.isParallel(ticker)) { + MinecraftServer server = ((ServerLevel) (Object) this).getServer(); + this.modzuozhi_teBarrier.submit(server, ticker::tick, ticker.getPos()); + } else { + ticker.tick(); + } + } + + @Inject(method = "tickBlockEntities", at = @At("RETURN")) + private void modzuozhi_teEnd(CallbackInfo ci) { + if (this.modzuozhi_teParallel && this.modzuozhi_teBarrier != null) { + this.modzuozhi_teBarrier.await(); + this.modzuozhi_teBarrier.drain(); + this.modzuozhi_teBarrier = null; + this.modzuozhi_teParallel = false; + } + } + + @Inject(method = "addBlockEntityTicker", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferTicker(TickingBlockEntity ticker, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> this.addBlockEntityTicker(ticker)); + ci.cancel(); + } + } + + @Inject(method = "addFreshBlockEntities", at = @At("HEAD"), cancellable = true) + private void modzuozhi_deferFresh(Collection entities, CallbackInfo ci) { + PostExecuteQueue queue = FineGrainScheduler.currentQueue(); + if (queue != null) { + queue.post(() -> this.addFreshBlockEntities(entities)); + ci.cancel(); + } + } +} diff --git a/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java b/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java new file mode 100644 index 0000000..4d00a54 --- /dev/null +++ b/src/main/java/dev/modzuozhi/mixin/YieldJobSiteGuardMixin.java @@ -0,0 +1,89 @@ +package dev.modzuozhi.mixin; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.GlobalPos; +import net.minecraft.core.Holder; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.behavior.BehaviorControl; +import net.minecraft.world.entity.ai.behavior.BehaviorUtils; +import net.minecraft.world.entity.ai.behavior.YieldJobSite; +import net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.ai.village.poi.PoiType; +import net.minecraft.world.entity.npc.Villager; +import net.minecraft.world.entity.npc.VillagerProfession; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.Optional; + +/** + * 跨维度安全:{@link YieldJobSite} 添加维度检查。 + *

+ * 原版 {@code YieldJobSite.create()} 不做维度检查,当 {@code POTENTIAL_JOB_SITE} 存了 + * 另一个维度的 {@code GlobalPos} 时,村民会跨维度让工作站。并行环境下需确保 AI 只在本维度 + * 操作,避免跨维度引用导致的不一致。 + *

+ * 借鉴 Chlorophyll(Unlicense)的 {@code YieldJobSiteMixin} 思路,增加 + * {@code globalPos.dimension() != villager.level().dimension()} 检查,维度不匹配时直接返回。 + */ +@Mixin(YieldJobSite.class) +public abstract class YieldJobSiteGuardMixin { + + @Shadow + private static boolean nearbyWantsJobsite(Holder holder, Villager villager, BlockPos pos) { + throw new AssertionError(); + } + + /** + * @reason 添加维度守卫,防止跨维度让工作站 + */ + @Overwrite + public static BehaviorControl create(float f) { + return BehaviorBuilder.create((instance) -> + instance.group( + instance.present(MemoryModuleType.POTENTIAL_JOB_SITE), + instance.absent(MemoryModuleType.JOB_SITE), + instance.present(MemoryModuleType.NEAREST_LIVING_ENTITIES), + instance.registered(MemoryModuleType.WALK_TARGET), + instance.registered(MemoryModuleType.LOOK_TARGET) + ).apply(instance, (memoryAccessor, memoryAccessor2, memoryAccessor3, memoryAccessor4, memoryAccessor5) -> + (serverLevel, villager, l) -> { + if (villager.isBaby()) { + return false; + } + if (villager.getVillagerData().getProfession() != VillagerProfession.NONE) { + return false; + } + GlobalPos globalPos = instance.get(memoryAccessor); + // 维度守卫:潜在工作站不在当前维度则跳过 + if (globalPos.dimension() != villager.level().dimension()) { + return true; + } + BlockPos blockPos = globalPos.pos(); + Optional> optional = serverLevel.getPoiManager().getType(blockPos); + + optional.flatMap(poiTypeHolder -> + instance.get(memoryAccessor3).stream() + .filter((livingEntity) -> livingEntity instanceof Villager && livingEntity != villager) + .map((livingEntity) -> (Villager) livingEntity) + .filter(LivingEntity::isAlive) + .filter((villagerx) -> nearbyWantsJobsite(poiTypeHolder, villagerx, blockPos)) + .findFirst() + ).ifPresent((villagerx) -> { + memoryAccessor4.erase(); + memoryAccessor5.erase(); + memoryAccessor.erase(); + if (villagerx.getBrain().getMemory(MemoryModuleType.JOB_SITE).isEmpty()) { + BehaviorUtils.setWalkAndLookTargetMemories(villagerx, blockPos, f, 1); + villagerx.getBrain().setMemory(MemoryModuleType.POTENTIAL_JOB_SITE, GlobalPos.of(serverLevel.dimension(), blockPos)); + } + }); + + return true; + } + ) + ); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/accesstransformer.cfg b/src/main/resources/META-INF/accesstransformer.cfg new file mode 100644 index 0000000..c860f6c --- /dev/null +++ b/src/main/resources/META-INF/accesstransformer.cfg @@ -0,0 +1,6 @@ +# 允许 dimthreads 式 swapThreadsAndRun 在非构造方法中写这些字段 +# -f 表示移除 final 修饰符,使 Java 21 允许跨方法写入 +public-f net.minecraft.world.level.Level thread # thread +public-f net.minecraft.server.level.ServerChunkCache mainThread # mainThread +# 第10步:允许在构造器中把 ClassInstanceMultiMap.byClass 换成 ConcurrentHashMap(修 find 的 computeIfAbsent 并发 CME) +public-f net.minecraft.util.ClassInstanceMultiMap byClass # byClass diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..73089b0 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,30 @@ +modLoader="javafml" +loaderVersion="${loader_version_range}" +license="${mod_license}" +issueTrackerURL="https://example.com/issues" + +[[mods]] +modId="${mod_id}" +version="${mod_version}" +displayName="${mod_name}" +authors="${mod_authors}" +description='''${mod_description}''' +logoFile="logo.png" +accesstransformer="META-INF/accesstransformer.cfg" + +[[dependencies.${mod_id}]] + modId="neoforge" + mandatory=true + versionRange="[21.1.0,)" + ordering="NONE" + side="BOTH" + +[[dependencies.${mod_id}]] + modId="minecraft" + mandatory=true + versionRange="${minecraft_version_range}" + ordering="NONE" + side="BOTH" + +[[mixins]] +config="dimensionalripper.mixins.json" \ No newline at end of file diff --git a/src/main/resources/dimensionalripper.mixins.json b/src/main/resources/dimensionalripper.mixins.json new file mode 100644 index 0000000..9995018 --- /dev/null +++ b/src/main/resources/dimensionalripper.mixins.json @@ -0,0 +1,72 @@ +{ + "required": true, + "minVersion": "0.8.5", + "package": "dev.modzuozhi.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "AttributeInstanceMixin", + "AttributeMapMixin", + "BabyFollowAdultGuardMixin", + "BinaryHeapMixin", + "BlockableEventLoopPollMixin", + "BlockDropCaptureMixin", + "BrainMixin", + "ChunkMapLoadOffloadMixin", + "ClassInstanceMultiMapMixin", + "CollectingNeighborUpdaterMixin", + "DistanceManagerMixin", + "EntityChangeDimensionMixin", + "EntityGetEntityCollisionsMixin", + "EntityLookupMixin", + "EntitySectionManagerMixin", + "EntitySectionMixin", + "EntitySectionStorageMixin", + "EntityTickListMixin", + "EntityTickParallelMixin", + "GameEventDispatcherMixin", + "GossipContainerMixin", + "GoToPotentialJobSiteGuardMixin", + "ILevelMixin", + "LevelChunkMixin", + "LevelExplodeMixin", + "LevelGetChunkMixin", + "LevelMixin", + "LevelTicksMixin", + "LivingEntityGetInBlockStateMixin", + "MinecraftServerDimMixin", + "MinecraftServerSaveMixin", + "MinecraftServerTimeSyncAccessor", + "NavigatingMobsMixin", + "NearestLivingEntitySensorMixin", + "PacketUtilsDimMixin", + "PoiCompetitorScanGuardMixin", + "PoiManagerDistanceTrackerMixin", + "PoiSectionMixin", + "PlayerListSaveMixin", + "PortalProcessorMixin", + "SectionStorageMixin", + "ServerChunkCacheAccessor", + "ServerChunkCacheGetChunkFallbackMixin", + "ServerChunkCacheMixin", + "ServerChunkCachePollMixin", + "ServerChunkCacheRunDistanceMixin", + "ServerCommonPacketListenerDimMixin", + "ServerConnectionListenerMixin", + "ServerLevelBlockEventMixin", + "ServerLevelChunkTickMixin", + "ServerLevelEntityCallbacksMixin", + "ServerLevelEntityTickMixin", + "ServerLevelPlayerPacketMixin", + "ServerPlayerChangeDimensionMixin", + "ServerPlayerPrefetchMixin", + "TickingBlockEntityParallelMixin", + "YieldJobSiteGuardMixin" + ], + "client": [ + ], + "server": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png new file mode 100644 index 0000000..ded6654 Binary files /dev/null and b/src/main/resources/logo.png differ