Merge branch 'hierachial_rewrite_improved_meshing' into mc_1215

This commit is contained in:
mcrcortex
2025-04-01 11:54:30 +10:00
45 changed files with 1348 additions and 1864 deletions

View File

@@ -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<AdapterInfo> 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<AdapterInfo> 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);
}
}

View File

@@ -2,12 +2,16 @@ package me.cortex.voxy.client;
import me.cortex.voxy.client.config.VoxyConfig; import me.cortex.voxy.client.config.VoxyConfig;
import me.cortex.voxy.client.saver.ContextSelectionSystem; 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.common.world.WorldEngine;
import me.cortex.voxy.commonImpl.IVoxyWorld; import me.cortex.voxy.commonImpl.IVoxyWorld;
import me.cortex.voxy.commonImpl.ImportManager; import me.cortex.voxy.commonImpl.ImportManager;
import me.cortex.voxy.commonImpl.VoxyInstance; import me.cortex.voxy.commonImpl.VoxyInstance;
import net.minecraft.client.world.ClientWorld; import net.minecraft.client.world.ClientWorld;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedDeque;
public class VoxyClientInstance extends VoxyInstance { public class VoxyClientInstance extends VoxyInstance {
private static final ContextSelectionSystem SELECTOR = new ContextSelectionSystem(); private static final ContextSelectionSystem SELECTOR = new ContextSelectionSystem();
@@ -25,6 +29,7 @@ public class VoxyClientInstance extends VoxyInstance {
if (vworld == null) { if (vworld == null) {
vworld = this.createWorld(SELECTOR.getBestSelectionOrCreate(world).createSectionStorageBackend()); vworld = this.createWorld(SELECTOR.getBestSelectionOrCreate(world).createSectionStorageBackend());
((IVoxyWorld)world).setWorldEngine(vworld); ((IVoxyWorld)world).setWorldEngine(vworld);
//testDbPerformance2(vworld);
} else { } else {
if (!this.activeWorlds.contains(vworld)) { if (!this.activeWorlds.contains(vworld)) {
throw new IllegalStateException("World referenced does not exist in instance"); throw new IllegalStateException("World referenced does not exist in instance");
@@ -32,4 +37,64 @@ public class VoxyClientInstance extends VoxyInstance {
} }
return vworld; 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<Long> 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");
}
} }

View File

@@ -63,12 +63,8 @@ public class VoxyCommands {
((IGetVoxyRenderSystem)wr).shutdownRenderer(); ((IGetVoxyRenderSystem)wr).shutdownRenderer();
} }
var w = ((IVoxyWorld)MinecraftClient.getInstance().world); var w = ((IVoxyWorld)MinecraftClient.getInstance().world);
if (w != null) { if (w != null) w.shutdownEngine();
if (w.getWorldEngine() != null) {
instance.stopWorld(w.getWorldEngine());
}
w.setWorldEngine(null);
}
VoxyCommon.shutdownInstance(); VoxyCommon.shutdownInstance();
VoxyCommon.createInstance(); VoxyCommon.createInstance();
if (wr!=null) { if (wr!=null) {

View File

@@ -3,7 +3,6 @@ package me.cortex.voxy.client.config;
import com.google.gson.FieldNamingPolicy; import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import me.cortex.voxy.client.saver.ContextSelectionSystem;
import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.Logger;
import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.FabricLoader;
@@ -24,7 +23,7 @@ public class VoxyConfig {
public boolean enabled = true; public boolean enabled = true;
public boolean enableRendering = true; public boolean enableRendering = true;
public boolean ingestEnabled = true; public boolean ingestEnabled = true;
//public int renderDistance = 128; public int sectionRenderDistance = 16;
public int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors()/2, 1); public int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors()/2, 1);
public float subDivisionSize = 128; public float subDivisionSize = 128;
public int secondaryLruCacheSize = 1024; public int secondaryLruCacheSize = 1024;

View File

@@ -46,13 +46,7 @@ public class VoxyConfigScreenFactory implements ModMenuApi {
} }
//Shutdown world //Shutdown world
if (world != null && ON_SAVE_RELOAD_ALL) { if (world != null && ON_SAVE_RELOAD_ALL) {
//This is a hack inserted for the client world thing world.shutdownEngine();
//TODO: FIXME: MAKE BETTER
var engine = world.getWorldEngine();
if (engine != null) {
VoxyCommon.getInstance().stopWorld(engine);
}
world.setWorldEngine(null);
} }
//Shutdown instance //Shutdown instance
if (ON_SAVE_RELOAD_ALL) { if (ON_SAVE_RELOAD_ALL) {
@@ -112,6 +106,20 @@ public class VoxyConfigScreenFactory implements ModMenuApi {
.setDefaultValue((int) DEFAULT.subDivisionSize) .setDefaultValue((int) DEFAULT.subDivisionSize)
.build()); .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) //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.lruCacheSize"), config.secondaryLruCacheSize, 16, 1<<13)
// .setTooltip(Text.translatable("voxy.config.general.lruCacheSize.tooltip")) // .setTooltip(Text.translatable("voxy.config.general.lruCacheSize.tooltip"))
// .setSaveConsumer(val ->{if (config.secondaryLruCacheSize != val) reload(); config.secondaryLruCacheSize = val;}) // .setSaveConsumer(val ->{if (config.secondaryLruCacheSize != val) reload(); config.secondaryLruCacheSize = val;})

View File

@@ -1,45 +1,18 @@
package me.cortex.voxy.client.core; package me.cortex.voxy.client.core;
import com.mojang.blaze3d.systems.RenderSystem;
import me.cortex.voxy.client.config.VoxyConfig; 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.model.ModelBakerySubsystem;
import me.cortex.voxy.client.core.rendering.*; import me.cortex.voxy.client.core.rendering.building.RenderDataFactory45;
import me.cortex.voxy.client.core.rendering.building.RenderDataFactory4;
import me.cortex.voxy.client.core.rendering.building.RenderGenerationService; 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.saver.ContextSelectionSystem;
import me.cortex.voxy.client.taskbar.Taskbar;
import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.Logger;
import me.cortex.voxy.common.util.MemoryBuffer;
import me.cortex.voxy.common.world.WorldEngine; 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.thread.ServiceThreadPool;
import me.cortex.voxy.common.world.WorldSection; import me.cortex.voxy.common.world.WorldSection;
import me.cortex.voxy.common.world.other.Mapper; 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.*;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static org.lwjgl.opengl.GL30C.*; 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<WorldSection> 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<String> 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<String> 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();
}
} }

View File

@@ -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<dx*dx+dz*dz) {
this.posX = x;
this.posZ = z;
this.tracker.moveCenter(((int)x)>>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));
}
}
}

View File

@@ -1,5 +1,7 @@
package me.cortex.voxy.client.core.rendering; 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.ModelBakerySubsystem;
import me.cortex.voxy.client.core.model.ModelStore; import me.cortex.voxy.client.core.model.ModelStore;
import me.cortex.voxy.client.core.rendering.building.BuiltSection; import me.cortex.voxy.client.core.rendering.building.BuiltSection;
@@ -52,7 +54,7 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
//Max sections: ~500k //Max sections: ~500k
//Max geometry: 1 gb //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()); 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 //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<T extends AbstractSectionRenderer<J, ?>, J extends Vi
world.getMapper().setBiomeCallback(this.modelService::addBiome); 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) { 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(); this.modelService.tick();
} }
@@ -174,6 +173,7 @@ public class RenderService<T extends AbstractSectionRenderer<J, ?>, J extends Vi
this.nodeCleaner.free(); this.nodeCleaner.free();
//Release all the unprocessed built geometry //Release all the unprocessed built geometry
this.geometryUpdateQueue.clear(BuiltSection::free); this.geometryUpdateQueue.clear(BuiltSection::free);
this.sectionUpdateQueue.clear(WorldSection::release);//Release anything thats in the queue
} }
public Viewport<?> getViewport() { public Viewport<?> getViewport() {

View File

@@ -9,7 +9,7 @@ import java.util.function.LongConsumer;
import static me.cortex.voxy.common.world.WorldEngine.UPDATE_TYPE_BLOCK_BIT; import static me.cortex.voxy.common.world.WorldEngine.UPDATE_TYPE_BLOCK_BIT;
public class SectionUpdateRouter implements ISectionWatcher { 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);} public interface IChildUpdate {void accept(WorldSection section);}
private final Long2ByteOpenHashMap[] slices = new Long2ByteOpenHashMap[SLICES]; private final Long2ByteOpenHashMap[] slices = new Long2ByteOpenHashMap[SLICES];

View File

@@ -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.Capabilities;
import me.cortex.voxy.client.core.gl.GlBuffer; import me.cortex.voxy.client.core.gl.GlBuffer;
import me.cortex.voxy.client.core.model.ColourDepthTextureData; 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.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.post.PostProcessing;
import me.cortex.voxy.client.core.rendering.util.DownloadStream; import me.cortex.voxy.client.core.rendering.util.DownloadStream;
import me.cortex.voxy.client.core.rendering.util.RawDownloadStream; import me.cortex.voxy.client.core.rendering.util.RawDownloadStream;
import me.cortex.voxy.client.core.util.IrisUtil; 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.Logger;
import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.thread.ServiceThreadPool;
import me.cortex.voxy.common.world.WorldEngine; 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.block.Blocks;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Camera; import net.minecraft.client.render.Camera;
@@ -22,25 +29,45 @@ import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
import org.lwjgl.system.MemoryUtil; import org.lwjgl.system.MemoryUtil;
import java.util.ArrayList;
import java.util.List; 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; import static org.lwjgl.opengl.GL30C.GL_DRAW_FRAMEBUFFER_BINDING;
public class VoxyRenderSystem { public class VoxyRenderSystem {
private final RenderService renderer; private final RenderService renderer;
private final PostProcessing postProcessing; private final PostProcessing postProcessing;
private final WorldEngine worldIn;
private final RenderDistanceTracker renderDistanceTracker;
public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) { public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) {
//Trigger the shared index buffer loading //Trigger the shared index buffer loading
SharedIndexBuffer.INSTANCE.id(); SharedIndexBuffer.INSTANCE.id();
Capabilities.init();//Ensure clinit is called Capabilities.init();//Ensure clinit is called
this.worldIn = world;
this.renderer = new RenderService(world, threadPool); this.renderer = new RenderService(world, threadPool);
this.postProcessing = new PostProcessing(); 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) { public void renderSetup(Frustum frustum, Camera camera) {
this.renderDistanceTracker.setCenterAndProcess(camera.getBlockPos().getX(), camera.getBlockPos().getZ());
this.renderer.setup(camera); this.renderer.setup(camera);
PrintfDebugUtil.tick(); PrintfDebugUtil.tick();
} }
@@ -173,4 +200,129 @@ public class VoxyRenderSystem {
Logger.info("Shutting down post processor"); 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);}} 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<WorldSection> 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<String> 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<String> 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();
}
} }

View File

@@ -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;
}
}

View File

@@ -20,6 +20,14 @@ import java.util.Map;
public class RenderDataFactory45 { public class RenderDataFactory45 {
private static final boolean VERIFY_MESHING = VoxyCommon.isVerificationFlagOn("verifyMeshing"); 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 WorldEngine world;
private final ModelFactory modelMan; private final ModelFactory modelMan;
@@ -121,8 +129,11 @@ public class RenderDataFactory45 {
long quad = data | Integer.toUnsignedLong(encodedPosition); long quad = data | Integer.toUnsignedLong(encodedPosition);
MemoryUtil.memPutLong(RenderDataFactory45.this.directionalQuadBufferPtr + (RenderDataFactory45.this.directionalQuadCounters[face]++)*8L + face*8L*(1<<16), quad); MemoryUtil.memPutLong(RenderDataFactory45.this.directionalQuadBufferPtr + (RenderDataFactory45.this.directionalQuadCounters[face]++)*8L + face*8L*(1<<16), quad);
//Update AABB bounds //Update AABB bounds
if (axis == 0) {//Y if (axis == 0) {//Y
RenderDataFactory45.this.minY = Math.min(RenderDataFactory45.this.minY, auxPos); 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 blockMesher = new Mesher();
private final Mesher seondaryblockMesher = new Mesher();//Used for dual non-opaque geometry
public RenderDataFactory45(WorldEngine world, ModelFactory modelManager, boolean emitMeshlets) { public RenderDataFactory45(WorldEngine world, ModelFactory modelManager, boolean emitMeshlets) {
this.world = world; this.world = world;
@@ -277,7 +289,8 @@ public class RenderDataFactory45 {
private void generateYZOpaqueInnerGeometry(int axis) { private void generateYZOpaqueInnerGeometry(int axis) {
for (int layer = 0; layer < 31; layer++) { for (int layer = 0; layer < 31; layer++) {
this.blockMesher.auxiliaryPosition = 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 pidx = axis==0 ?(layer*32+other):(other*32+layer);
int skipAmount = axis==0?32:1; int skipAmount = axis==0?32:1;
@@ -286,10 +299,13 @@ public class RenderDataFactory45 {
int msk = current ^ next; int msk = current ^ next;
if (msk == 0) { if (msk == 0) {
this.blockMesher.skip(32); cSkip += 32;
continue; continue;
} }
this.blockMesher.skip(cSkip);
cSkip = 0;
//TODO: For boarder sections, should NOT EMIT neighbors faces //TODO: For boarder sections, should NOT EMIT neighbors faces
int faceForwardMsk = msk & current; int faceForwardMsk = msk & current;
int cIdx = -1; int cIdx = -1;
@@ -321,6 +337,7 @@ public class RenderDataFactory45 {
); );
} }
} }
this.blockMesher.endRow(); this.blockMesher.endRow();
} }
this.blockMesher.finish(); this.blockMesher.finish();
@@ -333,14 +350,18 @@ public class RenderDataFactory45 {
for (int side = 0; side < 2; side++) {//-, + for (int side = 0; side < 2; side++) {//-, +
int layer = side == 0 ? 0 : 31; int layer = side == 0 ? 0 : 31;
this.blockMesher.auxiliaryPosition = layer; this.blockMesher.auxiliaryPosition = layer;
int cSkips = 0;
for (int other = 0; other < 32; other++) { for (int other = 0; other < 32; other++) {
int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer); int pidx = axis == 0 ? (layer * 32 + other) : (other * 32 + layer);
int msk = this.opaqueMasks[pidx]; int msk = this.opaqueMasks[pidx];
if (msk == 0) { if (msk == 0) {
this.blockMesher.skip(32); cSkips += 32;
continue; continue;
} }
this.blockMesher.skip(cSkips);
cSkips = 0;
int cIdx = -1; int cIdx = -1;
while (msk != 0) { while (msk != 0) {
int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index int index = Integer.numberOfTrailingZeros(msk);//Is also the x-axis index
@@ -384,6 +405,73 @@ public class RenderDataFactory45 {
this.blockMesher.doAuxiliaryFaceOffset = true; 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() { private void generateYZFaces() {
for (int axis = 0; axis < 2; axis++) {//Y then Z for (int axis = 0; axis < 2; axis++) {//Y then Z
this.blockMesher.axis = axis; this.blockMesher.axis = axis;
@@ -391,54 +479,7 @@ public class RenderDataFactory45 {
this.generateYZOpaqueInnerGeometry(axis); this.generateYZOpaqueInnerGeometry(axis);
this.generateYZOpaqueOuterGeometry(axis); this.generateYZOpaqueOuterGeometry(axis);
//this.generateYZNonOpaqueInnerGeometry(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();
}
}
} }
} }
@@ -633,6 +674,11 @@ public class RenderDataFactory45 {
mb.doAuxiliaryFaceOffset = true; mb.doAuxiliaryFaceOffset = true;
} }
private void generateXNonOpaqueInnerGeometry() {
}
private void generateXFaces() { private void generateXFaces() {
this.generateXOpaqueInnerGeometry(); this.generateXOpaqueInnerGeometry();
this.generateXOuterOpaqueGeometry(); this.generateXOuterOpaqueGeometry();
@@ -668,6 +714,8 @@ public class RenderDataFactory45 {
{//Reset all the block meshes {//Reset all the block meshes
this.blockMesher.reset(); this.blockMesher.reset();
this.blockMesher.doAuxiliaryFaceOffset = true; this.blockMesher.doAuxiliaryFaceOffset = true;
this.seondaryblockMesher.reset();
this.seondaryblockMesher.doAuxiliaryFaceOffset = true;
for (var mesher : this.xAxisMeshers) { for (var mesher : this.xAxisMeshers) {
mesher.reset(); mesher.reset();
mesher.doAuxiliaryFaceOffset = true; mesher.doAuxiliaryFaceOffset = true;

View File

@@ -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;
}
}

View File

@@ -216,8 +216,9 @@ public class HierarchicalOcclusionTraverser {
//TODO: Move the first queue to a persistent list so its not updated every frame //TODO: Move the first queue to a persistent list so its not updated every frame
ptr = UploadStream.INSTANCE.upload(this.scratchQueueA, 0, 4L*initialQueueSize); ptr = UploadStream.INSTANCE.upload(this.scratchQueueA, 0, 4L*initialQueueSize);
for (int i = 0; i < initialQueueSize; i++) { int i = 0;
MemoryUtil.memPutInt(ptr + 4L*i, this.nodeManager.getTopLevelNodeIds().getInt(i)); for (int node : this.nodeManager.getTopLevelNodeIds()) {
MemoryUtil.memPutInt(ptr + 4L*(i++), node);
} }
UploadStream.INSTANCE.commit(); UploadStream.INSTANCE.commit();

View File

@@ -1,6 +1,5 @@
package me.cortex.voxy.client.core.rendering.hierachical; 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.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
@@ -87,7 +86,7 @@ public class NodeManager {
private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap(); private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap();
private final NodeStore nodeData; private final NodeStore nodeData;
public final int maxNodeCount; public final int maxNodeCount;
private final IntArrayList topLevelNodeIds = new IntArrayList(); private final IntOpenHashSet topLevelNodeIds = new IntOpenHashSet();
private final LongOpenHashSet topLevelNodes = new LongOpenHashSet(); private final LongOpenHashSet topLevelNodes = new LongOpenHashSet();
private int activeNodeRequestCount; private int activeNodeRequestCount;
@@ -133,24 +132,26 @@ public class NodeManager {
} }
public void removeTopLevelNode(long pos) { 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); int nodeId = this.activeSectionMap.get(pos);
if (nodeId == -1) { if (nodeId == -1) {
Logger.error("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!"); throw new IllegalStateException("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!");
return; }
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; return this.topLevelNodeIds;
} }
@@ -599,6 +600,35 @@ public class NodeManager {
this._recurseRemoveNode(pos, false); 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<<i))==0) continue;
int mesh = req.getChildMesh(i);
if (mesh != EMPTY_GEOMETRY_ID && mesh != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(mesh);
//Unwatch the request position
long childPos = makeChildPos(pos, i);
//Remove from section tracker
int cId = this.activeSectionMap.remove(childPos);
if (cId == -1) {
throw new IllegalStateException("Child not in activeMap");
}
if ((cId&NODE_TYPE_MSK) != NODE_TYPE_REQUEST || (cId&REQUEST_TYPE_MSK) != REQUEST_TYPE_CHILD || (cId&NODE_ID_MSK) != reqId) {
throw new IllegalStateException("Invalid child active state map: " + cId);
}
if (!this.watcher.unwatch(childPos, WorldEngine.UPDATE_FLAGS)) {
throw new IllegalStateException("Pos was not being watched");
}
}
this.childRequests.release(reqId);//Release the request
this.activeNodeRequestCount--;
}
//Recursivly fully removes all nodes and children //Recursivly fully removes all nodes and children
private void _recurseRemoveNode(long pos, boolean onlyRemoveChildren) { private void _recurseRemoveNode(long pos, boolean onlyRemoveChildren) {
//NOTE: this also removes from the section map //NOTE: this also removes from the section map
@@ -612,8 +642,8 @@ public class NodeManager {
throw new IllegalStateException("Cannot remove pos that doesnt exist"); throw new IllegalStateException("Cannot remove pos that doesnt exist");
} }
int type = nodeId&NODE_TYPE_MSK; int type = nodeId&NODE_TYPE_MSK;
nodeId &= NODE_ID_MSK;
if (type == NODE_TYPE_INNER || type == NODE_TYPE_LEAF) { if (type == NODE_TYPE_INNER || type == NODE_TYPE_LEAF) {
nodeId &= NODE_ID_MSK;
if (!this.nodeData.nodeExists(nodeId)) { if (!this.nodeData.nodeExists(nodeId)) {
throw new IllegalStateException("Node exists in section map but not in nodeData"); throw new IllegalStateException("Node exists in section map but not in nodeData");
} }
@@ -628,34 +658,8 @@ public class NodeManager {
var req = this.childRequests.get(reqId); var req = this.childRequests.get(reqId);
childExistence ^= req.getMsk(); childExistence ^= req.getMsk();
//TODO FINISH this._removeRequest(reqId, req, pos);
//Delete all the request data
for (int i = 0; i < 8; i++) {
if ((req.getMsk()&(1<<i))==0) continue;
int mesh = req.getChildMesh(i);
if (mesh != EMPTY_GEOMETRY_ID && mesh != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(mesh);
//Unwatch the request position
long childPos = makeChildPos(pos, i);
//Remove from section tracker
int cId = this.activeSectionMap.remove(childPos);
if (cId == -1) {
throw new IllegalStateException("Child not in activeMap");
}
if ((cId&NODE_TYPE_MSK) != NODE_TYPE_REQUEST || (cId&REQUEST_TYPE_MSK) != REQUEST_TYPE_CHILD || (cId&NODE_ID_MSK) != reqId) {
throw new IllegalStateException("Invalid child active state map: " + cId);
}
if (!this.watcher.unwatch(childPos, WorldEngine.UPDATE_FLAGS)) {
throw new IllegalStateException("Pos was not being watched");
}
}
this.childRequests.release(reqId);//Release the request
this.activeNodeRequestCount--;
if (onlyRemoveChildren) { if (onlyRemoveChildren) {
this.nodeData.unmarkRequestInFlight(nodeId); this.nodeData.unmarkRequestInFlight(nodeId);
this.nodeData.setNodeRequest(nodeId, NULL_REQUEST_ID); this.nodeData.setNodeRequest(nodeId, NULL_REQUEST_ID);
@@ -745,10 +749,33 @@ public class NodeManager {
//TODO: probably need this.clearId(nodeId); //TODO: probably need this.clearId(nodeId);
this.invalidateNode(nodeId); this.invalidateNode(nodeId);
} }
} else { } else if (type == NODE_TYPE_REQUEST) {
if (!this.watcher.unwatch(pos, WorldEngine.UPDATE_FLAGS)) {
throw new IllegalStateException("Pos was not being watched");
}
if ((nodeId&REQUEST_TYPE_MSK) == REQUEST_TYPE_SINGLE) {
nodeId &= NODE_ID_MSK;
Logger.error("UNFINISHED OPERATION TODO: FIXME3"); var req = this.singleRequests.get(nodeId);
//NOTE: There are request type singles and request type child!!!! if (req.getPosition() != pos)
throw new IllegalStateException();
this.singleRequests.release(nodeId);
if (req.hasMeshSet()) {
int mesh = req.getMesh();
if (mesh != EMPTY_GEOMETRY_ID && mesh != NULL_GEOMETRY_ID)
this.geometryManager.removeSection(mesh);
}
} else {
nodeId &= NODE_ID_MSK;
var req = this.childRequests.get(nodeId);
if (req.getPosition() != pos)
throw new IllegalStateException();
this._removeRequest(nodeId, req, pos);
}
} else {
throw new IllegalStateException();
} }
} }

View File

@@ -42,4 +42,7 @@ class SingleNodeRequest {
public boolean hasChildExistenceSet() { public boolean hasChildExistenceSet() {
return (this.setMsk&2)!=0; return (this.setMsk&2)!=0;
} }
public boolean hasMeshSet() {
return (this.setMsk&1)!=0;
}
} }

View File

@@ -2,9 +2,7 @@ package me.cortex.voxy.client.core.rendering.hierachical;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.longs.Long2IntFunction;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import me.cortex.voxy.client.core.rendering.ISectionWatcher; import me.cortex.voxy.client.core.rendering.ISectionWatcher;
import me.cortex.voxy.client.core.rendering.building.BuiltSection; import me.cortex.voxy.client.core.rendering.building.BuiltSection;
import me.cortex.voxy.client.core.rendering.section.AbstractSectionGeometryManager; import me.cortex.voxy.client.core.rendering.section.AbstractSectionGeometryManager;
@@ -236,6 +234,10 @@ public class TestNodeManager {
public void verifyIntegrity() { public void verifyIntegrity() {
this.nodeManager.verifyIntegrity(this.watcher.updateTypes.keySet(), this.cleaner.active); this.nodeManager.verifyIntegrity(this.watcher.updateTypes.keySet(), this.cleaner.active);
} }
public void remTopPos(long pos) {
this.nodeManager.removeTopLevelNode(pos);
}
} }
private static void fillInALl(TestBase test, long pos, Long2IntFunction converter) { private static void fillInALl(TestBase test, long pos, Long2IntFunction converter) {
@@ -274,8 +276,8 @@ public class TestNodeManager {
public static void main(String[] args) { public static void main(String[] args) {
Logger.INSERT_CLASS = false; Logger.INSERT_CLASS = false;
int ITER_COUNT = 50_000; int ITER_COUNT = 5_000;
int INNER_ITER_COUNT = 500_000; int INNER_ITER_COUNT = 100_000;
boolean GEO_REM = true; boolean GEO_REM = true;
AtomicInteger finished = new AtomicInteger(); AtomicInteger finished = new AtomicInteger();
@@ -299,13 +301,14 @@ public class TestNodeManager {
} }
System.out.println("Finished " + finished.get() + " iterations out of " + ITER_COUNT); System.out.println("Finished " + finished.get() + " iterations out of " + ITER_COUNT);
} }
private static long rPos(Random r) { private static long rPos(Random r, LongList tops) {
int lvl = r.nextInt(5); int lvl = r.nextInt(5);
long top = tops.getLong(r.nextInt(tops.size()));
if (lvl==4) { if (lvl==4) {
return WorldEngine.getWorldSectionId(4,0,0,0); return top;
} }
int bound = 16>>lvl; int bound = 16>>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<List<StackTraceElement>> traces, boolean geoRemoval) { private static boolean runTest(int ITERS, int testIdx, Set<List<StackTraceElement>> traces, boolean geoRemoval) {
@@ -314,14 +317,30 @@ public class TestNodeManager {
Random r = new Random(testIdx * 1234L); Random r = new Random(testIdx * 1234L);
try { try {
var test = new TestBase(); var test = new TestBase();
LongList tops = new LongArrayList();
//Fuzzy bruteforce everything //Fuzzy bruteforce everything
test.putTopPos(POS_A); test.putTopPos(POS_A);
tops.add(POS_A);
for (int i = 0; i < ITERS; i++) { for (int i = 0; i < ITERS; i++) {
long pos = rPos(r); long pos = rPos(r, tops);
int op = r.nextInt(4); int op = r.nextInt(5);
int extra = r.nextInt(256); int extra = r.nextInt(256);
boolean hasGeometry = r.nextBoolean(); 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); test.request(pos);
} }
if (op == 1) { if (op == 1) {
@@ -336,8 +355,20 @@ public class TestNodeManager {
test.printNodeChanges(); test.printNodeChanges();
test.verifyIntegrity(); test.verifyIntegrity();
} }
test.childUpdate(POS_A, 0); for (long top : tops) {
test.meshUpdate(POS_A, 0, 0); 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) { if (test.geometryManager.memoryInUse != 0) {
throw new IllegalStateException(); throw new IllegalStateException();
} }

View File

@@ -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<res)||(((!load)&&res<0))) {
throw new IllegalStateException();
}
}
}
}
public void unload() {
this.fillRing(false);
}
//Moves the center from old to new and updates the operations map
public void moveCenter(int x, int z) {
//TODO, if the new center is greater than radius from current, unload all current and load all at new
if (this.radius+1<Math.abs(x-this.centerX) || this.radius+1<Math.abs(z-this.centerZ)) {
this.fillRing(false);
this.centerX = x;
this.centerZ = z;
this.fillRing(true);
} else {
if (x != this.centerX) {
moveX(x - this.centerX);
}
if (z != this.centerZ) {
moveZ(z - this.centerZ);
}
}
}
private void moveZ(int delta) {
if (delta == 0) return;
//Since +- 1 is the most common operation, fastpath it
if (delta == -1 || delta == 1) {
for (int i = 0; i <= this.radius * 2; i++) {
int x = this.centerX + i - this.radius;
//Multiply by the delta since its +-1 it also then makes it the correct orientation
int d = this.boundDist[i]*delta;
int pz = this.centerZ+d+delta;//Point to add (we need to offset by 1 in the mov direction)
int nz = this.centerZ-d;//Point to rem
if (0<this.operations.addTo(pack(x, pz), (byte) 1))//Load point
throw new IllegalStateException("x: "+x+", z: "+pz+" state: "+this.operations.get(pack(x, pz)));
if (this.operations.addTo(pack(x, nz), (byte) -1)<0)//Unload point
throw new IllegalStateException("x: "+x+", z: "+nz+" state: "+this.operations.get(pack(x, nz)));
}
this.centerZ += delta;
} else {
int sDelta = Integer.signum(delta);
for (int i = 0; i <= this.radius * 2; i++) {
int x = this.centerX + i - this.radius;
//Multiply by the delta since its +-1 it also then makes it the correct orientation
int d = this.boundDist[i]*sDelta;
int pz = this.centerZ+d;//Point to add (we need to offset by 1 in the mov direction)
for (int z = pz + (sDelta<0?delta:1); z <= pz + (sDelta<0?-1:delta); z++) {
if (0<this.operations.addTo(pack(x, z), (byte) 1))//Load point
throw new IllegalStateException();
}
int nz = this.centerZ-d;//Point to rem
for (int z = nz + (sDelta<0?(delta+1):0); z < nz + (sDelta<0?1:delta); z++) {
if (this.operations.addTo(pack(x, z), (byte) -1)<0)//Unload point
throw new IllegalStateException();
}
}
this.centerZ += delta;
}
}
private void moveX(int delta) {
if (delta == 0) return;
//Since +- 1 is the most common operation, fastpath it
if (delta == -1 || delta == 1) {
for (int i = 0; i <= this.radius * 2; i++) {
int z = this.centerZ + i - this.radius;
//Multiply by the delta since its +-1 it also then makes it the correct orientation
int d = this.boundDist[i]*delta;
int px = this.centerX+d+delta;//Point to add (we need to offset by 1 in the mov direction)
int nx = this.centerX-d;//Point to rem
if (0<this.operations.addTo(pack(px, z), (byte) 1))//Load point
throw new IllegalStateException();
if (this.operations.addTo(pack(nx, z), (byte) -1)<0)//Unload point
throw new IllegalStateException();
}
this.centerX += delta;
} else {
int sDelta = Integer.signum(delta);
for (int i = 0; i <= this.radius * 2; i++) {
int z = this.centerZ + i - this.radius;
//Multiply by the delta since its +-1 it also then makes it the correct orientation
int d = this.boundDist[i]*sDelta;
int px = this.centerX+d;//Point to add (we need to offset by 1 in the mov direction)
for (int x = px + (sDelta<0?delta:1); x <= px + (sDelta<0?-1:delta); x++) {
if (0<this.operations.addTo(pack(x, z), (byte) 1))//Load point
throw new IllegalStateException();
}
int nx = this.centerX-d;//Point to rem
for (int x = nx + (sDelta<0?(delta+1):0); x < nx + (sDelta<0?1:delta); x++) {
if (this.operations.addTo(pack(x, z), (byte) -1)<0)//Unload point
throw new IllegalStateException();
}
}
this.centerX += delta;
}
}
public interface IUpdateConsumer {
void accept(int x, int z);
}
//Processes N operations from the operations map
public int process(int N, IUpdateConsumer onAdd, IUpdateConsumer onRemove) {
if (this.operations.isEmpty()) {
return 0;
}
var iter = this.operations.long2ByteEntrySet().fastIterator();
int i = 0;
while (iter.hasNext() && N--!=0) {
var entry = iter.next();
if (entry.getByteValue()==0) {
iter.remove(); N++;
continue;
}
i++;
byte op = entry.getByteValue();
if (op != 1 && op != -1) {
throw new IllegalStateException();
}
boolean isAdd = op == 1;
long pos = entry.getLongKey();
int x = (int) (pos&0xFFFFFFFFL);
int z = (int) ((pos>>>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);
});
}
}
}

View File

@@ -65,7 +65,7 @@ public class RingUtil {
public static int[] generatingBoundingCorner2D(int radius) { public static int[] generatingBoundingCorner2D(int radius) {
IntOpenHashSet points = new IntOpenHashSet(); IntOpenHashSet points = new IntOpenHashSet();
//Do 2 pass (x and y) to generate and cover all points //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)); int other = (int) Math.floor(Math.sqrt(radius*radius - i*i));
//add points (x,other) and (other,x) as it covers the full spectrum //add points (x,other) and (other,x) as it covers the full spectrum
points.add((i<<16)|other); points.add((i<<16)|other);

View File

@@ -101,7 +101,7 @@ public abstract class ScanMesher2D {
//TODO: replace with much better method, TODO: check this is right!! //TODO: replace with much better method, TODO: check this is right!!
this.putNext(0); this.putNext(0);
if (1<count) { if (1<count) {
this.emitRanged(((1 << (count - 1)) - 1) << (this.currentIndex & 31)); this.emitRanged(((1 << (Math.min(count, 32) - 1)) - 1) << (this.currentIndex & 31));
this.currentIndex += count - 1; this.currentIndex += count - 1;
} }
/* /*

View File

@@ -0,0 +1,22 @@
package me.cortex.voxy.client.mixin.minecraft;
import me.cortex.voxy.client.GPUSelectorWindows2;
import net.minecraft.client.WindowEventHandler;
import net.minecraft.client.WindowSettings;
import net.minecraft.client.util.MonitorTracker;
import net.minecraft.client.util.Window;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Window.class)
public class MixinWindow {
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/Window;throwOnGlError()V"))
private void injectInitWindow(WindowEventHandler eventHandler, MonitorTracker monitorTracker, WindowSettings settings, String fullscreenVideoMode, String title, CallbackInfo ci) {
var prop = System.getProperty("voxy.forceGpuSelectionIndex", "NO");
if (!prop.equals("NO")) {
GPUSelectorWindows2.doSelector(Integer.parseInt(prop));
}
}
}

View File

@@ -52,11 +52,7 @@ public abstract class MixinWorldRenderer implements IGetVoxyRenderSystem {
this.shutdownRenderer(); this.shutdownRenderer();
if (this.world != null) { if (this.world != null) {
var engine = ((IVoxyWorld)this.world).getWorldEngine(); ((IVoxyWorld)this.world).shutdownEngine();
if (engine != null) {
VoxyCommon.getInstance().stopWorld(engine);
}
((IVoxyWorld)this.world).setWorldEngine(null);
} }
} }
} }

View File

@@ -28,7 +28,13 @@ public class Logger {
String error = (INSERT_CLASS?("["+stackEntry.getClassName()+"]: "):"") + Stream.of(args).map(Logger::objToString).collect(Collectors.joining(" ")); String error = (INSERT_CLASS?("["+stackEntry.getClassName()+"]: "):"") + Stream.of(args).map(Logger::objToString).collect(Collectors.joining(" "));
LOGGER.error(error, throwable); LOGGER.error(error, throwable);
if (VoxyCommon.IS_IN_MINECRAFT && !VoxyCommon.IS_DEDICATED_SERVER) { if (VoxyCommon.IS_IN_MINECRAFT && !VoxyCommon.IS_DEDICATED_SERVER) {
MinecraftClient.getInstance().executeSync(()->{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);
});
}
} }
} }

View File

@@ -3,8 +3,10 @@ package me.cortex.voxy.common.config;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.function.LongConsumer;
public interface IMappingStorage { public interface IMappingStorage {
void iterateStoredSectionPositions(LongConsumer consumer);
void putIdMapping(int id, ByteBuffer data); void putIdMapping(int id, ByteBuffer data);
Int2ObjectOpenHashMap<byte[]> getIdMappingsData(); Int2ObjectOpenHashMap<byte[]> getIdMappingsData();
void flush(); void flush();

View File

@@ -8,12 +8,14 @@ import me.cortex.voxy.common.config.storage.StorageConfig;
import me.cortex.voxy.common.util.ThreadLocalMemoryBuffer; import me.cortex.voxy.common.util.ThreadLocalMemoryBuffer;
import me.cortex.voxy.common.world.SaveLoadSystem; import me.cortex.voxy.common.world.SaveLoadSystem;
import me.cortex.voxy.common.world.SaveLoadSystem2; 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.WorldSection;
import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.common.world.other.Mapper;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.LongConsumer;
public class SectionSerializationStorage extends SectionStorage { public class SectionSerializationStorage extends SectionStorage {
private final StorageBackend backend; private final StorageBackend backend;
@@ -26,7 +28,7 @@ public class SectionSerializationStorage extends SectionStorage {
public int loadSection(WorldSection into) { public int loadSection(WorldSection into) {
var data = this.backend.getSectionData(into.key, MEMORY_CACHE.get().createUntrackedUnfreeableReference()); var data = this.backend.getSectionData(into.key, MEMORY_CACHE.get().createUntrackedUnfreeableReference());
if (data != null) { if (data != null) {
if (!SaveLoadSystem.deserialize(into, data)) { if (!SaveLoadSystem3.deserialize(into, data)) {
this.backend.deleteSectionData(into.key); this.backend.deleteSectionData(into.key);
//TODO: regenerate the section from children //TODO: regenerate the section from children
Arrays.fill(into._unsafeGetRawDataArray(), Mapper.AIR); Arrays.fill(into._unsafeGetRawDataArray(), Mapper.AIR);
@@ -46,7 +48,7 @@ public class SectionSerializationStorage extends SectionStorage {
@Override @Override
public void saveSection(WorldSection section) { public void saveSection(WorldSection section) {
var saveData = SaveLoadSystem.serialize(section); var saveData = SaveLoadSystem3.serialize(section);
this.backend.setSectionData(section.key, saveData); this.backend.setSectionData(section.key, saveData);
saveData.free(); saveData.free();
} }
@@ -71,6 +73,11 @@ public class SectionSerializationStorage extends SectionStorage {
this.backend.close(); this.backend.close();
} }
@Override
public void iterateStoredSectionPositions(LongConsumer consumer) {
this.backend.iterateStoredSectionPositions(consumer);
}
public static class Config extends SectionStorageConfig { public static class Config extends SectionStorageConfig {
public StorageConfig storage; public StorageConfig storage;

View File

@@ -10,7 +10,6 @@ import java.util.List;
import java.util.function.LongConsumer; import java.util.function.LongConsumer;
public abstract class StorageBackend implements IMappingStorage { 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 //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); public abstract MemoryBuffer getSectionData(long key, MemoryBuffer scratch);

View File

@@ -34,7 +34,9 @@ public class FragmentedStorageBackendAdaptor extends StorageBackend {
@Override @Override
public void iterateStoredSectionPositions(LongConsumer consumer) { 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 //TODO: reencode the key to be shifted one less OR

View File

@@ -21,6 +21,7 @@ public class RocksDBStorageBackend extends StorageBackend {
private final ColumnFamilyHandle worldSections; private final ColumnFamilyHandle worldSections;
private final ColumnFamilyHandle idMappings; private final ColumnFamilyHandle idMappings;
private final ReadOptions sectionReadOps; private final ReadOptions sectionReadOps;
private final WriteOptions sectionWriteOps;
//NOTE: closes in order //NOTE: closes in order
private final List<AbstractImmutableNativeReference> closeList = new ArrayList<>(); private final List<AbstractImmutableNativeReference> 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<ColumnFamilyDescriptor> cfDescriptors = Arrays.asList( final List<ColumnFamilyDescriptor> cfDescriptors = Arrays.asList(
new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts), new ColumnFamilyDescriptor(RocksDB.DEFAULT_COLUMN_FAMILY, cfOpts),
@@ -59,7 +62,8 @@ public class RocksDBStorageBackend extends StorageBackend {
final DBOptions options = new DBOptions() final DBOptions options = new DBOptions()
.setCreateIfMissing(true) .setCreateIfMissing(true)
.setCreateMissingColumnFamilies(true); .setCreateMissingColumnFamilies(true)
.setMaxTotalWalSize(1024*1024*512);//512 mb max WAL size
List<ColumnFamilyHandle> handles = new ArrayList<>(); List<ColumnFamilyHandle> handles = new ArrayList<>();
@@ -69,12 +73,14 @@ public class RocksDBStorageBackend extends StorageBackend {
handles); handles);
this.sectionReadOps = new ReadOptions(); this.sectionReadOps = new ReadOptions();
this.sectionWriteOps = new WriteOptions();
this.closeList.addAll(handles); this.closeList.addAll(handles);
this.closeList.add(this.db); this.closeList.add(this.db);
this.closeList.add(options); this.closeList.add(options);
this.closeList.add(cfOpts); this.closeList.add(cfOpts);
this.closeList.add(this.sectionReadOps); this.closeList.add(this.sectionReadOps);
this.closeList.add(this.sectionWriteOps);
this.worldSections = handles.get(1); this.worldSections = handles.get(1);
this.idMappings = handles.get(2); this.idMappings = handles.get(2);
@@ -87,7 +93,19 @@ public class RocksDBStorageBackend extends StorageBackend {
@Override @Override
public void iterateStoredSectionPositions(LongConsumer consumer) { 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 @Override
@@ -117,10 +135,10 @@ public class RocksDBStorageBackend extends StorageBackend {
//TODO: FIXME, use the ByteBuffer variant //TODO: FIXME, use the ByteBuffer variant
@Override @Override
public void setSectionData(long key, MemoryBuffer data) { public void setSectionData(long key, MemoryBuffer data) {
try { try (var stack = MemoryStack.stackPush()) {
var buffer = new byte[(int) data.size]; var keyBuff = stack.calloc(8);
UnsafeUtil.memcpy(data.address, buffer); MemoryUtil.memPutLong(MemoryUtil.memAddress(keyBuff), Long.reverseBytes(key));
this.db.put(this.worldSections, longToBytes(key), buffer); this.db.put(this.worldSections, this.sectionWriteOps, keyBuff, data.asByteBuffer());
} catch (RocksDBException e) { } catch (RocksDBException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@@ -18,6 +18,10 @@ public class ThreadLocalMemoryBuffer {
this.threadLocal = ThreadLocal.withInitial(()->createMemoryBuffer(size)); this.threadLocal = ThreadLocal.withInitial(()->createMemoryBuffer(size));
} }
public static MemoryBuffer create(long size) {
return createMemoryBuffer(size);
}
public MemoryBuffer get() { public MemoryBuffer get() {
return this.threadLocal.get(); return this.threadLocal.get();
} }

View File

@@ -30,22 +30,22 @@ public class SaveLoadSystem {
return x|(y<<10)|(z<<5); return x|(y<<10)|(z<<5);
} }
private record SerializationCache(long[] blockStateCache, short[] compressedCache, long[] lutCache, Long2ShortOpenHashMap lutMapCache) {
private static final ThreadLocal<short[]> SHORT_CACHE = ThreadLocal.withInitial(()->new short[32*32*32]); public SerializationCache() {
private static final ThreadLocal<long[]> LONG_CACHE = ThreadLocal.withInitial(()->new long[32*32*32]); this(new long[WorldSection.SECTION_VOLUME], new short[WorldSection.SECTION_VOLUME], new long[WorldSection.SECTION_VOLUME], new Long2ShortOpenHashMap(512));
private static final ThreadLocal<Long2ShortOpenHashMap> OTHER_THING_CACHE = ThreadLocal.withInitial(()-> { this.lutMapCache.defaultReturnValue((short) -1);
var thing = new Long2ShortOpenHashMap(512); }
thing.defaultReturnValue((short) -1); }
return thing; private static final ThreadLocal<SerializationCache> CACHE = ThreadLocal.withInitial(SerializationCache::new);
});
//TODO: Cache like long2short and the short and other data to stop allocs //TODO: Cache like long2short and the short and other data to stop allocs
public static MemoryBuffer serialize(WorldSection section) { public static MemoryBuffer serialize(WorldSection section) {
var data = section.copyData(); var cache = CACHE.get();
var compressed = SHORT_CACHE.get(); var data = cache.blockStateCache;
Long2ShortOpenHashMap LUT = OTHER_THING_CACHE.get();LUT.clear(); section.copyDataTo(data);
long[] lutValues = LONG_CACHE.get();//If there are more than this many states in a section... im concerned 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; short lutIndex = 0;
long pHash = 99; long pHash = 99;
for (int i = 0; i < data.length; i++) { 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); throw new IllegalStateException("lutLen impossibly large, max size should be 32768 but got size " + lutLen);
} }
//TODO: cache this in a thread local //TODO: cache this in a thread local
long[] lut = LONG_CACHE.get(); long[] lut = CACHE.get().lutCache;
long hash = 0; long hash = 0;
if (VERIFY_HASH_ON_LOAD) { if (VERIFY_HASH_ON_LOAD) {
hash = key ^ (lutLen * 1293481298141L); hash = key ^ (lutLen * 1293481298141L);

View File

@@ -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<SerializationCache> 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;
}
}

View File

@@ -5,6 +5,8 @@ import me.cortex.voxy.common.config.section.SectionStorage;
import me.cortex.voxy.common.util.TrackedObject; import me.cortex.voxy.common.util.TrackedObject;
import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.voxelization.VoxelizedSection;
import me.cortex.voxy.common.world.other.Mapper; import me.cortex.voxy.common.world.other.Mapper;
import me.cortex.voxy.commonImpl.VoxyInstance;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
public class WorldEngine { public class WorldEngine {
@@ -24,8 +26,7 @@ public class WorldEngine {
private final ActiveSectionTracker sectionTracker; private final ActiveSectionTracker sectionTracker;
private ISectionChangeCallback dirtyCallback; private ISectionChangeCallback dirtyCallback;
private ISectionSaveCallback saveCallback; private ISectionSaveCallback saveCallback;
private final int maxMipLevels; volatile boolean isLive = true;
private volatile boolean isLive = true;
public void setDirtyCallback(ISectionChangeCallback callback) { public void setDirtyCallback(ISectionChangeCallback callback) {
this.dirtyCallback = callback; this.dirtyCallback = callback;
@@ -37,12 +38,16 @@ public class WorldEngine {
public Mapper getMapper() {return this.mapper;} public Mapper getMapper() {return this.mapper;}
public boolean isLive() {return this.isLive;} public boolean isLive() {return this.isLive;}
public final @Nullable VoxyInstance instanceIn;
public WorldEngine(SectionStorage storage, int cacheCount) { 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) { public WorldEngine(SectionStorage storage, int cacheCount, @Nullable VoxyInstance instance) {
this.maxMipLevels = maxMipLayers; this.instanceIn = instance;
this.storage = storage; this.storage = storage;
this.mapper = new Mapper(this.storage); this.mapper = new Mapper(this.storage);
//5 cache size bits means that the section tracker has 32 separate maps that it uses //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) { public void markDirty(WorldSection section, int changeState) {
if (!this.isLive) throw new IllegalStateException("World is not live"); 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) { if (this.dirtyCallback != null) {
this.dirtyCallback.accept(section, changeState); 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<String> debug) { public void addDebugData(List<String> debug) {
debug.add("ACC/SCC: " + this.sectionTracker.getLoadedCacheCount()+"/"+this.sectionTracker.getSecondaryCacheSize());//Active cache count, Secondary cache counts debug.add("ACC/SCC: " + this.sectionTracker.getLoadedCacheCount()+"/"+this.sectionTracker.getSecondaryCacheSize());//Active cache count, Secondary cache counts
} }

View File

@@ -54,7 +54,7 @@ public final class WorldSection {
volatile int nonEmptyBlockCount = 0; volatile int nonEmptyBlockCount = 0;
volatile byte nonEmptyChildren; volatile byte nonEmptyChildren;
private final ActiveSectionTracker tracker; final ActiveSectionTracker tracker;
public final AtomicBoolean inSaveQueue = new AtomicBoolean(); public final AtomicBoolean inSaveQueue = new AtomicBoolean();
//When the first bit is set it means its loaded //When the first bit is set it means its loaded

View File

@@ -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();
}
}
}

View File

@@ -7,12 +7,14 @@ import me.cortex.voxy.common.voxelization.WorldConversionFactory;
import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceSlice;
import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.thread.ServiceThreadPool;
import me.cortex.voxy.common.world.WorldUpdater;
import me.cortex.voxy.commonImpl.IVoxyWorld; import me.cortex.voxy.commonImpl.IVoxyWorld;
import net.minecraft.util.math.ChunkSectionPos; import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.world.LightType; import net.minecraft.world.LightType;
import net.minecraft.world.chunk.ChunkNibbleArray; import net.minecraft.world.chunk.ChunkNibbleArray;
import net.minecraft.world.chunk.ChunkSection; import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.chunk.WorldChunk; import net.minecraft.world.chunk.WorldChunk;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
@@ -32,46 +34,51 @@ public class VoxelIngestService {
var vs = SECTION_CACHE.get().setPosition(task.cx, task.cy, task.cz); 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 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 { } 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( VoxelizedSection csec = WorldConversionFactory.convert(
SECTION_CACHE.get(), SECTION_CACHE.get(),
task.world.getMapper(), task.world.getMapper(),
section.getBlockStateContainer(), section.getBlockStateContainer(),
section.getBiomeContainer(), section.getBiomeContainer(),
supplier getLightingSupplier(task)
); );
WorldConversionFactory.mipSection(csec, task.world.getMapper()); 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) { private static boolean shouldIngestSection(ChunkSection section, int cx, int cy, int cz) {
return true; return true;
} }

View File

@@ -5,4 +5,5 @@ import me.cortex.voxy.common.world.WorldEngine;
public interface IVoxyWorld { public interface IVoxyWorld {
WorldEngine getWorldEngine(); WorldEngine getWorldEngine();
void setWorldEngine(WorldEngine engine); void setWorldEngine(WorldEngine engine);
void shutdownEngine();
} }

View File

@@ -99,7 +99,7 @@ public class VoxyInstance {
} }
protected WorldEngine createWorld(SectionStorage storage) { protected WorldEngine createWorld(SectionStorage storage) {
var world = new WorldEngine(storage, 2048); var world = new WorldEngine(storage, 2048, this);
world.setSaveCallback(this.savingService::enqueueSave); world.setSaveCallback(this.savingService::enqueueSave);
this.activeWorlds.add(world); this.activeWorlds.add(world);
return world; return world;

View File

@@ -7,6 +7,7 @@ import me.cortex.voxy.common.util.Pair;
import me.cortex.voxy.common.voxelization.VoxelizedSection; import me.cortex.voxy.common.voxelization.VoxelizedSection;
import me.cortex.voxy.common.voxelization.WorldConversionFactory; import me.cortex.voxy.common.voxelization.WorldConversionFactory;
import me.cortex.voxy.common.world.WorldEngine; 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.other.Mapper;
import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.SectionSavingService;
import net.minecraft.block.Block; 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)); 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); blockId = this.engine.getMapper().getIdForBlockState(state);
} }
} }
@@ -295,6 +299,7 @@ public class DHImporter implements IDataImporter {
} }
} }
} }
if ((x+1)%16==0) { if ((x+1)%16==0) {
for (int sz = 0; sz < 4; sz++) { for (int sz = 0; sz < 4; sz++) {
for (int sy = 0; sy < this.worldHeightSections; sy++) { for (int sy = 0; sy < this.worldHeightSections; sy++) {
@@ -302,7 +307,7 @@ public class DHImporter implements IDataImporter {
WorldConversionFactory.mipSection(section, this.engine.getMapper()); WorldConversionFactory.mipSection(section, this.engine.getMapper());
section.setPosition(X*4+(x>>4), sy+(this.bottomOfWorld>>4), (Z*4)+sz); 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(); int count = this.processedChunks.incrementAndGet();

View File

@@ -9,6 +9,7 @@ import me.cortex.voxy.common.voxelization.WorldConversionFactory;
import me.cortex.voxy.common.world.WorldEngine; import me.cortex.voxy.common.world.WorldEngine;
import me.cortex.voxy.common.thread.ServiceSlice; import me.cortex.voxy.common.thread.ServiceSlice;
import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.thread.ServiceThreadPool;
import me.cortex.voxy.common.world.WorldUpdater;
import me.cortex.voxy.common.world.service.SectionSavingService; import me.cortex.voxy.common.world.service.SectionSavingService;
import net.minecraft.block.Block; import net.minecraft.block.Block;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
@@ -473,7 +474,6 @@ public class WorldImporter implements IDataImporter {
); );
WorldConversionFactory.mipSection(csec, this.world.getMapper()); WorldConversionFactory.mipSection(csec, this.world.getMapper());
WorldUpdater.insertUpdate(this.world, csec);
this.world.insertUpdate(csec);
} }
} }

View File

@@ -25,4 +25,12 @@ public class MixinWorld implements IVoxyWorld {
} }
this.voxyWorld = engine; this.voxyWorld = engine;
} }
@Override
public void shutdownEngine() {
if (this.voxyWorld != null && this.voxyWorld.instanceIn != null) {
this.voxyWorld.instanceIn.stopWorld(this.voxyWorld);
this.setWorldEngine(null);
}
}
} }

View File

@@ -2,8 +2,6 @@
"voxy.config.title": "Voxy config", "voxy.config.title": "Voxy config",
"voxy.config.general": "General", "voxy.config.general": "General",
"voxy.config.threads": "Threads",
"voxy.config.storage": "Storage",
"voxy.config.general.enabled": "Enable Voxy", "voxy.config.general.enabled": "Enable Voxy",
"voxy.config.general.enabled.tooltip": "Fully enables or disables 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.serviceThreads.tooltip": "Number of threads the ServiceThreadPool can use",
"voxy.config.general.subDivisionSize": "Pixels^2 of subdivision size", "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)"
} }

View File

@@ -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 //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 // but im going to assume that since we are dealing with huge render distances, this shouldent matter that much
float extractFaceIndentation(uint faceData) { float extractFaceIndentation(uint faceData) {

View File

@@ -1,4 +1,3 @@
#line 1
#ifdef GL_ARB_gpu_shader_int64 #ifdef GL_ARB_gpu_shader_int64
#define Quad uint64_t #define Quad uint64_t

View File

@@ -1,4 +1,3 @@
#line 1
uint extractDetail(SectionMeta section) { uint extractDetail(SectionMeta section) {
return section.posA>>28; return section.posA>>28;
} }

View File

@@ -8,6 +8,7 @@
"minecraft.MixinDebugHud", "minecraft.MixinDebugHud",
"minecraft.MixinMinecraftClient", "minecraft.MixinMinecraftClient",
"minecraft.MixinThreadExecutor", "minecraft.MixinThreadExecutor",
"minecraft.MixinWindow",
"minecraft.MixinWorldRenderer", "minecraft.MixinWorldRenderer",
"sodium.MixinDefaultChunkRenderer", "sodium.MixinDefaultChunkRenderer",
"sodium.MixinRenderSectionManager" "sodium.MixinRenderSectionManager"