170 lines
9.9 KiB
Java
170 lines
9.9 KiB
Java
package dev.modzuozhi;
|
|
|
|
import com.mojang.authlib.GameProfile;
|
|
import dev.modzuozhi.core.compat.SableAssemblyDispatch;
|
|
import dev.modzuozhi.core.compat.PayloadMainThreadDispatch;
|
|
import dev.modzuozhi.core.dimthread.DimThreadCore;
|
|
import dev.modzuozhi.core.dimthread.FineGrainScheduler;
|
|
import dev.modzuozhi.core.dimthread.FaultGuard;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.world.level.block.Blocks;
|
|
import net.minecraft.world.level.material.Fluids;
|
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.concurrent.atomic.AtomicReference;
|
|
import net.neoforged.bus.api.SubscribeEvent;
|
|
import net.neoforged.fml.common.EventBusSubscriber;
|
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
|
import java.util.UUID;
|
|
|
|
/** Opt-in runtime fixture: no real user world or network connection. */
|
|
@EventBusSubscriber(modid = "dimensionalripper")
|
|
public final class InteractionFlowRegression {
|
|
private static boolean started, completed;
|
|
private static long startTime;
|
|
private static final BlockPos SOURCE = new BlockPos(8, 100, 8);
|
|
@SubscribeEvent
|
|
public static void tick(ServerTickEvent.Post event) {
|
|
var server = event.getServer();
|
|
var level = server.overworld();
|
|
if (server.getTickCount() < 15) return;
|
|
if (!started) {
|
|
started = true;
|
|
DimThreadCore.MANAGER.setPaused(server, true);
|
|
level.setChunkForced(0, 0, true);
|
|
level.getChunk(0, 0);
|
|
checkMissingChunkProbes(server);
|
|
for (int x = 4; x <= 12; x++) for (int z = 4; z <= 12; z++) {
|
|
level.setBlockAndUpdate(new BlockPos(x, 99, z), Blocks.STONE.defaultBlockState());
|
|
level.setBlockAndUpdate(new BlockPos(x, 100, z), Blocks.AIR.defaultBlockState());
|
|
}
|
|
level.setBlockAndUpdate(SOURCE.above(3), Blocks.CHEST.defaultBlockState());
|
|
// A real listener is now part of the shared dispatch's stale-connection contract.
|
|
var player = new net.neoforged.neoforge.common.util.FakePlayer(level,
|
|
new GameProfile(UUID.randomUUID(), "Regression"));
|
|
startTime = level.getGameTime();
|
|
DimThreadCore.MANAGER.setPaused(server, false);
|
|
SableAssemblyDispatch.dispatch(player, () -> {
|
|
if (!server.isSameThread() || !PayloadMainThreadDispatch.owns(server)
|
|
|| DimThreadCore.MANAGER.ownershipLoopsSnapshot().stream().anyMatch(loop -> loop.isTicking()))
|
|
throw new AssertionError("Interaction did not obtain exclusive server ownership");
|
|
if (level.getBlockEntity(SOURCE.above(3)) == null)
|
|
throw new AssertionError("Main-thread block entity lookup silently failed");
|
|
level.setBlockAndUpdate(SOURCE, Blocks.WATER.defaultBlockState());
|
|
completed = true;
|
|
});
|
|
}
|
|
if (server.getTickCount() == 180) {
|
|
DimThreadCore.MANAGER.setPaused(server, true);
|
|
if (!completed) throw new AssertionError("Interaction was not dispatched");
|
|
var progress = DimThreadCore.MANAGER.getLoop(level).progress(System.nanoTime());
|
|
if (!(progress.tps() > 0) || Double.isNaN(progress.averageWorkMs())
|
|
|| Double.isNaN(progress.sinceCompletedMs()) || !Double.isNaN(progress.runningMs()))
|
|
throw new AssertionError("Invalid drained-loop progress: " + progress);
|
|
ModZuozhi.LOGGER.info("TICK PROGRESS TEST: {}", progress);
|
|
if (level.getGameTime() <= startTime) throw new AssertionError("World clock stopped");
|
|
String diagnostic = ((dev.modzuozhi.core.compat.ScheduledTickDiagnostics) level.getFluidTicks())
|
|
.dimensionalripper$describeTicks(SOURCE, level.getGameTime());
|
|
if (diagnostic.contains(Long.toString(Long.MIN_VALUE))) throw new AssertionError("Missing tick instrumentation");
|
|
ModZuozhi.LOGGER.info("FLOW DIAGNOSTIC TEST: {}", diagnostic);
|
|
if (!level.getFluidState(SOURCE.east()).is(Fluids.FLOWING_WATER))
|
|
throw new AssertionError("Scheduled water tick did not spread: time=" + level.getGameTime()
|
|
+ " pending=" + level.getFluidTicks().count());
|
|
ModZuozhi.LOGGER.info("INTERACTION REGRESSION PASS: exclusive main-thread block entity lookup and scheduled water spread");
|
|
server.halt(false);
|
|
}
|
|
}
|
|
|
|
/** Run the actual transformed getChunk from two real scheduler subtasks. */
|
|
private static void checkMissingChunkProbes(net.minecraft.server.MinecraftServer server) {
|
|
var level = server.overworld();
|
|
var cache = level.getChunkSource();
|
|
var loaded = level.getChunk(0, 0);
|
|
int loadedCount = cache.getLoadedChunksCount();
|
|
var failure = new AtomicReference<Throwable>();
|
|
var completed = new AtomicInteger();
|
|
var barrier = FineGrainScheduler.beginBarrier("missing-chunk-regression");
|
|
long began = System.nanoTime();
|
|
for (int task = 0; task < 2; task++) {
|
|
barrier.submit(server, () -> {
|
|
try {
|
|
if (!FineGrainScheduler.inSubTask()) throw new AssertionError("Not a subtask");
|
|
for (int i = 0; i < 100_000; i++) {
|
|
// Alternate coordinates: no fake positive or negative cache entries.
|
|
int x = 100_000 + (i & 1);
|
|
if (cache.getChunk(x, -100_000, ChunkStatus.FULL, false) != null)
|
|
throw new AssertionError("Missing FULL probe returned a fabricated chunk");
|
|
if (cache.getChunk(x, -100_000, ChunkStatus.EMPTY, false) != null)
|
|
throw new AssertionError("Missing partial-status probe returned a chunk");
|
|
if (cache.getChunkNow(x, -100_000) != null)
|
|
throw new AssertionError("Missing getChunkNow returned a chunk");
|
|
if (cache.getChunk(0, 0, ChunkStatus.FULL, false) != loaded
|
|
|| cache.getChunk(0, 0, ChunkStatus.FULL, true) != loaded)
|
|
throw new AssertionError("Loaded chunk identity lost");
|
|
}
|
|
completed.incrementAndGet();
|
|
} catch (Throwable error) {
|
|
failure.compareAndSet(null, error);
|
|
}
|
|
});
|
|
}
|
|
barrier.await();
|
|
barrier.drain();
|
|
if (failure.get() != null) throw new AssertionError("Missing chunk regression", failure.get());
|
|
if (completed.get() != 2 || !FineGrainScheduler.isEnabled())
|
|
throw new AssertionError("Probes did not complete with parallelism enabled");
|
|
if (cache.getLoadedChunksCount() != loadedCount)
|
|
throw new AssertionError("Optional probe loaded chunks");
|
|
var rabbit = new net.minecraft.world.entity.animal.Rabbit(net.minecraft.world.entity.EntityType.RABBIT, level);
|
|
rabbit.setPos(8, 101, 8);
|
|
var routed = new AtomicInteger();
|
|
dev.modzuozhi.core.dimthread.EntityTickParallel.begin();
|
|
level.guardEntityTick(entity -> {
|
|
if (!FineGrainScheduler.inSubTask()) throw new AssertionError("Loaded interior lost parallelism");
|
|
routed.incrementAndGet();
|
|
}, rabbit);
|
|
if (routed.get() != 0) throw new AssertionError("Interior entity was not batched");
|
|
dev.modzuozhi.core.dimthread.EntityTickParallel.end();
|
|
if (routed.get() != 1) throw new AssertionError("Interior entity did not run exactly once");
|
|
rabbit.setDeltaMovement(10_000, 0, 0);
|
|
dev.modzuozhi.core.dimthread.EntityTickParallel.begin();
|
|
level.guardEntityTick(entity -> {
|
|
if (FineGrainScheduler.inSubTask()) throw new AssertionError("Unbounded collision scan on subtask");
|
|
routed.incrementAndGet();
|
|
}, rabbit);
|
|
if (routed.get() != 2) throw new AssertionError("Wide movement did not stay on owner");
|
|
dev.modzuozhi.core.dimthread.EntityTickParallel.end();
|
|
if (routed.get() != 2) throw new AssertionError("Owner entity tick duplicated");
|
|
ModZuozhi.LOGGER.info("ENTITY ROUTING REGRESSION PASS: loaded interior parallel, wide movement on owner, exactly once");
|
|
ModZuozhi.LOGGER.info("MISSING CHUNK REGRESSION PASS: 600000 absent reads + 400000 loaded reads, {} ms, fine enabled",
|
|
(System.nanoTime() - began) / 1_000_000);
|
|
|
|
// A required miss must fail instead of allocating a fake chunk or blocking forever.
|
|
var requiredBarrier = FineGrainScheduler.beginBarrier("required-chunk-regression");
|
|
requiredBarrier.submit(server, () -> {
|
|
try {
|
|
for (var status : new ChunkStatus[]{ChunkStatus.FULL, ChunkStatus.EMPTY}) {
|
|
try {
|
|
cache.getChunk(100_000, -100_000, status, true);
|
|
throw new AssertionError("Required missing chunk was fabricated");
|
|
} catch (IllegalStateException expected) {
|
|
if (!expected.getMessage().startsWith("Parallel task requested synchronous chunk generation:"))
|
|
throw expected;
|
|
}
|
|
}
|
|
} catch (Throwable error) {
|
|
failure.compareAndSet(null, error);
|
|
}
|
|
});
|
|
requiredBarrier.await();
|
|
requiredBarrier.drain();
|
|
if (failure.get() != null) throw new AssertionError("Required chunk regression", failure.get());
|
|
if (FineGrainScheduler.isEnabled() || !FaultGuard.isFineDegraded()
|
|
|| cache.getLoadedChunksCount() != loadedCount)
|
|
throw new AssertionError("Required miss did not fail safely");
|
|
FaultGuard.reset();
|
|
FineGrainScheduler.setEnabled(true);
|
|
ModZuozhi.LOGGER.info("REQUIRED CHUNK REGRESSION PASS: FULL/EMPTY fail fast, no generation; test-only fault reset");
|
|
}
|
|
}
|