aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/makamys/neodymium
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/makamys/neodymium')
-rw-r--r--src/main/java/makamys/neodymium/LODMod.java239
-rw-r--r--src/main/java/makamys/neodymium/MixinConfigPlugin.java72
-rw-r--r--src/main/java/makamys/neodymium/ducks/IWorldRenderer.java13
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinChunkCache.java29
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java35
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java29
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java40
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java25
-rw-r--r--src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java181
-rw-r--r--src/main/java/makamys/neodymium/renderer/ChunkMesh.java455
-rw-r--r--src/main/java/makamys/neodymium/renderer/FarChunkCache.java12
-rw-r--r--src/main/java/makamys/neodymium/renderer/FarWorldRenderer.java14
-rw-r--r--src/main/java/makamys/neodymium/renderer/GPUMemoryManager.java215
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODChunk.java184
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODRegion.java195
-rw-r--r--src/main/java/makamys/neodymium/renderer/LODRenderer.java730
-rw-r--r--src/main/java/makamys/neodymium/renderer/Mesh.java44
-rw-r--r--src/main/java/makamys/neodymium/renderer/MeshQuad.java426
-rw-r--r--src/main/java/makamys/neodymium/renderer/SimpleChunkMesh.java350
-rw-r--r--src/main/java/makamys/neodymium/util/BufferWriter.java46
-rw-r--r--src/main/java/makamys/neodymium/util/GuiHelper.java61
-rw-r--r--src/main/java/makamys/neodymium/util/MCUtil.java20
-rw-r--r--src/main/java/makamys/neodymium/util/SpriteUtil.java55
-rw-r--r--src/main/java/makamys/neodymium/util/Util.java88
24 files changed, 3558 insertions, 0 deletions
diff --git a/src/main/java/makamys/neodymium/LODMod.java b/src/main/java/makamys/neodymium/LODMod.java
new file mode 100644
index 0000000..47c948b
--- /dev/null
+++ b/src/main/java/makamys/neodymium/LODMod.java
@@ -0,0 +1,239 @@
+package makamys.neodymium;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.FontRenderer;
+import net.minecraft.client.gui.ScaledResolution;
+import net.minecraft.client.renderer.entity.RenderManager;
+import net.minecraft.entity.player.EntityPlayer;
+import net.minecraft.world.World;
+import net.minecraftforge.client.event.EntityViewRenderEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent;
+import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
+import net.minecraftforge.common.MinecraftForge;
+import net.minecraftforge.common.config.Configuration;
+import net.minecraftforge.event.world.ChunkEvent;
+import net.minecraftforge.event.world.WorldEvent;
+
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.lwjgl.input.Keyboard;
+import org.lwjgl.opengl.GL11;
+
+import cpw.mods.fml.common.FMLCommonHandler;
+import cpw.mods.fml.common.Mod;
+import cpw.mods.fml.common.Mod.EventHandler;
+import cpw.mods.fml.common.event.FMLInitializationEvent;
+import cpw.mods.fml.common.event.FMLPreInitializationEvent;
+import cpw.mods.fml.common.eventhandler.SubscribeEvent;
+import cpw.mods.fml.common.gameevent.TickEvent;
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import makamys.neodymium.renderer.LODRenderer;
+import makamys.neodymium.util.SpriteUtil;
+
+@Mod(modid = LODMod.MODID, version = LODMod.VERSION)
+public class LODMod
+{
+ public static final String MODID = "lodmod";
+ public static final String VERSION = "0.0";
+
+ public static final Logger LOGGER = LogManager.getLogger("lodmod");
+
+ public static LODRenderer renderer;
+
+ public static boolean enabled;
+ public static int chunkLoadsPerTick;
+ public static List<Class> blockClassBlacklist;
+ public static double fogStart;
+ public static double fogEnd;
+ public static double farPlaneDistanceMultiplier;
+ public static float maxSimpleMeshHeight;
+ public static boolean forceVanillaBiomeTemperature;
+ public static boolean hideUnderVanillaChunks;
+ public static boolean disableChunkMeshes;
+ public static boolean disableSimpleMeshes;
+ public static boolean saveMeshes;
+ public static boolean optimizeChunkMeshes;
+ public static int maxMeshesPerFrame;
+ public static int sortFrequency;
+ public static int gcRate;
+ public static int VRAMSize;
+ public static int debugPrefix;
+
+ private File configFile;
+
+ public static boolean fogEventWasPosted;
+
+ public static boolean ofFastRender;
+ public static boolean enableFog;
+
+ @EventHandler
+ public void preInit(FMLPreInitializationEvent event)
+ {
+ configFile = event.getSuggestedConfigurationFile();
+ reloadConfig();
+ }
+
+ private void reloadConfig() {
+ Configuration config = new Configuration(configFile);
+
+ config.load();
+ enabled = config.get("General", "enabled", true).getBoolean();
+ chunkLoadsPerTick = config.get("General", "chunkLoadsPerTick", 64).getInt();
+ blockClassBlacklist = Arrays.stream(config.get("General", "blockClassBlacklist", "net.minecraft.block.BlockRotatedPillar;biomesoplenty.common.blocks.BlockBOPLog;gregapi.block.multitileentity.MultiTileEntityBlock").getString().split(";"))
+ .map(className -> {
+ try {
+ return Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ fogStart = config.get("Fog", "fogStart", "0.4").getDouble();
+ fogEnd = config.get("Fog", "fogEnd", "0.8").getDouble();
+ farPlaneDistanceMultiplier = config.get("Fog", "farPlaneDistanceMultiplier", "1.0").getDouble();
+
+ maxSimpleMeshHeight = (float)config.get("Debug", "maxSimpleMeshHeight", 1000.0).getDouble();
+
+ forceVanillaBiomeTemperature = config.get("Simple mesh generation", "forceVanillaBiomeTemperature", true).getBoolean();
+
+ hideUnderVanillaChunks = config.getBoolean("hideUnderVanillaChunks", "render", true, "");
+ disableChunkMeshes = config.getBoolean("disableChunkMeshes", "render", true, "");
+ disableSimpleMeshes = config.getBoolean("disableSimpleMeshes", "render", false, "");
+ optimizeChunkMeshes = config.getBoolean("optimizeChunkMeshes", "render", true, "");
+ saveMeshes = config.getBoolean("saveMeshes", "render", false, "");
+ maxMeshesPerFrame = config.getInt("maxMeshesPerFrame", "render", -1, -1, Integer.MAX_VALUE, "");
+ sortFrequency = config.getInt("sortFrequency", "render", 1, 1, Integer.MAX_VALUE, "");
+ gcRate = config.getInt("gcRate", "render", 1, 1, Integer.MAX_VALUE, "Maximum number of meshes to relocate each frame.");
+ VRAMSize = config.getInt("VRAMSize", "render", 1024, 1, Integer.MAX_VALUE, "VRAM buffer size (MB).");
+ enableFog = config.getBoolean("enableFog", "render", true, "");
+ debugPrefix = config.getInt("debugPrefix", "debug", Keyboard.KEY_F4, -1, Integer.MAX_VALUE, "This key has to be held down while pressing the debug keybinds. LWJGL keycode. Setting this to 0 will make the keybinds usable without holding anything else down. Setting this to -1 will disable debug keybinds entirely.");
+
+ if(config.hasChanged()) {
+ config.save();
+ }
+ }
+
+ @EventHandler
+ public void init(FMLInitializationEvent event)
+ {
+ FMLCommonHandler.instance().bus().register(this);
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+
+ private void onPlayerWorldChanged(World newWorld) {
+ if(getRendererWorld() == null && newWorld != null) {
+ reloadConfig();
+ if(enabled) {
+ SpriteUtil.init();
+ }
+ }
+ if(renderer != null) {
+ renderer.destroy();
+ renderer = null;
+ }
+ if(enabled && newWorld != null) {
+ renderer = new LODRenderer(newWorld);
+ }
+ }
+
+ @SubscribeEvent
+ @SideOnly(Side.CLIENT)
+ public void onWorldUnload(WorldEvent.Unload event) {
+ if(event.world == getRendererWorld()) {
+ onPlayerWorldChanged(null);
+ }
+ }
+
+ @SubscribeEvent
+ public void onChunkLoad(ChunkEvent.Load event) {
+ if(!event.world.isRemote) return;
+
+ if(isActive()) {
+ renderer.onChunkLoad(event);
+ }
+ }
+
+ public static boolean isActive() {
+ return renderer != null && renderer.hasInited && !renderer.destroyPending;
+ }
+
+ private World getRendererWorld() {
+ return renderer != null ? renderer.world : null;
+ }
+
+ @SubscribeEvent
+ public void onClientTick(TickEvent.ClientTickEvent event) {
+ if(event.phase == TickEvent.Phase.START) {
+ EntityPlayer player = Minecraft.getMinecraft().thePlayer;
+ World world = player != null ? player.worldObj : null;
+ if(world != getRendererWorld()) {
+ onPlayerWorldChanged(world);
+ }
+
+ if(MixinConfigPlugin.isOptiFinePresent()) {
+ try {
+ ofFastRender = (boolean)Class.forName("Config").getMethod("isFastRender").invoke(null);
+ } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
+ | SecurityException | ClassNotFoundException e) {
+ // oops
+ }
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onServerTick(TickEvent.ServerTickEvent event) {
+ if(event.phase == TickEvent.Phase.START) {
+ if(isActive()) {
+ renderer.serverTick();
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderTick(TickEvent.RenderTickEvent event) {
+ if(event.phase == TickEvent.Phase.END) {
+ if(isActive()) {
+ renderer.onRenderTickEnd();
+ }
+ }
+ }
+
+ @SubscribeEvent
+ public void onRenderOverlay(RenderGameOverlayEvent event) {
+ FontRenderer fontRenderer = RenderManager.instance.getFontRenderer();
+ if(isActive() && event.type == ElementType.TEXT && fontRenderer != null && Minecraft.getMinecraft().gameSettings.showDebugInfo)
+ {
+ Minecraft mc = Minecraft.getMinecraft();
+ ScaledResolution scaledresolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight);
+ int w = scaledresolution.getScaledWidth();
+ int h = scaledresolution.getScaledHeight();
+
+ int yOffset = 0;
+ for(String s : renderer.getDebugText()) {
+ fontRenderer.drawStringWithShadow(s, w - fontRenderer.getStringWidth(s) - 10, 80 + yOffset, 0xFFFFFF);
+ yOffset += 10;
+ }
+ }
+ }
+
+
+ @SubscribeEvent
+ public void onRenderFog(EntityViewRenderEvent.RenderFogEvent event) {
+ fogEventWasPosted = true;
+ }
+
+ public static boolean shouldRenderVanillaWorld() {
+ return !isActive() || (isActive() && renderer.renderWorld && !renderer.rendererActive);
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/MixinConfigPlugin.java b/src/main/java/makamys/neodymium/MixinConfigPlugin.java
new file mode 100644
index 0000000..cc4bc30
--- /dev/null
+++ b/src/main/java/makamys/neodymium/MixinConfigPlugin.java
@@ -0,0 +1,72 @@
+package makamys.neodymium;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import org.spongepowered.asm.lib.tree.ClassNode;
+import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
+import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
+
+public class MixinConfigPlugin implements IMixinConfigPlugin {
+
+ private static boolean isOptiFinePresent = MixinConfigPlugin.class.getResource("/optifine/OptiFineTweaker.class") != null;
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public List<String> getMixins() {
+ List<String> mixins = new ArrayList<>();
+ mixins.addAll(Arrays.asList("MixinChunkCache",
+ "MixinEntityRenderer",
+ "MixinRenderGlobal",
+ "MixinWorldRenderer",
+ "MixinRenderBlocks"));
+
+ if (isOptiFinePresent()) {
+ System.out.println("Detected OptiFine");
+ mixins.add("MixinRenderGlobal_OptiFine");
+ }
+
+ return mixins;
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public static boolean isOptiFinePresent() {
+ return isOptiFinePresent;
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java b/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java
new file mode 100644
index 0000000..4525cda
--- /dev/null
+++ b/src/main/java/makamys/neodymium/ducks/IWorldRenderer.java
@@ -0,0 +1,13 @@
+package makamys.neodymium.ducks;
+
+import java.util.List;
+
+import org.spongepowered.asm.mixin.Mixin;
+
+import makamys.neodymium.renderer.ChunkMesh;
+import net.minecraft.client.renderer.WorldRenderer;
+
+public interface IWorldRenderer {
+ public List<ChunkMesh> getChunkMeshes();
+ public boolean isDrawn();
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java b/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java
new file mode 100644
index 0000000..b4a7368
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinChunkCache.java
@@ -0,0 +1,29 @@
+package makamys.neodymium.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.renderer.FarChunkCache;
+import makamys.neodymium.renderer.LODRenderer;
+import net.minecraft.world.ChunkCache;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.Chunk;
+
+@Mixin(ChunkCache.class)
+abstract class MixinChunkCache {
+
+ @Redirect(method = "<init>*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;getChunkFromChunkCoords(II)Lnet/minecraft/world/chunk/Chunk;"))
+ private Chunk redirectGetChunkFromChunkCoords(World world, int p1, int p2) {
+ Chunk chunk = world.getChunkFromChunkCoords(p1, p2);
+ if(LODMod.isActive() && FarChunkCache.class.isInstance(this.getClass()) && chunk.isEmpty()) {
+ Chunk myChunk = LODMod.renderer.getChunkFromChunkCoords(p1, p2);
+ if(myChunk != null) {
+ chunk = myChunk;
+ }
+ }
+ return chunk;
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java b/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java
new file mode 100644
index 0000000..66d95ae
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinEntityRenderer.java
@@ -0,0 +1,35 @@
+package makamys.neodymium.mixin;
+
+import org.lwjgl.opengl.GL11;
+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.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.EntityRenderer;
+import net.minecraft.entity.EntityLivingBase;
+
+@Mixin(EntityRenderer.class)
+abstract class MixinEntityRenderer {
+
+ @Shadow
+ private float farPlaneDistance;
+
+ @Inject(method = "setupCameraTransform", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/EntityRenderer;farPlaneDistance:F", shift = At.Shift.AFTER, ordinal = 1))
+ private void onConstructed(CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ farPlaneDistance *= LODMod.renderer.getFarPlaneDistanceMultiplier();
+ }
+ }
+
+ @Inject(method = "setupFog", at = @At(value = "RETURN"))
+ private void afterSetupFog(int mode, float alpha, CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.afterSetupFog(mode, alpha, farPlaneDistance);
+ }
+ }
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java b/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java
new file mode 100644
index 0000000..0f8ff41
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderBlocks.java
@@ -0,0 +1,29 @@
+package makamys.neodymium.mixin;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import cpw.mods.fml.relauncher.Side;
+import cpw.mods.fml.relauncher.SideOnly;
+import makamys.neodymium.LODMod;
+import makamys.neodymium.renderer.FarChunkCache;
+import net.minecraft.block.Block;
+import net.minecraft.client.renderer.RenderBlocks;
+import net.minecraft.world.IBlockAccess;
+import net.minecraft.world.World;
+import net.minecraft.world.chunk.Chunk;
+
+@Mixin(RenderBlocks.class)
+abstract class MixinRenderBlocks {
+
+ @Redirect(method = "renderBlockLiquid", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;shouldSideBeRendered(Lnet/minecraft/world/IBlockAccess;IIII)Z"))
+ public boolean shouldSideBeRendered(Block block, IBlockAccess ba, int x, int y, int z, int w) {
+ if(LODMod.isActive()) {
+ return LODMod.renderer.shouldSideBeRendered(block, ba, x, y, z, w);
+ } else {
+ return block.shouldSideBeRendered(ba, x, y, z, w);
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java
new file mode 100644
index 0000000..dc561a2
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal.java
@@ -0,0 +1,40 @@
+package makamys.neodymium.mixin;
+
+import java.nio.IntBuffer;
+
+import org.lwjgl.opengl.GL11;
+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.ModifyVariable;
+import org.spongepowered.asm.mixin.injection.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.RenderGlobal;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.entity.Entity;
+
+@Mixin(RenderGlobal.class)
+abstract class MixinRenderGlobal {
+
+ @Shadow
+ private WorldRenderer[] sortedWorldRenderers;
+
+ @Redirect(method = "renderSortedRenderers", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderAllRenderLists(ID)V"))
+ private void redirectRenderAllRenderLists(RenderGlobal thiz, int p1, double p2) {
+ if(LODMod.shouldRenderVanillaWorld()) {
+ thiz.renderAllRenderLists(p1, p2);
+ }
+ }
+
+ @Inject(method = "renderSortedRenderers", at = @At(value = "HEAD"))
+ public void preRenderSortedRenderers(int startRenderer, int numRenderers, int renderPass, double partialTickTime, CallbackInfoReturnable cir) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.preRenderSortedRenderers(renderPass, partialTickTime, sortedWorldRenderers);
+ }
+ }
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java
new file mode 100644
index 0000000..dad164c
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinRenderGlobal_OptiFine.java
@@ -0,0 +1,25 @@
+package makamys.neodymium.mixin;
+
+import java.nio.IntBuffer;
+
+import org.lwjgl.opengl.GL11;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Redirect;
+
+import makamys.neodymium.LODMod;
+import net.minecraft.client.renderer.RenderBlocks;
+import net.minecraft.client.renderer.RenderGlobal;
+
+@Mixin(RenderGlobal.class)
+abstract class MixinRenderGlobal_OptiFine {
+
+ // for OptiFine's Fast Render option
+ @Redirect(method = "renderSortedRenderersFast", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glCallLists(Ljava/nio/IntBuffer;)V"), remap=false)
+ private void redirectRenderAllRenderLists(IntBuffer buffer) {
+ if(LODMod.shouldRenderVanillaWorld()) {
+ GL11.glCallLists(buffer);
+ }
+ }
+
+}
diff --git a/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java b/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java
new file mode 100644
index 0000000..47450c3
--- /dev/null
+++ b/src/main/java/makamys/neodymium/mixin/MixinWorldRenderer.java
@@ -0,0 +1,181 @@
+package makamys.neodymium.mixin;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.lwjgl.opengl.GL11;
+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.Redirect;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.ducks.IWorldRenderer;
+import makamys.neodymium.renderer.ChunkMesh;
+import makamys.neodymium.renderer.FarChunkCache;
+import makamys.neodymium.renderer.FarWorldRenderer;
+import makamys.neodymium.renderer.LODRenderer;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.client.renderer.entity.RenderItem;
+import net.minecraft.entity.EntityLivingBase;
+import net.minecraft.util.AxisAlignedBB;
+import net.minecraft.world.ChunkCache;
+import net.minecraft.world.World;
+
+@Mixin(WorldRenderer.class)
+abstract class MixinWorldRenderer implements IWorldRenderer {
+
+ @Shadow
+ public int posX;
+ @Shadow
+ public int posY;
+ @Shadow
+ public int posZ;
+
+ @Shadow
+ private boolean isInFrustum;
+ @Shadow
+ public boolean[] skipRenderPass;
+ @Shadow
+ private int glRenderList;
+
+ @Shadow
+ public boolean needsUpdate;
+
+ boolean savedDrawnStatus;
+
+ public List<ChunkMesh> chunkMeshes;
+
+ @Redirect(method = "setPosition", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/RenderItem;renderAABB(Lnet/minecraft/util/AxisAlignedBB;)V"))
+ private void redirectRenderAABB(AxisAlignedBB p1) {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ RenderItem.renderAABB(p1);
+ }
+ }
+
+ @Redirect(method = "updateRenderer", at = @At(value = "NEW", target = "Lnet/minecraft/world/ChunkCache;"))
+ private ChunkCache redirectConstructChunkCache(World p1, int p2, int p3, int p4, int p5, int p6, int p7, int p8) {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ return new ChunkCache(p1, p2, p3, p4, p5, p6, p7, p8);
+ } else {
+ return new FarChunkCache(p1, p2, p3, p4, p5, p6, p7, p8);
+ }
+ }
+
+ @Inject(method = "updateRenderer", at = @At(value = "HEAD"))
+ private void preUpdateRenderer(CallbackInfo ci) {
+ saveDrawnStatus();
+
+ if(LODMod.isActive()) {
+ if(needsUpdate) {
+ if(chunkMeshes != null) {
+ chunkMeshes.clear();
+ } else {
+ chunkMeshes = new ArrayList<>();
+ }
+ } else {
+ chunkMeshes = null;
+ }
+ }
+ }
+
+ @Inject(method = "updateRenderer", at = @At(value = "RETURN"))
+ private void postUpdateRenderer(CallbackInfo ci) {
+ notifyIfDrawnStatusChanged();
+
+ if(LODMod.isActive()) {
+ if(chunkMeshes != null) {
+ LODMod.renderer.onWorldRendererPost(WorldRenderer.class.cast(this));
+ chunkMeshes.clear();
+ }
+ }
+ }
+
+ @Inject(method = "postRenderBlocks", at = @At(value = "HEAD"))
+ private void prePostRenderBlocks(int pass, EntityLivingBase entity, CallbackInfo ci) {
+ if(LODMod.isActive() && !LODMod.disableChunkMeshes) {
+ if(chunkMeshes != null) {
+ chunkMeshes.add(ChunkMesh.fromTessellator(pass, WorldRenderer.class.cast(this), Tessellator.instance));
+ }
+ }
+ }
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I"))
+ private int redirectPostRenderBlocksDraw() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ return Tessellator.instance.draw();
+ } else {
+ Tessellator.instance.reset();
+ return 0;
+ }
+ }
+
+ // There's probably a nicer way to do this
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPopMatrix()V"))
+ private void redirectPostRenderBlocksGL1() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ GL11.glPopMatrix();
+ }
+ }
+
+ @Redirect(method = "postRenderBlocks", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glEndList()V"))
+ private void redirectPostRenderBlocksGL2() {
+ if(!FarWorldRenderer.class.isInstance(this.getClass())) {
+ GL11.glEndList();
+ }
+ }
+
+ // XXX this is inconsistent, Forge callbacks are preserved in postRenderBlocks but not preRenderBlocks
+
+ @Inject(method = "preRenderBlocks", at = @At(value = "HEAD"))
+ private void preRenderBlocksInjector(CallbackInfo ci) {
+ if(FarWorldRenderer.class.isInstance(this.getClass())) {
+ Tessellator.instance.setTranslation((double)(-this.posX), (double)(-this.posY), (double)(-this.posZ));
+ ci.cancel();
+ }
+ }
+
+ @Inject(method = "setDontDraw", at = @At(value = "HEAD"))
+ private void preSetDontDraw(CallbackInfo ci) {
+ if(LODMod.isActive()) {
+ LODMod.renderer.onWorldRendererChanged(WorldRenderer.class.cast(this), LODRenderer.WorldRendererChange.DELETED);
+ }
+ }
+
+ @Override
+ public List<ChunkMesh> getChunkMeshes() {
+ return chunkMeshes;
+ }
+
+ @Inject(method = "updateInFrustum", at = @At(value = "HEAD"))
+ private void preUpdateInFrustum(CallbackInfo ci) {
+ saveDrawnStatus();
+ }
+
+ @Inject(method = "updateInFrustum", at = @At(value = "RETURN"))
+ private void postUpdateInFrustum(CallbackInfo ci) {
+ notifyIfDrawnStatusChanged();
+ }
+
+ private void saveDrawnStatus() {
+ savedDrawnStatus = isDrawn();
+ }
+
+ private void notifyIfDrawnStatusChanged() {
+ boolean drawn = isDrawn();
+ if(LODMod.isActive() && drawn != savedDrawnStatus) {
+ LODMod.renderer.onWorldRendererChanged(WorldRenderer.class.cast(this), drawn ? LODRenderer.WorldRendererChange.VISIBLE : LODRenderer.WorldRendererChange.INVISIBLE);
+ }
+ }
+
+ @Override
+ public boolean isDrawn() {
+ return isInFrustum && (!skipRenderPass[0] || !skipRenderPass[1]);
+ }
+}
diff --git a/src/main/java/makamys/neodymium/renderer/ChunkMesh.java b/src/main/java/makamys/neodymium/renderer/ChunkMesh.java
new file mode 100644
index 0000000..6c4cd59
--- /dev/null
+++ b/src/main/java/makamys/neodymium/renderer/ChunkMesh.java
@@ -0,0 +1,455 @@
+package makamys.neodymium.renderer;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.ShortBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.lwjgl.BufferUtils;
+
+import makamys.neodymium.LODMod;
+import makamys.neodymium.MixinConfigPlugin;
+import makamys.neodymium.ducks.IWorldRenderer;
+import makamys.neodymium.util.BufferWriter;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.renderer.Tessellator;
+import net.minecraft.client.renderer.WorldRenderer;
+import net.minecraft.client.renderer.texture.TextureAtlasSprite;
+import net.minecraft.client.renderer.texture.TextureMap;
+import net.minecraft.entity.Entity;
+import net.minecraft.nbt.NBTBase;
+import net.minecraft.nbt.NBTTagByteArray;
+import net.minecraft.tileentity.TileEntity;
+
+public class ChunkMesh extends Mesh {
+
+ Flags flags;
+
+ // TODO move this somewhere else
+ List<String> nameList = (List<String>) ((TextureMap)Minecraft.getMinecraft().getTextureManager().getTexture(TextureMap.locationBlocksTexture)).mapUploadedSprites.keySet().stream().collect(Collectors.toList());
+
+ public static int usedRAM = 0;
+ public static int instances = 0;
+
+ public ChunkMesh(int x, int y, int z, Flags flags, int quadCount, ByteBuffer buffer, int pass) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.flags = flags;
+ this.quadCount = quadCount;
+ this.pass = pass;