Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions early/src/main/java/cc/irori/refixes/early/EarlyOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public final class EarlyOptions {
public static final Value<Boolean> SHARED_INSTANCES_ENABLED = new Value<>();
public static final Value<String[]> SHARED_INSTANCES_EXCLUDED_PREFIXES = new Value<>();

/* Parallel Spatial Collection */
public static final Value<Boolean> PARALLEL_SPATIAL_COLLECTION = new Value<>();

/* Parallel Steering Threshold */
public static final Value<Integer> PARALLEL_STEERING_THRESHOLD = new Value<>();

// Private constructor to prevent instantiation
private EarlyOptions() {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cc.irori.refixes.early.mixin;

import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.protocol.ComponentUpdate;
import com.hypixel.hytale.protocol.ComponentUpdateType;
import com.hypixel.hytale.server.core.modules.entity.tracker.EntityTrackerSystems;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import java.util.Set;
import javax.annotation.Nonnull;
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;

// Fixes "Entity is not visible!" crash in EntityViewer.queueUpdate/queueRemove.

@Mixin(EntityTrackerSystems.EntityViewer.class)
public abstract class MixinEntityViewer {

@Shadow
public Set<Ref<EntityStore>> visible;

@Inject(method = "queueUpdate", at = @At("HEAD"), cancellable = true)
private void refixes$fixInvisibleUpdate(
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentUpdate update, CallbackInfo ci) {
if (!this.visible.contains(ref)) {
ci.cancel();
}
}

@Inject(method = "queueRemove", at = @At("HEAD"), cancellable = true)
private void refixes$fixInvisibleRemove(
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentUpdateType type, CallbackInfo ci) {
if (!this.visible.contains(ref)) {
ci.cancel();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cc.irori.refixes.early.mixin;

import cc.irori.refixes.early.EarlyOptions;
import cc.irori.refixes.early.util.ParallelSpatialCollector;
import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.ResourceType;
import com.hypixel.hytale.component.Store;
import com.hypixel.hytale.component.spatial.SpatialData;
import com.hypixel.hytale.component.spatial.SpatialResource;
import com.hypixel.hytale.component.spatial.SpatialSystem;
import com.hypixel.hytale.math.vector.Vector3d;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nonnull;
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;

/**
* Parallelizes the entity position collection phase of SpatialSystem.tick().
* The sequential forEachChunk loop is replaced with parallel per-chunk collection
* into thread-local buffers, followed by a sequential merge into SpatialData.
* The KDTree rebuild() remains single-threaded.
*/
@Mixin(SpatialSystem.class)
public abstract class MixinSpatialSystem<ECS_TYPE> {

@Shadow
@Final
private ResourceType<ECS_TYPE, SpatialResource<Ref<ECS_TYPE>, ECS_TYPE>> resourceType;

@Shadow
public abstract Vector3d getPosition(@Nonnull ArchetypeChunk<ECS_TYPE> chunk, int index);

@Inject(method = "tick(FILcom/hypixel/hytale/component/Store;)V", at = @At("HEAD"), cancellable = true)
private void refixes$parallelCollect(float dt, int systemIndex, Store<ECS_TYPE> store, CallbackInfo ci) {
if (!EarlyOptions.isAvailable() || !EarlyOptions.PARALLEL_SPATIAL_COLLECTION.get()) {
return;
}

ci.cancel();

SpatialResource<Ref<ECS_TYPE>, ECS_TYPE> spatialResource = store.getResource(this.resourceType);
SpatialData<Ref<ECS_TYPE>> spatialData = spatialResource.getSpatialData();
spatialData.clear();

// Collect matching archetype chunks via forEachChunk API
List<ParallelSpatialCollector.ChunkWork<ECS_TYPE>> chunks = new ArrayList<>();
store.forEachChunk(systemIndex, (archetypeChunk, commandBuffer) -> {
chunks.add(new ParallelSpatialCollector.ChunkWork<>(archetypeChunk, this::getPosition));
});

// Parallel collection and sequential merge into SpatialData
ParallelSpatialCollector.collectParallel(chunks, spatialData);

// Rebuild the spatial structure (KDTree) single-threaded
spatialResource.getSpatialStructure().rebuild(spatialData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cc.irori.refixes.early.mixin;

import cc.irori.refixes.early.EarlyOptions;
import com.hypixel.hytale.component.task.ParallelRangeTask;
import com.hypixel.hytale.server.npc.systems.SteeringSystem;
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;

/**
* Enables parallel steering when PARALLEL_ENTITY_TICKING is on.
*
* Only activates for archetype chunks exceeding the configurable threshold
* (default 64) to ensure the parallelism payoff exceeds the overhead and risk.
*/
@Mixin(SteeringSystem.class)
public class MixinSteeringSystem {

@Inject(method = "isParallel", at = @At("HEAD"), cancellable = true)
private void refixes$parallelSteering(int archetypeChunkSize, int taskCount, CallbackInfoReturnable<Boolean> cir) {
if (EarlyOptions.isAvailable() && EarlyOptions.PARALLEL_ENTITY_TICKING.get()) {
int threshold = EarlyOptions.PARALLEL_STEERING_THRESHOLD.get();
if (archetypeChunkSize >= threshold) {
cir.setReturnValue(taskCount > 0 || archetypeChunkSize > ParallelRangeTask.PARALLELISM);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
import com.hypixel.hytale.server.core.util.thread.TickingThread;
import java.util.concurrent.locks.LockSupport;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
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.Redirect;

// Replaces deprecated Thread.stop() with Thread.interrupt() on Java 21+
// Also relaxes isInThread() for parallel entity ticking worker threads

@Mixin(TickingThread.class)
public class MixinTickingThread {

@Shadow
private Thread thread;

@Unique
private static final HytaleLogger refixes$LOGGER = Logs.logger();

Expand All @@ -41,4 +47,17 @@ public class MixinTickingThread {
// Park for 100ns, loop will re-check and spin-wait naturally as it approaches the deadline
LockSupport.parkNanos(100_000L);
}

// Relaxes isInThread() to also return true for parallel entity ticking worker threads.
@Overwrite
public boolean isInThread() {
Thread current = Thread.currentThread();
if (current.equals(this.thread)) {
return true;
}
if (current instanceof java.util.concurrent.ForkJoinWorkerThread fjwt) {
return fjwt.getPool() == java.util.concurrent.ForkJoinPool.commonPool();
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package cc.irori.refixes.early.util;

import com.hypixel.hytale.component.ArchetypeChunk;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.component.spatial.SpatialData;
import com.hypixel.hytale.math.vector.Vector3d;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;

/**
* Parallelizes the entity position collection phase of SpatialSystem.tick().
* Each ForkJoin worker collects (position, ref) pairs into a local buffer,
* then all buffers are merged sequentially into SpatialData.
*/
public final class ParallelSpatialCollector {

private ParallelSpatialCollector() {}

// A collected spatial entry: position + entity reference.
public static final class Entry<ECS_TYPE> {
public final double x, y, z;
public final Ref<ECS_TYPE> ref;

public Entry(double x, double y, double z, Ref<ECS_TYPE> ref) {
this.x = x;
this.y = y;
this.z = z;
this.ref = ref;
}
}

// A work unit: one archetype chunk to collect positions from.
public static final class ChunkWork<ECS_TYPE> {
public final ArchetypeChunk<ECS_TYPE> chunk;
public final PositionExtractor<ECS_TYPE> extractor;

public ChunkWork(ArchetypeChunk<ECS_TYPE> chunk, PositionExtractor<ECS_TYPE> extractor) {
this.chunk = chunk;
this.extractor = extractor;
}
}

@FunctionalInterface
public interface PositionExtractor<ECS_TYPE> {
Vector3d getPosition(ArchetypeChunk<ECS_TYPE> chunk, int index);
}

// Collects entity positions from the given chunks in parallel, then merges into spatialData.
public static <ECS_TYPE> void collectParallel(
List<ChunkWork<ECS_TYPE>> chunks, SpatialData<Ref<ECS_TYPE>> spatialData) {
if (chunks.isEmpty()) {
return;
}

// For small workloads, collect sequentially
if (chunks.size() <= 2) {
collectSequential(chunks, spatialData);
return;
}

ForkJoinPool pool = ForkJoinPool.commonPool();
List<ForkJoinTask<List<Entry<ECS_TYPE>>>> tasks = new ArrayList<>(chunks.size());

for (ChunkWork<ECS_TYPE> work : chunks) {
tasks.add(pool.submit(() -> collectChunk(work)));
}

// Merge results sequentially into SpatialData
for (ForkJoinTask<List<Entry<ECS_TYPE>>> task : tasks) {
List<Entry<ECS_TYPE>> entries = task.join();
if (entries.isEmpty()) {
continue;
}
spatialData.addCapacity(entries.size());
for (Entry<ECS_TYPE> entry : entries) {
spatialData.append(new Vector3d(entry.x, entry.y, entry.z), entry.ref);
}
}
}

private static <ECS_TYPE> List<Entry<ECS_TYPE>> collectChunk(ChunkWork<ECS_TYPE> work) {
int size = work.chunk.size();
List<Entry<ECS_TYPE>> entries = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Vector3d position = work.extractor.getPosition(work.chunk, i);
if (position == null) {
continue;
}
Ref<ECS_TYPE> ref = work.chunk.getReferenceTo(i);
entries.add(new Entry<>(position.x, position.y, position.z, ref));
}
return entries;
}

private static <ECS_TYPE> void collectSequential(
List<ChunkWork<ECS_TYPE>> chunks, SpatialData<Ref<ECS_TYPE>> spatialData) {
for (ChunkWork<ECS_TYPE> work : chunks) {
int size = work.chunk.size();
spatialData.addCapacity(size);
for (int i = 0; i < size; i++) {
Vector3d position = work.extractor.getPosition(work.chunk, i);
if (position == null) {
continue;
}
Ref<ECS_TYPE> ref = work.chunk.getReferenceTo(i);
spatialData.append(position, ref);
}
}
}
}
5 changes: 4 additions & 1 deletion early/src/main/resources/refixes.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"MixinCommandBuffer",
"MixinCraftingManagerAccessor",
"MixinEntityTickingSystem",
"MixinEntityViewer",
"MixinFluidPlugin",
"MixinFluidReplicateChanges",
"MixinGamePacketHandler",
Expand Down Expand Up @@ -42,6 +43,8 @@
"MixinWorld",
"MixinWorldConfig",
"MixinWorldMapTracker",
"MixinWorldSpawningSystem"
"MixinWorldSpawningSystem",
"MixinSpatialSystem",
"MixinSteeringSystem"
]
}
4 changes: 4 additions & 0 deletions plugin/src/main/java/cc/irori/refixes/Refixes.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ private void registerEarlyOptions() {

EarlyOptions.PARALLEL_ENTITY_TICKING.setSupplier(
() -> experimentalConfig.getValue(ExperimentalConfig.PARALLEL_ENTITY_TICKING));
EarlyOptions.PARALLEL_SPATIAL_COLLECTION.setSupplier(
() -> experimentalConfig.getValue(ExperimentalConfig.PARALLEL_SPATIAL_COLLECTION));
EarlyOptions.PARALLEL_STEERING_THRESHOLD.setSupplier(
() -> experimentalConfig.getValue(ExperimentalConfig.PARALLEL_STEERING_THRESHOLD));

EarlyOptions.setAvailable(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ public class ExperimentalConfig extends Configuration<ExperimentalConfig> {
public static final ConfigurationKey<ExperimentalConfig, Boolean> PARALLEL_ENTITY_TICKING =
new ConfigurationKey<>("ParallelEntityTicking", ConfigField.BOOLEAN, false);

public static final ConfigurationKey<ExperimentalConfig, Boolean> PARALLEL_SPATIAL_COLLECTION =
new ConfigurationKey<>("ParallelSpatialCollection", ConfigField.BOOLEAN, false);

public static final ConfigurationKey<ExperimentalConfig, Integer> PARALLEL_STEERING_THRESHOLD =
new ConfigurationKey<>("ParallelSteeringThreshold", ConfigField.INTEGER, 64);

private static final ExperimentalConfig INSTANCE = new ExperimentalConfig();

public ExperimentalConfig() {
register(PARALLEL_ENTITY_TICKING);
register(
PARALLEL_ENTITY_TICKING,
PARALLEL_SPATIAL_COLLECTION,
PARALLEL_STEERING_THRESHOLD);
}

public static ExperimentalConfig get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ public class WatchdogService {

private volatile State state = State.ACTIVATING;

public WatchdogService() {
lastDefaultWorld = Universe.get().getDefaultWorld();
}
public WatchdogService() {}

public State getState() {
return state;
Expand All @@ -47,11 +45,14 @@ public void registerService() {

public void unregisterService() {
LOGGER.atInfo().log("Stopping server watchdog");
watchdogThread.interrupt();
if (watchdogThread != null) {
watchdogThread.interrupt();
}
}

private void start() {
LOGGER.atInfo().log("Starting server watchdog (default world: %s)", lastDefaultWorld.getName());
String worldName = lastDefaultWorld != null ? lastDefaultWorld.getName() : "<not loaded>";
LOGGER.atInfo().log("Starting server watchdog (default world: %s)", worldName);
watchdogThread = new Thread(this::runWatchdog, "Refixes-Watchdog");
watchdogThread.setDaemon(true);
watchdogThread.start();
Expand Down Expand Up @@ -193,7 +194,7 @@ private void watchForAutoRestartingWorlds() {
} else if (response != null) {
long elapsed = System.currentTimeMillis() - response;
if (elapsed > config.getValue(WatchdogConfig.THREAD_TIMEOUT_MS)) {
LOGGER.atSevere().log("World %s did not respond for %.2f seconds.", worldName, elapsed / 1000);
LOGGER.atSevere().log("World %s did not respond for %.2f seconds.", worldName, elapsed / 1000.0);
restart = true;
}
}
Expand Down