Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.commonImpl.VoxyCommon;
import me.cortex.voxy.commonImpl.WorldIdentifier;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.LevelRenderer;
import org.jetbrains.annotations.Nullable;
Expand All @@ -24,6 +25,11 @@ public abstract class MixinLevelRenderer implements IGetVoxyRenderSystem {
@Shadow private @Nullable ClientLevel level;
@Unique private VoxyRenderSystem renderer;

// Bumped on every shutdown/create (all on the render thread). An in-flight async engine
// load captures the value at start and re-checks it before installing the renderer, so a
// load that has been superseded by a level change / disconnect / reload is discarded.
@Unique private int voxy$loadGeneration;

@Override
public VoxyRenderSystem voxy$getRenderSystem() {
return this.renderer;
Expand Down Expand Up @@ -51,6 +57,8 @@ public abstract class MixinLevelRenderer implements IGetVoxyRenderSystem {

@Override
public void voxy$shutdownRenderer() {
// invalidate any in-flight async engine load
this.voxy$loadGeneration++;
if (this.renderer != null) {
this.renderer.shutdown();
this.renderer = null;
Expand Down Expand Up @@ -78,20 +86,62 @@ public abstract class MixinLevelRenderer implements IGetVoxyRenderSystem {
Logger.info("Not creating renderer due to null instance");
return;
}
WorldEngine world = WorldIdentifier.ofEngine(this.level);
if (world == null) {
Logger.error("Null world selected");

// Opening the WorldEngine (RocksDB open + Mapper/palette load) reads the on-disk LOD
// store and can take many seconds for a large store. Done here on the render thread
// (inside LevelRenderer.allChanged) it freezes the client on join. So build the engine
// on a background thread, then install the GL-bound VoxyRenderSystem back on the render
// thread once the engine is ready.
final ClientLevel capturedLevel = this.level;
final VoxyClientInstance capturedInstance = instance;
final int generation = ++this.voxy$loadGeneration;

Thread loader = new Thread(() -> {
WorldEngine world;
try {
world = WorldIdentifier.ofEngine(capturedLevel);
} catch (Throwable e) {
Logger.error("Async Voxy engine load failed", e);
return;
}
if (world == null) {
Logger.error("Null world selected");
return;
}
Minecraft.getInstance().execute(() ->
this.voxy$installRenderer(world, capturedInstance, capturedLevel, generation));
}, "Voxy-EngineLoader");
loader.setDaemon(true);
loader.start();
}

// Runs on the render thread (GL context current) after the engine has been built off-thread.
@Unique
private void voxy$installRenderer(WorldEngine world, VoxyClientInstance capturedInstance,
ClientLevel capturedLevel, int generation) {
if (generation != this.voxy$loadGeneration) {
// superseded by a shutdown / newer load; leave the engine cached (Voxy's idle-world
// cleaner frees it if unused) and do nothing.
return;
}
if (this.level != capturedLevel || this.level == null) {
return;
}
if (this.renderer != null) {
return;
}
if (VoxyCommon.getInstance() != capturedInstance) {
return;
}
try {
this.renderer = new VoxyRenderSystem(world, instance.getServiceManager());
this.renderer = new VoxyRenderSystem(world, capturedInstance.getServiceManager());
} catch (RuntimeException e) {
if (IrisUtil.irisShaderPackEnabled()) {
IrisUtil.disableIrisShaders();
} else {
throw e;
}
}
instance.updateDedicatedThreads();
capturedInstance.updateDedicatedThreads();
}
}