diff --git a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java index b626878fd..a8d72bc8e 100644 --- a/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java +++ b/src/main/java/me/cortex/voxy/client/mixin/minecraft/MixinLevelRenderer.java @@ -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; @@ -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; @@ -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; @@ -78,13 +86,55 @@ 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(); @@ -92,6 +142,6 @@ public abstract class MixinLevelRenderer implements IGetVoxyRenderSystem { throw e; } } - instance.updateDedicatedThreads(); + capturedInstance.updateDedicatedThreads(); } }