diff --git a/src/main/java/me/cortex/voxy/client/GPUSelectorWindows2.java b/src/main/java/me/cortex/voxy/client/GPUSelectorWindows2.java new file mode 100644 index 00000000..b1ff5bd4 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/GPUSelectorWindows2.java @@ -0,0 +1,259 @@ +package me.cortex.voxy.client; + +import me.cortex.voxy.common.Logger; +import org.lwjgl.system.JNI; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.system.windows.GDI32; +import org.lwjgl.system.windows.Kernel32; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.lwjgl.system.APIUtil.apiGetFunctionAddressOptional; + +public class GPUSelectorWindows2 { + private static final long D3DKMTSetProperties = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTSetProperties"); + private static final long D3DKMTEnumAdapters2 = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTEnumAdapters2"); + private static final long D3DKMTCloseAdapter = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTCloseAdapter"); + private static final long D3DKMTQueryAdapterInfo = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTQueryAdapterInfo"); + + private static int setPCIProperties(int type, int vendor, int device, int subSys) { + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(0x10).order(ByteOrder.nativeOrder()); + buff.putInt(0, vendor); + buff.putInt(4, device); + buff.putInt(8, subSys); + buff.putInt(12, 0); + return setProperties(type, buff); + } + } + + private static int setProperties(int type, ByteBuffer payload) { + if (D3DKMTSetProperties == 0) { + return -1; + } + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(0x18).order(ByteOrder.nativeOrder()); + buff.putInt(0, type); + buff.putInt(4, payload.remaining()); + buff.putLong(16, MemoryUtil.memAddress(payload)); + return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTSetProperties); + } + } + + private static int query(int handle, int type, ByteBuffer payload) { + if (D3DKMTQueryAdapterInfo == 0) { + return -1; + } + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(0x14).order(ByteOrder.nativeOrder()); + buff.putInt(0, handle); + buff.putInt(4, type); + buff.putLong(8, MemoryUtil.memAddress(payload)); + buff.putInt(16, payload.remaining()); + return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTQueryAdapterInfo); + } + } + + private static int closeHandle(int handle) { + if (D3DKMTCloseAdapter == 0) { + return -1; + } + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(0x4).order(ByteOrder.nativeOrder()); + buff.putInt(0, handle); + return JNI.callPI(MemoryUtil.memAddress(buff), D3DKMTCloseAdapter); + } + } + + private static int queryAdapterType(int handle, int[] out) { + int ret; + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(0x4).order(ByteOrder.nativeOrder()); + //KMTQAITYPE_ADAPTERTYPE + if ((ret=query(handle, 15, buff))<0) { + return ret; + } + out[0] = buff.getInt(0); + } + return 0; + } + + private record AdapterInfo(int type, long luid, int vendor, int device, int subSystem) { + @Override + public String toString() { + String LUID = Integer.toHexString((int) ((luid>>>32)&0xFFFFFFFFL))+"-"+Integer.toHexString((int) (luid&0xFFFFFFFFL)); + return "{type=%s, luid=%s, vendor=%s, device=%s, subSys=%s}".formatted(Integer.toString(type),LUID, Integer.toHexString(vendor), Integer.toHexString(device), Integer.toHexString(subSystem)); + } + } + private record PCIDeviceId(int vendor, int device, int subVendor, int subSystem, int revision, int busType) {} + private static int queryPCIAddress(int handle, int index, PCIDeviceId[] deviceOut) { + int ret = 0; + try (var stack = MemoryStack.stackPush()) { + var buff = stack.calloc(4*7).order(ByteOrder.nativeOrder()); + buff.putInt(0, index); + //KMTQAITYPE_PHYSICALADAPTERDEVICEIDS + if ((ret = query(handle, 31, buff)) < 0) { + return ret; + } + deviceOut[0] = new PCIDeviceId(buff.getInt(4),buff.getInt(8),buff.getInt(12),buff.getInt(16),buff.getInt(20),buff.getInt(24)); + return 0; + } + } + + private static int enumAdapters(Consumer consumer) { + if (D3DKMTEnumAdapters2 == 0) { + return -1; + } + + int ret = 0; + try (var stack = MemoryStack.stackPush()) { + var query = stack.calloc(0x10).order(ByteOrder.nativeOrder()); + if ((ret = JNI.callPI(MemoryUtil.memAddress(query), D3DKMTEnumAdapters2)) < 0) { + return ret; + } + int adapterCount = query.getInt(0); + var adapterList = stack.calloc(0x14 * adapterCount).order(ByteOrder.nativeOrder()); + query.putLong(8, MemoryUtil.memAddress(adapterList)); + if ((ret = JNI.callPI(MemoryUtil.memAddress(query), D3DKMTEnumAdapters2)) < 0) { + return ret; + } + adapterCount = query.getInt(0); + for (int adapterIndex = 0; adapterIndex < adapterCount; adapterIndex++) { + var adapter = adapterList.slice(adapterIndex*0x14, 0x14).order(ByteOrder.nativeOrder()); + //We only care about these 2 + int handle = adapter.getInt(0); + long luid = adapter.getLong(4); + + int[] type = new int[1]; + if ((ret = queryAdapterType(handle, type))<0) { + Logger.error("Query type error: " + ret); + //We errored + if (closeHandle(handle) < 0) { + throw new IllegalStateException(); + } + continue; + } + + + PCIDeviceId[] out = new PCIDeviceId[1]; + //Get the root adapter device + if ((ret = queryPCIAddress(handle, 0, out)) < 0) { + Logger.error("Query pci error: " + ret); + //We errored + if (closeHandle(handle) < 0) { + throw new IllegalStateException(); + } + continue; + } + + int subSys = (out[0].subSystem<<16)|out[0].subVendor;//It seems the pci subsystem address is a joined integer + consumer.accept(new AdapterInfo(type[0], luid, out[0].vendor, out[0].device, subSys)); + + if (closeHandle(handle) < 0) { + throw new IllegalStateException(); + } + } + } + + return 0; + } + + + //======================================================================================= + + private static final int[] HDC_STUB = { 0x48, 0x83, 0xC1, 0x0C, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x48, 0x89, 0x01, 0x48, 0xB8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x2F, 0x51, 0xFF, 0xD0, 0x59, 0x8B, 0x41, 0x08, 0x89, 0x41, 0xFC, 0x48, 0x31, 0xC0, 0x89, 0x41, 0x08, 0xC3 }; + private static void insertLong(long l, byte[] out, int offset) { + for (int i = 0; i < 8; i++) { + out[i+offset] = (byte)(l & 0xFF); + l >>= 8; + } + } + private static byte[] createFinishedHDCStub(long luid, long D3DKMTOpenAdapterFromLuid) { + byte[] stub = new byte[HDC_STUB.length]; + for (int i = 0; i < stub.length; i++) { + stub[i] = (byte) HDC_STUB[i]; + } + insertLong(luid, stub, 6); + insertLong(D3DKMTOpenAdapterFromLuid, stub, 19); + return stub; + } + + private static final long D3DKMTOpenAdapterFromLuid = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTOpenAdapterFromLuid"); + private static final long D3DKMTOpenAdapterFromHdc = apiGetFunctionAddressOptional(GDI32.getLibrary(), "D3DKMTOpenAdapterFromHdc"); + private static final long VirtualProtect = apiGetFunctionAddressOptional(Kernel32.getLibrary(), "VirtualProtect"); + + private static void VirtualProtect(long addr, long size) { + try (var stack = MemoryStack.stackPush()) { + var oldProtection = stack.calloc(4); + JNI.callPPPPI(addr, size, 0x40/*PAGE_EXECUTE_READWRITE*/, MemoryUtil.memAddress(oldProtection), VirtualProtect); + } + } + private static void memcpy(long ptr, byte[] data) { + for (int i = 0; i < data.length; i++) { + MemoryUtil.memPutByte(ptr + i, data[i]); + } + } + + private static void installHDCStub(long adapterLuid) { + if (D3DKMTOpenAdapterFromHdc == 0 || VirtualProtect == 0 || D3DKMTOpenAdapterFromLuid == 0) { + return; + } + Logger.info("AdapterLuid callback at: " + Long.toHexString(D3DKMTOpenAdapterFromLuid)); + var stub = createFinishedHDCStub(adapterLuid, D3DKMTOpenAdapterFromLuid); + + VirtualProtect(D3DKMTOpenAdapterFromHdc, stub.length); + memcpy(D3DKMTOpenAdapterFromHdc, stub); + } + + + private static void installQueryStub() { + if (D3DKMTQueryAdapterInfo == 0 || VirtualProtect == 0) { + return; + } + + VirtualProtect(D3DKMTQueryAdapterInfo, 0x10); + + var fixAndRetStub = new byte[] { 0x48, (byte) 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0x48, (byte) 0x8B, 0x0D, 0x15, 0x00, 0x00, 0x00, 0x48, (byte) 0x89, 0x08, 0x48, (byte) 0x8B, 0x0D, 0x13, 0x00, 0x00, 0x00, 0x48, (byte) 0x89, 0x48, 0x08, 0x48, 0x31, (byte) 0xC0, 0x48, (byte) 0xF7, (byte) 0xD0, (byte) 0xC3, 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0}; + long stubPtr = MemoryUtil.nmemAlloc(fixAndRetStub.length); + VirtualProtect(stubPtr, fixAndRetStub.length); + + + insertLong(D3DKMTQueryAdapterInfo, fixAndRetStub, 2); + insertLong((MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo)), fixAndRetStub, 38); + insertLong((MemoryUtil.memGetLong(D3DKMTQueryAdapterInfo+8)), fixAndRetStub, 38+8); + + memcpy(stubPtr, fixAndRetStub); + Logger.info("Restore stub at: " + Long.toHexString(stubPtr)); + + + var jmpStub = new byte[]{ 0x48, (byte) 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xFF, (byte) 0xE0}; + insertLong(stubPtr, jmpStub, 2); + + memcpy(D3DKMTQueryAdapterInfo, jmpStub); + Logger.info("D3DKMTQueryAdapterInfo at: " + Long.toHexString(D3DKMTQueryAdapterInfo)); + } + + + public static void doSelector(int index) { + List adapters = new ArrayList<>(); + //Must support rendering and must not be software renderer + if (enumAdapters(adapter->{if ((adapter.type&5)==1) adapters.add(adapter); }) < 0) { + return; + } + for (var adapter : adapters) { + Logger.error(adapter.toString()); + } + var adapter = adapters.get(index); + + installHDCStub(adapter.luid); + installQueryStub(); + + setPCIProperties(1/*USER*/, adapter.vendor, adapter.device, adapter.subSystem); + setPCIProperties(2/*USER GLOBAL*/, adapter.vendor, adapter.device, adapter.subSystem); + } +} diff --git a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java index 55867881..2a582098 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java +++ b/src/main/java/me/cortex/voxy/client/VoxyClientInstance.java @@ -2,12 +2,16 @@ package me.cortex.voxy.client; import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.saver.ContextSelectionSystem; +import me.cortex.voxy.common.util.Pair; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.commonImpl.IVoxyWorld; import me.cortex.voxy.commonImpl.ImportManager; import me.cortex.voxy.commonImpl.VoxyInstance; import net.minecraft.client.world.ClientWorld; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedDeque; + public class VoxyClientInstance extends VoxyInstance { private static final ContextSelectionSystem SELECTOR = new ContextSelectionSystem(); @@ -25,6 +29,7 @@ public class VoxyClientInstance extends VoxyInstance { if (vworld == null) { vworld = this.createWorld(SELECTOR.getBestSelectionOrCreate(world).createSectionStorageBackend()); ((IVoxyWorld)world).setWorldEngine(vworld); + //testDbPerformance2(vworld); } else { if (!this.activeWorlds.contains(vworld)) { throw new IllegalStateException("World referenced does not exist in instance"); @@ -32,4 +37,64 @@ public class VoxyClientInstance extends VoxyInstance { } return vworld; } + + + + private static void testDbPerformance(WorldEngine engine) { + Random r = new Random(123456); + r.nextLong(); + long start = System.currentTimeMillis(); + int c = 0; + long tA = 0; + long tR = 0; + for (int i = 0; i < 1_000_000; i++) { + if (i == 20_000) { + c = 0; + start = System.currentTimeMillis(); + } + c++; + int x = (r.nextInt(256*2+2)-256);//-32 + int z = (r.nextInt(256*2+2)-256);//-32 + int y = r.nextInt(2)-1; + int lvl = 0;//r.nextInt(5); + long t = System.nanoTime(); + var sec = engine.acquire(WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl)); + tA += System.nanoTime()-t; + t = System.nanoTime(); + sec.release(); + tR += System.nanoTime()-t; + } + long delta = System.currentTimeMillis() - start; + System.out.println("Total "+delta+"ms " + ((double)delta/c) + "ms average tA: " + tA + " tR: " + tR); + } + private static void testDbPerformance2(WorldEngine engine) { + Random r = new Random(123456); + r.nextLong(); + ConcurrentLinkedDeque queue = new ConcurrentLinkedDeque<>(); + var ser = engine.instanceIn.getThreadPool().createServiceNoCleanup("aa", 1, ()-> () ->{ + var sec = engine.acquire(queue.poll()); + sec.release(); + }); + int priming = 1_000_000; + for (int i = 0; i < 2_000_000+priming; i++) { + int x = (r.nextInt(256*2+2)-256)>>2;//-32 + int z = (r.nextInt(256*2+2)-256)>>2;//-32 + int y = r.nextInt(2)-1; + int lvl = 0;//r.nextInt(5); + queue.add(WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl)); + } + for (int i = 0; i < priming; i++) { + ser.execute(); + } + ser.blockTillEmpty(); + int c = queue.size(); + long start = System.currentTimeMillis(); + for (int i = 0; i < c; i++) { + ser.execute(); + } + ser.blockTillEmpty(); + long delta = System.currentTimeMillis() - start; + ser.shutdown(); + System.out.println("Total "+delta+"ms " + ((double)delta/c) + "ms average total, avg wrt threads: " + (((double)delta/c)*engine.instanceIn.getThreadPool().getThreadCount()) + "ms"); + } } diff --git a/src/main/java/me/cortex/voxy/client/VoxyCommands.java b/src/main/java/me/cortex/voxy/client/VoxyCommands.java index 9366d6aa..ba4a7681 100644 --- a/src/main/java/me/cortex/voxy/client/VoxyCommands.java +++ b/src/main/java/me/cortex/voxy/client/VoxyCommands.java @@ -63,12 +63,8 @@ public class VoxyCommands { ((IGetVoxyRenderSystem)wr).shutdownRenderer(); } var w = ((IVoxyWorld)MinecraftClient.getInstance().world); - if (w != null) { - if (w.getWorldEngine() != null) { - instance.stopWorld(w.getWorldEngine()); - } - w.setWorldEngine(null); - } + if (w != null) w.shutdownEngine(); + VoxyCommon.shutdownInstance(); VoxyCommon.createInstance(); if (wr!=null) { diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index 0007addb..37ea454f 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -3,7 +3,6 @@ package me.cortex.voxy.client.config; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import me.cortex.voxy.client.saver.ContextSelectionSystem; import me.cortex.voxy.common.Logger; import net.fabricmc.loader.api.FabricLoader; @@ -24,7 +23,7 @@ public class VoxyConfig { public boolean enabled = true; public boolean enableRendering = true; public boolean ingestEnabled = true; - //public int renderDistance = 128; + public int sectionRenderDistance = 16; public int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors()/2, 1); public float subDivisionSize = 128; public int secondaryLruCacheSize = 1024; diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java index 8607f588..2335ae14 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java @@ -46,13 +46,7 @@ public class VoxyConfigScreenFactory implements ModMenuApi { } //Shutdown world if (world != null && ON_SAVE_RELOAD_ALL) { - //This is a hack inserted for the client world thing - //TODO: FIXME: MAKE BETTER - var engine = world.getWorldEngine(); - if (engine != null) { - VoxyCommon.getInstance().stopWorld(engine); - } - world.setWorldEngine(null); + world.shutdownEngine(); } //Shutdown instance if (ON_SAVE_RELOAD_ALL) { @@ -112,6 +106,20 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setDefaultValue((int) DEFAULT.subDivisionSize) .build()); + category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.renderDistance"), config.sectionRenderDistance, 2, 64) + .setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip")) + .setSaveConsumer(val -> { + if (config.sectionRenderDistance != val) { + config.sectionRenderDistance = val; + var wrenderer = ((IGetVoxyRenderSystem) (MinecraftClient.getInstance().worldRenderer)); + if (wrenderer != null && wrenderer.getVoxyRenderSystem() != null) { + wrenderer.getVoxyRenderSystem().setRenderDistance(val); + } + } + }) + .setDefaultValue(DEFAULT.sectionRenderDistance) + .build()); + //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.lruCacheSize"), config.secondaryLruCacheSize, 16, 1<<13) // .setTooltip(Text.translatable("voxy.config.general.lruCacheSize.tooltip")) // .setSaveConsumer(val ->{if (config.secondaryLruCacheSize != val) reload(); config.secondaryLruCacheSize = val;}) diff --git a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java index 0bc27889..c4f7c213 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java @@ -1,45 +1,18 @@ package me.cortex.voxy.client.core; -import com.mojang.blaze3d.systems.RenderSystem; import me.cortex.voxy.client.config.VoxyConfig; -import me.cortex.voxy.client.core.gl.Capabilities; -import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.model.ModelBakerySubsystem; -import me.cortex.voxy.client.core.rendering.*; -import me.cortex.voxy.client.core.rendering.building.RenderDataFactory4; +import me.cortex.voxy.client.core.rendering.building.RenderDataFactory45; import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; -import me.cortex.voxy.client.core.rendering.post.PostProcessing; -import me.cortex.voxy.client.core.rendering.util.DownloadStream; -import me.cortex.voxy.client.core.util.IrisUtil; import me.cortex.voxy.client.saver.ContextSelectionSystem; -import me.cortex.voxy.client.taskbar.Taskbar; import me.cortex.voxy.common.Logger; -import me.cortex.voxy.common.util.MemoryBuffer; import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.commonImpl.importers.WorldImporter; import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.world.WorldSection; import me.cortex.voxy.common.world.other.Mapper; -import me.cortex.voxy.commonImpl.VoxyCommon; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.hud.ClientBossBar; -import net.minecraft.client.render.Camera; -import net.minecraft.client.render.Frustum; -import net.minecraft.client.util.math.MatrixStack; -import net.minecraft.entity.boss.BossBar; -import net.minecraft.text.Text; -import net.minecraft.util.math.MathHelper; -import net.minecraft.world.World; -import net.minecraft.world.chunk.WorldChunk; -import org.joml.Matrix4f; -import org.lwjgl.opengl.GL11; -import java.io.File; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.function.Predicate; import static org.lwjgl.opengl.GL30C.*; @@ -162,147 +135,5 @@ public class VoxelCore { - private void testMeshingPerformance() { - var modelService = new ModelBakerySubsystem(this.world.getMapper()); - var factory = new RenderDataFactory4(this.world, modelService.factory, false); - List sections = new ArrayList<>(); - - System.out.println("Loading sections"); - for (int x = -17; x <= 17; x++) { - for (int z = -17; z <= 17; z++) { - for (int y = -1; y <= 4; y++) { - var section = this.world.acquire(0, x, y, z); - - int nonAir = 0; - for (long state : section.copyData()) { - nonAir += Mapper.isAir(state)?0:1; - modelService.requestBlockBake(Mapper.getBlockId(state)); - } - - if (nonAir > 500 && Math.abs(x) <= 16 && Math.abs(z) <= 16) { - sections.add(section); - } else { - section.release(); - } - } - } - } - - System.out.println("Baking models"); - { - //Bake everything - while (!modelService.areQueuesEmpty()) { - modelService.tick(); - glFinish(); - } - } - - System.out.println("Ready!"); - - { - int iteration = 0; - while (true) { - long start = System.currentTimeMillis(); - for (var section : sections) { - var mesh = factory.generateMesh(section); - - mesh.free(); - } - long delta = System.currentTimeMillis() - start; - System.out.println("Iteration: " + (iteration++) + " took " + delta + "ms, for an average of " + ((float)delta/sections.size()) + "ms per section"); - //System.out.println("Quad count: " + factory.quadCount); - } - } - - } - - - private void testDbPerformance() { - Random r = new Random(123456); - r.nextLong(); - long start = System.currentTimeMillis(); - int c = 0; - for (int i = 0; i < 500_000; i++) { - if (i == 20_000) { - c = 0; - start = System.currentTimeMillis(); - } - c++; - int x = (r.nextInt(256*2+2)-256)>>1;//-32 - int z = (r.nextInt(256*2+2)-256)>>1;//-32 - int y = 0; - int lvl = 0;//r.nextInt(5); - this.world.acquire(WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl)).release(); - } - long delta = System.currentTimeMillis() - start; - System.out.println("Total "+delta+"ms " + ((double)delta/c) + "ms average" ); - } - - - - private void testFullMesh() { - var modelService = new ModelBakerySubsystem(this.world.getMapper()); - var completedCounter = new AtomicInteger(); - var generationService = new RenderGenerationService(this.world, modelService, this.serviceThreadPool, a-> {completedCounter.incrementAndGet(); a.free();}, false); - - - var r = new Random(12345); - { - for (int i = 0; i < 10_000; i++) { - int x = (r.nextInt(256*2+2)-256)>>1;//-32 - int z = (r.nextInt(256*2+2)-256)>>1;//-32 - int y = r.nextInt(10)-2; - int lvl = 0;//r.nextInt(5); - long key = WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl); - generationService.enqueueTask(key); - } - int i = 0; - while (true) { - modelService.tick(); - if (i++%5000==0) - System.out.println(completedCounter.get()); - glFinish(); - List a = new ArrayList<>(); - generationService.addDebugData(a); - if (a.getFirst().endsWith(" 0")) { - break; - } - } - } - - System.out.println("Running benchmark"); - while (true) - { - completedCounter.set(0); - long start = System.currentTimeMillis(); - int C = 200_000; - for (int i = 0; i < C; i++) { - int x = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32 - int z = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32 - int y = r.nextInt(10) - 2; - int lvl = 0;//r.nextInt(5); - long key = WorldEngine.getWorldSectionId(lvl, x >> lvl, y >> lvl, z >> lvl); - generationService.enqueueTask(key); - } - //int i = 0; - while (true) { - //if (i++%5000==0) - // System.out.println(completedCounter.get()); - modelService.tick(); - glFinish(); - List a = new ArrayList<>(); - generationService.addDebugData(a); - if (a.getFirst().endsWith(" 0")) { - break; - } - } - long delta = (System.currentTimeMillis()-start); - System.out.println("Time "+delta+"ms count: " + completedCounter.get() + " avg per mesh: " + ((double)delta/completedCounter.get()) + "ms"); - if (false) - break; - } - generationService.shutdown(); - modelService.shutdown(); - } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java new file mode 100644 index 00000000..2a5f02d6 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java @@ -0,0 +1,58 @@ +package me.cortex.voxy.client.core.rendering; + +import me.cortex.voxy.client.core.util.RingTracker; +import me.cortex.voxy.common.world.WorldEngine; + +import java.util.function.LongConsumer; + +public class RenderDistanceTracker { + private static final int CHECK_DISTANCE_BLOCKS = 128; + private final LongConsumer addTopLevelNode; + private final LongConsumer removeTopLevelNode; + private final int processRate; + private final int minSec; + private final int maxSec; + private RingTracker tracker; + private int renderDistance; + private double posX; + private double posZ; + public RenderDistanceTracker(int rate, int minSec, int maxSec, LongConsumer addTopLevelNode, LongConsumer removeTopLevelNode) { + this.addTopLevelNode = addTopLevelNode; + this.removeTopLevelNode = removeTopLevelNode; + this.renderDistance = 2; + this.tracker = new RingTracker(this.renderDistance, 0, 0, true); + this.processRate = rate; + this.minSec = minSec; + this.maxSec = maxSec; + } + + public void setRenderDistance(int renderDistance) { + this.tracker.unload(); + this.tracker.process(Integer.MAX_VALUE, this::add, this::rem); + this.renderDistance = renderDistance; + this.tracker = new RingTracker(renderDistance, ((int)this.posX)>>9, ((int)this.posZ)>>9, true); + } + + public void setCenterAndProcess(double x, double z) { + double dx = this.posX-x; + double dz = this.posZ-z; + if (CHECK_DISTANCE_BLOCKS*CHECK_DISTANCE_BLOCKS>9, ((int)z)>>9); + } + this.tracker.process(this.processRate, this::add, this::rem); + } + + private void add(int x, int z) { + for (int y = this.minSec; y <= this.maxSec; y++) { + this.addTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z)); + } + } + + private void rem(int x, int z) { + for (int y = this.minSec; y <= this.maxSec; y++) { + this.removeTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z)); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index 3016b907..d5cecc50 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -1,5 +1,7 @@ package me.cortex.voxy.client.core.rendering; +import io.netty.util.internal.MathUtil; +import me.cortex.voxy.client.core.gl.Capabilities; import me.cortex.voxy.client.core.model.ModelBakerySubsystem; import me.cortex.voxy.client.core.model.ModelStore; import me.cortex.voxy.client.core.rendering.building.BuiltSection; @@ -52,7 +54,7 @@ public class RenderService, J extends Vi //Max sections: ~500k //Max geometry: 1 gb - this.sectionRenderer = (T) createSectionRenderer(this.modelService.getStore(),1<<20, (1L<<32)-1024); + this.sectionRenderer = (T) createSectionRenderer(this.modelService.getStore(),1<<20, Math.min((1L<<(64-Long.numberOfLeadingZeros(Capabilities.INSTANCE.ssboMaxSize-1)))<<1, 1L<<32)-1024/*(1L<<32)-1024*/); Logger.info("Using renderer: " + this.sectionRenderer.getClass().getSimpleName()); //Do something incredibly hacky, we dont need to keep the reference to this around, so just connect and discard @@ -84,18 +86,15 @@ public class RenderService, J extends Vi world.getMapper().setBiomeCallback(this.modelService::addBiome); } - private int q = -60; + public void addTopLevelNode(long pos) { + this.nodeManager.insertTopLevelNode(pos); + } + + public void removeTopLevelNode(long pos) { + this.nodeManager.removeTopLevelNode(pos); + } + public void setup(Camera camera) { - final int W = 32; - final int H = 2; - boolean SIDED = false; - for (int i = 0; i<64 && q<((W*2+1)*(W*2+1)*H)&&q++>=0;i++) { - this.nodeManager.insertTopLevelNode(WorldEngine.getWorldSectionId(4, (q%(W*2+1))-(SIDED?0:W), ((q/(W*2+1))/(W*2+1))-1, ((q/(W*2+1))%(W*2+1))-(SIDED?0:W))); - } - if (q==((W*2+1)*(W*2+1)*H)) { - q++; - Logger.info("Finished loading render distance"); - } this.modelService.tick(); } @@ -174,6 +173,7 @@ public class RenderService, J extends Vi this.nodeCleaner.free(); //Release all the unprocessed built geometry this.geometryUpdateQueue.clear(BuiltSection::free); + this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue } public Viewport getViewport() { diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java index 6e2c4941..7ede4d35 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/SectionUpdateRouter.java @@ -9,7 +9,7 @@ import java.util.function.LongConsumer; import static me.cortex.voxy.common.world.WorldEngine.UPDATE_TYPE_BLOCK_BIT; public class SectionUpdateRouter implements ISectionWatcher { - private static final int SLICES = 1<<3; + private static final int SLICES = 1<<8; public interface IChildUpdate {void accept(WorldSection section);} private final Long2ByteOpenHashMap[] slices = new Long2ByteOpenHashMap[SLICES]; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java index c11b7436..4dc54f90 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java @@ -5,14 +5,21 @@ import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.core.gl.Capabilities; import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.model.ColourDepthTextureData; +import me.cortex.voxy.client.core.model.ModelBakerySubsystem; import me.cortex.voxy.client.core.model.ModelTextureBakery; +import me.cortex.voxy.client.core.rendering.building.RenderDataFactory45; +import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; import me.cortex.voxy.client.core.rendering.post.PostProcessing; import me.cortex.voxy.client.core.rendering.util.DownloadStream; import me.cortex.voxy.client.core.rendering.util.RawDownloadStream; import me.cortex.voxy.client.core.util.IrisUtil; +import me.cortex.voxy.client.core.util.RingTracker; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.world.WorldEngine; +import me.cortex.voxy.common.world.WorldSection; +import me.cortex.voxy.common.world.other.Mapper; +import me.cortex.voxy.commonImpl.VoxyCommon; import net.minecraft.block.Blocks; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.Camera; @@ -22,25 +29,45 @@ import org.joml.Matrix4f; import org.lwjgl.opengl.GL11; import org.lwjgl.system.MemoryUtil; +import java.util.ArrayList; import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; +import static org.lwjgl.opengl.GL11C.glFinish; import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING; public class VoxyRenderSystem { private final RenderService renderer; private final PostProcessing postProcessing; + private final WorldEngine worldIn; + private final RenderDistanceTracker renderDistanceTracker; public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) { //Trigger the shared index buffer loading SharedIndexBuffer.INSTANCE.id(); Capabilities.init();//Ensure clinit is called + this.worldIn = world; this.renderer = new RenderService(world, threadPool); this.postProcessing = new PostProcessing(); + + this.renderDistanceTracker = new RenderDistanceTracker(10, + MinecraftClient.getInstance().world.getBottomSectionCoord()>>5, + (MinecraftClient.getInstance().world.getTopSectionCoord()-1)>>5, + this.renderer::addTopLevelNode, + this.renderer::removeTopLevelNode); + + this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); } + public void setRenderDistance(int renderDistance) { + this.renderDistanceTracker.setRenderDistance(renderDistance); + } public void renderSetup(Frustum frustum, Camera camera) { + this.renderDistanceTracker.setCenterAndProcess(camera.getBlockPos().getX(), camera.getBlockPos().getZ()); + this.renderer.setup(camera); PrintfDebugUtil.tick(); } @@ -173,4 +200,129 @@ public class VoxyRenderSystem { Logger.info("Shutting down post processor"); if (this.postProcessing!=null){try {this.postProcessing.shutdown();} catch (Exception e) {Logger.error("Error shutting down post processor", e);}} } + + + + + + + + private void testMeshingPerformance() { + var modelService = new ModelBakerySubsystem(this.worldIn.getMapper()); + var factory = new RenderDataFactory45(this.worldIn, modelService.factory, false); + + List sections = new ArrayList<>(); + + System.out.println("Loading sections"); + for (int x = -17; x <= 17; x++) { + for (int z = -17; z <= 17; z++) { + for (int y = -1; y <= 4; y++) { + var section = this.worldIn.acquire(0, x, y, z); + + int nonAir = 0; + for (long state : section.copyData()) { + nonAir += Mapper.isAir(state)?0:1; + modelService.requestBlockBake(Mapper.getBlockId(state)); + } + + if (nonAir > 500 && Math.abs(x) <= 16 && Math.abs(z) <= 16) { + sections.add(section); + } else { + section.release(); + } + } + } + } + + System.out.println("Baking models"); + { + //Bake everything + while (!modelService.areQueuesEmpty()) { + modelService.tick(); + glFinish(); + } + } + + System.out.println("Ready!"); + + { + int iteration = 0; + while (true) { + long start = System.currentTimeMillis(); + for (var section : sections) { + var mesh = factory.generateMesh(section); + + mesh.free(); + } + long delta = System.currentTimeMillis() - start; + System.out.println("Iteration: " + (iteration++) + " took " + delta + "ms, for an average of " + ((float)delta/sections.size()) + "ms per section"); + //System.out.println("Quad count: " + factory.quadCount); + } + } + } + + private void testFullMesh() { + var modelService = new ModelBakerySubsystem(this.worldIn.getMapper()); + var completedCounter = new AtomicInteger(); + var generationService = new RenderGenerationService(this.worldIn, modelService, VoxyCommon.getInstance().getThreadPool(), a-> {completedCounter.incrementAndGet(); a.free();}, false); + + + var r = new Random(12345); + { + for (int i = 0; i < 10_000; i++) { + int x = (r.nextInt(256*2+2)-256)>>1;//-32 + int z = (r.nextInt(256*2+2)-256)>>1;//-32 + int y = r.nextInt(10)-2; + int lvl = 0;//r.nextInt(5); + long key = WorldEngine.getWorldSectionId(lvl, x>>lvl, y>>lvl, z>>lvl); + generationService.enqueueTask(key); + } + int i = 0; + while (true) { + modelService.tick(); + if (i++%5000==0) + System.out.println(completedCounter.get()); + glFinish(); + List a = new ArrayList<>(); + generationService.addDebugData(a); + if (a.getFirst().endsWith(" 0")) { + break; + } + } + } + + System.out.println("Running benchmark"); + while (true) + { + completedCounter.set(0); + long start = System.currentTimeMillis(); + int C = 200_000; + for (int i = 0; i < C; i++) { + int x = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32 + int z = (r.nextInt(256 * 2 + 2) - 256) >> 1;//-32 + int y = r.nextInt(10) - 2; + int lvl = 0;//r.nextInt(5); + long key = WorldEngine.getWorldSectionId(lvl, x >> lvl, y >> lvl, z >> lvl); + generationService.enqueueTask(key); + } + //int i = 0; + while (true) { + //if (i++%5000==0) + // System.out.println(completedCounter.get()); + modelService.tick(); + glFinish(); + List a = new ArrayList<>(); + generationService.addDebugData(a); + if (a.getFirst().endsWith(" 0")) { + break; + } + } + long delta = (System.currentTimeMillis()-start); + System.out.println("Time "+delta+"ms count: " + completedCounter.get() + " avg per mesh: " + ((double)delta/completedCounter.get()) + "ms"); + if (false) + break; + } + generationService.shutdown(); + modelService.shutdown(); + } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory4.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory4.java deleted file mode 100644 index 7568048e..00000000 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory4.java +++ /dev/null @@ -1,670 +0,0 @@ -package me.cortex.voxy.client.core.rendering.building; - -import me.cortex.voxy.client.core.gl.Capabilities; -import me.cortex.voxy.client.core.model.ModelFactory; -import me.cortex.voxy.client.core.model.ModelQueries; -import me.cortex.voxy.client.core.util.Mesher2D; -import me.cortex.voxy.client.core.util.ScanMesher2D; -import me.cortex.voxy.common.util.MemoryBuffer; -import me.cortex.voxy.common.util.UnsafeUtil; -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.common.world.WorldSection; -import me.cortex.voxy.common.world.other.Mapper; -import me.cortex.voxy.commonImpl.VoxyCommon; -import org.lwjgl.system.MemoryUtil; - -import java.util.Arrays; - - -public class RenderDataFactory4 { - private static final boolean VERIFY_MESHING = VoxyCommon.isVerificationFlagOn("verifyMeshing"); - - private final WorldEngine world; - private final ModelFactory modelMan; - - //private final long[] sectionData = new long[32*32*32*2]; - private final long[] sectionData = new long[32*32*32*2]; - - private final int[] opaqueMasks = new int[32*32]; - - //TODO: emit directly to memory buffer instead of long arrays - - //Each axis gets a max quad count of 2^16 (65536 quads) since that is the max the basic geometry manager can handle - private final MemoryBuffer directionalQuadBuffer = new MemoryBuffer(6*(8*(1<<16))); - private final long directionalQuadBufferPtr = this.directionalQuadBuffer.address; - private final int[] directionalQuadCounters = new int[6];//Maybe change to short? (or long /w raw pointers) - - - private int minX; - private int minY; - private int minZ; - private int maxX; - private int maxY; - private int maxZ; - - private int quadCount = 0; - - - //Wont work for double sided quads - private final class Mesher extends ScanMesher2D { - public int auxiliaryPosition = 0; - public boolean doAuxiliaryFaceOffset = true; - public int axis = 0; - - //Note x, z are in top right - @Override - protected void emitQuad(int x, int z, int length, int width, long data) { - RenderDataFactory4.this.quadCount++; - - if (VERIFY_MESHING) { - if (length<1||length>16) { - throw new IllegalStateException("length out of bounds: " + length); - } - if (width<1||width>16) { - throw new IllegalStateException("width out of bounds: " + width); - } - if (x<0||x>31) { - throw new IllegalStateException("x out of bounds: " + x); - } - if (z<0||z>31) { - throw new IllegalStateException("z out of bounds: " + z); - } - if (x-(length-1)<0 || z-(width-1)<0) { - throw new IllegalStateException("dim out of bounds: " + (x-(length-1))+", " + (z-(width-1))); - } - - } - x -= length-1; - z -= width-1; - - if (this.axis == 2) { - //Need to swizzle the data if on x axis - int tmp = x; - x = z; - z = tmp; - - tmp = length; - length = width; - width = tmp; - } - - //Lower 26 bits can be auxiliary data since that is where quad position information goes; - int auxData = (int) (data&((1<<26)-1)); - int faceSide = auxData&1; - data &= ~(data&((1<<26)-1)); - - final int axis = this.axis; - int face = (axis<<1)|faceSide; - int encodedPosition = face; - - //Shift up if is negative axis - int auxPos = this.auxiliaryPosition; - auxPos += this.doAuxiliaryFaceOffset?(1-faceSide):0;//Shift - - if (VERIFY_MESHING) { - if (auxPos > 31) { - throw new IllegalStateException("OOB face: " + auxPos + ", " + faceSide); - } - } - - encodedPosition |= ((width - 1) << 7) | ((length - 1) << 3); - - encodedPosition |= x << (axis==2?16:21); - encodedPosition |= z << (axis==1?16:11); - encodedPosition |= auxPos << (axis==0?16:(axis==1?11:21)); - - long quad = data | encodedPosition; - - - MemoryUtil.memPutLong(RenderDataFactory4.this.directionalQuadBufferPtr + (RenderDataFactory4.this.directionalQuadCounters[face]++)*8L + face*8L*(1<<16), quad); - } - } - - - private final Mesher blockMesher = new Mesher(); - - public RenderDataFactory4(WorldEngine world, ModelFactory modelManager, boolean emitMeshlets) { - this.world = world; - this.modelMan = modelManager; - } - - - //TODO: MAKE a render cache that caches each WorldSection directional face generation, cause then can just pull that directly - // instead of needing to regen the entire thing - - - //Ok so the idea for fluid rendering is to make it use a seperate mesher and use a different code path for it - // since fluid states are explicitly overlays over the base block - // can do funny stuff like double rendering - - private static final boolean USE_UINT64 = Capabilities.INSTANCE.INT64_t; - public static final int QUADS_PER_MESHLET = 14; - private static void writePos(long ptr, long pos) { - if (USE_UINT64) { - MemoryUtil.memPutLong(ptr, pos); - } else { - MemoryUtil.memPutInt(ptr, (int) (pos>>32)); - MemoryUtil.memPutInt(ptr + 4, (int)pos); - } - } - - private void prepareSectionData() { - final var sectionData = this.sectionData; - int opaque = 0; - - for (int i = 0; i < 32*32*32;) { - long block = sectionData[i + 32 * 32 * 32];//Get the block mapping - - int modelId = this.modelMan.getModelId(Mapper.getBlockId(block)); - long modelMetadata = this.modelMan.getModelMetadataFromClientId(modelId); - - sectionData[i * 2] = modelId | ((long) (Mapper.getLightId(block)) << 16) | (ModelQueries.isBiomeColoured(modelMetadata)?(((long) (Mapper.getBiomeId(block))) << 24):0); - sectionData[i * 2 + 1] = modelMetadata; - - boolean isFullyOpaque = ModelQueries.isFullyOpaque(modelMetadata); - opaque |= (isFullyOpaque ? 1:0) << (i & 31); - - //TODO: here also do bitmask of what neighboring sections are needed to compute (may be getting rid of this in future) - - //Do increment here - i++; - - if ((i & 31) == 0) { - this.opaqueMasks[(i >> 5) - 1] = opaque; - opaque = 0; - } - } - } - - - private void generateYZFaces() { - for (int axis = 0; axis < 2; axis++) { - this.blockMesher.axis = axis; - for (int layer = 0; layer < 31; layer++) { - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections - int pidx = axis==0 ?(layer*32+other):(other*32+layer); - int skipAmount = axis==0?32:1; - - int current = this.opaqueMasks[pidx]; - int next = this.opaqueMasks[pidx + skipAmount]; - - int msk = current ^ next; - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - //TODO: For boarder sections, should NOT EMIT neighbors faces - int faceForwardMsk = msk & current; - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - int facingForward = ((faceForwardMsk >> index) & 1); - - { - int idx = index + (pidx*32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - long B = this.sectionData[(idx + skipAmount * 32) * 2]; - - //Flip data with respect to facing direction - long selfModel = facingForward == 1 ? A : B; - long nextModel = facingForward == 1 ? B : A; - - //Example thing thats just wrong but as example - this.blockMesher.putNext(((long) facingForward) |//Facing - ((selfModel & 0xFFFF) << 26) | //ModelId - (((nextModel>>16)&0xFF) << 55) |//Lighting - ((selfModel&(0x1FFL<<24))<<(46-24))//biomeId - ); - } - } - this.blockMesher.endRow(); - } - this.blockMesher.finish(); - } - - if (true) { - this.blockMesher.doAuxiliaryFaceOffset = false; - //Hacky generate section side faces (without check neighbor section) - for (int side = 0; side < 2; side++) { - int layer = side == 0 ? 0 : 31; - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) { - int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); - int msk = this.opaqueMasks[pidx]; - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - { - int idx = index + (pidx * 32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - - //Example thing thats just wrong but as example - this.blockMesher.putNext((long) (side == 0 ? 0L : 1L) | - ((A & 0xFFFFL) << 26) | - (((0xFFL) & 0xFF) << 55) | - ((A&(0x1FFL<<24))<<(46-24)) - ); - } - } - this.blockMesher.endRow(); - } - - this.blockMesher.finish(); - } - this.blockMesher.doAuxiliaryFaceOffset = true; - } - } - } - - - private final Mesher[] xAxisMeshers = new Mesher[32]; - { - for (int i = 0; i < 32; i++) { - var mesher = new Mesher(); - mesher.auxiliaryPosition = i; - mesher.axis = 2;//X axis - this.xAxisMeshers[i] = mesher; - } - } - - private static final long X_I_MSK = 0x4210842108421L; - private void generateXFaces() { - for (int y = 0; y < 32; y++) { - long sumA = 0; - long sumB = 0; - long sumC = 0; - int partialHasCount = -1; - int msk = 0; - for (int z = 0; z < 32; z++) { - int lMsk = this.opaqueMasks[y*32+z]; - msk = (lMsk^(lMsk>>>1)); - msk &= -1>>>1;//Remove top bit as we dont actually know/have the data for that slice - - //Always increment cause can do funny trick (i.e. -1 on skip amount) - sumA += X_I_MSK; - sumB += X_I_MSK; - sumC += X_I_MSK; - - partialHasCount &= ~msk; - - if (z == 30 && partialHasCount != 0) {//Hackfix for incremental count overflow issue - int cmsk = partialHasCount; - while (cmsk!=0) { - int index = Integer.numberOfTrailingZeros(cmsk); - cmsk &= ~Integer.lowestOneBit(cmsk); - //TODO: fixme! check this is correct or if should be 30 - this.xAxisMeshers[index].skip(31); - } - //Clear the sum - sumA &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount), X_I_MSK)*0x1F); - sumB &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount)>>11, X_I_MSK)*0x1F); - sumC &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount)>>22, X_I_MSK)*0x1F); - } - - if (msk == 0) { - continue; - } - - /* - {//Dont need this as can just increment everything then -1 in mask - //Compute and increment skips for indexes - long imsk = Integer.toUnsignedLong(~msk);// we only want to increment where there isnt a face - sumA += Long.expand(imsk, X_I_MSK); - sumB += Long.expand(imsk>>11, X_I_MSK); - sumC += Long.expand(imsk>>22, X_I_MSK); - }*/ - - int faceForwardMsk = msk&lMsk; - int iter = msk; - while (iter!=0) { - int index = Integer.numberOfTrailingZeros(iter); - iter &= ~Integer.lowestOneBit(iter); - - var mesher = this.xAxisMeshers[index]; - - int skipCount;//Compute the skip count - {//TODO: Branch-less - //Compute skip and clear - if (index<11) { - skipCount = (int) (sumA>>(index*5)); - sumA &= ~(0x1FL<<(index*5)); - } else if (index<22) { - skipCount = (int) (sumB>>((index-11)*5)); - sumB &= ~(0x1FL<<((index-11)*5)); - } else { - skipCount = (int) (sumC>>((index-22)*5)); - sumC &= ~(0x1FL<<((index-22)*5)); - } - skipCount &= 0x1F; - skipCount--; - } - - if (skipCount != 0) { - mesher.skip(skipCount); - } - - int facingForward = ((faceForwardMsk>>index)&1); - { - int idx = index + (z * 32) + (y * 32 * 32); - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - long B = this.sectionData[(idx + 1) * 2]; - - //Flip data with respect to facing direction - long selfModel = facingForward==1?A:B; - long nextModel = facingForward==1?B:A; - - //Example thing thats just wrong but as example - mesher.putNext(((long) facingForward) |//Facing - ((selfModel & 0xFFFF) << 26) | //ModelId - (((nextModel>>16)&0xFF) << 55) |//Lighting - ((selfModel&(0x1FFL<<24))<<(46-24))//biomeId - ); - } - } - } - - //Need to skip the remaining entries in the skip array - { - msk = ~msk;//Invert the mask as we only need to set stuff that isnt 0 - while (msk!=0) { - int index = Integer.numberOfTrailingZeros(msk); - msk &= ~Integer.lowestOneBit(msk); - int skipCount; - if (index < 11) { - skipCount = (int) (sumA>>(index*5)); - } else if (index<22) { - skipCount = (int) (sumB>>((index-11)*5)); - } else { - skipCount = (int) (sumC>>((index-22)*5)); - } - skipCount &= 0x1F; - - if (skipCount != 0) { - this.xAxisMeshers[index].skip(skipCount); - } - } - } - } - - //Generate the side faces, hackily, using 0 and 1 mesher - if (true) { - var ma = this.xAxisMeshers[0]; - var mb = this.xAxisMeshers[31]; - ma.finish(); - mb.finish(); - ma.doAuxiliaryFaceOffset = false; - mb.doAuxiliaryFaceOffset = false; - for (int y = 0; y < 32; y++) { - int skipA = 0; - int skipB = 0; - for (int z = 0; z < 32; z++) { - int i = y*32+z; - int msk = this.opaqueMasks[i]; - if ((msk & 1) != 0) { - ma.skip(skipA); skipA = 0; - - long A = this.sectionData[(i<<5) * 2]; - - ma.putNext(0L | ((A&0xFFFF)<<26) | (((0xFFL)&0xFF)<<55)|((A&(0x1FFL<<24))<<(46-24))); - } else {skipA++;} - - if ((msk & (1<<31)) != 0) { - mb.skip(skipB); skipB = 0; - - long A = this.sectionData[(i*32+31) * 2]; - - mb.putNext(1L | ((A&0xFFFF)<<26) | (((0xFFL)&0xFF)<<55)|((A&(0x1FFL<<24))<<(46-24))); - } else {skipB++;} - } - ma.skip(skipA); - mb.skip(skipB); - } - ma.finish(); - mb.finish(); - ma.doAuxiliaryFaceOffset = true; - mb.doAuxiliaryFaceOffset = true; - } - - for (var mesher : this.xAxisMeshers) { - mesher.finish(); - } - } - - /* - private static long createQuad() { - ((long)clientModelId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(metadata, face)?self:facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelQueries.isBiomeColoured(metadata)?1:0)) | otherFlags - - - long data = Integer.toUnsignedLong(array[i*3+1]); - data |= ((long) array[i*3+2])<<32; - long encodedQuad = Integer.toUnsignedLong(QuadEncoder.encodePosition(face, otherAxis, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46); - }*/ - - //section is already acquired and gets released by the parent - public BuiltSection generateMesh(WorldSection section) { - //Copy section data to end of array so that can mutate array while reading safely - section.copyDataTo(this.sectionData, 32*32*32); - - this.quadCount = 0; - - this.minX = Integer.MAX_VALUE; - this.minY = Integer.MAX_VALUE; - this.minZ = Integer.MAX_VALUE; - this.maxX = Integer.MIN_VALUE; - this.maxY = Integer.MIN_VALUE; - this.maxZ = Integer.MIN_VALUE; - - Arrays.fill(this.directionalQuadCounters, (short) 0); - - - this.world.acquire(section.lvl, section.x+1, section.y, section.z).release(); - this.world.acquire(section.lvl, section.x-1, section.y, section.z).release(); - - this.world.acquire(section.lvl, section.x, section.y+1, section.z).release(); - this.world.acquire(section.lvl, section.x, section.y-1, section.z).release(); - - this.world.acquire(section.lvl, section.x, section.y, section.z+1).release(); - this.world.acquire(section.lvl, section.x, section.y, section.z-1).release(); - - - - //Prepare everything - this.prepareSectionData(); - - - this.generateYZFaces(); - this.generateXFaces(); - - - //TODO:NOTE! when doing face culling of translucent blocks, - // if the connecting type of the translucent block is the same AND the face is full, discard it - // this stops e.g. multiple layers of glass (and ocean) from having 3000 layers of quads etc - - if (this.quadCount == 0) { - return BuiltSection.emptyWithChildren(section.key, section.getNonEmptyChildren()); - } - - //TODO: FIXME AND OPTIMIZE, get rid of the stupid quad collector bullshit - - int[] offsets = new int[8]; - var buff = new MemoryBuffer(this.quadCount * 8L); - long ptr = buff.address; - int coff = 0; - - for (int face = 0; face < 6; face++) { - offsets[face + 2] = coff; - int size = this.directionalQuadCounters[face]; - UnsafeUtil.memcpy(this.directionalQuadBufferPtr + (face*(8*(1<<16))), ptr + coff*8L, (size* 8L)); - coff += size; - } - - - int aabb = 0; - aabb |= 0; - aabb |= 0<<5; - aabb |= 0<<10; - aabb |= (31)<<15; - aabb |= (31)<<20; - aabb |= (31)<<25; - - return new BuiltSection(section.key, section.getNonEmptyChildren(), aabb, buff, offsets); - - /* - buff = new MemoryBuffer(bufferSize * 8L); - long ptr = buff.address; - int coff = 0; - - //Ordering is: translucent, double sided quads, directional quads - offsets[0] = coff; - int size = this.translucentQuadCollector.size(); - LongArrayList arrayList = this.translucentQuadCollector; - for (int i = 0; i < size; i++) { - long data = arrayList.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - - offsets[1] = coff; - size = this.doubleSidedQuadCollector.size(); - arrayList = this.doubleSidedQuadCollector; - for (int i = 0; i < size; i++) { - long data = arrayList.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - - for (int face = 0; face < 6; face++) { - offsets[face + 2] = coff; - final LongArrayList faceArray = this.directionalQuadCollectors[face]; - size = faceArray.size(); - for (int i = 0; i < size; i++) { - long data = faceArray.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - } - - int aabb = 0; - aabb |= this.minX; - aabb |= this.minY<<5; - aabb |= this.minZ<<10; - aabb |= (this.maxX-this.minX)<<15; - aabb |= (this.maxY-this.minY)<<20; - aabb |= (this.maxZ-this.minZ)<<25; - - return new BuiltSection(section.key, section.getNonEmptyChildren(), aabb, buff, offsets); - */ - } - - public void free() { - this.directionalQuadBuffer.free(); - } - - - - - - - - - - - - - - - //Returns true if a face was placed - private boolean putFluidFaceIfCan(Mesher2D mesher, int face, int opposingFace, long self, long metadata, int selfClientModelId, int selfBlockId, long facingState, long facingMetadata, int facingClientModelId, int a, int b) { - int selfFluidClientId = this.modelMan.getFluidClientStateId(selfClientModelId); - long selfFluidMetadata = this.modelMan.getModelMetadataFromClientId(selfFluidClientId); - - int facingFluidClientId = -1; - if (ModelQueries.containsFluid(facingMetadata)) { - facingFluidClientId = this.modelMan.getFluidClientStateId(facingClientModelId); - } - - //If both of the states are the same, then dont render the fluid face - if (selfFluidClientId == facingFluidClientId) { - return false; - } - - if (facingFluidClientId != -1) { - //TODO: OPTIMIZE - if (this.world.getMapper().getBlockStateFromBlockId(selfBlockId).getBlock() == this.world.getMapper().getBlockStateFromBlockId(Mapper.getBlockId(facingState)).getBlock()) { - return false; - } - } - - - if (ModelQueries.faceOccludes(facingMetadata, opposingFace)) { - return false; - } - - //if the model has a fluid state but is not a liquid need to see if the solid state had a face rendered and that face is occluding, if so, dont render the fluid state face - if ((!ModelQueries.isFluid(metadata)) && ModelQueries.faceOccludes(metadata, face)) { - return false; - } - - - - //TODO:FIXME SOMEHOW THIS IS CRITICAL!!!!!!!!!!!!!!!!!! - // so there is one more issue need to be fixed, if water is layered ontop of eachother, the side faces depend on the water state ontop - // this has been hackfixed in the model texture bakery but a proper solution that doesnt explode the sides of the water textures needs to be done - // the issue is that the fluid rendering depends on the up state aswell not just the face state which is really really painful to account for - // e.g the sides of a full water is 8 high or something, not the full block height, this results in a gap between water layers - - - - long otherFlags = 0; - otherFlags |= ModelQueries.isTranslucent(selfFluidMetadata)?1L<<33:0; - otherFlags |= ModelQueries.isDoubleSided(selfFluidMetadata)?1L<<34:0; - mesher.put(a, b, ((long)selfFluidClientId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(selfFluidMetadata, face)?self:facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelQueries.isBiomeColoured(selfFluidMetadata)?1:0)) | otherFlags); - return true; - } - - //Returns true if a face was placed - private boolean putFaceIfCan(Mesher2D mesher, int face, int opposingFace, long metadata, int clientModelId, int selfBiome, long facingModelId, long facingMetadata, int selfLight, int facingLight, int a, int b) { - if (ModelQueries.cullsSame(metadata) && clientModelId == facingModelId) { - //If we are facing a block, and we are both the same state, dont render that face - return false; - } - - //If face can be occluded and is occluded from the facing block, then dont render the face - if (ModelQueries.faceCanBeOccluded(metadata, face) && ModelQueries.faceOccludes(facingMetadata, opposingFace)) { - return false; - } - - long otherFlags = 0; - otherFlags |= ModelQueries.isTranslucent(metadata)?1L<<33:0; - otherFlags |= ModelQueries.isDoubleSided(metadata)?1L<<34:0; - mesher.put(a, b, ((long)clientModelId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(metadata, face)?selfLight:facingLight))<<16) | (ModelQueries.isBiomeColoured(metadata)?(((long) Mapper.getBiomeId(selfBiome))<<24):0) | otherFlags); - return true; - } - - private static int getMeshletHoldingCount(int quads, int quadsPerMeshlet, int meshletSize) { - return ((quads+(quadsPerMeshlet-1))/quadsPerMeshlet)*meshletSize; - } - - public static int alignUp(int n, int alignment) { - return (n + alignment - 1) & -alignment; - } -} \ No newline at end of file diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory45.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory45.java index 05705867..e7618c73 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory45.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory45.java @@ -20,6 +20,14 @@ import java.util.Map; public class RenderDataFactory45 { private static final boolean VERIFY_MESHING = VoxyCommon.isVerificationFlagOn("verifyMeshing"); + //TODO: MAKE a render cache that caches each WorldSection directional face generation, cause then can just pull that directly + // instead of needing to regen the entire thing + + //Ok so the idea for fluid rendering is to make it use a seperate mesher and use a different code path for it + // since fluid states are explicitly overlays over the base block + // can do funny stuff like double rendering + + private final WorldEngine world; private final ModelFactory modelMan; @@ -121,8 +129,11 @@ public class RenderDataFactory45 { long quad = data | Integer.toUnsignedLong(encodedPosition); + MemoryUtil.memPutLong(RenderDataFactory45.this.directionalQuadBufferPtr + (RenderDataFactory45.this.directionalQuadCounters[face]++)*8L + face*8L*(1<<16), quad); + + //Update AABB bounds if (axis == 0) {//Y RenderDataFactory45.this.minY = Math.min(RenderDataFactory45.this.minY, auxPos); @@ -157,6 +168,7 @@ public class RenderDataFactory45 { private final Mesher blockMesher = new Mesher(); + private final Mesher seondaryblockMesher = new Mesher();//Used for dual non-opaque geometry public RenderDataFactory45(WorldEngine world, ModelFactory modelManager, boolean emitMeshlets) { this.world = world; @@ -277,7 +289,8 @@ public class RenderDataFactory45 { private void generateYZOpaqueInnerGeometry(int axis) { for (int layer = 0; layer < 31; layer++) { this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections + int cSkip = 0; + for (int other = 0; other < 32; other++) { int pidx = axis==0 ?(layer*32+other):(other*32+layer); int skipAmount = axis==0?32:1; @@ -286,10 +299,13 @@ public class RenderDataFactory45 { int msk = current ^ next; if (msk == 0) { - this.blockMesher.skip(32); + cSkip += 32; continue; } + this.blockMesher.skip(cSkip); + cSkip = 0; + //TODO: For boarder sections, should NOT EMIT neighbors faces int faceForwardMsk = msk & current; int cIdx = -1; @@ -321,6 +337,7 @@ public class RenderDataFactory45 { ); } } + this.blockMesher.endRow(); } this.blockMesher.finish(); @@ -333,14 +350,18 @@ public class RenderDataFactory45 { for (int side = 0; side < 2; side++) {//-, + int layer = side == 0 ? 0 : 31; this.blockMesher.auxiliaryPosition = layer; + int cSkips = 0; for (int other = 0; other < 32; other++) { int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); int msk = this.opaqueMasks[pidx]; if (msk == 0) { - this.blockMesher.skip(32); + cSkips += 32; continue; } + this.blockMesher.skip(cSkips); + cSkips = 0; + int cIdx = -1; while (msk != 0) { int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index @@ -384,6 +405,73 @@ public class RenderDataFactory45 { this.blockMesher.doAuxiliaryFaceOffset = true; } + private void generateYZNonOpaqueInnerGeometry(int axis) { + //Note: think is ok to just reuse.. blockMesher + this.seondaryblockMesher.doAuxiliaryFaceOffset = false; + this.blockMesher.axis = axis; + this.seondaryblockMesher.axis = axis; + for (int layer = 0; layer < 32; layer++) {//(should be 1->31, then have outer face mesher) + this.blockMesher.auxiliaryPosition = layer; + this.seondaryblockMesher.auxiliaryPosition = layer; + int cSkip = 0; + for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections + int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); + + int msk = this.nonOpaqueMasks[pidx]; + + if (msk == 0) { + cSkip += 32; + continue; + } + + this.blockMesher.skip(cSkip); + this.seondaryblockMesher.skip(cSkip); + cSkip = 0; + + int cIdx = -1; + while (msk != 0) { + int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index + int delta = index - cIdx - 1; + cIdx = index; //index--; + if (delta != 0) { + this.blockMesher.skip(delta); + this.seondaryblockMesher.skip(delta); + } + msk &= ~Integer.lowestOneBit(msk); + + { + int idx = index + (pidx * 32); + + //TODO: swap this out for something not getting the next entry + long A = this.sectionData[idx * 2]; + long B = this.sectionData[idx * 2+1]; + if (ModelQueries.isTranslucent(B)) { + this.blockMesher.putNext(0); + this.seondaryblockMesher.putNext(0); + continue; + } + + //Example thing thats just wrong but as example + this.blockMesher.putNext((long) (false ? 0L : 1L) | + ((A & 0xFFFFL) << 26) | + (((0xFFL) & 0xFF) << 55) | + ((A&(0x1FFL<<24))<<(46-24)) + ); + this.seondaryblockMesher.putNext((long) (true ? 0L : 1L) | + ((A & 0xFFFFL) << 26) | + (((0xFFL) & 0xFF) << 55) | + ((A&(0x1FFL<<24))<<(46-24)) + ); + } + } + this.blockMesher.endRow(); + this.seondaryblockMesher.endRow(); + } + this.blockMesher.finish(); + this.seondaryblockMesher.finish(); + } + } + private void generateYZFaces() { for (int axis = 0; axis < 2; axis++) {//Y then Z this.blockMesher.axis = axis; @@ -391,54 +479,7 @@ public class RenderDataFactory45 { this.generateYZOpaqueInnerGeometry(axis); this.generateYZOpaqueOuterGeometry(axis); - - if (false) {//Non fully opaque geometry - //Note: think is ok to just reuse.. blockMesher - this.blockMesher.axis = axis; - for (int layer = 0; layer < 32; layer++) { - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections - int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); - - int msk = this.nonOpaqueMasks[pidx]; - - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - { - int idx = index + (pidx * 32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - long B = this.sectionData[idx * 2+1]; - if (ModelQueries.isFluid(B)) { - this.blockMesher.putNext(0); - continue; - } - //Example thing thats just wrong but as example - this.blockMesher.putNext((long) (false ? 0L : 1L) | - ((A & 0xFFFFL) << 26) | - (((0xFFL) & 0xFF) << 55) | - ((A&(0x1FFL<<24))<<(46-24)) - ); - } - } - this.blockMesher.endRow(); - } - this.blockMesher.finish(); - } - } + //this.generateYZNonOpaqueInnerGeometry(axis); } } @@ -633,6 +674,11 @@ public class RenderDataFactory45 { mb.doAuxiliaryFaceOffset = true; } + + private void generateXNonOpaqueInnerGeometry() { + + } + private void generateXFaces() { this.generateXOpaqueInnerGeometry(); this.generateXOuterOpaqueGeometry(); @@ -668,6 +714,8 @@ public class RenderDataFactory45 { {//Reset all the block meshes this.blockMesher.reset(); this.blockMesher.doAuxiliaryFaceOffset = true; + this.seondaryblockMesher.reset(); + this.seondaryblockMesher.doAuxiliaryFaceOffset = true; for (var mesher : this.xAxisMeshers) { mesher.reset(); mesher.doAuxiliaryFaceOffset = true; diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory5.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory5.java deleted file mode 100644 index 9a7358d3..00000000 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderDataFactory5.java +++ /dev/null @@ -1,725 +0,0 @@ -package me.cortex.voxy.client.core.rendering.building; - -import me.cortex.voxy.client.core.gl.Capabilities; -import me.cortex.voxy.client.core.model.ModelFactory; -import me.cortex.voxy.client.core.model.ModelQueries; -import me.cortex.voxy.client.core.util.Mesher2D; -import me.cortex.voxy.client.core.util.ScanMesher2D; -import me.cortex.voxy.common.util.MemoryBuffer; -import me.cortex.voxy.common.util.UnsafeUtil; -import me.cortex.voxy.common.world.WorldEngine; -import me.cortex.voxy.common.world.WorldSection; -import me.cortex.voxy.common.world.other.Mapper; -import me.cortex.voxy.commonImpl.VoxyCommon; -import org.lwjgl.system.MemoryUtil; - -import java.util.Arrays; - - -public class RenderDataFactory5 { - private static final boolean VERIFY_MESHING = VoxyCommon.isVerificationFlagOn("verifyMeshing"); - - private final WorldEngine world; - private final ModelFactory modelMan; - - //private final long[] sectionData = new long[32*32*32*2]; - private final long[] sectionData = new long[32*32*32*2]; - - private final int[] opaqueMasks = new int[32*32]; - private final int[] nonOpaqueMasks = new int[32*32]; - - - //TODO: emit directly to memory buffer instead of long arrays - - //Each axis gets a max quad count of 2^16 (65536 quads) since that is the max the basic geometry manager can handle - private final MemoryBuffer directionalQuadBuffer = new MemoryBuffer(6*(8*(1<<16))); - private final long directionalQuadBufferPtr = this.directionalQuadBuffer.address; - private final int[] directionalQuadCounters = new int[6];//Maybe change to short? (or long /w raw pointers) - - - private int minX; - private int minY; - private int minZ; - private int maxX; - private int maxY; - private int maxZ; - - private int quadCount = 0; - - - //Wont work for double sided quads - private final class Mesher extends ScanMesher2D { - public int auxiliaryPosition = 0; - public boolean doAuxiliaryFaceOffset = true; - public int axis = 0; - - //Note x, z are in top right - @Override - protected void emitQuad(int x, int z, int length, int width, long data) { - RenderDataFactory5.this.quadCount++; - - if (VERIFY_MESHING) { - if (length<1||length>16) { - throw new IllegalStateException("length out of bounds: " + length); - } - if (width<1||width>16) { - throw new IllegalStateException("width out of bounds: " + width); - } - if (x<0||x>31) { - throw new IllegalStateException("x out of bounds: " + x); - } - if (z<0||z>31) { - throw new IllegalStateException("z out of bounds: " + z); - } - if (x-(length-1)<0 || z-(width-1)<0) { - throw new IllegalStateException("dim out of bounds: " + (x-(length-1))+", " + (z-(width-1))); - } - - } - x -= length-1; - z -= width-1; - - if (this.axis == 2) { - //Need to swizzle the data if on x axis - int tmp = x; - x = z; - z = tmp; - - tmp = length; - length = width; - width = tmp; - } - - //Lower 26 bits can be auxiliary data since that is where quad position information goes; - int auxData = (int) (data&((1<<26)-1)); - int faceSide = auxData&1; - data &= ~(data&((1<<26)-1)); - - final int axis = this.axis; - int face = (axis<<1)|faceSide; - int encodedPosition = face; - - //Shift up if is negative axis - int auxPos = this.auxiliaryPosition; - auxPos += this.doAuxiliaryFaceOffset?(1-faceSide):0;//Shift - - if (VERIFY_MESHING) { - if (auxPos > 31) { - throw new IllegalStateException("OOB face: " + auxPos + ", " + faceSide); - } - } - - encodedPosition |= ((width - 1) << 7) | ((length - 1) << 3); - - encodedPosition |= x << (axis==2?16:21); - encodedPosition |= z << (axis==1?16:11); - encodedPosition |= auxPos << (axis==0?16:(axis==1?11:21)); - - long quad = data | encodedPosition; - - - MemoryUtil.memPutLong(RenderDataFactory5.this.directionalQuadBufferPtr + (RenderDataFactory5.this.directionalQuadCounters[face]++)*8L + face*8L*(1<<16), quad); - } - } - - - private final Mesher blockMesher = new Mesher(); - private final Mesher partiallyOpaqueMesher = new Mesher(); - - public RenderDataFactory5(WorldEngine world, ModelFactory modelManager, boolean emitMeshlets) { - this.world = world; - this.modelMan = modelManager; - } - - - //TODO: MAKE a render cache that caches each WorldSection directional face generation, cause then can just pull that directly - // instead of needing to regen the entire thing - - - //Ok so the idea for fluid rendering is to make it use a seperate mesher and use a different code path for it - // since fluid states are explicitly overlays over the base block - // can do funny stuff like double rendering - - private static final boolean USE_UINT64 = Capabilities.INSTANCE.INT64_t; - public static final int QUADS_PER_MESHLET = 14; - private static void writePos(long ptr, long pos) { - if (USE_UINT64) { - MemoryUtil.memPutLong(ptr, pos); - } else { - MemoryUtil.memPutInt(ptr, (int) (pos>>32)); - MemoryUtil.memPutInt(ptr + 4, (int)pos); - } - } - - private void prepareSectionData() { - final var sectionData = this.sectionData; - int opaque = 0; - int notEmpty = 0; - - int neighborAcquireMsk = 0; - for (int i = 0; i < 32*32*32;) { - long block = sectionData[i + 32 * 32 * 32];//Get the block mapping - - int modelId = this.modelMan.getModelId(Mapper.getBlockId(block)); - long modelMetadata = this.modelMan.getModelMetadataFromClientId(modelId); - - sectionData[i * 2] = modelId | ((long) (Mapper.getLightId(block)) << 16) | (ModelQueries.isBiomeColoured(modelMetadata)?(((long) (Mapper.getBiomeId(block))) << 24):0); - sectionData[i * 2 + 1] = modelMetadata; - - boolean isFullyOpaque = ModelQueries.isFullyOpaque(modelMetadata); - opaque |= (isFullyOpaque ? 1:0) << (i & 31); - notEmpty |= (modelId!=0 ? 1:0) << (i & 31); - - //TODO: here also do bitmask of what neighboring sections are needed to compute (may be getting rid of this in future) - - //Do increment here - i++; - - if ((i & 31) == 0) { - this.opaqueMasks[(i >> 5) - 1] = opaque; - this.nonOpaqueMasks[(i >> 5) - 1] = notEmpty^opaque; - opaque = 0; - notEmpty = 0; - } - } - } - - - private void generateYZFaces() { - for (int axis = 0; axis < 2; axis++) { - this.blockMesher.axis = axis; - for (int layer = 0; layer < 31; layer++) { - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections - int pidx = axis==0 ?(layer*32+other):(other*32+layer); - int skipAmount = axis==0?32:1; - - int current = this.opaqueMasks[pidx]; - int next = this.opaqueMasks[pidx + skipAmount]; - - int msk = current ^ next; - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - //TODO: For boarder sections, should NOT EMIT neighbors faces - int faceForwardMsk = msk & current; - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - int facingForward = ((faceForwardMsk >> index) & 1); - - { - int idx = index + (pidx*32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - long B = this.sectionData[(idx + skipAmount * 32) * 2]; - - //Flip data with respect to facing direction - long selfModel = facingForward == 1 ? A : B; - long nextModel = facingForward == 1 ? B : A; - - //Example thing thats just wrong but as example - this.blockMesher.putNext(((long) facingForward) |//Facing - ((selfModel & 0xFFFF) << 26) | //ModelId - (((nextModel>>16)&0xFF) << 55) |//Lighting - ((selfModel&(0x1FFL<<24))<<(46-24))//biomeId - ); - } - } - this.blockMesher.endRow(); - } - this.blockMesher.finish(); - } - - if (true) { - this.blockMesher.doAuxiliaryFaceOffset = false; - //Hacky generate section side faces (without check neighbor section) - for (int side = 0; side < 2; side++) { - int layer = side == 0 ? 0 : 31; - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) { - int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); - int msk = this.opaqueMasks[pidx]; - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - { - int idx = index + (pidx * 32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - - //Example thing thats just wrong but as example - this.blockMesher.putNext((long) (side == 0 ? 0L : 1L) | - ((A & 0xFFFFL) << 26) | - (((0xFFL) & 0xFF) << 55) | - ((A&(0x1FFL<<24))<<(46-24)) - ); - } - } - this.blockMesher.endRow(); - } - - this.blockMesher.finish(); - } - this.blockMesher.doAuxiliaryFaceOffset = true; - } - - - {//Non fully opaque geometry - //Note: think is ok to just reuse.. blockMesher - this.blockMesher.axis = axis; - for (int layer = 0; layer < 32; layer++) { - this.blockMesher.auxiliaryPosition = layer; - for (int other = 0; other < 32; other++) {//TODO: need to do the faces that border sections - int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); - - int msk = this.nonOpaqueMasks[pidx]; - - if (msk == 0) { - this.blockMesher.skip(32); - continue; - } - - - int cIdx = -1; - while (msk != 0) { - int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index - int delta = index - cIdx - 1; - cIdx = index; //index--; - if (delta != 0) this.blockMesher.skip(delta); - msk &= ~Integer.lowestOneBit(msk); - - { - int idx = index + (pidx * 32); - - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - - //Example thing thats just wrong but as example - this.blockMesher.putNext((long) (false ? 0L : 1L) | - ((A & 0xFFFFL) << 26) | - (((0xFFL) & 0xFF) << 55) | - ((A&(0x1FFL<<24))<<(46-24)) - ); - } - } - this.blockMesher.endRow(); - } - this.blockMesher.finish(); - } - } - - - } - } - - - private final Mesher[] xAxisMeshers = new Mesher[32]; - { - for (int i = 0; i < 32; i++) { - var mesher = new Mesher(); - mesher.auxiliaryPosition = i; - mesher.axis = 2;//X axis - this.xAxisMeshers[i] = mesher; - } - } - - private static final long X_I_MSK = 0x4210842108421L; - private void generateXFaces() { - for (int y = 0; y < 32; y++) { - long sumA = 0; - long sumB = 0; - long sumC = 0; - int partialHasCount = -1; - int msk = 0; - for (int z = 0; z < 32; z++) { - int lMsk = this.opaqueMasks[y*32+z]; - msk = (lMsk^(lMsk>>>1)); - msk &= -1>>>1;//Remove top bit as we dont actually know/have the data for that slice - - //Always increment cause can do funny trick (i.e. -1 on skip amount) - sumA += X_I_MSK; - sumB += X_I_MSK; - sumC += X_I_MSK; - - partialHasCount &= ~msk; - - if (z == 30 && partialHasCount != 0) {//Hackfix for incremental count overflow issue - int cmsk = partialHasCount; - while (cmsk!=0) { - int index = Integer.numberOfTrailingZeros(cmsk); - cmsk &= ~Integer.lowestOneBit(cmsk); - //TODO: fixme! check this is correct or if should be 30 - this.xAxisMeshers[index].skip(31); - } - //Clear the sum - sumA &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount), X_I_MSK)*0x1F); - sumB &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount)>>11, X_I_MSK)*0x1F); - sumC &= ~(Long.expand(Integer.toUnsignedLong(partialHasCount)>>22, X_I_MSK)*0x1F); - } - - if (msk == 0) { - continue; - } - - /* - {//Dont need this as can just increment everything then -1 in mask - //Compute and increment skips for indexes - long imsk = Integer.toUnsignedLong(~msk);// we only want to increment where there isnt a face - sumA += Long.expand(imsk, X_I_MSK); - sumB += Long.expand(imsk>>11, X_I_MSK); - sumC += Long.expand(imsk>>22, X_I_MSK); - }*/ - - int faceForwardMsk = msk&lMsk; - int iter = msk; - while (iter!=0) { - int index = Integer.numberOfTrailingZeros(iter); - iter &= ~Integer.lowestOneBit(iter); - - var mesher = this.xAxisMeshers[index]; - - int skipCount;//Compute the skip count - {//TODO: Branch-less - //Compute skip and clear - if (index<11) { - skipCount = (int) (sumA>>(index*5)); - sumA &= ~(0x1FL<<(index*5)); - } else if (index<22) { - skipCount = (int) (sumB>>((index-11)*5)); - sumB &= ~(0x1FL<<((index-11)*5)); - } else { - skipCount = (int) (sumC>>((index-22)*5)); - sumC &= ~(0x1FL<<((index-22)*5)); - } - skipCount &= 0x1F; - skipCount--; - } - - if (skipCount != 0) { - mesher.skip(skipCount); - } - - int facingForward = ((faceForwardMsk>>index)&1); - { - int idx = index + (z * 32) + (y * 32 * 32); - //TODO: swap this out for something not getting the next entry - long A = this.sectionData[idx * 2]; - long B = this.sectionData[(idx + 1) * 2]; - - //Flip data with respect to facing direction - long selfModel = facingForward==1?A:B; - long nextModel = facingForward==1?B:A; - - //Example thing thats just wrong but as example - mesher.putNext(((long) facingForward) |//Facing - ((selfModel & 0xFFFF) << 26) | //ModelId - (((nextModel>>16)&0xFF) << 55) |//Lighting - ((selfModel&(0x1FFL<<24))<<(46-24))//biomeId - ); - } - } - } - - //Need to skip the remaining entries in the skip array - { - msk = ~msk;//Invert the mask as we only need to set stuff that isnt 0 - while (msk!=0) { - int index = Integer.numberOfTrailingZeros(msk); - msk &= ~Integer.lowestOneBit(msk); - int skipCount; - if (index < 11) { - skipCount = (int) (sumA>>(index*5)); - } else if (index<22) { - skipCount = (int) (sumB>>((index-11)*5)); - } else { - skipCount = (int) (sumC>>((index-22)*5)); - } - skipCount &= 0x1F; - - if (skipCount != 0) { - this.xAxisMeshers[index].skip(skipCount); - } - } - } - } - - //Generate the side faces, hackily, using 0 and 1 mesher - if (true) { - var ma = this.xAxisMeshers[0]; - var mb = this.xAxisMeshers[31]; - ma.finish(); - mb.finish(); - ma.doAuxiliaryFaceOffset = false; - mb.doAuxiliaryFaceOffset = false; - for (int y = 0; y < 32; y++) { - int skipA = 0; - int skipB = 0; - for (int z = 0; z < 32; z++) { - int i = y*32+z; - int msk = this.opaqueMasks[i]; - if ((msk & 1) != 0) { - ma.skip(skipA); skipA = 0; - - long A = this.sectionData[(i<<5) * 2]; - - ma.putNext(0L | ((A&0xFFFF)<<26) | (((0xFFL)&0xFF)<<55)|((A&(0x1FFL<<24))<<(46-24))); - } else {skipA++;} - - if ((msk & (1<<31)) != 0) { - mb.skip(skipB); skipB = 0; - - long A = this.sectionData[(i*32+31) * 2]; - - mb.putNext(1L | ((A&0xFFFF)<<26) | (((0xFFL)&0xFF)<<55)|((A&(0x1FFL<<24))<<(46-24))); - } else {skipB++;} - } - ma.skip(skipA); - mb.skip(skipB); - } - ma.finish(); - mb.finish(); - ma.doAuxiliaryFaceOffset = true; - mb.doAuxiliaryFaceOffset = true; - } - - for (var mesher : this.xAxisMeshers) { - mesher.finish(); - } - } - - /* - private static long createQuad() { - ((long)clientModelId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(metadata, face)?self:facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelQueries.isBiomeColoured(metadata)?1:0)) | otherFlags - - - long data = Integer.toUnsignedLong(array[i*3+1]); - data |= ((long) array[i*3+2])<<32; - long encodedQuad = Integer.toUnsignedLong(QuadEncoder.encodePosition(face, otherAxis, quad)) | ((data&0xFFFF)<<26) | (((data>>16)&0xFF)<<55) | (((data>>24)&0x1FF)<<46); - }*/ - - //section is already acquired and gets released by the parent - public BuiltSection generateMesh(WorldSection section) { - //Copy section data to end of array so that can mutate array while reading safely - section.copyDataTo(this.sectionData, 32*32*32); - - this.quadCount = 0; - - this.minX = Integer.MAX_VALUE; - this.minY = Integer.MAX_VALUE; - this.minZ = Integer.MAX_VALUE; - this.maxX = Integer.MIN_VALUE; - this.maxY = Integer.MIN_VALUE; - this.maxZ = Integer.MIN_VALUE; - - Arrays.fill(this.directionalQuadCounters, (short) 0); - - - this.world.acquire(section.lvl, section.x+1, section.y, section.z).release(); - this.world.acquire(section.lvl, section.x-1, section.y, section.z).release(); - - this.world.acquire(section.lvl, section.x, section.y+1, section.z).release(); - this.world.acquire(section.lvl, section.x, section.y-1, section.z).release(); - - this.world.acquire(section.lvl, section.x, section.y, section.z+1).release(); - this.world.acquire(section.lvl, section.x, section.y, section.z-1).release(); - - - - //Prepare everything - this.prepareSectionData(); - - - this.generateYZFaces(); - this.generateXFaces(); - - - //TODO:NOTE! when doing face culling of translucent blocks, - // if the connecting type of the translucent block is the same AND the face is full, discard it - // this stops e.g. multiple layers of glass (and ocean) from having 3000 layers of quads etc - - if (this.quadCount == 0) { - return BuiltSection.emptyWithChildren(section.key, section.getNonEmptyChildren()); - } - - //TODO: FIXME AND OPTIMIZE, get rid of the stupid quad collector bullshit - - int[] offsets = new int[8]; - var buff = new MemoryBuffer(this.quadCount * 8L); - long ptr = buff.address; - int coff = 0; - - for (int face = 0; face < 6; face++) { - offsets[face + 2] = coff; - int size = this.directionalQuadCounters[face]; - UnsafeUtil.memcpy(this.directionalQuadBufferPtr + (face*(8*(1<<16))), ptr + coff*8L, (size* 8L)); - coff += size; - } - - - int aabb = 0; - aabb |= 0; - aabb |= 0<<5; - aabb |= 0<<10; - aabb |= (31)<<15; - aabb |= (31)<<20; - aabb |= (31)<<25; - - return new BuiltSection(section.key, section.getNonEmptyChildren(), aabb, buff, offsets); - - /* - buff = new MemoryBuffer(bufferSize * 8L); - long ptr = buff.address; - int coff = 0; - - //Ordering is: translucent, double sided quads, directional quads - offsets[0] = coff; - int size = this.translucentQuadCollector.size(); - LongArrayList arrayList = this.translucentQuadCollector; - for (int i = 0; i < size; i++) { - long data = arrayList.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - - offsets[1] = coff; - size = this.doubleSidedQuadCollector.size(); - arrayList = this.doubleSidedQuadCollector; - for (int i = 0; i < size; i++) { - long data = arrayList.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - - for (int face = 0; face < 6; face++) { - offsets[face + 2] = coff; - final LongArrayList faceArray = this.directionalQuadCollectors[face]; - size = faceArray.size(); - for (int i = 0; i < size; i++) { - long data = faceArray.getLong(i); - MemoryUtil.memPutLong(ptr + ((coff++) * 8L), data); - } - } - - int aabb = 0; - aabb |= this.minX; - aabb |= this.minY<<5; - aabb |= this.minZ<<10; - aabb |= (this.maxX-this.minX)<<15; - aabb |= (this.maxY-this.minY)<<20; - aabb |= (this.maxZ-this.minZ)<<25; - - return new BuiltSection(section.key, section.getNonEmptyChildren(), aabb, buff, offsets); - */ - } - - public void free() { - this.directionalQuadBuffer.free(); - } - - - - - - - - - - - - - - - //Returns true if a face was placed - private boolean putFluidFaceIfCan(Mesher2D mesher, int face, int opposingFace, long self, long metadata, int selfClientModelId, int selfBlockId, long facingState, long facingMetadata, int facingClientModelId, int a, int b) { - int selfFluidClientId = this.modelMan.getFluidClientStateId(selfClientModelId); - long selfFluidMetadata = this.modelMan.getModelMetadataFromClientId(selfFluidClientId); - - int facingFluidClientId = -1; - if (ModelQueries.containsFluid(facingMetadata)) { - facingFluidClientId = this.modelMan.getFluidClientStateId(facingClientModelId); - } - - //If both of the states are the same, then dont render the fluid face - if (selfFluidClientId == facingFluidClientId) { - return false; - } - - if (facingFluidClientId != -1) { - //TODO: OPTIMIZE - if (this.world.getMapper().getBlockStateFromBlockId(selfBlockId).getBlock() == this.world.getMapper().getBlockStateFromBlockId(Mapper.getBlockId(facingState)).getBlock()) { - return false; - } - } - - - if (ModelQueries.faceOccludes(facingMetadata, opposingFace)) { - return false; - } - - //if the model has a fluid state but is not a liquid need to see if the solid state had a face rendered and that face is occluding, if so, dont render the fluid state face - if ((!ModelQueries.isFluid(metadata)) && ModelQueries.faceOccludes(metadata, face)) { - return false; - } - - - - //TODO:FIXME SOMEHOW THIS IS CRITICAL!!!!!!!!!!!!!!!!!! - // so there is one more issue need to be fixed, if water is layered ontop of eachother, the side faces depend on the water state ontop - // this has been hackfixed in the model texture bakery but a proper solution that doesnt explode the sides of the water textures needs to be done - // the issue is that the fluid rendering depends on the up state aswell not just the face state which is really really painful to account for - // e.g the sides of a full water is 8 high or something, not the full block height, this results in a gap between water layers - - - - long otherFlags = 0; - otherFlags |= ModelQueries.isTranslucent(selfFluidMetadata)?1L<<33:0; - otherFlags |= ModelQueries.isDoubleSided(selfFluidMetadata)?1L<<34:0; - mesher.put(a, b, ((long)selfFluidClientId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(selfFluidMetadata, face)?self:facingState))<<16) | ((((long) Mapper.getBiomeId(self))<<24) * (ModelQueries.isBiomeColoured(selfFluidMetadata)?1:0)) | otherFlags); - return true; - } - - //Returns true if a face was placed - private boolean putFaceIfCan(Mesher2D mesher, int face, int opposingFace, long metadata, int clientModelId, int selfBiome, long facingModelId, long facingMetadata, int selfLight, int facingLight, int a, int b) { - if (ModelQueries.cullsSame(metadata) && clientModelId == facingModelId) { - //If we are facing a block, and we are both the same state, dont render that face - return false; - } - - //If face can be occluded and is occluded from the facing block, then dont render the face - if (ModelQueries.faceCanBeOccluded(metadata, face) && ModelQueries.faceOccludes(facingMetadata, opposingFace)) { - return false; - } - - long otherFlags = 0; - otherFlags |= ModelQueries.isTranslucent(metadata)?1L<<33:0; - otherFlags |= ModelQueries.isDoubleSided(metadata)?1L<<34:0; - mesher.put(a, b, ((long)clientModelId) | (((long) Mapper.getLightId(ModelQueries.faceUsesSelfLighting(metadata, face)?selfLight:facingLight))<<16) | (ModelQueries.isBiomeColoured(metadata)?(((long) Mapper.getBiomeId(selfBiome))<<24):0) | otherFlags); - return true; - } - - private static int getMeshletHoldingCount(int quads, int quadsPerMeshlet, int meshletSize) { - return ((quads+(quadsPerMeshlet-1))/quadsPerMeshlet)*meshletSize; - } - - public static int alignUp(int n, int alignment) { - return (n + alignment - 1) & -alignment; - } -} \ No newline at end of file diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 0840607f..68a813fb 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -216,8 +216,9 @@ public class HierarchicalOcclusionTraverser { //TODO: Move the first queue to a persistent list so its not updated every frame ptr = UploadStream.INSTANCE.upload(this.scratchQueueA, 0, 4L*initialQueueSize); - for (int i = 0; i < initialQueueSize; i++) { - MemoryUtil.memPutInt(ptr + 4L*i, this.nodeManager.getTopLevelNodeIds().getInt(i)); + int i = 0; + for (int node : this.nodeManager.getTopLevelNodeIds()) { + MemoryUtil.memPutInt(ptr + 4L*(i++), node); } UploadStream.INSTANCE.commit(); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java index 3516cbc9..d9499603 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java @@ -1,6 +1,5 @@ package me.cortex.voxy.client.core.rendering.hierachical; -import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; @@ -87,7 +86,7 @@ public class NodeManager { private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap(); private final NodeStore nodeData; public final int maxNodeCount; - private final IntArrayList topLevelNodeIds = new IntArrayList(); + private final IntOpenHashSet topLevelNodeIds = new IntOpenHashSet(); private final LongOpenHashSet topLevelNodes = new LongOpenHashSet(); private int activeNodeRequestCount; @@ -133,24 +132,26 @@ public class NodeManager { } public void removeTopLevelNode(long pos) { + if (!this.topLevelNodes.remove(pos)) { + throw new IllegalStateException("Position not in top level map"); + } int nodeId = this.activeSectionMap.get(pos); if (nodeId == -1) { - Logger.error("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!"); - return; + throw new IllegalStateException("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!"); + } + if ((nodeId&NODE_TYPE_MSK)!=NODE_TYPE_REQUEST) { + int id = nodeId&NODE_ID_MSK; + if (!this.topLevelNodeIds.remove(id)) { + throw new IllegalStateException("Node id was not in top level node ids: " + nodeId + " pos: " + WorldEngine.pprintPos(pos)); + } } - //TODO: assert is top level node - - //TODO:FIXME augment topLevelNodeIds with a hashmap from node id to array index - // OR!! just ensure the list is always ordered?? maybe? idk i think hashmap is best - // since the array list might get shuffled as nodes are removed - // since need to move the entry at the end of the array to fill a hole made - - // remove from topLevelNodes aswell + //Remove the entire thing + this.recurseRemoveNode(pos); } - IntArrayList getTopLevelNodeIds() { + IntOpenHashSet getTopLevelNodeIds() { return this.topLevelNodeIds; } @@ -599,6 +600,35 @@ public class NodeManager { this._recurseRemoveNode(pos, false); } + + private void _removeRequest(int reqId, NodeChildRequest req, long pos) { + //Delete all the request data + for (int i = 0; i < 8; i++) { + if ((req.getMsk()&(1<>lvl; - return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound), r.nextInt(bound), r.nextInt(bound)); + return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound)+(WorldEngine.getX(top)<<4), r.nextInt(bound)+(WorldEngine.getY(top)<<4), r.nextInt(bound)+(WorldEngine.getZ(top)<<4)); } private static boolean runTest(int ITERS, int testIdx, Set> traces, boolean geoRemoval) { @@ -314,14 +317,30 @@ public class TestNodeManager { Random r = new Random(testIdx * 1234L); try { var test = new TestBase(); + LongList tops = new LongArrayList(); //Fuzzy bruteforce everything test.putTopPos(POS_A); + tops.add(POS_A); for (int i = 0; i < ITERS; i++) { - long pos = rPos(r); - int op = r.nextInt(4); + long pos = rPos(r, tops); + int op = r.nextInt(5); int extra = r.nextInt(256); boolean hasGeometry = r.nextBoolean(); - if (op == 0) { + boolean addRemTLN = r.nextInt(512) == 0; + boolean extraBool = r.nextBoolean(); + if (op == 0 && addRemTLN) { + pos = WorldEngine.getWorldSectionId(4, r.nextInt(5)-2, r.nextInt(5)-2, r.nextInt(5)-2); + boolean cont = tops.contains(pos); + if (cont&&extraBool&&tops.size()>1) { + extraBool = true; + test.remTopPos(pos); + tops.rem(pos); + } else if (!cont) { + extraBool = false; + test.putTopPos(pos); + tops.add(pos); + } + } else if (op == 0) { test.request(pos); } if (op == 1) { @@ -336,8 +355,20 @@ public class TestNodeManager { test.printNodeChanges(); test.verifyIntegrity(); } - test.childUpdate(POS_A, 0); - test.meshUpdate(POS_A, 0, 0); + for (long top : tops) { + test.remTopPos(top); + } + test.printNodeChanges(); + test.verifyIntegrity(); + if (test.nodeManager.getCurrentMaxNodeId() != -1) { + throw new IllegalStateException(); + } + if (!test.cleaner.active.isEmpty()) { + throw new IllegalStateException(); + } + if (!test.watcher.updateTypes.isEmpty()) { + throw new IllegalStateException(); + } if (test.geometryManager.memoryInUse != 0) { throw new IllegalStateException(); } diff --git a/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java b/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java new file mode 100644 index 00000000..0ab34ce1 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java @@ -0,0 +1,204 @@ +package me.cortex.voxy.client.core.util; + +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import me.cortex.voxy.common.Logger; + +import java.util.Random; + +//Tracks a ring and load/unload positions +// can process N of these load/unload positions +public class RingTracker { + //TODO: replace with custom map that removes elements if its mapped to 0 + private final Long2ByteOpenHashMap operations = new Long2ByteOpenHashMap(1<<13); + private final int[] boundDist; + private final int radius; + private int centerX; + private int centerZ; + + public RingTracker(int radius, int centerX, int centerZ, boolean fill) { + this.centerX = centerX; + this.centerZ = centerZ; + this.radius = radius; + this.boundDist = generateBoundingHalfCircleDistance(radius); + if (fill) { + this.fillRing(true); + } + } + + private static long pack(int x, int z) { + return Integer.toUnsignedLong(x)|(Integer.toUnsignedLong(z)<<32); + } + + private void fillRing(boolean load) { + for (int i = 0; i <= this.radius*2; i++) { + int x = this.centerX + i - this.radius; + int d = this.boundDist[i]; + for (int z = this.centerZ-d; z <= this.centerZ+d; z++) { + int res = this.operations.addTo(pack(x, z), (byte) (load?1:-1)); + if ((load&&0>>32)&0xFFFFFFFFL); + if (isAdd) { + onAdd.accept(x, z); + } else { + onRemove.accept(x, z); + } + iter.remove(); + } + return i; + } + + private int[] generateBoundingHalfCircleDistance(int radius) { + var ret = new int[radius*2+1]; + for (int i = -radius; i <= radius; i++) { + ret[i+radius] = (int)Math.sqrt(radius*radius - i*i); + } + return ret; + } + + public static void main(String[] args) { + for (int j = 0; j < 50; j++) { + Random r = new Random((j+18723)*1234); + var tracker = new RingTracker(r.nextInt(100)+1, 0, 0, true); + int R = r.nextInt(500); + for (int i = 0; i < 50_000; i++) { + int x = r.nextInt(R*2+1)-R; + int z = r.nextInt(R*2+1)-R; + tracker.moveCenter(x, z); + } + tracker.fillRing(false); + tracker.process(64, (x,z)->{ + Logger.info("Add:", x,",",z); + }, (x,z)->{ + Logger.info("Remove:", x,",",z); + }); + } + + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java b/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java index 087ae147..9edc1d6d 100644 --- a/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java +++ b/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java @@ -65,7 +65,7 @@ public class RingUtil { public static int[] generatingBoundingCorner2D(int radius) { IntOpenHashSet points = new IntOpenHashSet(); //Do 2 pass (x and y) to generate and cover all points - for (int i = 0; i <= radius; i++) { + for (int i = 1; i <= radius; i++) { int other = (int) Math.floor(Math.sqrt(radius*radius - i*i)); //add points (x,other) and (other,x) as it covers the full spectrum points.add((i<<16)|other); diff --git a/src/main/java/me/cortex/voxy/client/core/util/ScanMesher2D.java b/src/main/java/me/cortex/voxy/client/core/util/ScanMesher2D.java index cf9ba134..549f8a1f 100644 --- a/src/main/java/me/cortex/voxy/client/core/util/ScanMesher2D.java +++ b/src/main/java/me/cortex/voxy/client/core/util/ScanMesher2D.java @@ -101,7 +101,7 @@ public abstract class ScanMesher2D { //TODO: replace with much better method, TODO: check this is right!! this.putNext(0); if (1{var player = MinecraftClient.getInstance().player; if (player != null) player.sendMessage(Text.literal(error), true);}); + var instance = MinecraftClient.getInstance(); + if (instance != null) { + instance.executeSync(() -> { + var player = MinecraftClient.getInstance().player; + if (player != null) player.sendMessage(Text.literal(error), true); + }); + } } } diff --git a/src/main/java/me/cortex/voxy/common/config/IMappingStorage.java b/src/main/java/me/cortex/voxy/common/config/IMappingStorage.java index 9c57a2b2..dd3c6fdb 100644 --- a/src/main/java/me/cortex/voxy/common/config/IMappingStorage.java +++ b/src/main/java/me/cortex/voxy/common/config/IMappingStorage.java @@ -3,8 +3,10 @@ package me.cortex.voxy.common.config; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.nio.ByteBuffer; +import java.util.function.LongConsumer; public interface IMappingStorage { + void iterateStoredSectionPositions(LongConsumer consumer); void putIdMapping(int id, ByteBuffer data); Int2ObjectOpenHashMap getIdMappingsData(); void flush(); diff --git a/src/main/java/me/cortex/voxy/common/config/section/SectionSerializationStorage.java b/src/main/java/me/cortex/voxy/common/config/section/SectionSerializationStorage.java index 1df462a3..b5b1c6a9 100644 --- a/src/main/java/me/cortex/voxy/common/config/section/SectionSerializationStorage.java +++ b/src/main/java/me/cortex/voxy/common/config/section/SectionSerializationStorage.java @@ -8,12 +8,14 @@ import me.cortex.voxy.common.config.storage.StorageConfig; import me.cortex.voxy.common.util.ThreadLocalMemoryBuffer; import me.cortex.voxy.common.world.SaveLoadSystem; import me.cortex.voxy.common.world.SaveLoadSystem2; +import me.cortex.voxy.common.world.SaveLoadSystem3; import me.cortex.voxy.common.world.WorldSection; import me.cortex.voxy.common.world.other.Mapper; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongConsumer; public class SectionSerializationStorage extends SectionStorage { private final StorageBackend backend; @@ -26,7 +28,7 @@ public class SectionSerializationStorage extends SectionStorage { public int loadSection(WorldSection into) { var data = this.backend.getSectionData(into.key, MEMORY_CACHE.get().createUntrackedUnfreeableReference()); if (data != null) { - if (!SaveLoadSystem.deserialize(into, data)) { + if (!SaveLoadSystem3.deserialize(into, data)) { this.backend.deleteSectionData(into.key); //TODO: regenerate the section from children Arrays.fill(into._unsafeGetRawDataArray(), Mapper.AIR); @@ -46,7 +48,7 @@ public class SectionSerializationStorage extends SectionStorage { @Override public void saveSection(WorldSection section) { - var saveData = SaveLoadSystem.serialize(section); + var saveData = SaveLoadSystem3.serialize(section); this.backend.setSectionData(section.key, saveData); saveData.free(); } @@ -71,6 +73,11 @@ public class SectionSerializationStorage extends SectionStorage { this.backend.close(); } + @Override + public void iterateStoredSectionPositions(LongConsumer consumer) { + this.backend.iterateStoredSectionPositions(consumer); + } + public static class Config extends SectionStorageConfig { public StorageConfig storage; diff --git a/src/main/java/me/cortex/voxy/common/config/storage/StorageBackend.java b/src/main/java/me/cortex/voxy/common/config/storage/StorageBackend.java index 8d7ed045..9b0fcb3c 100644 --- a/src/main/java/me/cortex/voxy/common/config/storage/StorageBackend.java +++ b/src/main/java/me/cortex/voxy/common/config/storage/StorageBackend.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.function.LongConsumer; public abstract class StorageBackend implements IMappingStorage { - public abstract void iterateStoredSectionPositions(LongConsumer consumer); //Implementation may use the scratch buffer as the return value, it MUST NOT free the scratch buffer public abstract MemoryBuffer getSectionData(long key, MemoryBuffer scratch); diff --git a/src/main/java/me/cortex/voxy/common/config/storage/other/FragmentedStorageBackendAdaptor.java b/src/main/java/me/cortex/voxy/common/config/storage/other/FragmentedStorageBackendAdaptor.java index 55d76795..8f6a62e5 100644 --- a/src/main/java/me/cortex/voxy/common/config/storage/other/FragmentedStorageBackendAdaptor.java +++ b/src/main/java/me/cortex/voxy/common/config/storage/other/FragmentedStorageBackendAdaptor.java @@ -34,7 +34,9 @@ public class FragmentedStorageBackendAdaptor extends StorageBackend { @Override public void iterateStoredSectionPositions(LongConsumer consumer) { - throw new IllegalStateException("Not yet implemented"); + for (var backend : this.backends) { + backend.iterateStoredSectionPositions(consumer); + } } //TODO: reencode the key to be shifted one less OR diff --git a/src/main/java/me/cortex/voxy/common/config/storage/rocksdb/RocksDBStorageBackend.java b/src/main/java/me/cortex/voxy/common/config/storage/rocksdb/RocksDBStorageBackend.java index 8ff0f822..ad737c46 100644 --- a/src/main/java/me/cortex/voxy/common/config/storage/rocksdb/RocksDBStorageBackend.java +++ b/src/main/java/me/cortex/voxy/common/config/storage/rocksdb/RocksDBStorageBackend.java @@ -21,6 +21,7 @@ public class RocksDBStorageBackend extends StorageBackend { private final ColumnFamilyHandle worldSections; private final ColumnFamilyHandle idMappings; private final ReadOptions sectionReadOps; + private final WriteOptions sectionWriteOps; //NOTE: closes in order private final List closeList = new ArrayList<>(); @@ -49,7 +50,9 @@ public class RocksDBStorageBackend extends StorageBackend { } */ - final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions().optimizeUniversalStyleCompaction(); + final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions() + .optimizeUniversalStyleCompaction() + .optimizeForPointLookup(128); final List cfDescriptors = Arrays.asList( new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts), @@ -59,7 +62,8 @@ public class RocksDBStorageBackend extends StorageBackend { final DBOptions options = new DBOptions() .setCreateIfMissing(true) - .setCreateMissingColumnFamilies(true); + .setCreateMissingColumnFamilies(true) + .setMaxTotalWalSize(1024*1024*512);//512 mb max WAL size List handles = new ArrayList<>(); @@ -69,12 +73,14 @@ public class RocksDBStorageBackend extends StorageBackend { handles); this.sectionReadOps = new ReadOptions(); + this.sectionWriteOps = new WriteOptions(); this.closeList.addAll(handles); this.closeList.add(this.db); this.closeList.add(options); this.closeList.add(cfOpts); this.closeList.add(this.sectionReadOps); + this.closeList.add(this.sectionWriteOps); this.worldSections = handles.get(1); this.idMappings = handles.get(2); @@ -87,7 +93,19 @@ public class RocksDBStorageBackend extends StorageBackend { @Override public void iterateStoredSectionPositions(LongConsumer consumer) { - throw new IllegalStateException("Not yet implemented"); + try (var stack = MemoryStack.stackPush()) { + ByteBuffer keyBuff = stack.calloc(8); + long keyBuffPtr = MemoryUtil.memAddress(keyBuff); + var iter = this.db.newIterator(this.worldSections, this.sectionReadOps); + iter.seekToFirst(); + while (iter.isValid()) { + iter.key(keyBuff); + long key = Long.reverseBytes(MemoryUtil.memGetLong(keyBuffPtr)); + consumer.accept(key); + iter.next(); + } + iter.close(); + } } @Override @@ -117,10 +135,10 @@ public class RocksDBStorageBackend extends StorageBackend { //TODO: FIXME, use the ByteBuffer variant @Override public void setSectionData(long key, MemoryBuffer data) { - try { - var buffer = new byte[(int) data.size]; - UnsafeUtil.memcpy(data.address, buffer); - this.db.put(this.worldSections, longToBytes(key), buffer); + try (var stack = MemoryStack.stackPush()) { + var keyBuff = stack.calloc(8); + MemoryUtil.memPutLong(MemoryUtil.memAddress(keyBuff), Long.reverseBytes(key)); + this.db.put(this.worldSections, this.sectionWriteOps, keyBuff, data.asByteBuffer()); } catch (RocksDBException e) { throw new RuntimeException(e); } diff --git a/src/main/java/me/cortex/voxy/common/util/ThreadLocalMemoryBuffer.java b/src/main/java/me/cortex/voxy/common/util/ThreadLocalMemoryBuffer.java index aa32ffe3..a119f865 100644 --- a/src/main/java/me/cortex/voxy/common/util/ThreadLocalMemoryBuffer.java +++ b/src/main/java/me/cortex/voxy/common/util/ThreadLocalMemoryBuffer.java @@ -18,6 +18,10 @@ public class ThreadLocalMemoryBuffer { this.threadLocal = ThreadLocal.withInitial(()->createMemoryBuffer(size)); } + public static MemoryBuffer create(long size) { + return createMemoryBuffer(size); + } + public MemoryBuffer get() { return this.threadLocal.get(); } diff --git a/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem.java b/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem.java index 71775061..9a17263d 100644 --- a/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem.java +++ b/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem.java @@ -30,22 +30,22 @@ public class SaveLoadSystem { return x|(y<<10)|(z<<5); } - - private static final ThreadLocal SHORT_CACHE = ThreadLocal.withInitial(()->new short[32*32*32]); - private static final ThreadLocal LONG_CACHE = ThreadLocal.withInitial(()->new long[32*32*32]); - private static final ThreadLocal OTHER_THING_CACHE = ThreadLocal.withInitial(()-> { - var thing = new Long2ShortOpenHashMap(512); - thing.defaultReturnValue((short) -1); - return thing; - }); - + private record SerializationCache(long[] blockStateCache, short[] compressedCache, long[] lutCache, Long2ShortOpenHashMap lutMapCache) { + public SerializationCache() { + this(new long[WorldSection.SECTION_VOLUME], new short[WorldSection.SECTION_VOLUME], new long[WorldSection.SECTION_VOLUME], new Long2ShortOpenHashMap(512)); + this.lutMapCache.defaultReturnValue((short) -1); + } + } + private static final ThreadLocal CACHE = ThreadLocal.withInitial(SerializationCache::new); //TODO: Cache like long2short and the short and other data to stop allocs public static MemoryBuffer serialize(WorldSection section) { - var data = section.copyData(); - var compressed = SHORT_CACHE.get(); - Long2ShortOpenHashMap LUT = OTHER_THING_CACHE.get();LUT.clear(); - long[] lutValues = LONG_CACHE.get();//If there are more than this many states in a section... im concerned + var cache = CACHE.get(); + var data = cache.blockStateCache; + section.copyDataTo(data); + var compressed = cache.compressedCache; + Long2ShortOpenHashMap LUT = cache.lutMapCache; LUT.clear(); + long[] lutValues = cache.lutCache;//If there are more than this many states in a section... im concerned short lutIndex = 0; long pHash = 99; for (int i = 0; i < data.length; i++) { @@ -103,7 +103,7 @@ public class SaveLoadSystem { throw new IllegalStateException("lutLen impossibly large, max size should be 32768 but got size " + lutLen); } //TODO: cache this in a thread local - long[] lut = LONG_CACHE.get(); + long[] lut = CACHE.get().lutCache; long hash = 0; if (VERIFY_HASH_ON_LOAD) { hash = key ^ (lutLen * 1293481298141L); diff --git a/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem3.java b/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem3.java new file mode 100644 index 00000000..feaa6453 --- /dev/null +++ b/src/main/java/me/cortex/voxy/common/world/SaveLoadSystem3.java @@ -0,0 +1,106 @@ +package me.cortex.voxy.common.world; + +import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap; +import me.cortex.voxy.common.Logger; +import me.cortex.voxy.common.util.MemoryBuffer; +import me.cortex.voxy.common.util.ThreadLocalMemoryBuffer; +import me.cortex.voxy.common.util.UnsafeUtil; +import me.cortex.voxy.common.world.other.Mapper; +import me.cortex.voxy.commonImpl.VoxyCommon; +import org.lwjgl.system.MemoryUtil; + +public class SaveLoadSystem3 { + private record SerializationCache(long[] blockStateCache, Long2ShortOpenHashMap lutMapCache, MemoryBuffer memoryBuffer) { + public SerializationCache() { + this(new long[WorldSection.SECTION_VOLUME], + new Long2ShortOpenHashMap(512), + ThreadLocalMemoryBuffer.create(WorldSection.SECTION_VOLUME*2+WorldSection.SECTION_VOLUME*8+1024)); + this.lutMapCache.defaultReturnValue((short) -1); + } + } + public static int lin2z(int i) {//y,z,x + int x = i&0x1F; + int y = (i>>10)&0x1F; + int z = (i>>5)&0x1F; + return Integer.expand(x,0b1001001001001)|Integer.expand(y,0b10010010010010)|Integer.expand(z,0b100100100100100); + + //zyxzyxzyxzyxzyx + } + + public static int z2lin(int i) { + int x = Integer.compress(i, 0b1001001001001); + int y = Integer.compress(i, 0b10010010010010); + int z = Integer.compress(i, 0b100100100100100); + return x|(y<<10)|(z<<5); + } + + private static final ThreadLocal CACHE = ThreadLocal.withInitial(SerializationCache::new); + + //TODO: Cache like long2short and the short and other data to stop allocs + public static MemoryBuffer serialize(WorldSection section) { + var cache = CACHE.get(); + var data = cache.blockStateCache; + section.copyDataTo(data); + + Long2ShortOpenHashMap LUT = cache.lutMapCache; LUT.clear(); + + MemoryBuffer buffer = cache.memoryBuffer().createUntrackedUnfreeableReference(); + long ptr = buffer.address; + + MemoryUtil.memPutLong(ptr, section.key); ptr += 8; + long metadataPtr = ptr; ptr += 8; + + long blockPtr = ptr; ptr += WorldSection.SECTION_VOLUME*2; + for (int i = 0; i < WorldSection.SECTION_VOLUME; i++) { + long block = data[i]; + short mapping = LUT.putIfAbsent(block, (short) LUT.size()); + if (mapping == -1) { + mapping = (short) (LUT.size()-1); + MemoryUtil.memPutLong(ptr, block); ptr+=8; + } + MemoryUtil.memPutShort(blockPtr, mapping); blockPtr+=2; + } + if (LUT.size() >= 1<<16) { + throw new IllegalStateException(); + } + + long metadata = 0; + metadata |= Integer.toUnsignedLong(LUT.size());//Bottom 2 bytes + metadata |= Byte.toUnsignedLong(section.getNonEmptyChildren())<<16;//Next byte + //5 bytes free + + MemoryUtil.memPutLong(metadataPtr, metadata); + //TODO: do hash + + //TODO: rework the storage system to not need to do useless copies like this (this is an issue for serialization, deserialization has solved this already) + return buffer.subSize(ptr-buffer.address).copy(); + } + + public static boolean deserialize(WorldSection section, MemoryBuffer data) { + long ptr = data.address; + long key = MemoryUtil.memGetLong(ptr); ptr += 8; + + if (section.key != key) { + //throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.key); + Logger.error("Decompressed section not the same as requested. got: " + key + " expected: " + section.key); + return false; + } + + long metadata = MemoryUtil.memGetLong(ptr); ptr += 8; + section.nonEmptyChildren = (byte) ((metadata>>>16)&0xFF); + + int nonEmptyBlockCount = 0; + long lutBasePtr = ptr + WorldSection.SECTION_VOLUME*2; + var blockData = section.data; + for (int i = 0; i < WorldSection.SECTION_VOLUME; i++) { + short lutId = MemoryUtil.memGetShort(ptr); ptr+=2; + long blockId = MemoryUtil.memGetLong(lutBasePtr+Short.toUnsignedLong(lutId)*8L); + nonEmptyBlockCount += Mapper.isAir(blockId)?0:1; + blockData[i] = blockId; + } + section.nonEmptyBlockCount = nonEmptyBlockCount; + ptr = lutBasePtr + (metadata&0xFFFF)*8L; + + return true; + } +} diff --git a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java index b6e1fd53..ad6a2b26 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldEngine.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldEngine.java @@ -5,6 +5,8 @@ import me.cortex.voxy.common.config.section.SectionStorage; import me.cortex.voxy.common.util.TrackedObject; import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.world.other.Mapper; +import me.cortex.voxy.commonImpl.VoxyInstance; +import org.jetbrains.annotations.Nullable; import java.util.List; public class WorldEngine { @@ -24,8 +26,7 @@ public class WorldEngine { private final ActiveSectionTracker sectionTracker; private ISectionChangeCallback dirtyCallback; private ISectionSaveCallback saveCallback; - private final int maxMipLevels; - private volatile boolean isLive = true; + volatile boolean isLive = true; public void setDirtyCallback(ISectionChangeCallback callback) { this.dirtyCallback = callback; @@ -37,12 +38,16 @@ public class WorldEngine { public Mapper getMapper() {return this.mapper;} public boolean isLive() {return this.isLive;} + + public final @Nullable VoxyInstance instanceIn; + public WorldEngine(SectionStorage storage, int cacheCount) { - this(storage, MAX_LOD_LAYER+1, cacheCount);//The +1 is because its from 1 not from 0 + this(storage, cacheCount, null); } - private WorldEngine(SectionStorage storage, int maxMipLayers, int cacheCount) { - this.maxMipLevels = maxMipLayers; + public WorldEngine(SectionStorage storage, int cacheCount, @Nullable VoxyInstance instance) { + this.instanceIn = instance; + this.storage = storage; this.mapper = new Mapper(this.storage); //5 cache size bits means that the section tracker has 32 separate maps that it uses @@ -101,6 +106,9 @@ public class WorldEngine { public void markDirty(WorldSection section, int changeState) { if (!this.isLive) throw new IllegalStateException("World is not live"); + if (section.tracker != this.sectionTracker) { + throw new IllegalStateException("Section is not from here"); + } if (this.dirtyCallback != null) { this.dirtyCallback.accept(section, changeState); } @@ -109,89 +117,6 @@ public class WorldEngine { } } - - //TODO: move this to auxilery class so that it can take into account larger than 4 mip levels - //Executes an update to the world and automatically updates all the parent mip layers up to level 4 (e.g. where 1 chunk section is 1 block big) - - //NOTE: THIS RUNS ON THE THREAD IT WAS EXECUTED ON, when this method exits, the calling method may assume that VoxelizedSection is no longer needed - public void insertUpdate(VoxelizedSection section) {//TODO: add a bitset of levels to update and if it should force update - if (!this.isLive) throw new IllegalStateException("World is not live"); - boolean shouldCheckEmptiness = false; - WorldSection previousSection = null; - - for (int lvl = 0; lvl < this.maxMipLevels; lvl++) { - var worldSection = this.acquire(lvl, section.x >> (lvl + 1), section.y >> (lvl + 1), section.z >> (lvl + 1)); - - int emptinessStateChange = 0; - //Propagate the child existence state of the previous iteration to this section - if (lvl != 0 && shouldCheckEmptiness) { - emptinessStateChange = worldSection.updateEmptyChildState(previousSection); - //We kept the previous section acquired, so we need to release it - previousSection.release(); - previousSection = null; - } - - - int msk = (1<<(lvl+1))-1; - int bx = (section.x&msk)<<(4-lvl); - int by = (section.y&msk)<<(4-lvl); - int bz = (section.z&msk)<<(4-lvl); - - int nonAirCountDelta = 0; - boolean didStateChange = false; - - - {//Do a bunch of funny math - int baseVIdx = VoxelizedSection.getBaseIndexForLevel(lvl); - int baseSec = bx | (bz << 5) | (by << 10); - int secMsk = 0xF >> lvl; - secMsk |= (secMsk << 5) | (secMsk << 10); - var secD = worldSection.data; - for (int i = 0; i <= 0xFFF >> (lvl * 3); i++) { - int secIdx = Integer.expand(i, secMsk)+baseSec; - long newId = section.section[baseVIdx+i]; - long oldId = secD[secIdx]; secD[secIdx] = newId; - nonAirCountDelta += Mapper.isAir(oldId) == Mapper.isAir(newId) ? 0 : (Mapper.isAir(newId) ? -1 : 1); - didStateChange |= newId != oldId; - } - } - - if (nonAirCountDelta != 0) { - worldSection.addNonEmptyBlockCount(nonAirCountDelta); - if (lvl == 0) { - emptinessStateChange = worldSection.updateLvl0State() ? 2 : 0; - } - } - - if (didStateChange||(emptinessStateChange!=0)) { - this.markDirty(worldSection, (didStateChange?UPDATE_TYPE_BLOCK_BIT:0)|(emptinessStateChange!=0?UPDATE_TYPE_CHILD_EXISTENCE_BIT:0)); - } - - //Need to release the section after using it - if (didStateChange||(emptinessStateChange==2)) { - if (emptinessStateChange==2) { - //Major state emptiness change, bubble up - shouldCheckEmptiness = true; - //Dont release the section, it will be released on the next loop - previousSection = worldSection; - } else { - //Propagate up without state change - shouldCheckEmptiness = false; - previousSection = null; - worldSection.release(); - } - } else { - //If nothing changed just need to release, dont need to update parent mips - worldSection.release(); - break; - } - } - - if (previousSection != null) { - previousSection.release(); - } - } - public void addDebugData(List debug) { debug.add("ACC/SCC: " + this.sectionTracker.getLoadedCacheCount()+"/"+this.sectionTracker.getSecondaryCacheSize());//Active cache count, Secondary cache counts } diff --git a/src/main/java/me/cortex/voxy/common/world/WorldSection.java b/src/main/java/me/cortex/voxy/common/world/WorldSection.java index 8446fe5c..4b541185 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldSection.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldSection.java @@ -54,7 +54,7 @@ public final class WorldSection { volatile int nonEmptyBlockCount = 0; volatile byte nonEmptyChildren; - private final ActiveSectionTracker tracker; + final ActiveSectionTracker tracker; public final AtomicBoolean inSaveQueue = new AtomicBoolean(); //When the first bit is set it means its loaded diff --git a/src/main/java/me/cortex/voxy/common/world/WorldUpdater.java b/src/main/java/me/cortex/voxy/common/world/WorldUpdater.java new file mode 100644 index 00000000..341798f6 --- /dev/null +++ b/src/main/java/me/cortex/voxy/common/world/WorldUpdater.java @@ -0,0 +1,90 @@ +package me.cortex.voxy.common.world; + +import me.cortex.voxy.common.voxelization.VoxelizedSection; +import me.cortex.voxy.common.world.other.Mapper; + +import static me.cortex.voxy.common.world.WorldEngine.*; + +public class WorldUpdater { + //TODO: move this to auxilery class so that it can take into account larger than 4 mip levels + //Executes an update to the world and automatically updates all the parent mip layers up to level 4 (e.g. where 1 chunk section is 1 block big) + + //NOTE: THIS RUNS ON THE THREAD IT WAS EXECUTED ON, when this method exits, the calling method may assume that VoxelizedSection is no longer needed + public static void insertUpdate(WorldEngine into, VoxelizedSection section) {//TODO: add a bitset of levels to update and if it should force update + if (!into.isLive) throw new IllegalStateException("World is not live"); + boolean shouldCheckEmptiness = false; + WorldSection previousSection = null; + + for (int lvl = 0; lvl < MAX_LOD_LAYER+1; lvl++) { + var worldSection = into.acquire(lvl, section.x >> (lvl + 1), section.y >> (lvl + 1), section.z >> (lvl + 1)); + + int emptinessStateChange = 0; + //Propagate the child existence state of the previous iteration to this section + if (lvl != 0 && shouldCheckEmptiness) { + emptinessStateChange = worldSection.updateEmptyChildState(previousSection); + //We kept the previous section acquired, so we need to release it + previousSection.release(); + previousSection = null; + } + + + int msk = (1<<(lvl+1))-1; + int bx = (section.x&msk)<<(4-lvl); + int by = (section.y&msk)<<(4-lvl); + int bz = (section.z&msk)<<(4-lvl); + + int nonAirCountDelta = 0; + boolean didStateChange = false; + + + {//Do a bunch of funny math + int baseVIdx = VoxelizedSection.getBaseIndexForLevel(lvl); + int baseSec = bx | (bz << 5) | (by << 10); + int secMsk = 0xF >> lvl; + secMsk |= (secMsk << 5) | (secMsk << 10); + var secD = worldSection.data; + for (int i = 0; i <= 0xFFF >> (lvl * 3); i++) { + int secIdx = Integer.expand(i, secMsk)+baseSec; + long newId = section.section[baseVIdx+i]; + long oldId = secD[secIdx]; secD[secIdx] = newId; + nonAirCountDelta += Mapper.isAir(oldId) == Mapper.isAir(newId) ? 0 : (Mapper.isAir(newId) ? -1 : 1); + didStateChange |= newId != oldId; + } + } + + if (nonAirCountDelta != 0) { + worldSection.addNonEmptyBlockCount(nonAirCountDelta); + if (lvl == 0) { + emptinessStateChange = worldSection.updateLvl0State() ? 2 : 0; + } + } + + if (didStateChange||(emptinessStateChange!=0)) { + into.markDirty(worldSection, (didStateChange?UPDATE_TYPE_BLOCK_BIT:0)|(emptinessStateChange!=0?UPDATE_TYPE_CHILD_EXISTENCE_BIT:0)); + } + + //Need to release the section after using it + if (didStateChange||(emptinessStateChange==2)) { + if (emptinessStateChange==2) { + //Major state emptiness change, bubble up + shouldCheckEmptiness = true; + //Dont release the section, it will be released on the next loop + previousSection = worldSection; + } else { + //Propagate up without state change + shouldCheckEmptiness = false; + previousSection = null; + worldSection.release(); + } + } else { + //If nothing changed just need to release, dont need to update parent mips + worldSection.release(); + break; + } + } + + if (previousSection != null) { + previousSection.release(); + } + } +} diff --git a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java index 83b715e2..5d4d97c7 100644 --- a/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java +++ b/src/main/java/me/cortex/voxy/common/world/service/VoxelIngestService.java @@ -7,12 +7,14 @@ import me.cortex.voxy.common.voxelization.WorldConversionFactory; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceThreadPool; +import me.cortex.voxy.common.world.WorldUpdater; import me.cortex.voxy.commonImpl.IVoxyWorld; import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.world.LightType; import net.minecraft.world.chunk.ChunkNibbleArray; import net.minecraft.world.chunk.ChunkSection; import net.minecraft.world.chunk.WorldChunk; +import org.jetbrains.annotations.NotNull; import java.util.concurrent.ConcurrentLinkedDeque; @@ -32,46 +34,51 @@ public class VoxelIngestService { var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz); if (section.isEmpty() && task.blockLight==null && task.skyLight==null) {//If the chunk section has lighting data, propagate it - task.world.insertUpdate(vs.zero()); + WorldUpdater.insertUpdate(task.world, vs.zero()); } else { - ILightingSupplier supplier = (x,y,z) -> (byte) 0; - var sla = task.skyLight; - var bla = task.blockLight; - boolean sl = sla != null && !sla.isUninitialized(); - boolean bl = bla != null && !bla.isUninitialized(); - if (sl || bl) { - if (sl && bl) { - supplier = (x,y,z)-> { - int block = Math.min(15,bla.get(x, y, z)); - int sky = Math.min(15,sla.get(x, y, z)); - return (byte) (sky|(block<<4)); - }; - } else if (bl) { - supplier = (x,y,z)-> { - int block = Math.min(15,bla.get(x, y, z)); - int sky = 0; - return (byte) (sky|(block<<4)); - }; - } else { - supplier = (x,y,z)-> { - int block = 0; - int sky = Math.min(15,sla.get(x, y, z)); - return (byte) (sky|(block<<4)); - }; - } - } VoxelizedSection csec = WorldConversionFactory.convert( SECTION_CACHE.get(), task.world.getMapper(), section.getBlockStateContainer(), section.getBiomeContainer(), - supplier + getLightingSupplier(task) ); WorldConversionFactory.mipSection(csec, task.world.getMapper()); - task.world.insertUpdate(csec); + WorldUpdater.insertUpdate(task.world, csec); } } + @NotNull + private static ILightingSupplier getLightingSupplier(IngestSection task) { + ILightingSupplier supplier = (x,y,z) -> (byte) 0; + var sla = task.skyLight; + var bla = task.blockLight; + boolean sl = sla != null && !sla.isUninitialized(); + boolean bl = bla != null && !bla.isUninitialized(); + if (sl || bl) { + if (sl && bl) { + supplier = (x,y,z)-> { + int block = Math.min(15,bla.get(x, y, z)); + int sky = Math.min(15,sla.get(x, y, z)); + return (byte) (sky|(block<<4)); + }; + } else if (bl) { + supplier = (x,y,z)-> { + int block = Math.min(15,bla.get(x, y, z)); + int sky = 0; + return (byte) (sky|(block<<4)); + }; + } else { + supplier = (x,y,z)-> { + int block = 0; + int sky = Math.min(15,sla.get(x, y, z)); + return (byte) (sky|(block<<4)); + }; + } + } + return supplier; + } + private static boolean shouldIngestSection(ChunkSection section, int cx, int cy, int cz) { return true; } diff --git a/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java b/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java index b6ce3512..6dd70726 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java +++ b/src/main/java/me/cortex/voxy/commonImpl/IVoxyWorld.java @@ -5,4 +5,5 @@ import me.cortex.voxy.common.world.WorldEngine; public interface IVoxyWorld { WorldEngine getWorldEngine(); void setWorldEngine(WorldEngine engine); + void shutdownEngine(); } diff --git a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java index 63a792fe..6049f561 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java +++ b/src/main/java/me/cortex/voxy/commonImpl/VoxyInstance.java @@ -99,7 +99,7 @@ public class VoxyInstance { } protected WorldEngine createWorld(SectionStorage storage) { - var world = new WorldEngine(storage, 2048); + var world = new WorldEngine(storage, 2048, this); world.setSaveCallback(this.savingService::enqueueSave); this.activeWorlds.add(world); return world; diff --git a/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java b/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java index a9c88b73..e203ac0a 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java +++ b/src/main/java/me/cortex/voxy/commonImpl/importers/DHImporter.java @@ -7,6 +7,7 @@ import me.cortex.voxy.common.util.Pair; import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.voxelization.WorldConversionFactory; import me.cortex.voxy.common.world.WorldEngine; +import me.cortex.voxy.common.world.WorldUpdater; import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.common.world.service.SectionSavingService; import net.minecraft.block.Block; @@ -231,6 +232,9 @@ public class DHImporter implements IDataImporter { Logger.warn("Could not find block state with data", encEntry.substring(b)); } } + if (block == Blocks.AIR) { + Logger.warn("Could not find block entry with id:", bId); + } blockId = this.engine.getMapper().getIdForBlockState(state); } } @@ -295,6 +299,7 @@ public class DHImporter implements IDataImporter { } } } + if ((x+1)%16==0) { for (int sz = 0; sz < 4; sz++) { for (int sy = 0; sy < this.worldHeightSections; sy++) { @@ -302,7 +307,7 @@ public class DHImporter implements IDataImporter { WorldConversionFactory.mipSection(section, this.engine.getMapper()); section.setPosition(X*4+(x>>4), sy+(this.bottomOfWorld>>4), (Z*4)+sz); - this.engine.insertUpdate(section); + WorldUpdater.insertUpdate(this.engine, section); } int count = this.processedChunks.incrementAndGet(); diff --git a/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java b/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java index f34d6f7f..bf73d761 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java +++ b/src/main/java/me/cortex/voxy/commonImpl/importers/WorldImporter.java @@ -9,6 +9,7 @@ import me.cortex.voxy.common.voxelization.WorldConversionFactory; import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceThreadPool; +import me.cortex.voxy.common.world.WorldUpdater; import me.cortex.voxy.common.world.service.SectionSavingService; import net.minecraft.block.Block; import net.minecraft.block.BlockState; @@ -473,7 +474,6 @@ public class WorldImporter implements IDataImporter { ); WorldConversionFactory.mipSection(csec, this.world.getMapper()); - - this.world.insertUpdate(csec); + WorldUpdater.insertUpdate(this.world, csec); } } diff --git a/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java b/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java index 65d0bd90..5e3fdabd 100644 --- a/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java +++ b/src/main/java/me/cortex/voxy/commonImpl/mixin/minecraft/MixinWorld.java @@ -25,4 +25,12 @@ public class MixinWorld implements IVoxyWorld { } this.voxyWorld = engine; } + + @Override + public void shutdownEngine() { + if (this.voxyWorld != null && this.voxyWorld.instanceIn != null) { + this.voxyWorld.instanceIn.stopWorld(this.voxyWorld); + this.setWorldEngine(null); + } + } } diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index cda52dda..49c40d33 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -2,8 +2,6 @@ "voxy.config.title": "Voxy config", "voxy.config.general": "General", - "voxy.config.threads": "Threads", - "voxy.config.storage": "Storage", "voxy.config.general.enabled": "Enable Voxy", "voxy.config.general.enabled.tooltip": "Fully enables or disables voxy", @@ -18,5 +16,8 @@ "voxy.config.general.serviceThreads.tooltip": "Number of threads the ServiceThreadPool can use", "voxy.config.general.subDivisionSize": "Pixels^2 of subdivision size", - "voxy.config.general.subDivisionSize.tooltip": "Maximum size in pixels (squared) of screenspace AABB before subdiving to smaller LoDs (Smaller being higher quality)" + "voxy.config.general.subDivisionSize.tooltip": "Maximum size in pixels (squared) of screenspace AABB before subdiving to smaller LoDs (Smaller being higher quality)", + + "voxy.config.general.renderDistance": "Render distance", + "voxy.config.general.renderDistance.tooltip": "Render distance of voxy, each unit is equivalent to 32 chunks in vanilla (i.e. to get to vanilla render distance multiply by 32)" } \ No newline at end of file diff --git a/src/main/resources/assets/voxy/shaders/lod/block_model.glsl b/src/main/resources/assets/voxy/shaders/lod/block_model.glsl index e6f4b11d..f2454303 100644 --- a/src/main/resources/assets/voxy/shaders/lod/block_model.glsl +++ b/src/main/resources/assets/voxy/shaders/lod/block_model.glsl @@ -1,4 +1,3 @@ -#line 1 //TODO: FIXME: this isnt actually correct cause depending on the face (i think) it could be 1/64 th of a position off // but im going to assume that since we are dealing with huge render distances, this shouldent matter that much float extractFaceIndentation(uint faceData) { diff --git a/src/main/resources/assets/voxy/shaders/lod/quad_format.glsl b/src/main/resources/assets/voxy/shaders/lod/quad_format.glsl index 62046852..207a16c1 100644 --- a/src/main/resources/assets/voxy/shaders/lod/quad_format.glsl +++ b/src/main/resources/assets/voxy/shaders/lod/quad_format.glsl @@ -1,4 +1,3 @@ -#line 1 #ifdef GL_ARB_gpu_shader_int64 #define Quad uint64_t diff --git a/src/main/resources/assets/voxy/shaders/lod/section.glsl b/src/main/resources/assets/voxy/shaders/lod/section.glsl index be63ee63..1eed3f6b 100644 --- a/src/main/resources/assets/voxy/shaders/lod/section.glsl +++ b/src/main/resources/assets/voxy/shaders/lod/section.glsl @@ -1,4 +1,3 @@ -#line 1 uint extractDetail(SectionMeta section) { return section.posA>>28; } diff --git a/src/main/resources/client.voxy.mixins.json b/src/main/resources/client.voxy.mixins.json index 62646c45..950e28cc 100644 --- a/src/main/resources/client.voxy.mixins.json +++ b/src/main/resources/client.voxy.mixins.json @@ -8,6 +8,7 @@ "minecraft.MixinDebugHud", "minecraft.MixinMinecraftClient", "minecraft.MixinThreadExecutor", + "minecraft.MixinWindow", "minecraft.MixinWorldRenderer", "sodium.MixinDefaultChunkRenderer", "sodium.MixinRenderSectionManager"