Initial commit

This commit is contained in:
mcrcortex
2023-11-14 08:16:22 +10:00
commit 046a419e4f
89 changed files with 5798 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
package me.cortex.voxelmon;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.storage.LMDBInterface;
import me.cortex.voxelmon.core.world.storage.StorageBackend;
import me.cortex.voxelmon.importers.WorldImporter;
import org.lwjgl.system.MemoryUtil;
import java.io.File;
import java.nio.ByteBuffer;
import static org.lwjgl.util.lmdb.LMDB.MDB_NOLOCK;
import static org.lwjgl.util.lmdb.LMDB.MDB_NOSUBDIR;
public class Test {
public static void main1(String[] args) {
var dbi = new LMDBInterface.Builder()
.setMaxDbs(1)
.open("testdbdir.db", MDB_NOLOCK | MDB_NOSUBDIR)
.fetch();
dbi.setMapSize(1<<29);
var db = dbi.createDb(null);
db.transaction(obj->{
var key = ByteBuffer.allocateDirect(4);
var val = ByteBuffer.allocateDirect(1);
for (int i = 0; i < 1<<20; i++) {
key.putInt(0, i);
obj.put(key, val, 0);
}
return 1;
});
db.close();
dbi.close();
}
public static void main2(String[] args) throws Exception {
var storage = new StorageBackend(new File("run/storagefile.db"));
for (int i = 0; i < 2; i++) {
new Thread(()->{
//storage.getSectionData(1143914312599863680L);
storage.setSectionData(1143914312599863680L, MemoryUtil.memAlloc(12345));
}).start();
}
//storage.getSectionData(1143914312599863680L);
//storage.setSectionData(1143914312599863612L, ByteBuffer.allocateDirect(12345));
//storage.setSectionData(1143914312599863680L, ByteBuffer.allocateDirect(12345));
//storage.close();
System.out.println(storage.getIdMappings());
storage.putIdMapping(1, ByteBuffer.allocateDirect(12));
Thread.sleep(1000);
storage.close();
}
public static void main(String[] args) {
//WorldEngine engine = new WorldEngine(new File("storagefile2.db"), 5);
WorldImporter importer = new WorldImporter(null, null);
//importer.importWorld(new File("run/saves/Drehmal 2.2 Apotheosis Beta - 1.0.0/region/"));
importer.importWorldAsyncStart(new File("D:\\PrismLauncher-Windows-MSVC-Portable-7.1\\instances\\1.20.1(3)\\.minecraft\\.bobby\\build.docm77.de\\-8149132374211427218\\minecraft\\overworld\\"));
}
}

View File

@@ -0,0 +1,4 @@
package me.cortex.voxelmon;
public class Voxelmon {
}

View File

@@ -0,0 +1,188 @@
package me.cortex.voxelmon.core;
//Contains the logic to determine what is loaded and at what LoD level, dispatches render changes
// also determines what faces are built etc
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
import me.cortex.voxelmon.core.rendering.RenderTracker;
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
import me.cortex.voxelmon.core.util.DebugUtil;
import me.cortex.voxelmon.core.util.RingUtil;
import me.cortex.voxelmon.core.world.WorldEngine;
//Can use ring logic
// i.e. when a player moves the rings of each lod change (how it was doing in the original attempt)
// also have it do directional quad culling and rebuild the chunk if needed (this shouldent happen very often) (the reason is to significantly reduce draw calls)
// make the rebuild range like +-5 chunks along each axis (that means at higher levels, should only need to rebuild like)
// 4 sections or something
public class DistanceTracker {
private final TransitionRing2D[] rings;
private final RenderTracker tracker;
public DistanceTracker(RenderTracker tracker, int rings) {
this.rings = new TransitionRing2D[rings+1];
this.tracker = tracker;
int DIST = 16;
this.rings[0] = new TransitionRing2D(5, DIST, (x,z)->{
if (true) return;
for (int y = -2; y < 10; y++) {
this.tracker.remLvl0(x, y, z);
}
}, (x, z) -> {
for (int y = -2; y < 10; y++) {
this.tracker.addLvl0(x, y, z);
}
});
for (int i = 1; i < rings; i++) {
int capRing = i;
this.rings[i] = new TransitionRing2D(5+i, DIST, (x, z) -> this.dec(capRing, x, z), (x, z) -> this.inc(capRing, x, z));
}
}
private void inc(int lvl, int x, int z) {
for (int y = -2>>lvl; y < 10>>lvl; y++) {
this.tracker.inc(lvl, x, y, z);
}
}
private void dec(int lvl, int x, int z) {
for (int y = -2>>lvl; y < 10>>lvl; y++) {
this.tracker.dec(lvl, x, y, z);
}
}
//How it works is there are N ring zones (one zone for each lod boundary)
// the transition zone is what determines what lods are rendered etc (and it biases higher lod levels cause its easier)
// the transition zone is only ever checked when the player moves 1<<(4+lodlvl) blocks, its position is set
//if the center suddenly changes (say more than 1<<(7+lodlvl) block) then invalidate the entire ring and recompute
// the lod sections
public void setCenter(int x, int y, int z) {
for (var ring : rings) {
if (ring != null) {
ring.update(x, z);
}
}
}
//TODO: add a new class thing that can track the central axis point so that
// geometry can be rebuilt with new flags with correct facing geometry built
// (could also make it so that it emits 3x the amount of draw calls, but that seems very bad idea)
private interface Transition2DCallback {
void callback(int x, int z);
}
private static final class TransitionRing2D {
private final int triggerRangeSquared;
private final int shiftSize;
private final Transition2DCallback enter;
private final Transition2DCallback exit;
private final int[] cornerPoints;
private final int radius;
private int lastUpdateX;
private int lastUpdateZ;
private int currentX;
private int currentZ;
//Note radius is in shiftScale
private TransitionRing2D(int shiftSize, int radius, Transition2DCallback onEntry, Transition2DCallback onExit) {
//trigger just less than every shiftSize scale
this.triggerRangeSquared = 1<<((shiftSize<<1) - 1);
this.shiftSize = shiftSize;
this.enter = onEntry;
this.exit = onExit;
this.cornerPoints = RingUtil.generatingBoundingCorner2D(radius);
this.radius = radius;
}
private long Prel(int x, int z) {
return (Integer.toUnsignedLong(this.currentZ + z)<<32)|Integer.toUnsignedLong(this.currentX + x);
}
public void update(int x, int z) {
int dx = this.lastUpdateX - x;
int dz = this.lastUpdateZ - z;
int distSquared = dx*dx + dz*dz;
if (distSquared < this.triggerRangeSquared) {
return;
}
//Update the last update position
this.lastUpdateX = x;
this.lastUpdateZ = z;
//Compute movement if it happened
int nx = x>>this.shiftSize;
int nz = z>>this.shiftSize;
if (nx == this.currentX && nz == this.currentZ) {
//No movement
return;
}
//FIXME: not right, needs to only call load/unload on entry and exit, cause atm its acting like a loaded circle
Long2IntOpenHashMap ops = new Long2IntOpenHashMap();
int dir = nz<this.currentZ?-1:1;
while (nz != this.currentZ) {
for (int corner : this.cornerPoints) {
int cx = corner>>>16;
int cz = corner&0xFFFF;
ops.addTo(Prel( cx, cz+Math.max(0, dir)), dir);
ops.addTo(Prel( cx,-cz+Math.min(0, dir)),-dir);
if (cx != 0) {
ops.addTo(Prel(-cx, cz+Math.max(0, dir)), dir);
ops.addTo(Prel(-cx,-cz+Math.min(0, dir)),-dir);
}
}
//ops.addTo(Prel(0, this.radius+Math.max(0, dir)), dir);
//ops.addTo(Prel(0, -this.radius+Math.min(0, dir)), -dir);
this.currentZ += dir;
}
dir = nx<this.currentX?-1:1;
while (nx != this.currentX) {
for (int corner : this.cornerPoints) {
int cx = corner&0xFFFF;
int cz = corner>>>16;
ops.addTo(Prel( cx+Math.max(0, dir), cz), dir);
ops.addTo(Prel(-cx+Math.min(0, dir), cz),-dir);
if (cz != 0) {
ops.addTo(Prel(cx + Math.max(0, dir), -cz), dir);
ops.addTo(Prel(-cx + Math.min(0, dir), -cz), -dir);
}
}
this.currentX += dir;
}
ops.forEach((pos,val)->{
if (val > 0) {
this.enter.callback((int) (long)pos, (int) (pos>>32));
}
if (val < 0) {
this.exit.callback((int) (long)pos, (int) (pos>>32));
}
});
ops.clear();
}
}
}

View File

@@ -0,0 +1,225 @@
package me.cortex.voxelmon.core;
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
import me.cortex.voxelmon.core.rendering.Gl46FarWorldRenderer;
import me.cortex.voxelmon.core.rendering.RenderTracker;
import me.cortex.voxelmon.core.rendering.SharedIndexBuffer;
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
import me.cortex.voxelmon.core.util.DebugUtil;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import me.cortex.voxelmon.core.util.RingUtil;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import me.cortex.voxelmon.core.world.other.BiomeColour;
import me.cortex.voxelmon.core.world.other.BlockStateColour;
import me.cortex.voxelmon.core.world.other.ColourResolver;
import me.cortex.voxelmon.importers.WorldImporter;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Direction;
import net.minecraft.world.chunk.WorldChunk;
import org.lwjgl.system.MemoryUtil;
import java.io.File;
import java.util.*;
//Core class that ingests new data from sources and updates the required systems
//3 primary services:
// ingest service: this takes in unloaded chunk events from the client, processes the chunk and critically also updates the lod view of the world
// render data builder service: this service builds the render data from build requests it also handles the collecting of build data for the selected region (only axis aligned single lod tasks)
// serialization service: serializes changed world data and ensures that the database and any loaded data are in sync such that the database can never be more updated than loaded data, also performs compression on serialization
//there are multiple subsystems
//player tracker system (determines what lods are loaded and used by the player)
//updating system (triggers render data rebuilds when something from the ingest service causes an LOD change)
//the render system simply renders what data it has, its responsable for gpu memory layouts in arenas and rendering in an optimal way, it makes no requests back to any of the other systems or services, it just applies render data updates
//There is strict forward only dataflow
//Ingest -> world engine -> raw render data -> render data
public class VoxelCore {
public static VoxelCore INSTANCE = new VoxelCore();
private final WorldEngine world;
private final DistanceTracker distanceTracker;
private final RenderGenerationService renderGen;
private final RenderTracker renderTracker;
private final AbstractFarWorldRenderer renderer;
public VoxelCore() {
//Trigger the shared index buffer loading
SharedIndexBuffer.INSTANCE.id();
this.renderer = new Gl46FarWorldRenderer();
this.world = new WorldEngine(new File("ethoslab.db"), 16, 5);//"hc9.db"//"storagefile.db"
this.renderTracker = new RenderTracker(this.world, this.renderer);
this.renderGen = new RenderGenerationService(this.world, this.renderTracker,4);
this.world.setRenderTracker(this.renderTracker);
this.renderTracker.setRenderGen(this.renderGen);
this.distanceTracker = new DistanceTracker(this.renderTracker, 5);
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
/*
Random r = new Random();
for (int ring = 0; ring < 5; ring++) {
for (int x = -32; x < 32; x++) {
for (int z = -32; z < 32; z++) {
if ((-16 < x && x < 16) && (-16 < z && z < 16)) {
continue;
}
var b = new MemoryBuffer(1000 * 8);
for (long j = 0; j < b.size; j += 8) {
MemoryUtil.memPutLong(b.address + j, r.nextLong());
}
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(ring, x, 2>>ring, z), b, null));
}
}
}*/
//WorldImporter importer = new WorldImporter(this.world, MinecraftClient.getInstance().world);
//importer.importWorldAsyncStart(new File("saves/Etho's LP Ep550/region"));
Set<Block> biomeTintableAllFaces = new HashSet<>(List.of(Blocks.OAK_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.VINE, Blocks.MANGROVE_LEAVES,
Blocks.TALL_GRASS, Blocks.LARGE_FERN));
biomeTintableAllFaces.add(Blocks.SPRUCE_LEAVES);
biomeTintableAllFaces.add(Blocks.BIRCH_LEAVES);
biomeTintableAllFaces.add(Blocks.PINK_PETALS);
biomeTintableAllFaces.addAll(List.of(Blocks.FERN, Blocks.GRASS, Blocks.POTTED_FERN));
Set<Block> biomeTintableUpFace = new HashSet<>(List.of(Blocks.GRASS_BLOCK));
Set<Block> waterTint = new HashSet<>(List.of(Blocks.WATER));
int i = 0;
for (var state : this.world.getMapper().getBlockStates()) {
int tintMsk = 0;
if (biomeTintableAllFaces.contains(state.getBlock())) {
tintMsk |= (1<<6)-1;
}
if (biomeTintableUpFace.contains(state.getBlock())) {
tintMsk |= 1<<Direction.UP.getId();
}
if (waterTint.contains(state.getBlock())) {
tintMsk |= 1<<6;
}
this.renderer.enqueueUpdate(new BlockStateColour(i++, tintMsk, ColourResolver.resolveColour(state)));
}
i = 0;
for (var biome : this.world.getMapper().getBiomes()) {
long dualColour = ColourResolver.resolveBiomeColour(biome);
this.renderer.enqueueUpdate(new BiomeColour(i++, (int) dualColour, (int) (dualColour>>32)));
}
}
public void enqueueIngest(WorldChunk worldChunk) {
this.world.ingestService.enqueueIngest(worldChunk);
}
public void renderSetup(Frustum frustum, Camera camera) {
this.distanceTracker.setCenter(camera.getBlockPos().getX(), camera.getBlockPos().getY(), camera.getBlockPos().getZ());
this.renderer.setupRender(frustum, camera);
}
public void renderOpaque(MatrixStack matrices, double cameraX, double cameraY, double cameraZ) {
matrices.push();
matrices.translate(-cameraX, -cameraY, -cameraZ);
DebugUtil.setPositionMatrix(matrices);
matrices.pop();
/*
for (int i = 0; i < 5; i++) {
for (int y = 0; y < Math.max(1, 10>>i); y++) {
for (int x = -32; x < 32; x++) {
for (int z = -32; z < 32; z++) {
if (-16 < x && x < 16 && -16 < z && z < 16) {
continue;
}
var sec = this.world.getOrLoadAcquire(i, x, y, z);
this.renderGen.enqueueTask(sec);
sec.release();
}
}
}
}*/
//DebugRenderUtil.renderAABB(new Box(0,100,0,1,101,1), 0,1,0,0.1f);
//DebugRenderUtil.renderAABB(new Box(1,100,1,2,101,2), 1,0,0,0.1f);
/*
int LEVEL = 4;
int SEC_Y = 1>>LEVEL;
int X = 47>>LEVEL;
int Z = 32>>LEVEL;
var section = world.getOrLoadAcquire(LEVEL,X,SEC_Y,Z);
var data = section.copyData();
int SCALE = 1<<LEVEL;
int Y_OFFSET = SEC_Y<<(5+LEVEL);
int X_OFFSET = X<<(5+LEVEL);
int Z_OFFSET = Z<<(5+LEVEL);
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
for (int x = 0; x < 32; x++) {
var point = data[WorldSection.getIndex(x,y,z)];
if (point != 0) {
//var colours = world.getMapper().getColours(point);
//int colour = colours[Direction.UP.getId()];
//DebugUtil.renderAABB(new Box(x*SCALE,y*SCALE+Y_OFFSET,z*SCALE,x*SCALE+SCALE,y*SCALE+SCALE+Y_OFFSET,z*SCALE+SCALE), colour|0xFF);
point >>>= 27;
DebugUtil.renderAABB(new Box(x*SCALE + X_OFFSET,y*SCALE+Y_OFFSET,z*SCALE+Z_OFFSET,x*SCALE+SCALE + X_OFFSET,y*SCALE+SCALE+Y_OFFSET,z*SCALE+SCALE+Z_OFFSET), (float) (point&7)/7,(float) ((point>>3)&7)/7,(float) ((point>>6)&7)/7,1f);
}
}
}
}
section.release();
*/
/*
var points = RingUtil.generatingBoundingCorner2D(4);
for (var point : points) {
int x = point>>>16;
int y = point&0xFFFF;
DebugUtil.renderAABB(new Box(x,150,y,x+1,151,y+1), 1,1,0,1);
}
*/
this.renderer.renderFarAwayOpaque(matrices, cameraX, cameraY, cameraZ);
}
public void addDebugInfo(List<String> debug) {
debug.add("");
debug.add("");
debug.add("VoxelCore");
debug.add("Ingest service tasks: " + this.world.ingestService.getTaskCount());
debug.add("Saving service tasks: " + this.world.savingService.getTaskCount());
debug.add("Render service tasks: " + this.renderGen.getTaskCount());
debug.add("Loaded cache sizes: " + Arrays.toString(this.world.getLoadedSectionCacheSizes()));
this.renderer.addDebugData(debug);
}
//Note: when doing translucent rendering, only need to sort when generating the geometry, or when crossing into the center zone
// cause in 99.99% of cases the sections dont need to be sorted
// since they are AABBS crossing the normal is impossible without one of the axis being equal
public void shutdown() {
try {this.renderGen.shutdown();} catch (Exception e) {System.err.println(e);}
try {this.renderer.shutdown();} catch (Exception e) {System.err.println(e);}
try {this.world.shutdown();} catch (Exception e) {System.err.println(e);}
}
}

View File

@@ -0,0 +1,28 @@
package me.cortex.voxelmon.core.gl;
import me.cortex.voxelmon.core.util.TrackedObject;
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
import static org.lwjgl.opengl.GL44C.glBufferStorage;
import static org.lwjgl.opengl.GL45C.glCreateBuffers;
import static org.lwjgl.opengl.GL45C.glNamedBufferStorage;
public class GlBuffer extends TrackedObject {
public final int id;
private final long size;
public GlBuffer(long size, int flags) {
this.id = glCreateBuffers();
this.size = size;
glNamedBufferStorage(this.id, size, flags);
}
@Override
public void free() {
this.free0();
glDeleteBuffers(this.id);
}
public long size() {
return this.size;
}
}

View File

@@ -0,0 +1,32 @@
package me.cortex.voxelmon.core.gl;
import me.cortex.voxelmon.core.util.TrackedObject;
import static org.lwjgl.opengl.GL32.*;
public class GlFence extends TrackedObject {
private final long fence;
private boolean signaled;
public GlFence() {
this.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
public boolean signaled() {
if (!this.signaled) {
int ret = glClientWaitSync(this.fence, 0, 0);
if (ret == GL_ALREADY_SIGNALED || ret == GL_CONDITION_SATISFIED) {
this.signaled = true;
} else if (ret != GL_TIMEOUT_EXPIRED) {
throw new IllegalStateException("Poll for fence failed, glError: " + glGetError());
}
}
return this.signaled;
}
@Override
public void free() {
super.free0();
glDeleteSync(this.fence);
}
}

View File

@@ -0,0 +1,25 @@
package me.cortex.voxelmon.core.gl;
import me.cortex.voxelmon.core.util.TrackedObject;
import static org.lwjgl.opengl.ARBFramebufferObject.*;
import static org.lwjgl.opengl.GL45C.glCreateFramebuffers;
import static org.lwjgl.opengl.GL45C.glNamedFramebufferTexture;
public class GlFramebuffer extends TrackedObject {
public final int id;
public GlFramebuffer() {
this.id = glCreateFramebuffers();
}
public GlFramebuffer bind(int attachment, GlTexture texture) {
glNamedFramebufferTexture(this.id, attachment, texture.id, 0);
return this;
}
@Override
public void free() {
super.free0();
glDeleteFramebuffers(this.id);
}
}

View File

@@ -0,0 +1,36 @@
package me.cortex.voxelmon.core.gl;
import me.cortex.voxelmon.core.util.TrackedObject;
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_FLUSH_EXPLICIT_BIT;
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_UNSYNCHRONIZED_BIT;
import static org.lwjgl.opengl.ARBMapBufferRange.GL_MAP_WRITE_BIT;
import static org.lwjgl.opengl.GL15.glDeleteBuffers;
import static org.lwjgl.opengl.GL45C.*;
public class GlPersistentMappedBuffer extends TrackedObject {
public final int id;
private final long size;
private final long addr;
public GlPersistentMappedBuffer(long size, int flags) {
this.id = glCreateBuffers();
this.size = size;
glNamedBufferStorage(this.id, size, GL_CLIENT_STORAGE_BIT|GL_MAP_PERSISTENT_BIT|(flags&(GL_MAP_WRITE_BIT|GL_MAP_READ_BIT)));
this.addr = nglMapNamedBufferRange(this.id, 0, size, flags|GL_MAP_PERSISTENT_BIT);
}
@Override
public void free() {
this.free0();
glUnmapBuffer(this.id);
glDeleteBuffers(this.id);
}
public long size() {
return this.size;
}
public long addr() {
return this.addr;
}
}

View File

@@ -0,0 +1,34 @@
package me.cortex.voxelmon.core.gl;
import me.cortex.voxelmon.core.util.TrackedObject;
import static org.lwjgl.opengl.ARBFramebufferObject.glDeleteFramebuffers;
import static org.lwjgl.opengl.ARBFramebufferObject.glGenFramebuffers;
import static org.lwjgl.opengl.GL11C.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11C.glDeleteTextures;
import static org.lwjgl.opengl.GL45C.glCreateTextures;
import static org.lwjgl.opengl.GL45C.glTextureStorage2D;
public class GlTexture extends TrackedObject {
final int id;
private final int type;
public GlTexture(int type) {
this.id = glCreateTextures(type);
this.type = type;
}
public GlTexture store(int format, int levels, int width, int height) {
if (this.type == GL_TEXTURE_2D) {
glTextureStorage2D(this.id, format, levels, width, height);
} else {
throw new IllegalStateException("Unknown texture type");
}
return this;
}
@Override
public void free() {
super.free0();
glDeleteTextures(this.id);
}
}

View File

@@ -0,0 +1,5 @@
package me.cortex.voxelmon.core.gl.shader;
public interface IShaderProcessor {
String process(ShaderType type, String source);
}

View File

@@ -0,0 +1,108 @@
package me.cortex.voxelmon.core.gl.shader;
import me.cortex.voxelmon.core.util.TrackedObject;
import org.lwjgl.opengl.GL20C;
import java.util.HashMap;
import java.util.Map;
import static org.lwjgl.opengl.GL20.glDeleteProgram;
import static org.lwjgl.opengl.GL20.glUseProgram;
public class Shader extends TrackedObject {
private final int id;
private Shader(int program) {
id = program;
}
public static Builder make(IShaderProcessor processor) {
return new Builder(processor);
}
public static Builder make() {
return new Builder((aa,source)->source);
}
public void bind() {
glUseProgram(this.id);
}
public void free() {
super.free0();
glDeleteProgram(this.id);
}
public static class Builder {
private final Map<ShaderType, String> sources = new HashMap<>();
private final IShaderProcessor processor;
private Builder(IShaderProcessor processor) {
this.processor = processor;
}
public Builder add(ShaderType type, String id) {
this.addSource(type, ShaderLoader.parse(id));
return this;
}
public Builder addSource(ShaderType type, String source) {
this.sources.put(type, this.processor.process(type, source));
return this;
}
public Shader compile() {
int program = GL20C.glCreateProgram();
int[] shaders = this.sources.entrySet().stream().mapToInt(a->createShader(a.getKey(), a.getValue())).toArray();
for (int i : shaders) {
GL20C.glAttachShader(program, i);
}
GL20C.glLinkProgram(program);
for (int i : shaders) {
GL20C.glDetachShader(program, i);
GL20C.glDeleteShader(i);
}
printProgramLinkLog(program);
verifyProgramLinked(program);
return new Shader(program);
}
private static void printProgramLinkLog(int program) {
String log = GL20C.glGetProgramInfoLog(program);
if (!log.isEmpty()) {
System.err.println(log);
}
}
private static void verifyProgramLinked(int program) {
int result = GL20C.glGetProgrami(program, GL20C.GL_LINK_STATUS);
if (result != GL20C.GL_TRUE) {
throw new RuntimeException("Shader program linking failed, see log for details");
}
}
private static int createShader(ShaderType type, String src) {
int shader = GL20C.glCreateShader(type.gl);
GL20C.glShaderSource(shader, src);
GL20C.glCompileShader(shader);
String log = GL20C.glGetShaderInfoLog(shader);
if (!log.isEmpty()) {
System.err.println(log);
}
int result = GL20C.glGetShaderi(shader, GL20C.GL_COMPILE_STATUS);
if (result != GL20C.GL_TRUE) {
GL20C.glDeleteShader(shader);
throw new RuntimeException("Shader compilation failed, see log for details");
}
return shader;
}
}
}

View File

@@ -0,0 +1,12 @@
package me.cortex.voxelmon.core.gl.shader;
import me.jellysquid.mods.sodium.client.gl.shader.ShaderConstants;
import me.jellysquid.mods.sodium.client.gl.shader.ShaderParser;
import net.minecraft.util.Identifier;
public class ShaderLoader {
public static String parse(String id) {
return ShaderParser.parseShader("#import <" + id + ">", ShaderConstants.builder().build());
//return me.jellysquid.mods.sodium.client.gl.shader.ShaderLoader.getShaderSource(new Identifier(id));
}
}

View File

@@ -0,0 +1,20 @@
package me.cortex.voxelmon.core.gl.shader;
import static org.lwjgl.opengl.GL20.GL_FRAGMENT_SHADER;
import static org.lwjgl.opengl.GL20.GL_VERTEX_SHADER;
import static org.lwjgl.opengl.GL43C.GL_COMPUTE_SHADER;
import static org.lwjgl.opengl.NVMeshShader.GL_MESH_SHADER_NV;
import static org.lwjgl.opengl.NVMeshShader.GL_TASK_SHADER_NV;
public enum ShaderType {
VERTEX(GL_VERTEX_SHADER),
FRAGMENT(GL_FRAGMENT_SHADER),
COMPUTE(GL_COMPUTE_SHADER),
MESH(GL_MESH_SHADER_NV),
TASK(GL_TASK_SHADER_NV);
public final int gl;
ShaderType(int glEnum) {
gl = glEnum;
}
}

View File

@@ -0,0 +1,133 @@
package me.cortex.voxelmon.core.rendering;
//NOTE: an idea on how to do it is so that any render section, we _keep_ aquired (yes this will be very memory intensive)
// could maybe tosomething else
import com.mojang.blaze3d.systems.RenderSystem;
import me.cortex.voxelmon.core.gl.GlBuffer;
import me.cortex.voxelmon.core.gl.shader.Shader;
import me.cortex.voxelmon.core.gl.shader.ShaderType;
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
import me.cortex.voxelmon.core.rendering.util.UploadStream;
import me.cortex.voxelmon.core.world.other.BiomeColour;
import me.cortex.voxelmon.core.world.other.BlockStateColour;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import org.joml.FrustumIntersection;
import org.joml.Matrix4f;
import org.lwjgl.system.MemoryUtil;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
import static org.lwjgl.opengl.GL15.GL_ELEMENT_ARRAY_BUFFER;
import static org.lwjgl.opengl.GL15.glBindBuffer;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL31.GL_UNIFORM_BUFFER;
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
import static org.lwjgl.opengl.GL42.GL_COMMAND_BARRIER_BIT;
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
import static org.lwjgl.opengl.GL43.*;
//can make it so that register the key of the sections we have rendered, then when a section changes and is registered,
// dispatch an update to the render section data builder which then gets consumed by the render system and updates
// the rendered data
//Contains all the logic to render the world and manage gpu memory
// processes section load,unload,update render data and renders the world each frame
//Todo: tinker with having the compute shader where each thread is a position to render? maybe idk
public abstract class AbstractFarWorldRenderer {
protected final int vao = glGenVertexArrays();
protected final GlBuffer uniformBuffer;
protected final GeometryManager geometry;
private final ConcurrentLinkedDeque<BlockStateColour> stateUpdateQueue = new ConcurrentLinkedDeque<>();
private final ConcurrentLinkedDeque<BiomeColour> biomeUpdateQueue = new ConcurrentLinkedDeque<>();
protected final GlBuffer stateDataBuffer;
protected final GlBuffer biomeDataBuffer;
protected final GlBuffer light = null;
//Current camera base level section position
protected int sx;
protected int sy;
protected int sz;
protected FrustumIntersection frustum;
public AbstractFarWorldRenderer() {
this.uniformBuffer = new GlBuffer(1024, 0);
//TODO: make these both dynamically sized
this.stateDataBuffer = new GlBuffer((1<<16)*28, 0);//Capacity for 1<<16 entries
this.biomeDataBuffer = new GlBuffer(512*4*2, 0);//capacity for 1<<9 entries
this.geometry = new GeometryManager();
}
protected abstract void setupVao();
public void setupRender(Frustum frustum, Camera camera) {
this.frustum = frustum.frustumIntersection;
this.sx = camera.getBlockPos().getX() >> 5;
this.sy = camera.getBlockPos().getY() >> 5;
this.sz = camera.getBlockPos().getZ() >> 5;
//TODO: move this to a render function that is only called
// once per frame when using multi viewport mods
//it shouldent matter if its called multiple times a frame however, as its synced with fences
UploadStream.INSTANCE.tick();
this.geometry.uploadResults();
//Upload any block state changes
while (!this.stateUpdateQueue.isEmpty()) {
var stateUpdate = this.stateUpdateQueue.pop();
long ptr = UploadStream.INSTANCE.upload(this.stateDataBuffer, stateUpdate.id()*28L, 28);
MemoryUtil.memPutInt(ptr, stateUpdate.biomeTintMsk()); ptr+=4;
for (int faceColour : stateUpdate.faceColours()) {
MemoryUtil.memPutInt(ptr, faceColour); ptr+=4;
}
}
//Upload any biome changes
while (!this.biomeUpdateQueue.isEmpty()) {
var biomeUpdate = this.biomeUpdateQueue.pop();
long ptr = UploadStream.INSTANCE.upload(this.biomeDataBuffer, biomeUpdate.id()*8L, 8);
MemoryUtil.memPutInt(ptr, biomeUpdate.foliageColour()); ptr+=4;
MemoryUtil.memPutInt(ptr, biomeUpdate.waterColour()); ptr+=4;
}
}
public abstract void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz);
public void enqueueUpdate(BlockStateColour stateColour) {
this.stateUpdateQueue.add(stateColour);
}
public void enqueueUpdate(BiomeColour biomeColour) {
this.biomeUpdateQueue.add(biomeColour);
}
public void enqueueResult(BuiltSectionGeometry result) {
this.geometry.enqueueResult(result);
}
public void addDebugData(List<String> debug) {
}
public void shutdown() {
glDeleteVertexArrays(this.vao);
this.geometry.free();
this.uniformBuffer.free();
this.stateDataBuffer.free();
this.biomeDataBuffer.free();
}
}

View File

@@ -0,0 +1,170 @@
package me.cortex.voxelmon.core.rendering;
import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.cortex.voxelmon.core.gl.GlBuffer;
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
import me.cortex.voxelmon.core.rendering.util.BufferArena;
import me.cortex.voxelmon.core.rendering.util.UploadStream;
import me.cortex.voxelmon.core.util.IndexUtil;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import org.lwjgl.system.MemoryUtil;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedDeque;
public class GeometryManager {
private static final int SECTION_METADATA_SIZE = 32;
private record SectionMeta(long position, long opaqueGeometryPtr, int opaqueQuadCount, long translucentGeometryPtr, int translucentQuadCount) {
public void writeMetadata(long ptr) {
//THIS IS DUE TO ENDIANNESS and that we are splitting a long into 2 ints
MemoryUtil.memPutInt(ptr, (int) (this.position>>32)); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.position); ptr += 4;
MemoryUtil.memPutInt(ptr, (int) this.opaqueGeometryPtr); ptr += 4;
MemoryUtil.memPutInt(ptr, this.opaqueQuadCount); ptr += 4;
}
}
private final ConcurrentLinkedDeque<BuiltSectionGeometry> buildResults = new ConcurrentLinkedDeque<>();
private int sectionCount = 0;
private final Long2IntOpenHashMap pos2id = new Long2IntOpenHashMap();
private final LongArrayList id2pos = new LongArrayList();
private final ObjectArrayList<SectionMeta> sectionMetadata = new ObjectArrayList<>();
private final GlBuffer sectionMetaBuffer;
private final BufferArena geometryBuffer;
public GeometryManager() {
this.sectionMetaBuffer = new GlBuffer(1L << 23, 0);
this.geometryBuffer = new BufferArena((1L << 31) - 1024, 8);
this.pos2id.defaultReturnValue(-1);
}
public void enqueueResult(BuiltSectionGeometry sectionGeometry) {
this.buildResults.add(sectionGeometry);
}
private SectionMeta createMeta(BuiltSectionGeometry geometry) {
long geometryPtr = this.geometryBuffer.upload(geometry.geometryBuffer);
//TODO: support translucent geometry
return new SectionMeta(geometry.position, geometryPtr, (int) (geometry.geometryBuffer.size/8), -1,0);
}
private void freeMeta(SectionMeta meta) {
if (meta.opaqueGeometryPtr != -1) {
this.geometryBuffer.free(meta.opaqueGeometryPtr);
}
if (meta.translucentGeometryPtr != -1) {
this.geometryBuffer.free(meta.translucentGeometryPtr);
}
}
void uploadResults() {
while (!this.buildResults.isEmpty()) {
var result = this.buildResults.pop();
boolean isDelete = result.geometryBuffer == null && result.translucentGeometryBuffer == null;
if (isDelete) {
int id = -1;
if ((id = this.pos2id.remove(result.position)) != -1) {
if (this.id2pos.getLong(id) != result.position) {
throw new IllegalStateException("Removed position id not the same requested");
}
var meta = this.sectionMetadata.get(id);
this.freeMeta(meta);
this.sectionCount--;
if (id == this.sectionCount) {
//if we are at the end of the array dont have to do anything (maybe just upload a blank data, just to be sure)
//Remove the last element
this.sectionMetadata.remove(id);
this.id2pos.removeLong(id);
} else {
long swapLodPos = this.id2pos.getLong(this.sectionCount);
this.pos2id.put(swapLodPos, id);
this.id2pos.set(id, swapLodPos);
//Remove from the lists
this.id2pos.removeLong(this.sectionCount);
var swapMeta = this.sectionMetadata.remove(this.sectionCount);
this.sectionMetadata.set(id, swapMeta);
if (swapMeta.position != swapLodPos) {
throw new IllegalStateException();
}
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long) SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
swapMeta.writeMetadata(ptr);
}
}
} else {
int id = -1;
if ((id = this.pos2id.get(result.position)) != -1) {
//Update the existing data
var meta = this.sectionMetadata.get(id);
if (meta.position != result.position) {
throw new IllegalStateException("Meta position != result position");
}
//Delete the old data
this.freeMeta(meta);
//Create the new meta
meta = this.createMeta(result);
this.sectionMetadata.set(id, meta);
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
meta.writeMetadata(ptr);
} else {
//Add to the end of the array
id = this.sectionCount++;
this.pos2id.put(result.position, id);
this.id2pos.add(result.position);
//Create the new meta
var meta = this.createMeta(result);
this.sectionMetadata.add(meta);
long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE);
meta.writeMetadata(ptr);
}
}
//Assert some invarients
if (this.id2pos.size() != this.sectionCount || this.sectionCount != this.pos2id.size()) {
throw new IllegalStateException("Invariants broken");
}
result.free();
}
}
public int getSectionCount() {
return this.sectionCount;
}
public void free() {
this.sectionMetaBuffer.free();
this.geometryBuffer.free();
}
public int geometryId() {
return this.geometryBuffer.id();
}
public int metaId() {
return this.sectionMetaBuffer.id;
}
public float getGeometryBufferUsage() {
return this.geometryBuffer.usage();
}
}

View File

@@ -0,0 +1,121 @@
package me.cortex.voxelmon.core.rendering;
import com.mojang.blaze3d.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem;
import me.cortex.voxelmon.core.gl.GlBuffer;
import me.cortex.voxelmon.core.gl.shader.Shader;
import me.cortex.voxelmon.core.gl.shader.ShaderType;
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
import me.cortex.voxelmon.core.rendering.util.UploadStream;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import me.cortex.voxelmon.mixin.joml.AccessFrustumIntersection;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.system.MemoryUtil;
import java.util.List;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL40C.GL_DRAW_INDIRECT_BUFFER;
import static org.lwjgl.opengl.GL42.*;
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.GL43.*;
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BUFFER;
public class Gl46FarWorldRenderer extends AbstractFarWorldRenderer {
private final Shader commandGen = Shader.make()
.add(ShaderType.COMPUTE, "voxelmon:lod/gl46/cmdgen.comp")
.compile();
private final Shader lodShader = Shader.make()
.add(ShaderType.VERTEX, "voxelmon:lod/gl46/quads.vert")
.add(ShaderType.FRAGMENT, "voxelmon:lod/gl46/quads.frag")
.compile();
private final GlBuffer glCommandBuffer = new GlBuffer(100_000*5*4, 0);
public Gl46FarWorldRenderer() {
super();
setupVao();
}
@Override
protected void setupVao() {
glBindVertexArray(this.vao);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, this.glCommandBuffer.id);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, SharedIndexBuffer.INSTANCE.id());
glBindBufferBase(GL_UNIFORM_BUFFER, 0, this.uniformBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, this.geometry.geometryId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, this.glCommandBuffer.id);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, this.geometry.metaId());
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, this.stateDataBuffer.id);//State LUT
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, this.biomeDataBuffer.id);//Biome LUT
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, 0);//Lighting LUT
glBindVertexArray(0);
}
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
if (this.geometry.getSectionCount() == 0) {
return;
}
RenderLayer.getCutoutMipped().startDrawing();
//RenderSystem.enableBlend();
//RenderSystem.defaultBlendFunc();
this.updateUniformBuffer(stack, cx, cy, cz);
UploadStream.INSTANCE.commit();
glBindVertexArray(this.vao);
this.commandGen.bind();
glDispatchCompute((this.geometry.getSectionCount()+127)/128, 1, 1);
glMemoryBarrier(GL_COMMAND_BARRIER_BIT|GL_SHADER_STORAGE_BARRIER_BIT|GL_UNIFORM_BARRIER_BIT);
this.lodShader.bind();
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_SHORT, 0, this.geometry.getSectionCount(), 0);
//ARBIndirectParameters.glMultiDrawElementsIndirectCountARB(
glMemoryBarrier(GL_PIXEL_BUFFER_BARRIER_BIT|GL_FRAMEBUFFER_BARRIER_BIT);
//TODO: add gpu occlusion culling here (after the lod drawing) (maybe, finish the rest of the PoC first)
glBindVertexArray(0);
RenderLayer.getCutoutMipped().endDrawing();
}
private void updateUniformBuffer(MatrixStack stack, double cx, double cy, double cz) {
long ptr = UploadStream.INSTANCE.upload(this.uniformBuffer, 0, this.uniformBuffer.size());
var mat = new Matrix4f(RenderSystem.getProjectionMatrix()).mul(stack.peek().getPositionMatrix());
var innerTranslation = new Vector3f((float) (cx-(this.sx<<5)), (float) (cy-(this.sy<<5)), (float) (cz-(this.sz<<5)));
mat.translate(-innerTranslation.x, -innerTranslation.y, -innerTranslation.z);
mat.getToAddress(ptr); ptr += 4*4*4;
MemoryUtil.memPutInt(ptr, this.sx); ptr += 4;
MemoryUtil.memPutInt(ptr, this.sy); ptr += 4;
MemoryUtil.memPutInt(ptr, this.sz); ptr += 4;
MemoryUtil.memPutInt(ptr, this.geometry.getSectionCount()); ptr += 4;
var planes = ((AccessFrustumIntersection)this.frustum).getPlanes();
for (var plane : planes) {
plane.getToAddress(ptr); ptr += 4*4;
}
innerTranslation.getToAddress(ptr); ptr += 4*3;
}
@Override
public void shutdown() {
super.shutdown();
this.commandGen.free();
this.lodShader.free();
}
@Override
public void addDebugData(List<String> debug) {
debug.add("Geometry buffer usage: " + ((float)Math.round((this.geometry.getGeometryBufferUsage()*100000))/1000) + "%");
debug.add("Render Sections: " + this.geometry.getSectionCount());
}
}

View File

@@ -0,0 +1,56 @@
package me.cortex.voxelmon.core.rendering;
import me.cortex.voxelmon.core.gl.shader.Shader;
import me.cortex.voxelmon.core.gl.shader.ShaderType;
import me.cortex.voxelmon.core.rendering.util.UploadStream;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.util.math.MatrixStack;
import static org.lwjgl.opengl.ARBMultiDrawIndirect.glMultiDrawElementsIndirect;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import static org.lwjgl.opengl.GL11.GL_UNSIGNED_SHORT;
import static org.lwjgl.opengl.GL30.glBindVertexArray;
import static org.lwjgl.opengl.GL42.*;
import static org.lwjgl.opengl.GL42.GL_FRAMEBUFFER_BARRIER_BIT;
import static org.lwjgl.opengl.GL43.GL_SHADER_STORAGE_BARRIER_BIT;
import static org.lwjgl.opengl.GL43.glDispatchCompute;
import static org.lwjgl.opengl.NVMeshShader.glDrawMeshTasksNV;
//TODO: make this a 2 phase culling system
// first phase renders the terrain, in the terrain task shader it also checks if the section was not visible in the frustum but now is
// and then renders it and marks it as being in the frustum
public class NvFarWorldRenderer extends AbstractFarWorldRenderer {
private final Shader primaryTerrainRaster = Shader.make()
.add(ShaderType.TASK, "voxelmon:lod/nvmesh/primary.task")
.add(ShaderType.MESH, "voxelmon:lod/nvmesh/primary.mesh")
.add(ShaderType.FRAGMENT, "voxelmon:lod/nvmesh/primary.frag")
.compile();
@Override
protected void setupVao() {
}
@Override
public void renderFarAwayOpaque(MatrixStack stack, double cx, double cy, double cz) {
if (this.geometry.getSectionCount() == 0) {
return;
}
RenderLayer.getCutoutMipped().startDrawing();
UploadStream.INSTANCE.commit();
glBindVertexArray(this.vao);
this.primaryTerrainRaster.bind();
glDrawMeshTasksNV(0, this.geometry.getSectionCount());
glBindVertexArray(0);
RenderLayer.getCutoutMipped().endDrawing();
}
@Override
public void shutdown() {
super.shutdown();
}
}

View File

@@ -0,0 +1,16 @@
package me.cortex.voxelmon.core.rendering;
import me.cortex.voxelmon.core.gl.GlFramebuffer;
import me.cortex.voxelmon.core.gl.GlTexture;
public class PostProcessing {
private final GlFramebuffer framebuffer;
private GlTexture colour;
private GlTexture depth;
public PostProcessing() {
this.framebuffer = new GlFramebuffer();
}
}

View File

@@ -0,0 +1,194 @@
package me.cortex.voxelmon.core.rendering;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import me.cortex.voxelmon.core.rendering.building.BuiltSectionGeometry;
import me.cortex.voxelmon.core.rendering.building.RenderGenerationService;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import net.minecraft.client.MinecraftClient;
import net.minecraft.util.math.Direction;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
//Tracks active sections, dispatches updates to the build system, everything related to rendering flows through here
public class RenderTracker {
private static final class ActiveSectionObject {
private int buildFlags;
}
private final WorldEngine world;
private RenderGenerationService renderGen;
private final AbstractFarWorldRenderer renderer;
//private final Long2ObjectOpenHashMap<Object> activeSections = new Long2ObjectOpenHashMap<>();
private final ConcurrentHashMap<Long,Object> activeSections = new ConcurrentHashMap<>(50000,0.75f, 16);
private static final Object O = new Object();
public void setRenderGen(RenderGenerationService renderGen) {
this.renderGen = renderGen;
}
public RenderTracker(WorldEngine world, AbstractFarWorldRenderer renderer) {
this.world = world;
this.renderer = renderer;
var loader = new Thread(()->{
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int OX = 0;//-27;
int OZ = 0;//276;
int DROP = 48;
//Do ring rendering
for (int i = 0; i < 5; i++) {
for (int x = -DROP; x <= DROP; x++) {
for (int z = -DROP; z <= DROP; z++) {
int d = x*x+z*z;
if (d<(DROP/2-1)*(DROP/2) || d>DROP*DROP)
continue;
for (int y = -3>>i; y < Math.max(1, 10 >> i); y++) {
var sec = this.world.getOrLoadAcquire(i, x + (OX>>(1+i)), y, z + (OZ>>(1+i)));
//this.renderGen.enqueueTask(sec);
sec.release();
}
try {
while (this.renderGen.getTaskCount() > 1000) {
Thread.sleep(50);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
loader.setDaemon(true);
//loader.start();
}
//Adds a lvl 0 section into the world renderer
public void addLvl0(int x, int y, int z) {
this.renderGen.enqueueTask(0, x, y, z);
this.activeSections.put(WorldEngine.getWorldSectionId(0, x, y, z), O);
}
//Removes a lvl 0 section from the world renderer
public void remLvl0(int x, int y, int z) {
this.activeSections.remove(WorldEngine.getWorldSectionId(0, x, y, z));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(0, x, y, z), null, null));
}
//Increases from lvl-1 to lvl at the coordinates (which are in lvl space)
public void inc(int lvl, int x, int y, int z) {
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)));
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1));
this.activeSections.put(WorldEngine.getWorldSectionId(lvl, x, y, z), O);
//TODO: make a seperate object to hold the build data and link it with the location in a
// concurrent hashmap or something, this is so that e.g. the build data position
// can be updated
this.renderGen.enqueueTask(lvl, x, y, z);
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)), null, null));
this.renderer.enqueueResult(new BuiltSectionGeometry(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), null, null));
}
//Decreases from lvl to lvl-1 at the coordinates (which are in lvl space)
public void dec(int lvl, int x, int y, int z) {
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1), (z<<1)+1), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1), (y<<1)+1, (z<<1)+1), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1), (z<<1)+1), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)), O);
this.activeSections.put(WorldEngine.getWorldSectionId(lvl-1, (x<<1)+1, (y<<1)+1, (z<<1)+1), O);
this.activeSections.remove(WorldEngine.getWorldSectionId(lvl, x, y, z));
this.renderer.enqueueResult(new BuiltSectionGeometry(lvl, x, y, z, null, null));
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1));
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1), (z<<1)+1);
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1));
this.renderGen.enqueueTask(lvl - 1, (x<<1), (y<<1)+1, (z<<1)+1);
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1));
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1), (z<<1)+1);
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1));
this.renderGen.enqueueTask(lvl - 1, (x<<1)+1, (y<<1)+1, (z<<1)+1);
}
//Updates a sections direction mask (e.g. if the player goes past the axis, the chunk must be updated)
public void updateDirMask(int lvl, int x, int y, int z, int newMask) {
}
//Called by the world engine when a section gets dirtied
public void sectionUpdated(WorldSection section) {
//this.renderGen.enqueueTask(section);
}
//called by the RenderGenerationService about built geometry, the RenderTracker checks if it can use the result (e.g. the LoD hasnt changed/still correct etc)
// and dispatches it to the renderer
// it also batch collects the geometry sections until all the geometry for an operation is collected, then it executes the operation, its removes flickering
public void processBuildResult(BuiltSectionGeometry section) {
//Check that we still want the section
if (this.activeSections.containsKey(section.position)) {
this.renderer.enqueueResult(section);
} else {
section.free();
}
}
public int getBuildFlagsOrAbort(WorldSection section) {
var holder = this.activeSections.get(section.getKey());
int buildMask = 0;
if (holder != null) {
if (section.z< (((int)MinecraftClient.getInstance().cameraEntity.getPos().z)>>(5+section.lvl))+1) {
buildMask |= 1<< Direction.SOUTH.getId();
}
if (section.z>(((int)MinecraftClient.getInstance().cameraEntity.getPos().z)>>(5+section.lvl))-1) {
buildMask |= 1<<Direction.NORTH.getId();
}
if (section.x<(((int)MinecraftClient.getInstance().cameraEntity.getPos().x)>>(5+section.lvl))+1) {
buildMask |= 1<<Direction.EAST.getId();
}
if (section.x>(((int)MinecraftClient.getInstance().cameraEntity.getPos().x)>>(5+section.lvl))-1) {
buildMask |= 1<<Direction.WEST.getId();
}
buildMask |= 1<<Direction.UP.getId();
buildMask |= ((1<<6)-1)^(1);
}
return buildMask;
}
public boolean shouldStillBuild(int lvl, int x, int y, int z) {
return this.activeSections.containsKey(WorldEngine.getWorldSectionId(lvl, x, y, z));
}
}

View File

@@ -0,0 +1,39 @@
package me.cortex.voxelmon.core.rendering;
import me.cortex.voxelmon.core.rendering.util.BufferArena;
import me.cortex.voxelmon.core.util.IndexUtil;
public class SharedIndexBuffer {
public static final SharedIndexBuffer INSTANCE = new SharedIndexBuffer();
private int commonIndexBufferOffset = -1;
private int commonIndexQuadCount;
private final BufferArena indexBuffer;
public SharedIndexBuffer() {
this.indexBuffer = new BufferArena((1L << 16)*(2*6), 2 * 6);
this.getSharedIndexBuffer(16380);
}
public int getSharedIndexBuffer(int newQuadCount) {
if (this.commonIndexBufferOffset == -1 || this.commonIndexQuadCount < newQuadCount) {
if (this.commonIndexBufferOffset != -1) {
this.indexBuffer.free(this.commonIndexBufferOffset);
}
var buffer = IndexUtil.generateQuadIndices(newQuadCount);
long offset = this.indexBuffer.upload(buffer);
if (offset >= 1L<<31) {
throw new IllegalStateException();
}
this.commonIndexBufferOffset = (int) offset;
buffer.free();
this.commonIndexQuadCount = newQuadCount;
}
return this.commonIndexBufferOffset * 6;
}
public int id() {
return this.indexBuffer.id();
}
}

View File

@@ -0,0 +1,29 @@
package me.cortex.voxelmon.core.rendering.building;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import me.cortex.voxelmon.core.util.TrackedObject;
import me.cortex.voxelmon.core.world.WorldEngine;
public class BuiltSectionGeometry {
public final long position;
public final MemoryBuffer geometryBuffer;
public final MemoryBuffer translucentGeometryBuffer;
public BuiltSectionGeometry(int lvl, int x, int y, int z, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
this(WorldEngine.getWorldSectionId(lvl, x, y, z), geometryBuffer, translucentGeometryBuffer);
}
public BuiltSectionGeometry(long position, MemoryBuffer geometryBuffer, MemoryBuffer translucentGeometryBuffer) {
this.position = position;
this.geometryBuffer = geometryBuffer;
this.translucentGeometryBuffer = translucentGeometryBuffer;
}
public void free() {
if (this.geometryBuffer != null) {
this.geometryBuffer.free();
}
if (this.translucentGeometryBuffer != null) {
this.translucentGeometryBuffer.free();
}
}
}

View File

@@ -0,0 +1,83 @@
package me.cortex.voxelmon.core.rendering.building;
import me.cortex.voxelmon.core.util.Mesher2D;
import me.cortex.voxelmon.core.world.other.Mapper;
/*
8 - Light (can probably make it 3,3 bit lighting then i get 2 spare bits for other things)
8 - R
8 - G
8 - B
4 - A
5 - x
5 - y
5 - z
4 - w
4 - h
3 - face
*/
//TODO: might be able to fit it within 32 bits _very hackily_ (keep the same position data)
// but then have a per section LUT
//V2 QUAD FORMAT (enables animations to work)
/*
1 - spare
8 - light
9 - biome id
20 - block id
5 - x
5 - y
5 - z
4 - w
4 - h
3 - face
*/
public class QuadFormat {
//Note: the encodedMeshedData is from the Mesher2D
public static int encodePosition(int face, int otherAxis, int encodedMeshedData) {
if (Mesher2D.getW(encodedMeshedData) > 16 || Mesher2D.getH(encodedMeshedData) > 16) {
throw new IllegalStateException("Width or height > 16");
}
int out = face;
out |= switch (face >> 1) {
case 0 ->
(Mesher2D.getX(encodedMeshedData) << 21) |
(otherAxis << 16) |
(Mesher2D.getZ(encodedMeshedData) << 11) |
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
((Mesher2D.getH(encodedMeshedData)-1) << 3);
case 1 ->
(Mesher2D.getX(encodedMeshedData) << 21) |
(Mesher2D.getZ(encodedMeshedData) << 16) |
(otherAxis << 11) |
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
((Mesher2D.getH(encodedMeshedData)-1) << 3);
case 2 ->
(otherAxis << 21) |
(Mesher2D.getX(encodedMeshedData) << 16) |
(Mesher2D.getZ(encodedMeshedData) << 11) |
((Mesher2D.getW(encodedMeshedData)-1) << 7) |
((Mesher2D.getH(encodedMeshedData)-1) << 3);
default -> throw new IllegalStateException("Unexpected value: " + (face >> 1));
};
return out;
}
//TODO: finish
public static long encode(Mapper mapper, long id, int encodedPosition) {
return ((id>>>27)<<26)|Integer.toUnsignedLong(encodedPosition);
}
public static long encode(Mapper mapper, long id, int face, int other, int encodedMeshedData) {
return encode(mapper, id, encodePosition(face, other, encodedMeshedData));
}
}

View File

@@ -0,0 +1,312 @@
package me.cortex.voxelmon.core.rendering.building;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import me.cortex.voxelmon.core.util.Mesher2D;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import me.cortex.voxelmon.core.world.other.Mapper;
import net.minecraft.util.math.Direction;
import org.lwjgl.system.MemoryUtil;
//TODO: make this only emit quads that are facing the rebuild location
// HAVE IT AS FLAGS so that a range around the player can be built with all quads etc
// this will cut down on the amount of quads by an insane about and help alot overall
public class RenderDataFactory {
private final Mesher2D mesher = new Mesher2D(5,15);//15
private final LongArrayList outData = new LongArrayList(1000);
private final WorldEngine world;
public RenderDataFactory(WorldEngine world) {
this.world = world;
}
//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
//section is already acquired and gets released by the parent
//buildMask in the lower 6 bits contains the faces to build, the next 6 bits are whether the edge face builds against
// its neigbor or not (0 if it does 1 if it doesnt (0 is default behavior))
public BuiltSectionGeometry generateMesh(WorldSection section, int buildMask) {
//TODO: to speed it up more, check like section.isEmpty() and stuff like that, have masks for if a slice/layer is entirly air etc
//TODO: instead of having it check its neighbors with the same lod level, compare against 1 level lower, this will prevent cracks and seams from
// appearing between lods
if (section.definitelyEmpty()) {
return new BuiltSectionGeometry(section.getKey(), null, null);
}
var data = section.copyData();
long[] connectedData = null;
int dirId = Direction.UP.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int y = 0; y < 32; y++) {
this.mesher.reset();
for (int z = 0; z < 32; z++) {
for (int x = 0; x < 32; x++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (y < 31) {
var up = data[WorldSection.getIndex(x, y + 1, z)];
if (up != Mapper.AIR) {
continue;
}
}
if (y == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y + 1, section.z);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(x, 0, z)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(x, z, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), y, Mesher2D.getZ(quad))], 1, y, quad));
}
}
connectedData = null;
}
dirId = Direction.EAST.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int x = 0; x < 32; x++) {
this.mesher.reset();
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (x < 31) {
var up = data[WorldSection.getIndex(x + 1, y, z)];
if (up != Mapper.AIR) {
continue;
}
}
if (x == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x + 1, section.y, section.z);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(0, y, z)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(y, z, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(x, Mesher2D.getX(quad), Mesher2D.getZ(quad))], 5, x, quad));
}
}
connectedData = null;
}
dirId = Direction.SOUTH.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int z = 0; z < 32; z++) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (z < 31) {
var up = data[WorldSection.getIndex(x, y, z + 1)];
if (up != Mapper.AIR) {
continue;
}
}
if (z == 31 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z + 1);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(x, y, 0)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(x, y, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), Mesher2D.getZ(quad), z)], 3, z, quad));
}
}
connectedData = null;
}
dirId = Direction.WEST.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int x = 31; x != -1; x--) {
this.mesher.reset();
for (int y = 0; y < 32; y++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (x != 0) {
var up = data[WorldSection.getIndex(x - 1, y, z)];
if (up != Mapper.AIR) {
continue;
}
}
if (x == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x - 1, section.y, section.z);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(31, y, z)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(y, z, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(x, Mesher2D.getX(quad), Mesher2D.getZ(quad))], 4, x, quad));
}
}
connectedData = null;
}
dirId = Direction.NORTH.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int z = 31; z != -1; z--) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int y = 0; y < 32; y++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (z != 0) {
var up = data[WorldSection.getIndex(x, y, z - 1)];
if (up != Mapper.AIR) {
continue;
}
}
if (z == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y, section.z - 1);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(x, y, 31)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(x, y, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), Mesher2D.getZ(quad), z)], 2, z, quad));
}
}
connectedData = null;
}
dirId = Direction.DOWN.getId();
if ((buildMask&(1<<dirId))!=0) {
for (int y = 31; y != -1; y--) {
this.mesher.reset();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
var self = data[WorldSection.getIndex(x, y, z)];
if (self == Mapper.AIR) {
continue;
}
if (y != 0) {
var up = data[WorldSection.getIndex(x, y - 1, z)];
if (up != Mapper.AIR) {
continue;
}
}
if (y == 0 && ((buildMask>>(6+dirId))&1) == 0) {
//Load and copy the data into a local cache, TODO: optimize so its not doing billion of copies
if (connectedData == null) {
var connectedSection = this.world.getOrLoadAcquire(section.lvl, section.x, section.y - 1, section.z);
connectedData = connectedSection.copyData();
connectedSection.release();
}
var up = connectedData[WorldSection.getIndex(x, 31, z)];
if (up != Mapper.AIR) {
continue;
}
}
this.mesher.put(x, z, self);
}
}
var quads = this.mesher.process();
for (int i = 0; i < quads.length; i++) {
var quad = quads[i];
this.outData.add(QuadFormat.encode(null, data[WorldSection.getIndex(Mesher2D.getX(quad), y, Mesher2D.getZ(quad))], 0, y, quad));
}
}
connectedData = null;
}
if (this.outData.isEmpty()) {
return new BuiltSectionGeometry(section.getKey(), null, null);
}
var output = new MemoryBuffer(this.outData.size()*8L);
for (int i = 0; i < this.outData.size(); i++) {
MemoryUtil.memPutLong(output.address + i * 8L, this.outData.getLong(i));
}
this.outData.clear();
return new BuiltSectionGeometry(section.getKey(), output, null);
}
}

View File

@@ -0,0 +1,133 @@
package me.cortex.voxelmon.core.rendering.building;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import me.cortex.voxelmon.core.rendering.AbstractFarWorldRenderer;
import me.cortex.voxelmon.core.rendering.RenderTracker;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import net.minecraft.util.math.Direction;
import net.minecraft.world.chunk.WorldChunk;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Semaphore;
import java.util.function.Supplier;
public class RenderGenerationService {
//TODO: make it accept either a section or section position and have a concurrent hashset to determine if
// a section is in the build queue
private record BuildTask(Supplier<WorldSection> sectionSupplier) {}
private volatile boolean running = true;
private final Thread[] workers;
private final ConcurrentLinkedDeque<BuildTask> taskQueue = new ConcurrentLinkedDeque<>();
private final Semaphore taskCounter = new Semaphore(0);
private final WorldEngine world;
private final RenderTracker tracker;
public RenderGenerationService(WorldEngine world, RenderTracker tracker, int workers) {
this.world = world;
this.tracker = tracker;
this.workers = new Thread[workers];
for (int i = 0; i < workers; i++) {
this.workers[i] = new Thread(this::renderWorker);
this.workers[i].setDaemon(true);
this.workers[i].setName("Render generation service #" + i);
this.workers[i].start();
}
}
//TODO: add a generated render data cache
private void renderWorker() {
//Thread local instance of the factory
var factory = new RenderDataFactory(this.world);
while (this.running) {
this.taskCounter.acquireUninterruptibly();
if (!this.running) break;
var task = this.taskQueue.pop();
var section = task.sectionSupplier.get();
if (section == null) {
continue;
}
section.assertNotFree();
int buildFlags = this.tracker.getBuildFlagsOrAbort(section);
if (buildFlags != 0) {
this.tracker.processBuildResult(factory.generateMesh(section, buildFlags));
}
section.release();
}
}
//TODO: Add a priority system, higher detail sections must always be updated before lower detail
// e.g. priorities NONE->lvl0 and lvl1 -> lvl0 over lvl0 -> lvl1
//TODO: make it pass either a world section, _or_ coodinates so that the render thread has to do the loading of the sections
// not the calling method
//TODO: maybe make it so that it pulls from the world to stop the inital loading absolutly butt spamming the queue
// and thus running out of memory
//TODO: REDO THIS ENTIRE THING
// render tasks should not be bound to a WorldSection, instead it should be bound to either a WorldSection or
// an LoD position, the issue is that if we bound to a LoD position we loose all the info of the WorldSection
// like if its in the render queue and if we should abort building the render data
//1 proposal fix is a Long2ObjectLinkedOpenHashMap<WorldSection> which means we can abort if needed,
// also gets rid of dependency on a WorldSection (kinda)
public void enqueueTask(int lvl, int x, int y, int z) {
this.taskQueue.add(new BuildTask(()->{
if (this.tracker.shouldStillBuild(lvl, x, y, z)) {
return this.world.getOrLoadAcquire(lvl, x, y, z);
} else {
return null;
}
}));
this.taskCounter.release();
}
public void enqueueTask(WorldSection section) {
//TODO: fixme! buildMask could have changed
//if (!section.inRenderQueue.getAndSet(true)) {
// //TODO: add a boolean for needsRenderUpdate that can be set to false if the section is no longer needed
// // to be rendered, e.g. LoD level changed so that lod is no longer being rendered
// section.acquire();
// this.taskQueue.add(new BuildTask(()->section));
// this.taskCounter.release();
//}
}
public int getTaskCount() {
return this.taskCounter.availablePermits();
}
public void shutdown() {
boolean anyAlive = false;
for (var worker : this.workers) {
anyAlive |= worker.isAlive();
}
if (!anyAlive) {
System.err.println("Render gen workers already dead on shutdown! this is very very bad, check log for errors from this thread");
return;
}
//Wait for the ingest to finish
while (this.taskCounter.availablePermits() != 0) {
Thread.onSpinWait();
}
//Shutdown
this.running = false;
this.taskCounter.release(1000);
//Wait for thread to join
try {
for (var worker : this.workers) {
worker.join();
}
} catch (InterruptedException e) {throw new RuntimeException(e);}
}
}

View File

@@ -0,0 +1,53 @@
package me.cortex.voxelmon.core.rendering.util;
import me.cortex.voxelmon.core.gl.GlBuffer;
import me.cortex.voxelmon.core.util.AllocationArena;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import org.lwjgl.system.MemoryUtil;
public class BufferArena {
private final long size;
private final int elementSize;
private final GlBuffer buffer;
private final AllocationArena allocationMap = new AllocationArena();
private long used;
public BufferArena(long capacity, int elementSize) {
if (capacity%elementSize != 0) {
throw new IllegalArgumentException("Capacity not a multiple of element size");
}
this.size = capacity;
this.elementSize = elementSize;
this.buffer = new GlBuffer(capacity, 0);
this.allocationMap.setLimit(capacity/elementSize);
}
public long upload(MemoryBuffer buffer) {
if (buffer.size%this.elementSize!=0) {
throw new IllegalArgumentException("Buffer size not multiple of elementSize");
}
int size = (int) (buffer.size/this.elementSize);
long addr = this.allocationMap.alloc(size);
long uploadPtr = UploadStream.INSTANCE.upload(this.buffer, addr * this.elementSize, buffer.size);
MemoryUtil.memCopy(buffer.address, uploadPtr, buffer.size);
this.used += size;
return addr;
}
public void free(long allocation) {
this.used -= this.allocationMap.free(allocation);
}
public void free() {
this.buffer.free();
}
public int id() {
this.buffer.assertNotFreed();
return this.buffer.id;
}
public float usage() {
return (float) ((double)this.used/(this.buffer.size()/this.elementSize));
}
}

View File

@@ -0,0 +1,124 @@
package me.cortex.voxelmon.core.rendering.util;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import me.cortex.voxelmon.core.gl.GlBuffer;
import me.cortex.voxelmon.core.gl.GlFence;
import me.cortex.voxelmon.core.gl.GlPersistentMappedBuffer;
import me.cortex.voxelmon.core.util.AllocationArena;
import java.util.ArrayDeque;
import java.util.Deque;
import static me.cortex.voxelmon.core.util.AllocationArena.SIZE_LIMIT;
import static org.lwjgl.opengl.ARBDirectStateAccess.glCopyNamedBufferSubData;
import static org.lwjgl.opengl.ARBDirectStateAccess.glFlushMappedNamedBufferRange;
import static org.lwjgl.opengl.ARBMapBufferRange.*;
import static org.lwjgl.opengl.GL11.glFinish;
import static org.lwjgl.opengl.GL42.glMemoryBarrier;
import static org.lwjgl.opengl.GL42C.GL_BUFFER_UPDATE_BARRIER_BIT;
import static org.lwjgl.opengl.GL44.GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT;
public class UploadStream {
private final AllocationArena allocationArena = new AllocationArena();
private final GlPersistentMappedBuffer uploadBuffer;
private final Deque<UploadFrame> frames = new ArrayDeque<>();
private final LongArrayList thisFrameAllocations = new LongArrayList();
private final Deque<UploadData> uploadList = new ArrayDeque<>();
private final LongArrayList flushList = new LongArrayList();
public UploadStream(long size) {
this.uploadBuffer = new GlPersistentMappedBuffer(size,GL_MAP_WRITE_BIT|GL_MAP_UNSYNCHRONIZED_BIT|GL_MAP_FLUSH_EXPLICIT_BIT);
this.allocationArena.setLimit(size);
}
private long caddr = -1;
private long offset = 0;
public long upload(GlBuffer buffer, long destOffset, long size) {
if (size > Integer.MAX_VALUE) {
throw new IllegalArgumentException();
}
long addr;
if (this.caddr == -1 || !this.allocationArena.expand(this.caddr, (int) size)) {
this.caddr = this.allocationArena.alloc((int) size);//TODO: replace with allocFromLargest
if (this.caddr == SIZE_LIMIT) {
this.commit();
int attempts = 10;
while (--attempts != 0 && this.caddr == SIZE_LIMIT) {
glFinish();
this.tick();
this.caddr = this.allocationArena.alloc((int) size);
}
if (this.caddr == SIZE_LIMIT) {
throw new IllegalStateException("Could not allocate memory segment big enough for upload even after force flush");
}
}
this.flushList.add(this.caddr);
this.offset = size;
addr = this.caddr;
} else {//Could expand the allocation so just update it
addr = this.caddr + this.offset;
this.offset += size;
}
if (this.caddr + size > this.uploadBuffer.size()) {
throw new IllegalStateException();
}
this.uploadList.add(new UploadData(buffer, addr, destOffset, size));
return this.uploadBuffer.addr() + addr;
}
public void commit() {
//First flush all the allocations and enqueue them to be freed
{
for (long alloc : flushList) {
glFlushMappedNamedBufferRange(this.uploadBuffer.id, alloc, this.allocationArena.getSize(alloc));
this.thisFrameAllocations.add(alloc);
}
this.flushList.clear();
}
glMemoryBarrier(GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT);
//Execute all the copies
for (var entry : this.uploadList) {
glCopyNamedBufferSubData(this.uploadBuffer.id, entry.target.id, entry.uploadOffset, entry.targetOffset, entry.size);
}
this.uploadList.clear();
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
this.caddr = -1;
this.offset = 0;
}
public void tick() {
this.commit();
if (!this.thisFrameAllocations.isEmpty()) {
this.frames.add(new UploadFrame(new GlFence(), new LongArrayList(this.thisFrameAllocations)));
this.thisFrameAllocations.clear();
}
while (!this.frames.isEmpty()) {
//Since the ordering of frames is the ordering of the gl commands if we encounter an unsignaled fence
// all the other fences should also be unsignaled
if (!this.frames.peek().fence.signaled()) {
break;
}
//Release all the allocations from the frame
var frame = this.frames.pop();
frame.allocations.forEach(allocationArena::free);
frame.fence.free();
}
}
private record UploadFrame(GlFence fence, LongArrayList allocations) {}
private record UploadData(GlBuffer target, long uploadOffset, long targetOffset, long size) {}
//A upload instance instead of passing one around by reference
// MUST ONLY BE USED ON THE RENDER THREAD
public static final UploadStream INSTANCE = new UploadStream(1<<25);//32 mb upload buffer
}

View File

@@ -0,0 +1,179 @@
package me.cortex.voxelmon.core.util;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
import java.util.Random;
//FIXME: NOTE: if there is a free block of size > 2^30 EVERYTHING BREAKS, need to either increase size
// or automatically split and manage multiple blocks which is very painful
//OR instead of addr, defer to a long[] and use indicies
//TODO: replace the LongAVLTreeSet with a custom implementation that doesnt cause allocations when searching
// and see if something like a RBTree is any better
public class AllocationArena {
public static final long SIZE_LIMIT = -1;
private final int ADDR_BITS = 34;//This gives max size per allocation of 2^30 and max address of 2^39
private final int SIZE_BITS = 64 - ADDR_BITS;
private final long SIZE_MSK = (1L<<SIZE_BITS)-1;
private final long ADDR_MSK = (1L<<ADDR_BITS)-1;
private final LongRBTreeSet FREE = new LongRBTreeSet();//Size Address
private final LongRBTreeSet TAKEN = new LongRBTreeSet();//Address Size
private long sizeLimit = Long.MAX_VALUE;
private long totalSize;
//Flags
public boolean resized;//If the required memory of the entire buffer grew
public long getSize() {
return totalSize;
}
/*
public long allocFromLargest(int size) {//Allocates from the largest avalible block, this is useful for expanding later on
}*/
public long alloc(int size) {//TODO: add alignment support
if (size == 0) throw new IllegalArgumentException();
//This is stupid, iterator is not inclusive
var iter = FREE.iterator(((long) size << ADDR_BITS)-1);
if (!iter.hasNext()) {//No free space for allocation
//Create new allocation
resized = true;
long addr = totalSize;
if (totalSize+size>sizeLimit) {
return SIZE_LIMIT;
}
totalSize += size;
TAKEN.add((addr<<SIZE_BITS)|((long) size));
return addr;
} else {
long slot = iter.nextLong();
iter.remove();
if ((slot >>> ADDR_BITS) == size) {//If the allocation and slot is the same size, just add it to the taken
TAKEN.add((slot<<SIZE_BITS)|(slot >>> ADDR_BITS));
} else {
TAKEN.add(((slot&ADDR_MSK)<<SIZE_BITS)|size);
FREE.add((((slot >>> ADDR_BITS)-size)<<ADDR_BITS)|((slot&ADDR_MSK)+size));
}
resized = false;
return slot&ADDR_MSK;
}
}
public int free(long addr) {//Returns size of freed memory
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
var iter = TAKEN.iterator(addr<<SIZE_BITS);//Dont need to include -1 as size != 0
long slot = iter.nextLong();
if (slot>>SIZE_BITS != addr) {
throw new IllegalStateException();
}
long size = slot&SIZE_MSK;
iter.remove();
//Note: if there is a previous entry, it means that it is guaranteed for the ending address to either
// be the addr, or indicate a free slot that needs to be merged
if (iter.hasPrevious()) {
long prevSlot = iter.previousLong();
long endAddr = (prevSlot>>>SIZE_BITS) + (prevSlot&SIZE_MSK);
if (endAddr != addr) {//It means there is a free slot that needs to get merged into
long delta = (addr - endAddr);
FREE.remove((delta<<ADDR_BITS)|endAddr);//Free the slot to be merged into
//Generate a new slot to get put into FREE
slot = (endAddr<<SIZE_BITS) | ((slot&SIZE_MSK) + delta);
}
iter.nextLong();//Need to reset the iter into its state
}//If there is no previous it means were at the start of the buffer, we might need to merge with block 0 if we are not block 0
else if (!FREE.isEmpty()) {// if free is not empty it means we must merge with block of free starting at 0
//if (addr != 0)//FIXME: this is very dodgy solution, if addr == 0 it means its impossible for there to be a previous element
if (FREE.remove(addr<<ADDR_BITS)) {//Attempt to remove block 0, this is very dodgy as it assumes block zero is 0 addr n size
slot = addr + size;//slot at address 0 and size of 0 block + new block
}
}
//If there is a next element it is guarenteed to either be the next block, or indicate that there is
// a block that needs to be merged into
if (iter.hasNext()) {
long nextSlot = iter.nextLong();
long endAddr = (slot>>>SIZE_BITS) + (slot&SIZE_MSK);
if (endAddr != nextSlot>>>SIZE_BITS) {//It means there is a memory block to be merged in FREE
long delta = ((nextSlot>>>SIZE_BITS) - endAddr);
FREE.remove((delta<<ADDR_BITS)|endAddr);
slot = (slot&(ADDR_MSK<<SIZE_BITS)) | ((slot&SIZE_MSK) + delta);
}
}// if there is no next block it means that we have reached the end of the allocation sections and we can shrink the buffer
else {
resized = true;
totalSize -= (slot&SIZE_MSK);
return (int) size;
}
resized = false;
//Need to swap around the slot to be in FREE format
slot = (slot>>>SIZE_BITS) | (slot<<ADDR_BITS);
FREE.add(slot);//Add the free slot into segments
return (int) size;
}
//Attempts to expand an allocation, returns true on success
public boolean expand(long addr, int extra) {
addr &= ADDR_MSK;//encase addr stores shit in its upper bits
var iter = TAKEN.iterator(addr<<SIZE_BITS);
if (!iter.hasNext()) {
return false;
}
long slot = iter.nextLong();
if (slot>>SIZE_BITS != addr) {
throw new IllegalStateException();
}
long updatedSlot = (slot & (ADDR_MSK << SIZE_BITS)) | ((slot & SIZE_MSK) + extra);
resized = false;
if (iter.hasNext()) {
long next = iter.nextLong();
long endAddr = (slot>>>SIZE_BITS)+(slot&SIZE_MSK);
long delta = (next>>>SIZE_BITS) - endAddr;
if (extra <= delta) {
FREE.remove((delta<<ADDR_BITS)|endAddr);//Should assert this
iter.previousLong();//FOR SOME REASON NEED TO DO IT TWICE I HAVE NO IDEA WHY
iter.previousLong();
iter.remove();//Remove the allocation so it can be updated
TAKEN.add(updatedSlot);//Update the taken allocation
if (extra != delta) {//More space than needed, need to add a new FREE block
FREE.add(((delta-extra)<<ADDR_BITS)|(endAddr+extra));
}
//else There is exactly enough free space, so removing the free block and updating the allocation is enough
return true;
} else {
return false;//Not enough room to expand
}
} else {//We are at the end of the buffer, we can expand as we like
if (totalSize+extra>sizeLimit)//If expanding and we would exceed the size limit, dont resize
return false;
iter.remove();
TAKEN.add(updatedSlot);
totalSize += extra;
resized = true;
return true;
}
}
public long getSize(long addr) {
addr &= ADDR_MSK;
var iter = TAKEN.iterator(addr << SIZE_BITS);
if (!iter.hasNext())
throw new IllegalArgumentException();
long slot = iter.nextLong();
if (slot>>SIZE_BITS != addr) {
throw new IllegalStateException();
}
return slot&SIZE_MSK;
}
public void setLimit(long size) {
this.sizeLimit = size;
}
}

View File

@@ -0,0 +1,31 @@
package me.cortex.voxelmon.core.util;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
public class ByteBufferBackedInputStream extends InputStream {
ByteBuffer buf;
public ByteBufferBackedInputStream(ByteBuffer buf) {
this.buf = buf;
}
public int read() throws IOException {
if (!this.buf.hasRemaining()) {
return -1;
}
return this.buf.get() & 0xFF;
}
public int read(byte[] bytes, int off, int len) throws IOException {
if (!this.buf.hasRemaining()) {
return -1;
}
len = Math.min(len, this.buf.remaining());
this.buf.get(bytes, off, len);
return len;
}
}

View File

@@ -0,0 +1,72 @@
package me.cortex.voxelmon.core.util;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.render.*;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Box;
import org.joml.Matrix4f;
public class DebugUtil {
private static Matrix4f positionMatrix = new Matrix4f().identity();
public static void setPositionMatrix(MatrixStack stack) {
positionMatrix = new Matrix4f(stack.peek().getPositionMatrix());
}
public static void renderAABB(Box aabb, int colour) {
renderAABB(aabb, (float)(colour>>>24)/255, (float)((colour>>>16)&0xFF)/255, (float)((colour>>>8)&0xFF)/255, (float)(colour&0xFF)/255);
}
public static void renderBox(int x, int y, int z, int size, float r, float g, float b) {
renderAABB(new Box(x, y, z, x+size, y+size, z+size), r, g, b, 1.0f);
}
public static void renderAABB(Box aabb, float r, float g, float b, float a) {
RenderSystem.setShaderFogEnd(9999999);
RenderSystem.setShader(GameRenderer::getPositionProgram);
RenderSystem.setShaderColor(r, g, b, a);
RenderSystem.enableBlend();
RenderSystem.disableCull();
RenderSystem.enableDepthTest();
//RenderSystem.depthMask(false);
var tessellator = RenderSystem.renderThreadTesselator();
var builder = tessellator.getBuffer();
builder.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION);
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.minX, (float) aabb.minY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.minZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.maxY, (float) aabb.maxZ).next();
builder.vertex(positionMatrix, (float) aabb.maxX, (float) aabb.minY, (float) aabb.maxZ).next();
tessellator.draw();
RenderSystem.disableBlend();
RenderSystem.enableCull();
RenderSystem.disableDepthTest();
RenderSystem.setShaderColor(1,1,1,1);
RenderSystem.depthMask(true);
}
}

View File

@@ -0,0 +1,25 @@
package me.cortex.voxelmon.core.util;
import org.lwjgl.system.MemoryUtil;
public class IndexUtil {
public static MemoryBuffer generateQuadIndices(int quadCount) {
if ((quadCount*4) >= 1<<16) {
throw new IllegalArgumentException("Quad count to large");
}
MemoryBuffer buffer = new MemoryBuffer(quadCount * 6L * 2);
long ptr = buffer.address;
for(int i = 0; i < quadCount*4; i += 4) {
MemoryUtil.memPutShort(ptr + (0*2), (short) i);
MemoryUtil.memPutShort(ptr + (1*2), (short) (i + 1));
MemoryUtil.memPutShort(ptr + (2*2), (short) (i + 2));
MemoryUtil.memPutShort(ptr + (3*2), (short) (i + 1));
MemoryUtil.memPutShort(ptr + (4*2), (short) (i + 3));
MemoryUtil.memPutShort(ptr + (5*2), (short) (i + 2));
ptr += 6 * 2;
}
return buffer;
}
}

View File

@@ -0,0 +1,24 @@
package me.cortex.voxelmon.core.util;
import org.lwjgl.system.MemoryUtil;
public class MemoryBuffer extends TrackedObject {
public final long address;
public final long size;
public MemoryBuffer(long size) {
this.size = size;
this.address = MemoryUtil.nmemAlloc(size);
}
public void cpyTo(long dst) {
super.assertNotFreed();
MemoryUtil.memCopy(this.address, dst, this.size);
}
@Override
public void free() {
super.free0();
MemoryUtil.nmemFree(this.address);
}
}

View File

@@ -0,0 +1,167 @@
package me.cortex.voxelmon.core.util;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Random;
public class Mesher2D {
private final int size;
private final int maxSize;
private final long[] data;
private final BitSet meshed;
private int[] quadCache;
public Mesher2D(int sizeBits, int maxSize) {
this.size = sizeBits;
this.maxSize = maxSize;
this.data = new long[1<<(sizeBits<<1)];
this.meshed = new BitSet(1<<(sizeBits<<1));
this.quadCache = new int[128];
}
private int getIdx(int x, int z) {
int M = (1<<this.size)-1;
if (x>M || z>M) {
throw new IllegalStateException();
}
return ((z&M)<<this.size)|(x&M);
}
public Mesher2D put(int x, int z, long data) {
this.data[this.getIdx(x, z)] = data;
return this;
}
public static int getX(int data) {
return (data>>24)&0xFF;
}
public static int getZ(int data) {
return (data>>16)&0xFF;
}
public static int getH(int data) {
return (data>>8)&0xFF;
}
public static int getW(int data) {
return data&0xFF;
}
//
private static int encodeQuad(int x, int z, int sx, int sz) {
return ((x&0xFF)<<24)|((z&0xFF)<<16)|((sx&0xFF)<<8)|((sz&0xFF)<<0);
}
private boolean canMerge(int x, int z, long match) {
int id = this.getIdx(x, z);
return this.data[id] == match && !this.meshed.get(id);
}
public int[] process() {
//TODO: replace this loop with a loop over a bitset of data that has been put into the mesher, have the this.meshed be removed
// and just clear the databitset when its meshed
int[] quads = this.quadCache;
int idxCount = 0;
//TODO: add different strategies/ways to mesh
for (int z = 0; z < 1<<this.size; z++) {
for (int x = 0; x < 1<<this.size; x++) {
int idx = this.getIdx(x,z);
long data = this.data[idx];
if (data == 0 || this.meshed.get(idx)) {
//if this position has been meshed or is empty, dont mesh and continue
continue;
}
boolean ex = x != ((1<<this.size)-1);
boolean ez = z != ((1<<this.size)-1);
int endX = x;
int endZ = z;
while (ex || ez) {
//Expand in the x direction
if (ex) {
if (endX + 1 > this.maxSize || endX+1 == (1 << this.size) - 1) {
ex = false;
}
}
if (ex) {
for (int tz = z; tz < endZ+1; tz++) {
if (!this.canMerge(endX + 1, tz, data)) {
ex = false;
}
}
}
if (ex) {
endX++;
}
if (ez) {
if (endZ + 1 > this.maxSize || endZ+1 == (1<<this.size)-1) {
ez = false;
}
}
if (ez) {
for (int tx = x; tx < endX+1; tx++) {
if (!this.canMerge(tx, endZ + 1, data)) {
ez = false;
}
}
}
if (ez) {
endZ++;
}
}
//Mark the sections as meshed
for (int mx = x; mx <= endX; mx++) {
for (int mz = z; mz <= endZ; mz++) {
this.meshed.set(this.getIdx(mx, mz));
}
}
int encodedQuad = encodeQuad(x, z, endX - x + 1, endZ - z + 1);
{
int pIdx = idxCount++;
if (pIdx == quads.length) {
var newArray = new int[quads.length + 64];
System.arraycopy(quads, 0, newArray, 0, quads.length);
quads = newArray;
}
quads[pIdx] = encodedQuad;
}
}
}
var out = new int[idxCount];
System.arraycopy(quads, 0, out, 0, idxCount);
this.quadCache = quads;
return out;
}
public static void main(String[] args) {
var r = new Random(123451);
int a = 0;
long total = 0;
for (int i = 0; i < 200000; i++) {
var mesh = new Mesher2D(5,16);
for (int j = 0; j < 512; j++) {
mesh.put(r.nextInt(32), r.nextInt(32), r.nextInt(100));
}
long s = System.nanoTime();
var result = mesh.process();
total += System.nanoTime() - s;
a += result.hashCode();
}
System.out.println(total/(1e+6));
System.out.println((double) (total/(1e+6))/200000);
//mesh.put(0,0,1);
}
public void reset() {
this.meshed.clear();
Arrays.fill(this.data, 0);
}
}

View File

@@ -0,0 +1,78 @@
package me.cortex.voxelmon.core.util;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
public class RingUtil {
private static int computeR(int rd2, int a, int b) {
return rd2 - (a*a) - (b*b);
}
private static int computeR(int rd2, int a) {
return rd2 - (a*a);
}
private static int pack(int a, int b, int d) {
int m = ((1<<10)-1);
return (a&m)|((b&m)<<10)|(d<<20);
}
private static int pack(int a, int b) {
int m = ((1<<16)-1);
return (a&m)|((b&m)<<16);
}
public static int[] generateBoundingHalfSphere(int radius) {
IntArrayList points = new IntArrayList();
int rd2 = radius*radius;
//Generate full sphere points for each axis
for (int a = - radius; a <= radius; a++) {
for (int b = - radius; b <= radius; b++) {
//Basicly do a rearranged form of
// r^2 = x^2 + y^2 + z^2
int pd = computeR(rd2, a, b);
if (pd < 0) {//Cant have -ve space
continue;
}
pd = (int) Math.sqrt(pd);
points.add(pack(a,b,pd));
}
}
return points.toIntArray();
}
public static int[] generateBoundingHalfCircle(int radius) {
IntArrayList points = new IntArrayList();
int rd2 = radius*radius;
//Generate full sphere points for each axis
for (int a = - radius; a <= radius; a++) {
//Basicly do a rearranged form of
// r^2 = x^2 + y^2
int pd = computeR(rd2, a);
if (pd < 0) {//Cant have -ve space
continue;
}
pd = (int) Math.sqrt(pd);
points.add(pack(a,pd));
}
return points.toIntArray();
}
public static int[] generatingBoundingCorner2D(int radius) {
IntOpenHashSet points = new IntOpenHashSet();
//Do 2 pass (x and y) to generate and cover all points
for (int i = 0; i <= radius; i++) {
int other = (int) Math.floor(Math.sqrt(radius*radius - i*i));
//add points (x,other) and (other,x) as it covers the full spectrum
points.add((i<<16)|other);
//points.add((other<<16)|i);
}
return points.toIntArray();
}
}

View File

@@ -0,0 +1,47 @@
package me.cortex.voxelmon.core.util;
import java.lang.ref.Cleaner;
public abstract class TrackedObject {
private final Ref ref;
public TrackedObject() {
this.ref = register(this);
}
protected void free0() {
if (this.isFreed()) {
throw new IllegalStateException("Object " + this + " was double freed.");
}
this.ref.freedRef[0] = true;
this.ref.cleanable.clean();
}
public abstract void free();
public void assertNotFreed() {
if (isFreed()) {
throw new IllegalStateException("Object " + this + " should not be free, but is");
}
}
public boolean isFreed() {
return this.ref.freedRef[0];
}
public record Ref(Cleaner.Cleanable cleanable, boolean[] freedRef) {}
private static final Cleaner cleaner = Cleaner.create();
public static Ref register(Object obj) {
String clazz = obj.getClass().getName();
Throwable trace = new Throwable();
trace.fillInStackTrace();
boolean[] freed = new boolean[1];
var clean = cleaner.register(obj, ()->{
if (!freed[0]) {
System.err.println("Object named: "+ clazz+" was not freed, location at:\n" + trace);
System.err.flush();
}
});
return new Ref(clean, freed);
}
}

View File

@@ -0,0 +1,5 @@
package me.cortex.voxelmon.core.voxelization;
public interface I3dByteSupplier {
byte supply(int x, int y, int z);
}

View File

@@ -0,0 +1,5 @@
package me.cortex.voxelmon.core.voxelization;
public interface I3dSupplier <T> {
T supply(int x, int y, int z);
}

View File

@@ -0,0 +1,59 @@
package me.cortex.voxelmon.core.voxelization;
import me.cortex.voxelmon.core.world.other.Mapper;
//16x16x16 block section
public class VoxelizedSection {
public final int x;
public final int y;
public final int z;
private final long[] section;
private final long populationMsk;
private final long[][] subSections;
public VoxelizedSection(long[] section, long populationMsk, long[][] subSections, int x, int y, int z) {
this.section = section;
this.populationMsk = populationMsk;
this.subSections = subSections;
this.x = x;
this.y = y;
this.z = z;
}
private static int getIdx(int x, int y, int z, int shiftBy, int size) {
int M = (1<<size)-1;
x = (x>>shiftBy)&M;
y = (y>>shiftBy)&M;
z = (z>>shiftBy)&M;
return (y<<(size<<1))|(z<<size)|(x);
}
public long get(int lvl, int x, int y, int z) {
if (lvl < 2) {
int subIdx = getIdx(x,y,z,2-lvl,2);
var subSec = this.subSections[subIdx];
if (subSec == null) {
return Mapper.AIR;
}
if (lvl == 0) {
return subSec[getIdx(x,y,z,0,2)];
} else if (lvl == 1) {
return subSec[4*4*4+getIdx(x,y,z,0,1)];
}
} else {
if (lvl == 2) {
return section[getIdx(x,y,z,0,2)];
} else if (lvl == 3) {
return section[4*4*4+getIdx(x,y,z,0,1)];
} else if (lvl == 4) {
return section[4*4*4+2*2*2];
}
}
return Mapper.UNKNOWN_MAPPING;
}
public static VoxelizedSection createEmpty(int x, int y, int z) {
return new VoxelizedSection(new long[4*4*4+2*2*2+1], 0, new long[4*4*4][], x, y, z);
}
}

View File

@@ -0,0 +1,101 @@
package me.cortex.voxelmon.core.voxelization;
import me.cortex.voxelmon.core.world.other.Mipper;
import me.cortex.voxelmon.core.world.other.Mapper;
import net.minecraft.block.BlockState;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.chunk.PalettedContainer;
import net.minecraft.world.chunk.ReadableContainer;
public class WorldConversionFactory {
private static int I(int x, int y, int z) {
return (y<<4)|(z<<2)|x;
}
private static int J(int x, int y, int z) {
return ((y<<2)|(z<<1)|x) + 4*4*4;
}
public static VoxelizedSection convert(Mapper stateMapper,
PalettedContainer<BlockState> blockContainer,
ReadableContainer<RegistryEntry<Biome>> biomeContainer,
I3dByteSupplier lightSupplier,
int sx,
int sy,
int sz) {
long[] section = new long[4*4*4+2*2*2+1];//Mipping
long[][] subSections = new long[4*4*4][];
long[] current = new long[4*4*4+2*2*2];
long msk = 0;
for (int oy = 0; oy < 4; oy++) {
for (int oz = 0; oz < 4; oz++) {
for (int ox = 0; ox < 4; ox++) {
RegistryEntry<Biome> biome = biomeContainer.get(ox, oy, oz);
int nonAir = 0;
for (int iy = 0; iy < 4; iy++) {
for (int iz = 0; iz < 4; iz++) {
for (int ix = 0; ix < 4; ix++) {
int x = (ox<<2)|ix;
int y = (oy<<2)|iy;
int z = (oz<<2)|iz;
var state = blockContainer.get(x, y, z);
if (!state.isAir()) {
nonAir++;
current[I(ix, iy, iz)] = stateMapper.getBaseId(lightSupplier.supply(x,y,z), state, biome);
}
}
}
}
if (nonAir != 0) {
{//Generate mipping
//Mip L1
int i = 0;
for (int y = 0; y < 4; y += 2) {
for (int z = 0; z < 4; z += 2) {
for (int x = 0; x < 4; x += 2) {
current[4 * 4 * 4 + i++] = Mipper.mip(
current[I(x, y, z)], current[I(x+1, y, z)], current[I(x, y, z+1)], current[I(x+1, y, z+1)],
current[I(x, y+1, z)], current[I(x+1, y+1, z)], current[I(x, y+1, z+1)], current[I(x+1, y+1, z+1)],
stateMapper);
}
}
}
//Mip L2
section[I(ox, oy, oz)] = Mipper.mip(
current[J(0,0,0)], current[J(1,0,0)], current[J(0,0,1)], current[J(1,0,1)],
current[J(0,1,0)], current[J(1,1,0)], current[J(0,1,1)], current[J(1,1,1)],
stateMapper);
}
//Update existence mask
msk |= 1L<<I(ox, oy, oz);
subSections[I(ox, oy, oz)] = current;
current = new long[4*4*4+2*2*2+1];
}
}
}
}
{//Generate mipping
//Mip L3
int i = 0;
for (int y = 0; y < 4; y+=2) {
for (int z = 0; z < 4; z += 2) {
for (int x = 0; x < 4; x += 2) {
section[4 * 4 * 4 + i++] = Mipper.mip(section[I(x, y, z)], section[I(x+1, y, z)], section[I(x, y, z+1)], section[I(x+1, y, z+1)],
section[I(x, y+1, z)], section[I(x+1, y+1, z)], section[I(x, y+1, z+1)], section[I(x+1, y+1, z+1)],
stateMapper);
}
}
}
//Mip L4
section[4*4*4+2*2*2] = Mipper.mip(section[J(0, 0, 0)], section[J(1, 0, 0)], section[J(0, 0, 1)], section[J(1, 0, 1)],
section[J(0, 1, 0)], section[J(1, 1, 0)], section[J(0, 1, 1)], section[J(1, 1, 1)],
stateMapper);
}
return new VoxelizedSection(section, msk, subSections, sx, sy, sz);
}
}

View File

@@ -0,0 +1,122 @@
package me.cortex.voxelmon.core.world;
import it.unimi.dsi.fastutil.ints.Int2ShortOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.longs.Long2ShortOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.shorts.ShortArrayList;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.zstd.Zstd;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Map;
import java.util.Random;
import static org.lwjgl.util.zstd.Zstd.*;
public class SaveLoadSystem {
public static byte[] serialize(WorldSection section) {
var data = section.copyData();
var compressed = new Short[data.length];
Long2ShortOpenHashMap LUT = new Long2ShortOpenHashMap();
LongArrayList LUTVAL = new LongArrayList();
for (int i = 0; i < data.length; i++) {
long block = data[i];
short mapping = LUT.computeIfAbsent(block, id->{
LUTVAL.add(id);
return (short)(LUTVAL.size()-1);
});
compressed[i] = mapping;
}
long[] lut = LUTVAL.toLongArray();
ByteBuffer raw = MemoryUtil.memAlloc(compressed.length*2+lut.length*8+512);
long hash = section.getKey()^(lut.length*1293481298141L);
raw.putLong(section.getKey());
raw.putInt(lut.length);
for (long id : lut) {
raw.putLong(id);
hash *= 1230987149811L;
hash += 12831;
hash ^= id;
}
for (int i = 0; i < compressed.length; i++) {
short block = compressed[i];
raw.putShort(block);
hash *= 1230987149811L;
hash += 12831;
hash ^= (block*1827631L) ^ data[i];
}
raw.putLong(hash);
raw.limit(raw.position());
raw.rewind();
ByteBuffer compressedData = MemoryUtil.memAlloc((int)ZSTD_COMPRESSBOUND(raw.remaining()));
long compressedSize = ZSTD_compress(compressedData, raw, 19);
byte[] out = new byte[(int) compressedSize];
compressedData.limit((int) compressedSize);
compressedData.get(out);
MemoryUtil.memFree(raw);
MemoryUtil.memFree(compressedData);
//Compress into a key + data pallet format
return out;
}
public static WorldSection deserialize(WorldEngine world, int lvl, int x, int y, int z, byte[] data) {
var buff = MemoryUtil.memAlloc(data.length);
buff.put(data);
buff.rewind();
var decompressed = MemoryUtil.memAlloc(32*32*32*4*2);
long size = ZSTD_decompress(decompressed, buff);
MemoryUtil.memFree(buff);
decompressed.limit((int) size);
long hash = 0;
long key = decompressed.getLong();
int lutLen = decompressed.getInt();
long[] lut = new long[lutLen];
hash = key^(lut.length*1293481298141L);
for (int i = 0; i < lutLen; i++) {
lut[i] = decompressed.getLong();
hash *= 1230987149811L;
hash += 12831;
hash ^= lut[i];
}
var section = new WorldSection(lvl, x, y, z, world);
section.definitelyEmpty = false;
if (section.getKey() != key) {
throw new IllegalStateException("Decompressed section not the same as requested. got: " + key + " expected: " + section.getKey());
}
for (int i = 0; i < section.data.length; i++) {
short lutId = decompressed.getShort();
section.data[i] = lut[lutId];
hash *= 1230987149811L;
hash += 12831;
hash ^= (lutId*1827631L) ^ section.data[i];
}
long expectedHash = decompressed.getLong();
if (expectedHash != hash) {
//throw new IllegalStateException("Hash mismatch got: " + hash + " expected: " + expectedHash);
System.err.println("Hash mismatch got: " + hash + " expected: " + expectedHash + " removing region");
return null;
}
if (decompressed.hasRemaining()) {
//throw new IllegalStateException("Decompressed section had excess data");
System.err.println("Decompressed section had excess data removing region");
return null;
}
MemoryUtil.memFree(decompressed);
return section;
}
}

View File

@@ -0,0 +1,236 @@
package me.cortex.voxelmon.core.world;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import me.cortex.voxelmon.core.rendering.RenderTracker;
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
import me.cortex.voxelmon.core.world.other.Mapper;
import me.cortex.voxelmon.core.world.service.SectionSavingService;
import me.cortex.voxelmon.core.world.service.VoxelIngestService;
import me.cortex.voxelmon.core.world.storage.StorageBackend;
import java.io.File;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicReference;
//Use an LMDB backend to store the world, use a local inmemory cache for lod sections
// automatically manages and invalidates sections of the world as needed
public class WorldEngine {
private static final int ACTIVE_CACHE_SIZE = 10;
public final StorageBackend storage;
private final Mapper mapper;
public final VoxelIngestService ingestService = new VoxelIngestService(this);
public final SectionSavingService savingService;
private RenderTracker renderTracker;
public void setRenderTracker(RenderTracker tracker) {
this.renderTracker = tracker;
}
public Mapper getMapper() {return this.mapper;}
private final int maxMipLevels;
//Loaded section world cache
private final Long2ObjectOpenHashMap<WorldSection>[] loadedSectionCache;
//TODO: also segment this up into an array
private final Long2ObjectOpenHashMap<AtomicReference<WorldSection>> sectionLoadingLocks = new Long2ObjectOpenHashMap<>();
//What this is used for is to keep N sections acquired per layer, this stops sections from constantly being
// loaded and unloaded when accessed close together
private final ConcurrentLinkedDeque<WorldSection>[] activeSectionCache;
public WorldEngine(File storagePath, int savingServiceWorkers, int maxMipLayers) {
this.maxMipLevels = maxMipLayers;
this.loadedSectionCache = new Long2ObjectOpenHashMap[maxMipLayers];
this.activeSectionCache = new ConcurrentLinkedDeque[maxMipLayers];
for (int i = 0; i < maxMipLayers; i++) {
this.loadedSectionCache[i] = new Long2ObjectOpenHashMap<>(1<<(16-i));
this.activeSectionCache[i] = new ConcurrentLinkedDeque<>();
}
this.storage = new StorageBackend(storagePath);
this.mapper = new Mapper(this.storage);
this.savingService = new SectionSavingService(this, savingServiceWorkers);
}
//TODO: Fixme/optimize, cause as the lvl gets higher, the size of x,y,z gets smaller so i can dynamically compact the format
// depending on the lvl, which should optimize colisions and whatnot
public static long getWorldSectionId(int lvl, int x, int y, int z) {
return ((long)lvl<<60)|((long)(y&0xFF)<<52)|((long)(z&((1<<24)-1))<<28)|((long)(x&((1<<24)-1))<<4);//NOTE: 4 bits spare for whatever
}
public static int getLvl(long packed) {
return (int) (packed>>>60);
}
public static int getX(long packed) {
return (int) ((packed<<12)>>40);
}
public static int getY(long packed) {
return (int) ((packed<<4)>>56);
}
public static int getZ(long packed) {
return (int) ((packed<<4)>>40);
}
//Try to unload the section from the world atomically, this is called from the saving service, or any release call which results in the refcount being 0
public void tryUnload(WorldSection section) {
synchronized (this.loadedSectionCache[section.lvl]) {
if (section.getRefCount() != 0) {
return;
}
//TODO: make a thing where it checks if the section is dirty, if it is, enqueue it for a save first and return
section.setFreed();
var removedSection = this.loadedSectionCache[section.lvl].remove(section.getKey());
if (removedSection != section) {
throw new IllegalStateException("Removed section not the same as attempted to remove");
}
if (section.isAcquired()) {
throw new IllegalStateException("Section that was just removed got reacquired");
}
}
}
//Internal helper method for getOrLoad to segment up code
private WorldSection unsafeLoadSection(long key, int lvl, int x, int y, int z) {
var data = this.storage.getSectionData(key);
if (data == null) {
return new WorldSection(lvl, x, y, z, this);
} else {
var ret = SaveLoadSystem.deserialize(this, lvl, x, y, z, data);
if (ret != null) {
return ret;
} else {
this.storage.deleteSectionData(key);
return new WorldSection(lvl, x, y, z, this);
}
}
}
//Gets a loaded section or loads the section from storage
public WorldSection getOrLoadAcquire(int lvl, int x, int y, int z) {
long key = getWorldSectionId(lvl, x, y, z);
AtomicReference<WorldSection> lock = null;
AtomicReference<WorldSection> gotLock = null;
synchronized (this.loadedSectionCache[lvl]) {
var result = this.loadedSectionCache[lvl].get(key);
if (result != null) {
result.acquire();
return result;
}
lock = new AtomicReference<>(null);
synchronized (this.sectionLoadingLocks) {
var finalLock = lock;
gotLock = this.sectionLoadingLocks.computeIfAbsent(key, a -> finalLock);
}
}
//We acquired the lock so load it
if (gotLock == lock) {
WorldSection loadedSection = this.unsafeLoadSection(key, lvl, x, y, z);
loadedSection.acquire();
//Insert the loaded section and set the loading lock to the loaded value
synchronized (this.loadedSectionCache[lvl]) {
this.loadedSectionCache[lvl].put(key, loadedSection);
synchronized (this.sectionLoadingLocks) {
this.sectionLoadingLocks.remove(key);
lock.set(loadedSection);
}
}
//Add to the active acquired cache and remove the last item if the size is over the limit
{
loadedSection.acquire();
this.activeSectionCache[lvl].add(loadedSection);
if (this.activeSectionCache[lvl].size() > ACTIVE_CACHE_SIZE) {
var last = this.activeSectionCache[lvl].pop();
last.release();
}
}
return loadedSection;
} else {
lock = gotLock;
//Another thread got the lock so spin wait for the section to load
while (lock.get() == null) {
Thread.onSpinWait();
}
var section = lock.get();
//Fixme: try find a better solution for this
//The issue with this is that the section could be unloaded when we acquire it cause of so many threading pain
// so lock the section cache, try acquire the section, if we fail we must load the section again
synchronized (this.loadedSectionCache[lvl]) {
if (section.tryAcquire()) {
//We acquired the section successfully, return it
return section;
}
}
//We failed to acquire the section, we must reload it
return this.getOrLoadAcquire(lvl, x, y, z);
}
}
//Marks a section as dirty, enqueuing it for saving and or render data rebuilding
private void markDirty(WorldSection section) {
this.renderTracker.sectionUpdated(section);
//TODO: add an option for having synced saving, that is when call enqueueSave, that will instead, instantly
// save to the db, this can be useful for just reducing the amount of thread pools in total
// might have some issues with threading if the same section is saved from multiple threads?
this.savingService.enqueueSave(section);
}
//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)
public void insertUpdate(VoxelizedSection section) {
//The >>1 is cause the world sections size is 32x32x32 vs the 16x16x16 of the voxelized section
for (int lvl = 0; lvl < this.maxMipLevels; lvl++) {
var worldSection = this.getOrLoadAcquire(lvl, section.x>>(lvl+1), section.y>>(lvl+1), section.z>>(lvl+1));
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);
boolean didChange = false;
for (int y = by; y < (16>>lvl)+by; y++) {
for (int z = bz; z < (16>>lvl)+bz; z++) {
for (int x = bx; x < (16>>lvl)+bx; x++) {
long newId = section.get(lvl, x-bx, y-by, z-bz);
long oldId = worldSection.set(x, y, z, newId);
didChange |= newId != oldId;
}
}
}
//Need to release the section after using it
if (didChange) {
//Mark the section as dirty (enqueuing saving and geometry rebuild) and move to parent mip level
this.markDirty(worldSection);
worldSection.release();
} else {
//If nothing changed just need to release, dont need to update parent mips
worldSection.release();
break;
}
}
}
public int[] getLoadedSectionCacheSizes() {
var res = new int[this.maxMipLevels];
for (int i = 0; i < this.maxMipLevels; i++) {
res[i] = this.loadedSectionCache[i].size();
}
return res;
}
public void shutdown() {
try {this.storage.flush();} catch (Exception e) {System.err.println(e);}
//Shutdown in this order to preserve as much data as possible
try {this.ingestService.shutdown();} catch (Exception e) {System.err.println(e);}
try {this.savingService.shutdown();} catch (Exception e) {System.err.println(e);}
try {this.storage.close();} catch (Exception e) {System.err.println(e);}
}
}

View File

@@ -0,0 +1,125 @@
package me.cortex.voxelmon.core.world;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
//Represents a loaded world section at a specific detail level
// holds a 32x32x32 region of detail
public final class WorldSection {
public final int lvl;
public final int x;
public final int y;
public final int z;
////Maps from a local id to global meaning it should be much cheaper to store in memory probably
//private final int[] dataMapping = null;
//private final short[] data = new short[32*32*32];
final long[] data = new long[32*32*32];
boolean definitelyEmpty = true;
private final WorldEngine world;
public WorldSection(int lvl, int x, int y, int z, WorldEngine worldIn) {
this.lvl = lvl;
this.x = x;
this.y = y;
this.z = z;
this.world = worldIn;
}
@Override
public int hashCode() {
return ((x*1235641+y)*8127451+z)*918267913+lvl;
}
public final AtomicBoolean inSaveQueue = new AtomicBoolean();
private final AtomicInteger usageCounts = new AtomicInteger();
public int acquire() {
this.assertNotFree();
return this.usageCounts.getAndAdd(1);
}
//TODO: Fixme i dont think this is fully thread safe/correct
public boolean tryAcquire() {
if (this.freed) {
return false;
}
this.usageCounts.getAndAdd(1);
if (this.freed) {
return false;
}
return true;
}
public int release() {
this.assertNotFree();
int i = this.usageCounts.addAndGet(-1);
if (i < 0) {
throw new IllegalStateException();
}
//NOTE: cant actually check for not free as at this stage it technically could be unloaded, as soon
//this.assertNotFree();
//Try to unload the section if its empty
if (i == 0) {
this.world.tryUnload(this);
}
return i;
}
private volatile boolean freed = false;
void setFreed() {
this.assertNotFree();
this.freed = true;
}
public void assertNotFree() {
if (this.freed) {
throw new IllegalStateException();
}
}
public boolean isAcquired() {
return this.usageCounts.get() != 0;
}
public int getRefCount() {
return this.usageCounts.get();
}
public long getKey() {
return WorldEngine.getWorldSectionId(this.lvl, this.x, this.y, this.z);
}
public static int getIndex(int x, int y, int z) {
int M = (1<<5)-1;
if (x<0||x>M||y<0||y>M||z<0||z>M) {
throw new IllegalArgumentException("Out of bounds: " + x + ", " + y + ", " + z);
}
return ((y&M)<<10)|((z&M)<<5)|(x&M);
}
public long set(int x, int y, int z, long id) {
int idx = getIndex(x,y,z);
long old = this.data[idx];
this.data[idx] = id;
return old;
}
//Generates a copy of the data array, this is to help with atomic operations like rendering
public long[] copyData() {
return Arrays.copyOf(this.data, this.data.length);
}
public boolean definitelyEmpty() {
return this.definitelyEmpty;
}
}
//TODO: for serialization, make a huffman encoding tree on the integers since that should be very very efficent for compression

View File

@@ -0,0 +1,4 @@
package me.cortex.voxelmon.core.world.other;
public record BiomeColour(int id, int foliageColour, int waterColour) {
}

View File

@@ -0,0 +1,4 @@
package me.cortex.voxelmon.core.world.other;
public record BlockStateColour(int id, int biomeTintMsk, int[] faceColours) {
}

View File

@@ -0,0 +1,138 @@
package me.cortex.voxelmon.core.world.other;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.model.BakedQuad;
import net.minecraft.client.texture.NativeImage;
import net.minecraft.client.texture.Sprite;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.random.LocalRandom;
import net.minecraft.world.biome.BiomeKeys;
import java.util.List;
public class ColourResolver {
//TODO: sample from multiple random values and avg it
public static int[] resolveColour(BlockState state) {
return resolveColour(state, 1234567890L);
}
//The way this works is it takes the and computes its colour, it then computes the area of the quad and the normal direction
// it adds each area and colour to a per direcition colour
// for non specific axis dimensions it takes the normal of each quad computes the dot between it and each of the directions
// and averages that
// if the colour doesnt exist for a specific axis set it to the average of the other axis and or make it translucent
//TODO: fixme: finish
public static int[] resolveColour(BlockState state, long randomValue) {
if (state == Blocks.AIR.getDefaultState()) {
return new int[6];
}
int[][] builder = new int[6][5];
var random = new LocalRandom(randomValue);
if (state.getFluidState().isEmpty()) {
for (Direction direction : new Direction[]{Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST, null}) {
var quads = MinecraftClient.getInstance()
.getBakedModelManager()
.getBlockModels()
.getModel(state)
.getQuads(state, direction, random);
for (var quad : quads) {
long weightColour = resolveQuadColour(quad);
int colour = (int) weightColour;
int weight = (int) (weightColour>>32);
if (direction == null) {
//TODO: apply normal multiplication to weight
for (int i = 0; i < 6; i++) {
builder[i][0] += weight;
builder[i][4] += weight * ((colour>>>24)&0xFF);
builder[i][3] += weight * ((colour>>>16)&0xFF);
builder[i][2] += weight * ((colour>>>8)&0xFF);
builder[i][1] += weight * ((colour>>>0)&0xFF);
}
} else {
builder[direction.getId()][0] += weight;
builder[direction.getId()][4] += weight*((colour>>>24)&0xFF);
builder[direction.getId()][3] += weight*((colour>>>16)&0xFF);
builder[direction.getId()][2] += weight*((colour>>>8)&0xFF);
builder[direction.getId()][1] += weight*((colour>>>0)&0xFF);
}
}
}
} else {
//TODO FIXME: need to account for both the fluid and block state at the same time
//FIXME: make it not hacky and use the fluid handler thing from fabric
long weightColour = resolveNI(MinecraftClient.getInstance().getBakedModelManager().getBlockModels().getModelParticleSprite(state).getContents().image);
for (int i = 0; i < 6; i++) {
builder[i][0] = 1;
builder[i][1] += (weightColour>>0)&0xFF;
builder[i][2] += (weightColour>>8)&0xFF;
builder[i][3] += (weightColour>>16)&0xFF;
builder[i][4] += (weightColour>>24)&0xFF;
}
}
int[] out = new int[6];
for (int i = 0; i < 6; i++) {
int c = builder[i][0];
if (c == 0) {
continue;
}
int r = builder[i][4]/c;
int g = builder[i][3]/c;
int b = builder[i][2]/c;
int a = builder[i][1]/c;
out[i] = (r<<24)|(g<<16)|(b<<8)|a;
}
return out;
}
private static long resolveQuadColour(BakedQuad quad) {
return resolveNI(quad.getSprite().getContents().image);
}
private static long resolveNI(NativeImage image) {
int r = 0;
int g = 0;
int b = 0;
int a = 0;
int count = 0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int colour = image.getColor(x, y);
if (((colour >>> 24)&0xFF) == 0) {
continue;
}
r += (colour >>> 0) & 0xFF;
g += (colour >>> 8) & 0xFF;
b += (colour >>> 16) & 0xFF;
a += (colour >>> 24) & 0xFF;
count++;
}
}
if (count == 0) {
return 0;
}
r /= count;
g /= count;
b /= count;
a /= count;
int colour = (r<<24)|(g<<16)|(b<<8)|a;
return Integer.toUnsignedLong(colour)|(((long)count)<<32);
}
public static long resolveBiomeColour(String biomeId) {
var biome = MinecraftClient.getInstance().world.getRegistryManager().get(RegistryKeys.BIOME).get(new Identifier(biomeId));
int ARGBFoliage = biome.getFoliageColor();
int ARGBWater = biome.getWaterColor();
return Integer.toUnsignedLong(((ARGBFoliage&0xFFFFFF)<<8)|(ARGBFoliage>>>24)) | (Integer.toUnsignedLong(((ARGBWater&0xFFFFFF)<<8)|(ARGBWater>>>24))<<32);
}
}

View File

@@ -0,0 +1,237 @@
package me.cortex.voxelmon.core.world.other;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.cortex.voxelmon.core.util.MemoryBuffer;
import me.cortex.voxelmon.core.world.storage.StorageBackend;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.world.biome.Biome;
import org.lwjgl.system.MemoryUtil;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
//There are independent mappings for biome and block states, these get combined in the shader and allow for more
// variaty of things
public class Mapper {
private static final int BLOCK_STATE_TYPE = 1;
private static final int BIOME_TYPE = 2;
private final StorageBackend storage;
public static final int UNKNOWN_MAPPING = -1;
public static final int AIR = 0;
private final Object2ObjectOpenHashMap<BlockState, StateEntry> block2stateEntry = new Object2ObjectOpenHashMap<>();
private final ObjectArrayList<StateEntry> blockId2stateEntry = new ObjectArrayList<>();
private final Object2ObjectOpenHashMap<String, BiomeEntry> biome2biomeEntry = new Object2ObjectOpenHashMap<>();
private final ObjectArrayList<BiomeEntry> biomeId2biomeEntry = new ObjectArrayList<>();
public Mapper(StorageBackend storage) {
this.storage = storage;
//Insert air since its a special entry (index 0)
var airEntry = new StateEntry(0, Blocks.AIR.getDefaultState());
block2stateEntry.put(airEntry.state, airEntry);
blockId2stateEntry.add(airEntry);
this.loadFromStorage();
}
private void loadFromStorage() {
var mappings = this.storage.getIdMappings();
List<StateEntry> sentries = new ArrayList<>();
List<BiomeEntry> bentries = new ArrayList<>();
for (var entry : mappings.int2ObjectEntrySet()) {
int entryType = entry.getIntKey()>>>30;
int id = entry.getIntKey() & ((1<<30)-1);
if (entryType == BLOCK_STATE_TYPE) {
var sentry = StateEntry.deserialize(id, entry.getValue());
sentries.add(sentry);
if (this.block2stateEntry.put(sentry.state, sentry) != null) {
throw new IllegalStateException("Multiple mappings for blockstate");
}
} else if (entryType == BIOME_TYPE) {
var bentry = BiomeEntry.deserialize(id, entry.getValue());
bentries.add(bentry);
if (this.biome2biomeEntry.put(bentry.biome, bentry) != null) {
throw new IllegalStateException("Multiple mappings for biome entry");
}
} else {
throw new IllegalStateException("Unknown entryType");
}
}
//Insert into the arrays
sentries.stream().sorted(Comparator.comparing(a->a.id)).forEach(entry -> {
if (this.blockId2stateEntry.size() != entry.id) {
throw new IllegalStateException("Block entry not ordered");
}
this.blockId2stateEntry.add(entry);
});
bentries.stream().sorted(Comparator.comparing(a->a.id)).forEach(entry -> {
if (this.biomeId2biomeEntry.size() != entry.id) {
throw new IllegalStateException("Biome entry not ordered");
}
this.biomeId2biomeEntry.add(entry);
});
}
private StateEntry registerNewBlockState(BlockState state) {
StateEntry entry = new StateEntry(this.blockId2stateEntry.size(), state);
this.block2stateEntry.put(state, entry);
this.blockId2stateEntry.add(entry);
byte[] serialized = entry.serialize();
ByteBuffer buffer = MemoryUtil.memAlloc(serialized.length);
buffer.put(serialized);
buffer.rewind();
this.storage.putIdMapping(entry.id | (BLOCK_STATE_TYPE<<30), buffer);
MemoryUtil.memFree(buffer);
return entry;
}
private BiomeEntry registerNewBiome(String biome) {
BiomeEntry entry = new BiomeEntry(this.biome2biomeEntry.size(), biome);
this.biome2biomeEntry.put(biome, entry);
this.biomeId2biomeEntry.add(entry);
byte[] serialized = entry.serialize();
ByteBuffer buffer = MemoryUtil.memAlloc(serialized.length);
buffer.put(serialized);
buffer.rewind();
this.storage.putIdMapping(entry.id | (BIOME_TYPE<<30), buffer);
MemoryUtil.memFree(buffer);
return entry;
}
//TODO:FIXME: IS VERY SLOW NEED TO MAKE IT LOCK FREE
public long getBaseId(byte light, BlockState state, RegistryEntry<Biome> biome) {
StateEntry sentry = null;
BiomeEntry bentry = null;
synchronized (this.block2stateEntry) {
sentry = this.block2stateEntry.get(state);
if (sentry == null) {
sentry = this.registerNewBlockState(state);
}
}
synchronized (this.biome2biomeEntry) {
String biomeId = biome.getKey().get().getValue().toString();
bentry = this.biome2biomeEntry.get(biomeId);
if (bentry == null) {
bentry = this.registerNewBiome(biomeId);
}
}
return (Byte.toUnsignedLong(light)<<56)|(Integer.toUnsignedLong(bentry.id) << 47)|(Integer.toUnsignedLong(sentry.id)<<27);
}
public BlockState[] getBlockStates() {
synchronized (this.block2stateEntry) {
BlockState[] out = new BlockState[this.blockId2stateEntry.size()];
int i = 0;
for (var entry : this.blockId2stateEntry) {
if (entry.id != i++) {
throw new IllegalStateException();
}
out[i-1] = entry.state;
}
return out;
}
}
public String[] getBiomes() {
synchronized (this.biome2biomeEntry) {
String[] out = new String[this.biome2biomeEntry.size()];
int i = 0;
for (var entry : this.biomeId2biomeEntry) {
if (entry.id != i++) {
throw new IllegalStateException();
}
out[i-1] = entry.biome;
}
return out;
}
}
public static final class StateEntry {
private final int id;
private final BlockState state;
public StateEntry(int id, BlockState state) {
this.id = id;
this.state = state;
}
public byte[] serialize() {
try {
var serialized = new NbtCompound();
serialized.putInt("id", this.id);
serialized.put("block_state", BlockState.CODEC.encodeStart(NbtOps.INSTANCE, this.state).result().get());
var out = new ByteArrayOutputStream();
NbtIo.writeCompressed(serialized, out);
return out.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static StateEntry deserialize(int id, byte[] data) {
try {
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data));
if (compound.getInt("id") != id) {
throw new IllegalStateException("Encoded id != expected id");
}
BlockState state = BlockState.CODEC.parse(NbtOps.INSTANCE, compound.getCompound("block_state")).get().orThrow();
return new StateEntry(id, state);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static final class BiomeEntry {
private final int id;
private final String biome;
public BiomeEntry(int id, String biome) {
this.id = id;
this.biome = biome;
}
public byte[] serialize() {
try {
var serialized = new NbtCompound();
serialized.putInt("id", this.id);
serialized.putString("biome_id", this.biome);
var out = new ByteArrayOutputStream();
NbtIo.writeCompressed(serialized, out);
return out.toByteArray();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static BiomeEntry deserialize(int id, byte[] data) {
try {
var compound = NbtIo.readCompressed(new ByteArrayInputStream(data));
if (compound.getInt("id") != id) {
throw new IllegalStateException("Encoded id != expected id");
}
String biome = compound.getString("biome_id");
return new BiomeEntry(id, biome);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,39 @@
package me.cortex.voxelmon.core.world.other;
//Mipper for data
public class Mipper {
//TODO: also pass in the level its mipping from, cause at lower levels you want to preserve block details
// but at higher details you want more air
public static long mip(long I000, long I100, long I001, long I101,
long I010, long I110, long I011, long I111,
Mapper mapper) {
//TODO: mip with respect to all the variables, what that means is take whatever has the highest count and return that
//TODO: also average out the light level and set that as the new light level
//For now just take the most top corner
if (I111 != 0) {
return I111;
}
if (I110 != 0) {
return I110;
}
if (I011 != 0) {
return I011;
}
if (I010 != 0) {
return I010;
}
if (I101 != 0) {
return I101;
}
if (I100 != 0) {
return I100;
}
if (I001 != 0) {
return I001;
}
if (I000 != 0) {
return I000;
}
return 0;
}
}

View File

@@ -0,0 +1,99 @@
package me.cortex.voxelmon.core.world.service;
import me.cortex.voxelmon.core.world.SaveLoadSystem;
import me.cortex.voxelmon.core.world.WorldEngine;
import me.cortex.voxelmon.core.world.WorldSection;
import net.minecraft.world.chunk.WorldChunk;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Semaphore;
//TODO: add an option for having synced saving, that is when call enqueueSave, that will instead, instantly
// save to the db, this can be useful for just reducing the amount of thread pools in total
// might have some issues with threading if the same section is saved from multiple threads?
public class SectionSavingService {
private volatile boolean running = true;
private final Thread[] workers;
private final ConcurrentLinkedDeque<WorldSection> saveQueue = new ConcurrentLinkedDeque<>();
private final Semaphore saveCounter = new Semaphore(0);
private final WorldEngine world;
public SectionSavingService(WorldEngine worldEngine, int workers) {
this.workers = new Thread[workers];
for (int i = 0; i < workers; i++) {
var worker = new Thread(this::saveWorker);
worker.setDaemon(false);
worker.setName("Saving service #" + i);
worker.start();
this.workers[i] = worker;
}
this.world = worldEngine;
}
private void saveWorker() {
while (running) {
this.saveCounter.acquireUninterruptibly();
if (!this.running) break;
var section = this.saveQueue.pop();
section.assertNotFree();
section.inSaveQueue.set(false);
//TODO: stop converting between all these types and just use a native buffer all the time
var saveData = SaveLoadSystem.serialize(section);
//Note: this is done like this because else the gc can collect the buffer before the transaction is completed
// thus the transaction reads from undefined memory
ByteBuffer buffer = MemoryUtil.memAlloc(saveData.length);
buffer.put(saveData).rewind();
this.world.storage.setSectionData(section.getKey(), buffer);
MemoryUtil.memFree(buffer);
section.release();
}
}
public void enqueueSave(WorldSection section) {
//If its not enqueued for saving then enqueue it
if (!section.inSaveQueue.getAndSet(true)) {
//Acquire the section for use
section.acquire();
this.saveQueue.add(section);
this.saveCounter.release();
}
}
public void shutdown() {
boolean anyAlive = false;
for (var worker : this.workers) {
anyAlive |= worker.isAlive();
}
if (!anyAlive) {
System.err.println("Section saving workers already dead on shutdown! this is very very bad, check log for errors from this thread");
return;
}
//Wait for all the saving to finish
while (this.saveCounter.availablePermits() != 0) {
Thread.onSpinWait();
}
//Shutdown
this.running = false;
this.saveCounter.release(1000);
//Wait for threads to join
try {
for (var worker : this.workers) {
worker.join();
}
} catch (InterruptedException e) {throw new RuntimeException(e);}
}
public int getTaskCount() {
return this.saveCounter.availablePermits();
}
}

View File

@@ -0,0 +1,80 @@
package me.cortex.voxelmon.core.world.service;
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
import me.cortex.voxelmon.core.voxelization.WorldConversionFactory;
import me.cortex.voxelmon.core.world.WorldEngine;
import net.minecraft.world.chunk.WorldChunk;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.Semaphore;
public class VoxelIngestService {
private volatile boolean running = true;
private final Thread worker;
private final ConcurrentLinkedDeque<WorldChunk> ingestQueue = new ConcurrentLinkedDeque<>();
private final Semaphore ingestCounter = new Semaphore(0);
private final WorldEngine world;
public VoxelIngestService(WorldEngine world) {
this.worker = new Thread(this::ingestWorker);
this.worker.setDaemon(false);
this.worker.setName("Ingest service");
this.worker.start();
this.world = world;
}
private void ingestWorker() {
while (this.running) {
this.ingestCounter.acquireUninterruptibly();
if (!this.running) break;
var chunk = this.ingestQueue.pop();
int i = chunk.getBottomSectionCoord() - 1;
for (var section : chunk.getSectionArray()) {
i++;
if (section.isEmpty()) {
this.world.insertUpdate(VoxelizedSection.createEmpty(chunk.getPos().x, i, chunk.getPos().z));
} else {
VoxelizedSection csec = WorldConversionFactory.convert(
this.world.getMapper(),
section.getBlockStateContainer(),
section.getBiomeContainer(),
(x, y, z) -> (byte) 0,
chunk.getPos().x,
i,
chunk.getPos().z
);
this.world.insertUpdate(csec);
}
}
}
}
public void enqueueIngest(WorldChunk chunk) {
this.ingestQueue.add(chunk);
this.ingestCounter.release();
}
public int getTaskCount() {
return this.ingestCounter.availablePermits();
}
public void shutdown() {
if (!this.worker.isAlive()) {
System.err.println("Ingest worker already dead on shutdown! this is very very bad, check log for errors from this thread");
return;
}
//Wait for the ingest to finish
while (this.ingestCounter.availablePermits() != 0) {
Thread.onSpinWait();
}
//Shutdown
this.running = false;
this.ingestCounter.release(1000);
//Wait for thread to join
try {this.worker.join();} catch (InterruptedException e) {throw new RuntimeException(e);}
}
}

View File

@@ -0,0 +1,26 @@
package me.cortex.voxelmon.core.world.storage;
import org.lwjgl.util.lmdb.MDBVal;
import static me.cortex.voxelmon.core.world.storage.LMDBInterface.E;
import static org.lwjgl.util.lmdb.LMDB.*;
public class Cursor implements AutoCloseable {
private final long cursor;
public Cursor(long cursor) {
this.cursor = cursor;
}
public int get(int op, MDBVal key, MDBVal data) {
int e = mdb_cursor_get(this.cursor, key, data, op);
if (e != MDB_SUCCESS && e != MDB_NOTFOUND) {
E(e);
}
return e;
}
@Override
public void close() {
mdb_cursor_close(this.cursor);
}
}

View File

@@ -0,0 +1,139 @@
package me.cortex.voxelmon.core.world.storage;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.util.lmdb.LMDB;
import org.lwjgl.util.lmdb.MDBEnvInfo;
import java.nio.IntBuffer;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.util.lmdb.LMDB.*;
public class LMDBInterface {
private final long env;
private LMDBInterface(long env) {
this.env = env;
}
public static class Builder {
private final long env;
public Builder() {
//Create the environment
try (var stack = stackPush()) {
PointerBuffer pp = stack.mallocPointer(1);
E(mdb_env_create(pp));
this.env = pp.get(0);
}
}
public Builder setMaxDbs(int maxDbs) {
E(mdb_env_set_maxdbs(this.env, maxDbs));
return this;
}
public Builder open(String directory, int flags) {
E(mdb_env_open(this.env, directory, flags, 0664));
return this;
}
public LMDBInterface fetch() {
return new LMDBInterface(this.env);
}
}
public void close() {
mdb_env_close(env);
}
public static void E(int rc) {
if (rc != MDB_SUCCESS) {
throw new IllegalStateException("Code: " + rc + " msg: " + mdb_strerror(rc));
}
}
public void setMapSize(long size) {
E(mdb_env_set_mapsize(this.env, size));
}
public <T> T transaction(TransactionCallback<T> transaction) {
return transaction(0, transaction);
}
public <T> T transaction(int flags, TransactionCallback<T> transaction) {
return transaction(0, flags, transaction);
}
public <T> T transaction(long parent, int flags, TransactionCallback<T> transaction) {
T ret;
try (var stack = stackPush()) {
PointerBuffer pp = stack.mallocPointer(1);
E(mdb_txn_begin(this.env, parent, flags, pp));
long txn = pp.get(0);
int err;
try {
ret = transaction.exec(stack, txn);
err = mdb_txn_commit(txn);
} catch (Throwable t) {
mdb_txn_abort(txn);
throw t;
}
E(err);
}
return ret;
}
public Database createDb(String name) {
return this.createDb(name, MDB_CREATE|MDB_INTEGERKEY);
}
public Database createDb(String name, int flags) {
return new Database(name, flags);
}
public LMDBInterface flush(boolean force) {
E(mdb_env_sync(this.env, force));
return this;
}
public long getMapSize() {
try (MemoryStack stack = MemoryStack.stackPush()) {
MDBEnvInfo info = MDBEnvInfo.calloc(stack);
E(mdb_env_info(this.env, info));
return info.me_mapsize();
}
}
public class Database {
private final int dbi;
public Database(String name, int flags) {
this.dbi = LMDBInterface.this.transaction((stack, txn)-> {
IntBuffer ip = stack.mallocInt(1);
E(mdb_dbi_open(txn, name, flags, ip));
return ip.get(0);
});
}
public void close() {
mdb_dbi_close(LMDBInterface.this.env, this.dbi);
}
//TODO: make a MDB_RDONLY varient
public <T> T transaction(TransactionWrappedCallback<T> callback) {
return this.transaction(0, callback);
}
public <T> T transaction(int flags, TransactionWrappedCallback<T> callback) {
return LMDBInterface.this.transaction(flags, (stack, transaction) -> {
return callback.exec(new TransactionWrapper(transaction, stack).set(this));
});
}
public int getDBI() {
return this.dbi;
}
}
}

View File

@@ -0,0 +1,156 @@
package me.cortex.voxelmon.core.world.storage;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.lwjgl.util.lmdb.MDBVal;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import static org.lwjgl.util.lmdb.LMDB.*;
public class StorageBackend {
private static final long GROW_SIZE = 1<<25;//Grow by 33 mb each time
private final AtomicInteger accessingCounts = new AtomicInteger();
private final ReentrantLock resizeLock = new ReentrantLock();
private final LMDBInterface dbi;
private final LMDBInterface.Database sectionDatabase;
private final LMDBInterface.Database idMappingDatabase;
public StorageBackend(File file) {
this.dbi = new LMDBInterface.Builder()
.setMaxDbs(2)
.open(file.getAbsolutePath(), MDB_NOSUBDIR)//MDB_NOLOCK (IF I DO THIS, must sync the db manually)// TODO: THIS
.fetch();
this.dbi.setMapSize(GROW_SIZE);
this.sectionDatabase = this.dbi.createDb("world_sections");
this.idMappingDatabase = this.dbi.createDb("id_mapping");
}
private void growEnv() {
long size = this.dbi.getMapSize() + GROW_SIZE;
System.out.println("Growing DBI env size to: " + size + " bytes");
this.dbi.setMapSize(size);
}
//TODO: try optimize this hellscape of spagetti locking
private <T> T resizingTransaction(Supplier<T> transaction) {
while (true) {
try {
return this.synchronizedTransaction(transaction);
} catch (Throwable e) {
if (e.getMessage().startsWith("Code: -30792")) {
if (this.resizeLock.tryLock()) {
//We must wait until all the other transactions have finished before we can resize
while (this.accessingCounts.get() != 0) {
Thread.onSpinWait();
}
this.growEnv();
this.resizeLock.unlock();
}
} else {
throw e;
}
}
}
}
private <T> T synchronizedTransaction(Supplier<T> transaction) {
try {
this.accessingCounts.getAndAdd(1);
//Check if its locked, if it is locked then need to release the access, wait till resize is finished then
while (this.resizeLock.isLocked()) {
this.accessingCounts.getAndAdd(-1);
while (this.resizeLock.isLocked()) {
Thread.onSpinWait();
}
this.accessingCounts.getAndAdd(1);
}
return transaction.get();
} finally {
this.accessingCounts.getAndAdd(-1);
}
}
//TODO: make batch get and updates
public byte[] getSectionData(long key) {
return this.synchronizedTransaction(() -> this.sectionDatabase.transaction(MDB_RDONLY, transaction->{
var buff = transaction.stack.malloc(8);
buff.putLong(0, key);
var bb = transaction.get(buff);
if (bb == null) {
return null;
}
var res = new byte[bb.remaining()];
bb.get(res);
return res;
}));
}
//TODO: pad data to like some alignemnt so that when the section gets saved or updated
// it can use the same allocation
public void setSectionData(long key, ByteBuffer data) {
this.resizingTransaction(() -> this.sectionDatabase.transaction(transaction->{
var keyBuff = transaction.stack.malloc(8);
keyBuff.putLong(0, key);
transaction.put(keyBuff, data, 0);
return null;
}));
}
public void deleteSectionData(long key) {
this.synchronizedTransaction(() -> this.sectionDatabase.transaction(transaction->{
var keyBuff = transaction.stack.malloc(8);
keyBuff.putLong(0, key);
transaction.del(keyBuff);
return null;
}));
}
public void putIdMapping(int id, ByteBuffer data) {
this.resizingTransaction(()->this.idMappingDatabase.transaction(transaction->{
var keyBuff = transaction.stack.malloc(4);
keyBuff.putInt(0, id);
transaction.put(keyBuff, data, 0);
return null;
}));
}
public Int2ObjectOpenHashMap<byte[]> getIdMappings() {
return this.synchronizedTransaction(() -> {
Int2ObjectOpenHashMap<byte[]> mapping = new Int2ObjectOpenHashMap<>();
this.idMappingDatabase.transaction(MDB_RDONLY, transaction -> {
try (var cursor = transaction.createCursor()) {
var keyPtr = MDBVal.malloc(transaction.stack);
var valPtr = MDBVal.malloc(transaction.stack);
while (cursor.get(MDB_NEXT, keyPtr, valPtr) != MDB_NOTFOUND) {
int keyVal = keyPtr.mv_data().getInt(0);
byte[] data = new byte[(int) valPtr.mv_size()];
Objects.requireNonNull(valPtr.mv_data()).get(data);
mapping.put(keyVal, data);
}
}
return null;
});
return mapping;
});
}
public void flush() {
this.dbi.flush(true);
}
public void close() {
this.sectionDatabase.close();
this.idMappingDatabase.close();
this.dbi.close();
}
}

View File

@@ -0,0 +1,7 @@
package me.cortex.voxelmon.core.world.storage;
import org.lwjgl.system.MemoryStack;
public interface TransactionCallback<T> {
T exec(MemoryStack stack, long transaction);
}

View File

@@ -0,0 +1,5 @@
package me.cortex.voxelmon.core.world.storage;
public interface TransactionWrappedCallback<T> {
T exec(TransactionWrapper wrapper);
}

View File

@@ -0,0 +1,77 @@
package me.cortex.voxelmon.core.world.storage;
import org.lwjgl.PointerBuffer;
import org.lwjgl.system.MemoryStack;
import org.lwjgl.util.lmdb.MDBVal;
import java.nio.ByteBuffer;
import static me.cortex.voxelmon.core.world.storage.LMDBInterface.E;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.util.lmdb.LMDB.*;
public class TransactionWrapper {
public final MemoryStack stack;
private final long transaction;
private int dbi;
public TransactionWrapper(long transaction, MemoryStack stack) {
this.transaction = transaction;
this.stack = stack;
}
public TransactionWrapper set(LMDBInterface.Database db) {
this.dbi = db.getDBI();
return this;
}
public TransactionWrapper put(ByteBuffer key, ByteBuffer val, int flags) {
try (var stack = stackPush()) {
E(mdb_put(this.transaction, this.dbi, MDBVal.malloc(stack).mv_data(key), MDBVal.malloc(stack).mv_data(val), flags));
return this;
}
}
public TransactionWrapper del(ByteBuffer key) {
try (var stack = stackPush()) {
E(mdb_del(this.transaction, this.dbi, MDBVal.malloc(stack).mv_data(key), null));
return this;
}
}
/*
public TransactionWrapper put(long keyPtr, long keyLen, long valPtr, int valLen, int flags) {
//TODO: instead give TransactionWrapper its own scratch buffer that it can use
try (var stack = stackPush()) {
long ptr = stack.nmalloc(4*8);
MemoryUtil.memPutLong(ptr, keyPtr);
MemoryUtil.memPutLong(ptr+8, keyLen);
MemoryUtil.memPutLong(ptr+16, valPtr);
MemoryUtil.memPutLong(ptr+24, valLen);
E(nmdb_put(this.transaction, this.dbi, ptr, ptr + 16, flags));
return this;
}
}*/
public ByteBuffer get(ByteBuffer key) {
//TODO: instead give TransactionWrapper its own scratch buffer that it can use
try (var stack = stackPush()) {
var ret = MDBVal.malloc(stack);
int retVal = mdb_get(this.transaction, this.dbi, MDBVal.calloc(stack).mv_data(key), ret);
if (retVal == MDB_NOTFOUND) {
return null;
} else {
E(retVal);
}
return ret.mv_data();
}
}
public Cursor createCursor() {
try (var stack = stackPush()) {
PointerBuffer pb = stack.mallocPointer(1);
E(mdb_cursor_open(transaction, dbi, pb));
return new Cursor(pb.get(0));
}
}
}

View File

@@ -0,0 +1,65 @@
package me.cortex.voxelmon.importers;
import me.cortex.voxelmon.core.world.WorldEngine;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import org.lwjgl.system.MemoryUtil;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
public class BobbyImporter {
private final WorldEngine world;
private final World mcWorld;
public BobbyImporter(WorldEngine worldEngine, World mcWorld) {
this.world = worldEngine;
this.mcWorld = mcWorld;
}
//TODO: make the importer run in another thread with a progress callback
public void importBobby(Path directory) {
directory.forEach(child->{
var file = child.toFile();
if (!file.isFile()) {
return;
}
var name = file.getName();
var sections = name.split("\\.");
if (sections.length != 4 || (!sections[0].equals("r")) || (!sections[3].equals("mca"))) {
throw new IllegalStateException();
}
int rx = Integer.parseInt(sections[1]);
int rz = Integer.parseInt(sections[2]);
});
}
private void importRegionFile(Path file, int x, int z) throws IOException {
try (var fileStream = FileChannel.open(file, StandardOpenOption.READ)) {
var sectorsSavesBB = MemoryUtil.memAlloc(8192);
if (fileStream.read(sectorsSavesBB) != 8192) {
throw new IllegalStateException("Header of region file invalid");
}
var sectorsSaves = sectorsSavesBB.asIntBuffer();
//Find and load all saved chunks
for (int idx = 0; idx < 1024; idx++) {
int sectorMeta = sectorsSaves.get(idx);
if (sectorMeta == 0) {
//Empty chunk
continue;
}
int sectorStart = sectorMeta>>>8;
int sectorCount = sectorMeta&((1<<8)-1);
}
}
}
private void importChunkNBT(ChunkPos pos, NbtCompound chunk) {
}
}

View File

@@ -0,0 +1,180 @@
package me.cortex.voxelmon.importers;
import com.mojang.serialization.Codec;
import me.cortex.voxelmon.core.util.ByteBufferBackedInputStream;
import me.cortex.voxelmon.core.voxelization.VoxelizedSection;
import me.cortex.voxelmon.core.voxelization.WorldConversionFactory;
import me.cortex.voxelmon.core.world.WorldEngine;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.BiomeKeys;
import net.minecraft.world.chunk.PalettedContainer;
import net.minecraft.world.chunk.ReadableContainer;
import net.minecraft.world.storage.ChunkStreamVersion;
import org.lwjgl.system.MemoryUtil;
import java.io.*;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.Objects;
public class WorldImporter {
private final WorldEngine world;
private final World mcWorld;
private final Codec<ReadableContainer<RegistryEntry<Biome>>> biomeCodec;
public WorldImporter(WorldEngine worldEngine, World mcWorld) {
this.world = worldEngine;
this.mcWorld = mcWorld;
var biomeRegistry = mcWorld.getRegistryManager().get(RegistryKeys.BIOME);
this.biomeCodec = PalettedContainer.createReadableContainerCodec(biomeRegistry.getIndexedEntries(), biomeRegistry.createEntryCodec(), PalettedContainer.PaletteProvider.BIOME, biomeRegistry.entryOf(BiomeKeys.PLAINS));
}
private Thread worker;
public void importWorldAsyncStart(File directory) {
this.worker = new Thread(() -> {
Arrays.stream(directory.listFiles()).parallel().forEach(file -> {
if (!file.isFile()) {
return;
}
var name = file.getName();
var sections = name.split("\\.");
if (sections.length != 4 || (!sections[0].equals("r")) || (!sections[3].equals("mca"))) {
System.err.println("Unknown file: " + name);
return;
}
int rx = Integer.parseInt(sections[1]);
int rz = Integer.parseInt(sections[2]);
try {
this.importRegionFile(file.toPath(), rx, rz);
} catch (Exception e) {
e.printStackTrace();
}
});
System.err.println("Done");
});
this.worker.setName("World importer");
this.worker.start();
}
private void importRegionFile(Path file, int x, int z) throws IOException {
try (var fileStream = FileChannel.open(file, StandardOpenOption.READ)) {
var sectorsSavesBB = MemoryUtil.memAlloc(8192);
if (fileStream.read(sectorsSavesBB, 0) != 8192) {
System.err.println("Header of region file invalid");
return;
}
sectorsSavesBB.rewind();
var sectorsSaves = sectorsSavesBB.order(ByteOrder.BIG_ENDIAN).asIntBuffer();
//Find and load all saved chunks
for (int idx = 0; idx < 1024; idx++) {
int sectorMeta = sectorsSaves.get(idx);
if (sectorMeta == 0) {
//Empty chunk
continue;
}
int sectorStart = sectorMeta>>>8;
int sectorCount = sectorMeta&((1<<8)-1);
var data = MemoryUtil.memAlloc(sectorCount*4096).order(ByteOrder.BIG_ENDIAN);
fileStream.read(data, sectorStart*4096L);
data.flip();
{
int m = data.getInt();
byte b = data.get();
if (m == 0) {
System.err.println("Chunk is allocated, but stream is missing");
} else {
int n = m - 1;
if ((b & 128) != 0) {
if (n != 0) {
System.err.println("Chunk has both internal and external streams");
}
System.err.println("Chunk has external stream which is not supported");
} else if (n > data.remaining()) {
System.err.println("Chunk stream is truncated: expected "+n+" but read " + data.remaining());
} else if (n < 0) {
System.err.println("Declared size of chunk is negative");
} else {
try (var decompressedData = this.decompress(b, new ByteBufferBackedInputStream(data))) {
if (decompressedData == null) {
System.err.println("Error decompressing chunk data");
} else {
var nbt = NbtIo.readCompound(decompressedData);
this.importChunkNBT(nbt);
}
}
}
}
}
MemoryUtil.memFree(data);
}
MemoryUtil.memFree(sectorsSavesBB);
}
}
private DataInputStream decompress(byte flags, InputStream stream) throws IOException {
ChunkStreamVersion chunkStreamVersion = ChunkStreamVersion.get(flags);
if (chunkStreamVersion == null) {
System.err.println("Chunk has invalid chunk stream version");
return null;
} else {
return new DataInputStream(chunkStreamVersion.wrap(stream));
}
}
private void importChunkNBT(NbtCompound chunk) {
try {
int x = chunk.getInt("xPos");
int z = chunk.getInt("zPos");
for (var sectionE : chunk.getList("sections", NbtElement.COMPOUND_TYPE)) {
var section = (NbtCompound) sectionE;
int y = section.getInt("Y");
this.importSectionNBT(x, y, z, section);
}
} catch (Exception e) {
System.err.println("Exception importing world chunk:");
e.printStackTrace();
}
}
private static final Codec<PalettedContainer<BlockState>> BLOCK_STATE_CODEC = PalettedContainer.createPalettedContainerCodec(Block.STATE_IDS, BlockState.CODEC, PalettedContainer.PaletteProvider.BLOCK_STATE, Blocks.AIR.getDefaultState());
private void importSectionNBT(int x, int y, int z, NbtCompound section) {
if (section.getCompound("block_states").isEmpty()) {
return;
}
var blockStates = BLOCK_STATE_CODEC.parse(NbtOps.INSTANCE, section.getCompound("block_states")).result().get();
var biomes = this.biomeCodec.parse(NbtOps.INSTANCE, section.getCompound("biomes")).result().get();
VoxelizedSection csec = WorldConversionFactory.convert(
this.world.getMapper(),
blockStates,
biomes,
(bx, by, bz) -> (byte) 0,
x,
y,
z
);
this.world.insertUpdate(csec);
}
}

View File

@@ -0,0 +1,11 @@
package me.cortex.voxelmon.mixin.joml;
import org.joml.FrustumIntersection;
import org.joml.Vector4f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(value = FrustumIntersection.class, remap = false)
public interface AccessFrustumIntersection {
@Accessor Vector4f[] getPlanes();
}

View File

@@ -0,0 +1,15 @@
package me.cortex.voxelmon.mixin.minecraft;
import net.minecraft.client.render.BackgroundRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
@Mixin(BackgroundRenderer.class)
public class MixinBackgroundRenderer {
@ModifyConstant(method = "applyFog", constant = @Constant(floatValue = 192.0F))
private static float changeFog(float fog) {
return 9999999f;
}
}

View File

@@ -0,0 +1,19 @@
package me.cortex.voxelmon.mixin.minecraft;
import me.cortex.voxelmon.core.VoxelCore;
import net.minecraft.client.world.ClientChunkManager;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.chunk.WorldChunk;
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;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(ClientChunkManager.class)
public class MixinClientChunkManager {
@Inject(method = "unload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientChunkManager$ClientChunkMap;compareAndSet(ILnet/minecraft/world/chunk/WorldChunk;Lnet/minecraft/world/chunk/WorldChunk;)Lnet/minecraft/world/chunk/WorldChunk;", shift = At.Shift.BEFORE), locals = LocalCapture.CAPTURE_FAILHARD)
private void injectUnload(ChunkPos pos, CallbackInfo ci, int index, WorldChunk worldChunk) {
//VoxelCore.INSTANCE.enqueueIngest(worldChunk);
}
}

View File

@@ -0,0 +1,19 @@
package me.cortex.voxelmon.mixin.minecraft;
import me.cortex.voxelmon.core.VoxelCore;
import net.minecraft.client.gui.hud.DebugHud;
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.CallbackInfoReturnable;
import java.util.List;
@Mixin(DebugHud.class)
public class MixinDebugHud {
@Inject(method = "getRightText", at = @At("TAIL"))
private void injectDebug(CallbackInfoReturnable<List<String>> cir) {
var ret = cir.getReturnValue();
VoxelCore.INSTANCE.addDebugInfo(ret);
}
}

View File

@@ -0,0 +1,16 @@
package me.cortex.voxelmon.mixin.minecraft;
import net.minecraft.client.render.GameRenderer;
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.CallbackInfoReturnable;
@Mixin(GameRenderer.class)
public class MixinGameRenderer {
@Inject(method = "getFarPlaneDistance", at = @At("HEAD"), cancellable = true)
public void method_32796(CallbackInfoReturnable<Float> cir) {
cir.setReturnValue(16 * 3000f);
cir.cancel();
}
}

View File

@@ -0,0 +1,16 @@
package me.cortex.voxelmon.mixin.minecraft;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.RunArgs;
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(MinecraftClient.class)
public class MixinMinecraftClient {
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resource/PeriodicNotificationManager;<init>(Lnet/minecraft/util/Identifier;Lit/unimi/dsi/fastutil/objects/Object2BooleanFunction;)V", shift = At.Shift.AFTER))
private void injectRenderDoc(RunArgs args, CallbackInfo ci) {
//System.load("C:\\Program Files\\RenderDoc\\renderdoc.dll");
}
}

View File

@@ -0,0 +1,49 @@
package me.cortex.voxelmon.mixin.minecraft;
import me.cortex.voxelmon.Voxelmon;
import me.cortex.voxelmon.core.VoxelCore;
import net.minecraft.client.render.*;
import net.minecraft.client.util.math.MatrixStack;
import org.joml.Matrix4f;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(WorldRenderer.class)
public abstract class MixinWorldRenderer {
@Shadow protected abstract void renderLayer(RenderLayer renderLayer, MatrixStack matrices, double cameraX, double cameraY, double cameraZ, Matrix4f positionMatrix);
@Shadow protected abstract void setupTerrain(Camera camera, Frustum frustum, boolean hasForcedFrustum, boolean spectator);
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V"))
private void injectSetup(WorldRenderer instance, Camera camera, Frustum frustum, boolean hasForcedFrustum, boolean spectator) {
//Call the actual terrain setup method
this.setupTerrain(camera, frustum, hasForcedFrustum, spectator);
//Call our setup method
VoxelCore.INSTANCE.renderSetup(frustum, camera);
}
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;renderLayer(Lnet/minecraft/client/render/RenderLayer;Lnet/minecraft/client/util/math/MatrixStack;DDDLorg/joml/Matrix4f;)V", ordinal = 2))
private void injectOpaqueRender(WorldRenderer instance, RenderLayer renderLayer, MatrixStack matrices, double cameraX, double cameraY, double cameraZ, Matrix4f positionMatrix) {
//Call the actual render method
this.renderLayer(renderLayer, matrices, cameraX, cameraY, cameraZ, positionMatrix);
VoxelCore.INSTANCE.renderOpaque(matrices, cameraX, cameraY, cameraZ);
}
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F"))
private float redirectMax(float a, float b) {
return a;
}
@Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getViewDistance()F"))
private float changeRD(GameRenderer instance) {
float viewDistance = instance.getViewDistance();
return 16*1512;
}
}

View File

@@ -0,0 +1,56 @@
struct Frustum {
vec4 planes[6];
};
layout(binding = 0, std140) uniform SceneUniform {
mat4 MVP;
ivec3 baseSectionPos;
int sectionCount;
Frustum frustum;
vec3 cameraSubPos;
int _padA;
};
struct State {
uint biomeTintMsk;
uint faceColours[6];
};
struct Biome {
uint foliage;
uint water;
};
struct SectionMeta {
uvec4 header;
uvec4 drawdata;
};
//TODO: see if making the stride 2*4*4 bytes or something cause you get that 16 byte write
struct DrawCommand {
uint count;
uint instanceCount;
uint firstIndex;
int baseVertex;
uint baseInstance;
};
layout(binding = 1, std430) readonly restrict buffer QuadBuffer {
Quad quadData[];
};
layout(binding = 2, std430) writeonly restrict buffer DrawBuffer {
DrawCommand cmdBuffer[];
};
layout(binding = 3, std430) readonly restrict buffer SectionBuffer {
SectionMeta sectionData[];
};
layout(binding = 4, std430) readonly restrict buffer StateBuffer {
State stateData[];
};
layout(binding = 5, std430) readonly restrict buffer BiomeBuffer {
Biome biomeData[];
};

View File

@@ -0,0 +1,97 @@
#version 460
#extension GL_ARB_gpu_shader_int64 : enable
#import <voxelmon:lod/gl46/quad_format.glsl>
#import <voxelmon:lod/gl46/bindings.glsl>
//https://github.com/KhronosGroup/GLSL/blob/master/extensions/ext/GL_EXT_shader_16bit_storage.txt
// adds support for uint8_t which can use for compact visibility buffer
layout(local_size_x = 128, local_size_y = 1, local_size_x = 1) in;
bool testFrustumPoint(vec4 plane, vec3 min, vec3 max) {
vec3 point = mix(max, min, lessThan(plane.xyz, vec3(0))) * plane.xyz;
return (point.x + point.y + point.z) >= -plane.w;
}
bool testFrustum(Frustum frust, vec3 min, vec3 max) {
return testFrustumPoint(frust.planes[0], min, max) &&
testFrustumPoint(frust.planes[1], min, max) &&
testFrustumPoint(frust.planes[2], min, max) &&
testFrustumPoint(frust.planes[3], min, max) &&
testFrustumPoint(frust.planes[4], min, max) &&
testFrustumPoint(frust.planes[5], min, max);
}
uint extractDetail(SectionMeta section) {
return section.header.x>>28;
}
/*
uint count;
uint instanceCount;
uint firstIndex;
int baseVertex;
uint baseInstance;
*/
ivec3 extractPosition(SectionMeta section) {
int y = ((int(section.header.x)<<4)>>24);
int x = (int(section.header.y)<<4)>>8;
int z = int((section.header.x&((1<<20)-1))<<4);
z |= int(section.header.y>>28);
z <<= 8;
z >>= 8;
return ivec3(x,y,z);
}
uint extractQuadStart(SectionMeta meta) {
return meta.header.z;
}
uint extractQuadCount(SectionMeta meta) {
return meta.header.w;
}
uint encodeLocalLodPos(uint detail, ivec3 pos) {
uvec3 detla = (pos - (baseSectionPos >> detail))&((1<<9)-1);
return (detail<<29)|(detla.x<<20)|(detla.y<<11)|(detla.z<<2);
}
void main() {
if (gl_GlobalInvocationID.x >= sectionCount) {
return;
}
SectionMeta meta = sectionData[gl_GlobalInvocationID.x];
uint detail = extractDetail(meta);
ivec3 ipos = extractPosition(meta);
//TODO: fixme; i dont think this is correct
vec3 cornerPos = vec3(((ipos<<detail)-baseSectionPos)<<5)-cameraSubPos;
bool shouldRender = testFrustum(frustum, cornerPos, cornerPos+vec3(1<<(detail+5)));
//This prevents overflow of the relative position encoder
if (shouldRender) {
shouldRender = !any(lessThan(ivec3(254), abs(ipos-(baseSectionPos>>detail))));
}
if (shouldRender) {
DrawCommand cmd;
cmd.count = extractQuadCount(meta) * 6;
cmd.instanceCount = 1;
cmd.firstIndex = 0;
cmd.baseVertex = int(extractQuadStart(meta))<<2;
cmd.baseInstance = encodeLocalLodPos(detail, ipos);
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
} else {
DrawCommand cmd;
cmd.count = 0;
cmd.instanceCount = 0;
cmd.firstIndex = 0;
cmd.baseVertex = 0;
cmd.baseInstance = 0;
cmdBuffer[gl_GlobalInvocationID.x] = cmd;
}
}

View File

@@ -0,0 +1,4 @@
#version 460 core
#extension GL_ARB_gpu_shader_int64 : enable
#import <voxelmon:lod/gl46/bindings.glsl>

View File

@@ -0,0 +1,4 @@
#version 460 core
#extension GL_ARB_gpu_shader_int64 : enable
#import <voxelmon:lod/gl46/bindings.glsl>

View File

@@ -0,0 +1,49 @@
#ifdef GL_ARB_gpu_shader_int64
#define Quad uint64_t
#define Eu32(data, amountBits, shift) (uint((data)>>(shift))&((1u<<(amountBits))-1))
vec3 extractPos(uint64_t quad) {
//TODO: pull out the majic constants into #defines (specifically the shift amount)
return vec3(Eu32(quad, 5, 21), Eu32(quad, 5, 16), Eu32(quad, 5, 11));
}
ivec2 extractSize(uint64_t quad) {
return ivec2(Eu32(quad, 4, 3), Eu32(quad, 4, 7)) + ivec2(1);//the + 1 is cause you cant actually have a 0 size quad
}
uint extractFace(uint64_t quad) {
return Eu32(quad, 3, 0);
}
uint extractStateId(uint64_t quad) {
return Eu32(quad, 20, 26);
}
uint extractBiomeId(uint64_t quad) {
return Eu32(quad, 9, 46);
}
uint extractLightId(uint64_t quad) {
return Eu32(quad, 8, 55);
}
#else
//TODO: FIXME, ivec2 swaps around the data of the x and y cause its written in little endian
#define Quad ivec2
#define Eu32(data, amountBits, shift) (uint((data)>>(shift))&((1u<<(amountBits))-1))
vec3 extractPos(ivec2 quad) {
return vec3(Eu32(quad.y, 5, 21), Eu32(quad.y, 5, 16), Eu32(quad.y, 5, 11));
}
ivec2 extractSize(ivec2 quad) {
return ivec2(Eu32(quad.y, 4, 3), Eu32(quad.y, 4, 7)) + ivec2(1);//the + 1 is cause you cant actually have a 0 size quad
}
uint extractFace(ivec2 quad) {
return quad.y&7;
}
#endif

View File

@@ -0,0 +1,8 @@
#version 460 core
layout(location = 0) in flat vec4 colour;
layout(location = 0) out vec4 outColour;
void main() {
//TODO: randomly discard the fragment with respect to the alpha value
outColour = colour;
}

View File

@@ -0,0 +1,92 @@
#version 460 core
#extension GL_ARB_gpu_shader_int64 : enable
#import <voxelmon:lod/gl46/quad_format.glsl>
#import <voxelmon:lod/gl46/bindings.glsl>
layout(location = 0) out flat vec4 colour;
uint extractLodLevel() {
return uint(gl_BaseInstance)>>29;
}
//Note the last 2 bits of gl_BaseInstance are unused
//Gives a relative position of +-255 relative to the player center in its respective lod
ivec3 extractRelativeLodPos() {
return (ivec3(gl_BaseInstance)<<ivec3(3,12,21))>>ivec3(23);
}
vec4 uint2vec4RGBA(uint colour) {
return vec4((uvec4(colour)>>uvec4(24,16,8,0))&uvec4(0xFF))/255;
}
void main() {
int cornerIdx = gl_VertexID&3;
Quad quad = quadData[uint(gl_VertexID)>>2];
vec3 innerPos = extractPos(quad);
uint face = extractFace(quad);
uint lodLevel = extractLodLevel();
ivec3 lodCorner = ((extractRelativeLodPos()<<lodLevel) - (baseSectionPos&(ivec3((1<<lodLevel)-1))))<<5;
vec3 corner = innerPos * (1<<lodLevel) + lodCorner;
//TODO: see if backface culling is even needed, since everything (should) be back culled already
//Flip the quad rotation by its face (backface culling)
if ((face&1) != 0) {
cornerIdx ^= 1;
}
if ((face>>1) == 0) {
cornerIdx ^= 1;
}
ivec2 size = extractSize(quad) * ivec2((cornerIdx>>1)&1, cornerIdx&1) * (1<<lodLevel);
vec3 pos = corner;
//NOTE: can just make instead of face, make it axis (can also make it 2 bit instead of 3 bit then)
// since the only reason face is needed is to ensure backface culling orientation thing
uint axis = face>>1;
if (axis == 0) {
pos.xz += size;
pos.y += (face&1)<<lodLevel;
} else if (axis == 1) {
pos.xy += size;
pos.z += (face&1)<<lodLevel;
} else {
pos.yz += size;
pos.x += (face&1)<<lodLevel;
}
gl_Position = MVP * vec4(pos,1);
uint stateId = extractStateId(quad);
uint biomeId = extractBiomeId(quad);
State stateInfo = stateData[stateId];
colour = uint2vec4RGBA(stateInfo.faceColours[face]);
if (((stateInfo.biomeTintMsk>>face)&1) == 1) {
vec4 biomeColour = uint2vec4RGBA(biomeData[biomeId].foliage);
colour *= biomeColour;
}
//Apply water tint
if (((stateInfo.biomeTintMsk>>6)&1) == 1) {
colour *= vec4(0.247, 0.463, 0.894, 1);
}
//gl_Position = MVP * vec4(vec3(((cornerIdx)&1)+10,10,((cornerIdx>>1)&1)+10),1);
//uint i = uint(quad>>32);
uint i = uint(gl_BaseInstance);
i ^= i>>16;
i *= 124128573;
i ^= i>>16;
i *= 4211346123;
i ^= i>>16;
i *= 462312435;
i ^= i>>16;
i *= 542354341;
i ^= i>>16;
//uint i = uint(lodLevel+12)*215387625;
//colour *= vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7)*0.7+0.3,1);
//colour = vec4(vec3(float((uint(i)>>2)&7)/7,float((uint(i)>>5)&7)/7,float((uint(i)>>8)&7)/7),1);
}

View File

@@ -0,0 +1,8 @@
#version 460 core
layout(location = 0) in flat vec4 colour;
layout(location = 0) out vec4 outColour;
void main() {
//TODO: randomly discard the fragment with respect to the alpha value
outColour = colour;
}

View File

@@ -0,0 +1,18 @@
#version 460 core
layout(location = 0) out flat vec4 colour;
layout(binding = 0, std140) uniform SceneUniform {
mat4 MVP;
ivec3 baseSectionPos;
int _pad1;
};
void main() {
int cornerIdx = gl_VertexID&3;
gl_Position = MVP * vec4(vec3(cornerIdx&1,((cornerIdx>>1)&1),0),1);
colour = vec4(float((uint(gl_VertexID)>>2)&7)/7,float((uint(gl_VertexID)>>5)&7)/7,float((uint(gl_VertexID)>>8)&7)/7,1);
}

View File

@@ -0,0 +1,28 @@
//Contains the definision of a ray and step functions
struct Ray {
ivec3 pos;
vec3 innerPos;
vec3 dir;
vec3 invDir;
};
Ray ray;
void setup(vec3 origin, vec3 direction) {
ray.pos = ivec3(origin);
ray.innerPos = origin - ray.pos;
direction *= inversesqrt(direction);
ray.dir = direction;
ray.invDir = 1/direction;
}
void step(ivec3 aabb) {
//TODO:check for innerPos>=1 and step into that voxel
vec3 t2f = (aabb - ray.innerPos) * ray.invDir;
float mint2f = min(t2f.x, min(t2f.y, t2f.z));
bvec3 msk = lessThanEqual(t2f.xyz, vec3(mint2f));
vec3 newIP = mint2f * ray.dir + ray.innerPos;
ivec3 offset = min(aabb-1, ivec3(newIP));
ray.pos += offset + ivec3(msk);
ray.innerPos = mix(vec3(0), newIP - offset, not(msk));
}

View File

@@ -0,0 +1,8 @@
struct Voxel {
};
//TODO: add tlas and blas voxel fetching (rings and all)
void getVoxel() {
}

View File

@@ -0,0 +1,2 @@
//Glue code between ray stepper and voxel storage
// its the primary ray tracer

View File

@@ -0,0 +1,21 @@
{
"schemaVersion": 1,
"id": "voxelmon",
"version": "${version}",
"name": "voxelmon",
"description": "",
"authors": [],
"contact": {
},
"license": "All-Rights-Reserved",
"icon": "assets/voxelmon/icon.png",
"environment": "client",
"entrypoints": {
},
"mixins": [
"voxelmon.mixins.json"
],
"depends": {
"fabricloader": ">=0.14.22"
}
}

View File

@@ -0,0 +1,4 @@
accessWidener v1 named
accessible field net/minecraft/client/texture/SpriteContents image Lnet/minecraft/client/texture/NativeImage;
accessible field net/minecraft/client/render/Frustum frustumIntersection Lorg/joml/FrustumIntersection;

View File

@@ -0,0 +1,19 @@
{
"required": true,
"package": "me.cortex.voxelmon.mixin",
"compatibilityLevel": "JAVA_17",
"client": [
"minecraft.MixinBackgroundRenderer",
"minecraft.MixinClientChunkManager",
"minecraft.MixinDebugHud",
"minecraft.MixinGameRenderer",
"minecraft.MixinMinecraftClient",
"minecraft.MixinWorldRenderer"
],
"injectors": {
"defaultRequire": 1
},
"mixins": [
"joml.AccessFrustumIntersection"
]
}